{"product_id":"xay-dựng-mcp-server-dầu-tien-hướng-dẫn-step-by-step","title":"Xây dựng MCP Server đầu tiên — Hướng dẫn step-by-step","description":"\u003ch2\u003eGiới thiệu\u003c\/h2\u003e\n\u003cp\u003eSau khi đã hiểu MCP là gì, bước tiếp theo tự nhiên là xây dựng MCP Server riêng. Đây là cách bạn tạo custom tools cho Claude Code và Claude Desktop — biến Claude thành một AI agent có thể thao tác trực tiếp với systems của bạn.\u003c\/p\u003e\n\n\u003cp\u003eTrong bài này, chúng ta sẽ xây dựng một MCP Server thực tế bằng TypeScript: bắt đầu từ server đọc file đơn giản, sau đó mở rộng thành một weather API server hoàn chỉnh.\u003c\/p\u003e\n\n\u003ch2\u003ePrerequisites\u003c\/h2\u003e\n\n\u003ch3\u003eYêu cầu môi trường\u003c\/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eNode.js 18+\u003c\/strong\u003e — kiểm tra: \u003ccode\u003enode --version\u003c\/code\u003e\n\u003c\/li\u003e\n\u003cli\u003e\u003cstrong\u003enpm hoặc pnpm\u003c\/strong\u003e\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eTypeScript 5+\u003c\/strong\u003e — install global: \u003ccode\u003enpm install -g typescript\u003c\/code\u003e\n\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eClaude Desktop hoặc Claude Code\u003c\/strong\u003e để test\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003ch3\u003eHiểu cơ bản về TypeScript và async\/await\u003c\/h3\u003e\n\u003cp\u003eBài hướng dẫn này giả định bạn đã quen với TypeScript cơ bản và async programming. Nếu chưa, hãy xem qua TypeScript handbook trước.\u003c\/p\u003e\n\n\u003ch2\u003eSetup project\u003c\/h2\u003e\n\n\u003ch3\u003eKhởi tạo project\u003c\/h3\u003e\n\u003cpre\u003e\u003ccode\u003e# Tạo thư mục project\nmkdir my-mcp-server\ncd my-mcp-server\n\n# Khởi tạo npm project\nnpm init -y\n\n# Cài MCP SDK và dependencies\nnpm install @modelcontextprotocol\/sdk zod\n\n# Cài dev dependencies\nnpm install -D typescript @types\/node tsx\n\n# Tạo tsconfig\nnpx tsc --init\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003cp\u003eCập nhật \u003ccode\u003etsconfig.json\u003c\/code\u003e:\u003c\/p\u003e\n\u003cpre\u003e\u003ccode\u003e{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"Node16\",\n    \"moduleResolution\": \"Node16\",\n    \"outDir\": \".\/dist\",\n    \"rootDir\": \".\/src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\/**\/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003cp\u003eCập nhật \u003ccode\u003epackage.json\u003c\/code\u003e:\u003c\/p\u003e\n\u003cpre\u003e\u003ccode\u003e{\n  \"name\": \"my-mcp-server\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"main\": \"dist\/index.js\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"dev\": \"tsx src\/index.ts\",\n    \"start\": \"node dist\/index.js\"\n  }\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eMCP Server đầu tiên — Đọc file\u003c\/h2\u003e\n\n\u003ch3\u003eTạo server cơ bản\u003c\/h3\u003e\n\u003cp\u003eTạo file \u003ccode\u003esrc\/index.ts\u003c\/code\u003e:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003eimport { Server } from \"@modelcontextprotocol\/sdk\/server\/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol\/sdk\/server\/stdio.js\";\nimport {\n  CallToolRequestSchema,\n  ListToolsRequestSchema,\n} from \"@modelcontextprotocol\/sdk\/types.js\";\nimport { z } from \"zod\";\nimport { readFileSync, existsSync } from \"fs\";\nimport { resolve } from \"path\";\n\n\/\/ Khởi tạo server với metadata\nconst server = new Server(\n  {\n    name: \"my-file-server\",\n    version: \"1.0.0\",\n  },\n  {\n    capabilities: {\n      tools: {}, \/\/ Server này cung cấp tools\n    },\n  }\n);\n\n\/\/ Định nghĩa danh sách tools\nserver.setRequestHandler(ListToolsRequestSchema, async () =\u0026gt; {\n  return {\n    tools: [\n      {\n        name: \"read_file\",\n        description: \"Đọc nội dung của một file text\",\n        inputSchema: {\n          type: \"object\",\n          properties: {\n            path: {\n              type: \"string\",\n              description: \"Đường dẫn tuyệt đối đến file cần đọc\",\n            },\n          },\n          required: [\"path\"],\n        },\n      },\n      {\n        name: \"check_file_exists\",\n        description: \"Kiểm tra file có tồn tại hay không\",\n        inputSchema: {\n          type: \"object\",\n          properties: {\n            path: {\n              type: \"string\",\n              description: \"Đường dẫn đến file cần kiểm tra\",\n            },\n          },\n          required: [\"path\"],\n        },\n      },\n    ],\n  };\n});\n\n\/\/ Xử lý tool calls\nserver.setRequestHandler(CallToolRequestSchema, async (request) =\u0026gt; {\n  const { name, arguments: args } = request.params;\n\n  if (name === \"read_file\") {\n    const { path } = z.object({ path: z.string() }).parse(args);\n    const absolutePath = resolve(path);\n\n    if (!existsSync(absolutePath)) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `Lỗi: File không tồn tại: ${absolutePath}`,\n          },\n        ],\n        isError: true,\n      };\n    }\n\n    try {\n      const content = readFileSync(absolutePath, \"utf-8\");\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: content,\n          },\n        ],\n      };\n    } catch (error) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `Lỗi đọc file: ${error instanceof Error ? error.message : String(error)}`,\n          },\n        ],\n        isError: true,\n      };\n    }\n  }\n\n  if (name === \"check_file_exists\") {\n    const { path } = z.object({ path: z.string() }).parse(args);\n    const absolutePath = resolve(path);\n    const exists = existsSync(absolutePath);\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: exists\n            ? `File tồn tại: ${absolutePath}`\n            : `File không tồn tại: ${absolutePath}`,\n        },\n      ],\n    };\n  }\n\n  return {\n    content: [{ type: \"text\", text: `Tool không tồn tại: ${name}` }],\n    isError: true,\n  };\n});\n\n\/\/ Khởi động server với stdio transport\nasync function main() {\n  const transport = new StdioServerTransport();\n  await server.connect(transport);\n  console.error(\"MCP File Server đã khởi động\"); \/\/ Log ra stderr\n}\n\nmain().catch(console.error);\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch3\u003eBuild và test\u003c\/h3\u003e\n\u003cpre\u003e\u003ccode\u003e# Build TypeScript\nnpm run build\n\n# Test thủ công\necho '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\/list\",\"params\":{}}' | node dist\/index.js\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eĐăng ký server với Claude Code\u003c\/h2\u003e\n\n\u003cpre\u003e\u003ccode\u003e# Thêm server vào Claude Code\nclaude mcp add my-file-server node \/absolute\/path\/to\/my-mcp-server\/dist\/index.js\n\n# Kiểm tra server đã được thêm\nclaude mcp list\n\n# Test trong Claude Code\nclaude \"Đọc file \/etc\/hosts và tóm tắt nội dung\"\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eĐăng ký server với Claude Desktop\u003c\/h2\u003e\n\n\u003cp\u003eThêm vào \u003ccode\u003eclaude_desktop_config.json\u003c\/code\u003e:\u003c\/p\u003e\n\u003cpre\u003e\u003ccode\u003e{\n  \"mcpServers\": {\n    \"my-file-server\": {\n      \"command\": \"node\",\n      \"args\": [\"\/absolute\/path\/to\/my-mcp-server\/dist\/index.js\"]\n    }\n  }\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003cp\u003eRestart Claude Desktop. Bạn sẽ thấy tools \u003ccode\u003eread_file\u003c\/code\u003e và \u003ccode\u003echeck_file_exists\u003c\/code\u003e xuất hiện trong danh sách available tools.\u003c\/p\u003e\n\n\u003ch2\u003eThêm Resources\u003c\/h2\u003e\n\n\u003cp\u003eNgoài Tools, MCP Server có thể expose Resources — dữ liệu mà Claude có thể đọc. Thêm resource support vào server:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003eimport {\n  ListResourcesRequestSchema,\n  ReadResourceRequestSchema,\n} from \"@modelcontextprotocol\/sdk\/types.js\";\n\n\/\/ Khai báo capabilities bao gồm resources\nconst server = new Server(\n  { name: \"my-file-server\", version: \"1.0.0\" },\n  {\n    capabilities: {\n      tools: {},\n      resources: {}, \/\/ Thêm resources capability\n    },\n  }\n);\n\n\/\/ Handler cho list resources\nserver.setRequestHandler(ListResourcesRequestSchema, async () =\u0026gt; {\n  return {\n    resources: [\n      {\n        uri: \"file:\/\/\/var\/log\/app.log\",\n        name: \"Application Log\",\n        description: \"Log file của ứng dụng\",\n        mimeType: \"text\/plain\",\n      },\n    ],\n  };\n});\n\n\/\/ Handler cho read resource\nserver.setRequestHandler(ReadResourceRequestSchema, async (request) =\u0026gt; {\n  const { uri } = request.params;\n\n  if (uri === \"file:\/\/\/var\/log\/app.log\") {\n    const content = readFileSync(\"\/var\/log\/app.log\", \"utf-8\");\n    return {\n      contents: [\n        {\n          uri,\n          mimeType: \"text\/plain\",\n          text: content,\n        },\n      ],\n    };\n  }\n\n  throw new Error(`Resource không tồn tại: ${uri}`);\n});\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eVí dụ thực tế — Weather API Server\u003c\/h2\u003e\n\n\u003cp\u003eBây giờ hãy build một server thực tế hơn: gọi weather API và trả kết quả cho Claude.\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003eimport { Server } from \"@modelcontextprotocol\/sdk\/server\/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol\/sdk\/server\/stdio.js\";\nimport {\n  CallToolRequestSchema,\n  ListToolsRequestSchema,\n} from \"@modelcontextprotocol\/sdk\/types.js\";\nimport { z } from \"zod\";\n\nconst server = new Server(\n  { name: \"weather-server\", version: \"1.0.0\" },\n  { capabilities: { tools: {} } }\n);\n\nserver.setRequestHandler(ListToolsRequestSchema, async () =\u0026gt; ({\n  tools: [\n    {\n      name: \"get_weather\",\n      description: \"Lấy thông tin thời tiết hiện tại cho một thành phố\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          city: {\n            type: \"string\",\n            description: \"Tên thành phố (tiếng Anh), ví dụ: Hanoi, Ho Chi Minh City\",\n          },\n          units: {\n            type: \"string\",\n            enum: [\"metric\", \"imperial\"],\n            description: \"Đơn vị nhiệt độ: metric (Celsius) hoặc imperial (Fahrenheit)\",\n            default: \"metric\",\n          },\n        },\n        required: [\"city\"],\n      },\n    },\n  ],\n}));\n\nserver.setRequestHandler(CallToolRequestSchema, async (request) =\u0026gt; {\n  const { name, arguments: args } = request.params;\n\n  if (name === \"get_weather\") {\n    const { city, units = \"metric\" } = z\n      .object({\n        city: z.string(),\n        units: z.enum([\"metric\", \"imperial\"]).optional().default(\"metric\"),\n      })\n      .parse(args);\n\n    const apiKey = process.env.OPENWEATHER_API_KEY;\n    if (!apiKey) {\n      return {\n        content: [\n          { type: \"text\", text: \"Lỗi: OPENWEATHER_API_KEY chưa được cấu hình\" },\n        ],\n        isError: true,\n      };\n    }\n\n    try {\n      const url = `https:\/\/api.openweathermap.org\/data\/2.5\/weather?q=${encodeURIComponent(city)}\u0026amp;units=${units}\u0026amp;appid=${apiKey}`;\n      const response = await fetch(url);\n\n      if (!response.ok) {\n        const error = await response.json() as { message?: string };\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: `Lỗi từ API: ${error.message || response.statusText}`,\n            },\n          ],\n          isError: true,\n        };\n      }\n\n      const data = await response.json() as {\n        name: string;\n        sys: { country: string };\n        main: { temp: number; feels_like: number; humidity: number };\n        weather: Array\u0026lt;{ description: string }\u0026gt;;\n        wind: { speed: number };\n      };\n\n      const tempUnit = units === \"metric\" ? \"°C\" : \"°F\";\n      const windUnit = units === \"metric\" ? \"m\/s\" : \"mph\";\n\n      const summary = [\n        `Thời tiết tại ${data.name}, ${data.sys.country}:`,\n        `- Nhiệt độ: ${data.main.temp}${tempUnit} (cảm giác như ${data.main.feels_like}${tempUnit})`,\n        `- Điều kiện: ${data.weather[0].description}`,\n        `- Độ ẩm: ${data.main.humidity}%`,\n        `- Gió: ${data.wind.speed} ${windUnit}`,\n      ].join(\"\\n\");\n\n      return {\n        content: [{ type: \"text\", text: summary }],\n      };\n    } catch (error) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `Lỗi kết nối: ${error instanceof Error ? error.message : String(error)}`,\n          },\n        ],\n        isError: true,\n      };\n    }\n  }\n\n  return {\n    content: [{ type: \"text\", text: `Tool không tồn tại: ${name}` }],\n    isError: true,\n  };\n});\n\nasync function main() {\n  const transport = new StdioServerTransport();\n  await server.connect(transport);\n  console.error(\"Weather MCP Server đã khởi động\");\n}\n\nmain().catch(console.error);\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003cp\u003eĐăng ký với Claude Desktop:\u003c\/p\u003e\n\u003cpre\u003e\u003ccode\u003e{\n  \"mcpServers\": {\n    \"weather\": {\n      \"command\": \"node\",\n      \"args\": [\"\/path\/to\/weather-server\/dist\/index.js\"],\n      \"env\": {\n        \"OPENWEATHER_API_KEY\": \"your_api_key_here\"\n      }\n    }\n  }\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003cp\u003eSau khi cấu hình, bạn có thể hỏi Claude: \"Thời tiết Hà Nội hôm nay thế nào?\" và Claude sẽ gọi tool để lấy dữ liệu thực.\u003c\/p\u003e\n\n\u003ch2\u003eTesting MCP Server\u003c\/h2\u003e\n\n\u003ch3\u003eUnit testing handlers\u003c\/h3\u003e\n\u003cp\u003eTest handlers riêng biệt mà không cần khởi động full server:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003e\/\/ tests\/handlers.test.ts\nimport { describe, it, expect } from \"vitest\";\nimport { readFileSync, writeFileSync, unlinkSync } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\n\ndescribe(\"read_file tool\", () =\u0026gt; {\n  it(\"đọc file thành công\", async () =\u0026gt; {\n    \/\/ Tạo temp file\n    const tmpPath = join(tmpdir(), \"test-mcp.txt\");\n    writeFileSync(tmpPath, \"Hello MCP World\");\n\n    \/\/ Import và test handler function trực tiếp\n    const content = readFileSync(tmpPath, \"utf-8\");\n    expect(content).toBe(\"Hello MCP World\");\n\n    \/\/ Cleanup\n    unlinkSync(tmpPath);\n  });\n\n  it(\"trả về lỗi khi file không tồn tại\", () =\u0026gt; {\n    const fakePath = \"\/nonexistent\/path\/file.txt\";\n    const { existsSync } = require(\"fs\");\n    expect(existsSync(fakePath)).toBe(false);\n  });\n});\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch3\u003eIntegration testing với MCP Inspector\u003c\/h3\u003e\n\u003cp\u003eAnthropic cung cấp MCP Inspector — tool để test MCP servers interactively:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003e# Cài MCP Inspector\nnpm install -g @modelcontextprotocol\/inspector\n\n# Chạy inspector với server của bạn\nnpx @modelcontextprotocol\/inspector node dist\/index.js\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003cp\u003eInspector mở giao diện web tại localhost:5173, cho phép:\u003c\/p\u003e\n\u003cul\u003e\n\u003cli\u003eXem danh sách tools, resources, prompts server expose\u003c\/li\u003e\n\u003cli\u003eGọi tool với custom input và xem response\u003c\/li\u003e\n\u003cli\u003eDebug message exchange giữa client và server\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003ch3\u003eTesting với Claude Code trực tiếp\u003c\/h3\u003e\n\u003cp\u003eCách test nhanh nhất là thêm server vào Claude Code và test bằng ngôn ngữ tự nhiên:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003e# Thêm server đang develop (dùng tsx để không cần build)\nclaude mcp add my-server tsx \/path\/to\/server\/src\/index.ts\n\n# Test\nclaude \"Dùng tool read_file để đọc \/etc\/hostname\"\nclaude \"Check xem file \/tmp\/test.txt có tồn tại không\"\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eThêm Prompts vào MCP Server\u003c\/h2\u003e\n\n\u003ch3\u003ePrompts là gì và khi nào dùng\u003c\/h3\u003e\n\u003cp\u003eNgoài Tools và Resources, MCP Server có thể cung cấp Prompts — template workflows được định nghĩa sẵn. User có thể invoke prompt bằng slash commands trong supported clients.\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003eimport {\n  ListPromptsRequestSchema,\n  GetPromptRequestSchema,\n} from \"@modelcontextprotocol\/sdk\/types.js\";\n\nserver.setRequestHandler(ListPromptsRequestSchema, async () =\u0026gt; ({\n  prompts: [\n    {\n      name: \"analyze_file\",\n      description: \"Phân tích một file và đưa ra nhận xét\",\n      arguments: [\n        {\n          name: \"filepath\",\n          description: \"Đường dẫn đến file cần phân tích\",\n          required: true,\n        },\n      ],\n    },\n  ],\n}));\n\nserver.setRequestHandler(GetPromptRequestSchema, async (request) =\u0026gt; {\n  const { name, arguments: args } = request.params;\n\n  if (name === \"analyze_file\") {\n    const filepath = args?.filepath as string;\n    return {\n      description: \"Phân tích file\",\n      messages: [\n        {\n          role: \"user\",\n          content: {\n            type: \"text\",\n            text: `Hãy đọc file ${filepath} và phân tích:\n1. Mục đích của file\n2. Cấu trúc và tổ chức\n3. Điểm mạnh trong code\/content\n4. Điểm cần cải thiện\n5. Đề xuất cụ thể`,\n          },\n        },\n      ],\n    };\n  }\n\n  throw new Error(`Prompt không tồn tại: ${name}`);\n});\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eError Handling tốt trong MCP Server\u003c\/h2\u003e\n\n\u003cp\u003eMột MCP Server production-ready cần handle errors rõ ràng:\u003c\/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eValidation errors:\u003c\/strong\u003e Dùng zod để validate input, throw với message rõ ràng\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eExternal API errors:\u003c\/strong\u003e Catch và wrap với context hữu ích\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eisError flag:\u003c\/strong\u003e Set \u003ccode\u003eisError: true\u003c\/code\u003e trong response khi có lỗi để Claude biết\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eLogging:\u003c\/strong\u003e Log ra \u003ccode\u003estderr\u003c\/code\u003e (không phải stdout) vì stdout dùng cho MCP protocol\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003ch2\u003ePublishing MCP Server\u003c\/h2\u003e\n\n\u003cp\u003eĐể share server với cộng đồng:\u003c\/p\u003e\n\u003col\u003e\n\u003cli\u003ePublish lên npm: \u003ccode\u003enpm publish\u003c\/code\u003e\n\u003c\/li\u003e\n\u003cli\u003eĐặt tên convention: \u003ccode\u003emcp-server-[tên]\u003c\/code\u003e hoặc \u003ccode\u003e@scope\/mcp-server-[tên]\u003c\/code\u003e\n\u003c\/li\u003e\n\u003cli\u003eThêm README với hướng dẫn cài đặt rõ ràng\u003c\/li\u003e\n\u003cli\u003eSubmit lên awesome-mcp-servers repository trên GitHub\u003c\/li\u003e\n\u003c\/ol\u003e\n\n\u003ch2\u003eBest Practices khi build MCP Server\u003c\/h2\u003e\n\n\u003ch3\u003eIdempotency và side effects\u003c\/h3\u003e\n\u003cp\u003eThiết kế tools với tư duy rõ ràng về side effects:\u003c\/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eRead-only tools:\u003c\/strong\u003e Không có side effects, safe to call nhiều lần. Ví dụ: \u003ccode\u003eread_file\u003c\/code\u003e, \u003ccode\u003esearch_database\u003c\/code\u003e, \u003ccode\u003eget_weather\u003c\/code\u003e\n\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eMutating tools:\u003c\/strong\u003e Có side effects, nên có confirmation step hoặc dry-run mode. Ví dụ: \u003ccode\u003ewrite_file\u003c\/code\u003e, \u003ccode\u003esend_email\u003c\/code\u003e, \u003ccode\u003edelete_record\u003c\/code\u003e\n\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003cp\u003eĐặt tên tools phản ánh rõ ràng liệu chúng có destructive hay không:\u003c\/p\u003e\n\u003cpre\u003e\u003ccode\u003e\/\/ Rõ ràng\nread_file       \/\/ Read-only, safe\ncreate_file     \/\/ Creates new file\noverwrite_file  \/\/ Destructive, cần cẩn thận\ndelete_file     \/\/ Destructive, cần confirm\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch3\u003eTool descriptions chất lượng cao\u003c\/h3\u003e\n\u003cp\u003eClaude quyết định khi nào gọi tool dựa trên description. Description tốt dẫn đến usage đúng; description kém dẫn đến wrong tool calls:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003e\/\/ BAD - quá chung chung\n{\n  name: \"process\",\n  description: \"Xử lý data\",\n}\n\n\/\/ GOOD - cụ thể và có context\n{\n  name: \"analyze_csv_file\",\n  description: \"Đọc và phân tích file CSV. Trả về: số rows, column names, sample data (5 rows đầu), và basic statistics (min\/max\/mean cho numeric columns). Dùng khi user muốn hiểu cấu trúc hoặc nội dung của file CSV.\",\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch3\u003eInput validation chặt chẽ\u003c\/h3\u003e\n\u003cp\u003eDùng zod hoặc JSON Schema validation cho mọi input. Đừng trust input từ AI model:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003eimport { z } from \"zod\";\n\nconst ReadFileInput = z.object({\n  path: z\n    .string()\n    .min(1)\n    .refine((p) =\u0026gt; !p.includes(\"..\"), \"Path traversal không được phép\")\n    .refine((p) =\u0026gt; p.startsWith(\"\/allowed\/\"), \"Chỉ đọc trong thư mục được phép\"),\n});\n\n\/\/ Trong handler\ntry {\n  const { path } = ReadFileInput.parse(args);\n  \/\/ safe to proceed\n} catch (error) {\n  if (error instanceof z.ZodError) {\n    return {\n      content: [{ type: \"text\", text: `Input không hợp lệ: ${error.errors.map(e =\u0026gt; e.message).join(\", \")}` }],\n      isError: true,\n    };\n  }\n  throw error;\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch3\u003eRate limiting cho external APIs\u003c\/h3\u003e\n\u003cp\u003eNếu tools của bạn gọi external APIs, implement rate limiting trong server để tránh bị block:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003eimport Bottleneck from \"bottleneck\";\n\n\/\/ Giới hạn 10 requests\/giây cho external API\nconst limiter = new Bottleneck({\n  maxConcurrent: 1,\n  minTime: 100, \/\/ 100ms giữa các requests\n});\n\nasync function callExternalAPI(params: any) {\n  return limiter.schedule(() =\u0026gt; fetch(\"https:\/\/api.example.com\/...\", params));\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eDeploying MCP Server\u003c\/h2\u003e\n\n\u003ch3\u003eLocal development server\u003c\/h3\u003e\n\u003cp\u003eTrong quá trình development, dùng \u003ccode\u003etsx\u003c\/code\u003e để không cần rebuild mỗi lần thay đổi:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003e# Thêm vào Claude Code với tsx (auto-reload khi file thay đổi)\nclaude mcp add my-server tsx \/path\/to\/server\/src\/index.ts\n\n# Sau khi build production\nclaude mcp add my-server node \/path\/to\/server\/dist\/index.js\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch3\u003ePackaging và distribution\u003c\/h3\u003e\n\u003cp\u003eĐể share server với team hoặc cộng đồng:\u003c\/p\u003e\n\n\u003cpre\u003e\u003ccode\u003e\/\/ package.json — setup để chạy trực tiếp qua npx\n{\n  \"name\": \"@yourorg\/mcp-server-myapp\",\n  \"version\": \"1.0.0\",\n  \"bin\": {\n    \"mcp-server-myapp\": \".\/dist\/index.js\"\n  },\n  \"files\": [\"dist\/\"],\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"prepublishOnly\": \"npm run build\"\n  }\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003cp\u003eSau khi publish, users chỉ cần:\u003c\/p\u003e\n\u003cpre\u003e\u003ccode\u003e{\n  \"mcpServers\": {\n    \"myapp\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@yourorg\/mcp-server-myapp\"]\n    }\n  }\n}\u003c\/code\u003e\u003c\/pre\u003e\n\n\u003ch2\u003eKết luận\u003c\/h2\u003e\n\u003cp\u003eXây dựng MCP Server không phức tạp như bạn nghĩ. Với MCP SDK, bạn chỉ cần định nghĩa tools và handlers — SDK lo phần còn lại (protocol, transport, serialization).\u003c\/p\u003e\n\n\u003cp\u003eBắt đầu với server đọc file đơn giản, test với Claude Code, rồi mở rộng dần. Khi đã hiểu pattern cơ bản, việc thêm tools mới chỉ là thêm entry vào \u003ccode\u003etools\u003c\/code\u003e array và thêm case vào handler.\u003c\/p\u003e\n\n\u003cp\u003eCustom MCP Server là cách mạnh nhất để tích hợp Claude vào existing workflow của bạn — biến Claude từ chat tool thành một agent thực sự làm việc với systems của bạn.\u003c\/p\u003e","brand":"Minh Tuấn","offers":[{"title":"Default Title","offer_id":47721068429524,"sku":null,"price":0.0,"currency_code":"VND","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0821\/0264\/9044\/files\/xay-d_ng-mcp-server-d_u-tien-h_ng-d_n-step-by-step.jpg?v=1774504050","url":"https:\/\/claude.vn\/en\/products\/xay-d%e1%bb%b1ng-mcp-server-d%e1%ba%a7u-tien-h%c6%b0%e1%bb%9bng-d%e1%ba%abn-step-by-step","provider":"CLAUDE.VN","version":"1.0","type":"link"}