Tóm tắt: Cài @modelcontextprotocol/sdk và zod. Tạo new McpServer({ name, version }), gọi server.registerTool(name, { description, inputSchema: { field: z... } }, handler), sau đó server.connect(new StdioServerTransport()). Handler trả về { content: [{ type: "text", text }] }, và thêm isError: true khi lỗi. Bản Python tương đương xem FastMCP.
Ví dụ dưới đây đã chạy thử với @modelcontextprotocol/sdk 1.29.0, zod 3.25 và Node 22. Ở bản này server.tool(...) đã bị đánh dấu deprecated, nên dùng registerTool. SDK đang được phát triển nhanh, có thể có major version mới đổi tên package hoặc import path. Nâng cấp thì đọc README/changelog trước.
Luồng hoạt động
flowchart LR A["src/index.ts"] -->|"tsc"| B["build/index.js"] B --> C["MCP Inspector"] C -->|OK| D["claude mcp add"] D --> E[Claude Code] E -->|"stdio JSON-RPC"| B B -->|"ping / inventory"| F[(Router)]
Cấu hình
Tạo project
mkdir netops-mcp-ts && cd netops-mcp-ts
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npm pkg set type=module
npm pkg set scripts.build="tsc"{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "build",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}Server
import { execFile } from "node:child_process";
import { isIP } from "node:net";
import { promisify } from "node:util";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const run = promisify(execFile);
const DEVICES: Record<string, { mgmt: string; role: string; os: string }> = {
"pe1-hni": { mgmt: "198.51.100.11", role: "pe", os: "iosxr" },
"pe2-hcm": { mgmt: "198.51.100.12", role: "pe", os: "junos" },
};
const server = new McpServer({ name: "netops-ts", version: "0.1.0" });
server.registerTool(
"get_device",
{
title: "Tra inventory",
description: "Tra thiết bị theo tên, trả về IP quản trị, role, OS. Chỉ đọc.",
inputSchema: { name: z.string().describe("Tên trong inventory, vd pe1-hni") },
annotations: { readOnlyHint: true },
},
async ({ name }) => {
const dev = DEVICES[name];
if (!dev) {
const valid = Object.keys(DEVICES).join(", ");
return { content: [{ type: "text", text: `Không có ${name}. Hợp lệ: ${valid}` }], isError: true };
}
return { content: [{ type: "text", text: JSON.stringify(dev) }] };
},
);
server.registerTool(
"ping",
{
title: "Ping",
description: "Ping một IPv4/IPv6 từ máy chạy server. Chỉ nhận IP, không nhận hostname.",
inputSchema: {
target: z.string().refine((s) => isIP(s) !== 0, "target phải là địa chỉ IP"),
count: z.number().int().min(1).max(10).default(3),
},
annotations: { readOnlyHint: true, openWorldHint: true },
},
async ({ target, count }) => {
try {
// execFile không qua shell, không có nguy cơ chèn lệnh
const { stdout } = await run("ping", ["-c", String(count), "-W", "1", target], { timeout: 30_000 });
return { content: [{ type: "text", text: stdout.slice(-1500) }] };
} catch (err: any) {
// ping trả exit code khác 0 khi mất gói: vẫn trả output cho model đọc
return { content: [{ type: "text", text: String(err.stdout || err.message).slice(-1500) }], isError: true };
}
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("netops-ts MCP server đang chạy trên stdio"); // stderr, KHÔNG dùng console.logNhững điểm cần nhớ:
inputSchemalà object gồm các field zod (raw shape). SDK tự sinh JSON Schema chotools/listvà validate input trước khi gọi handler. Input sai thì client nhận lỗiInput validation errorkèm message củarefine..describe()trên từng field thànhdescriptiontrong schema. Model dựa vào đó để điền tham số, nên viết rõ (xem thiết kế tool).annotations.readOnlyHintchỉ là gợi ý cho client, không thay được việc kiểm tra quyền trong code.- Import phải có đuôi
.js(.../server/mcp.js) vì project dùng ESM vớimoduleResolution: Node16.
Build và test
npm run build
npx @modelcontextprotocol/inspector node build/index.js
# test nhanh không cần UI: gửi initialize + tools/list qua stdin
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | node build/index.jsGắn vào Claude Code
claude mcp add netops-ts -- node /opt/netops-mcp-ts/build/index.js
claude mcp add netops-ts -s project -- node build/index.js # ghi vào .mcp.json cho cả team
claude mcp listNếu cần chạy server qua HTTP, SDK có StreamableHTTPServerTransport (@modelcontextprotocol/sdk/server/streamableHttp.js) để gắn vào Express hoặc node:http. Trước khi mở server ra mạng, đọc transport và xác thực.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
Cannot find module '@modelcontextprotocol/sdk/server/mcp' |
Import thiếu đuôi .js |
Dùng .../server/mcp.js, .../server/stdio.js |
SyntaxError: Cannot use import statement outside a module |
package.json chưa có "type": "module" |
npm pkg set type=module |
tsc báo lỗi top-level await |
module trong tsconfig quá cũ (CommonJS) |
module/moduleResolution = Node16 (hoặc NodeNext), target từ ES2022 |
| Claude Code báo server failed / parse error | Dùng console.log nên làm bẩn stdout |
Mọi log dùng console.error |
Lỗi type ở inputSchema |
Truyền z.object({...}) ở chỗ SDK cũ chỉ nhận raw shape, hoặc lệch major version zod |
Truyền { field: z.string() }, kiểm tra peer dependency zod của SDK |
Tool không xuất hiện trong /mcp |
Đường dẫn build/index.js tương đối nhưng Claude Code chạy ở cwd khác |
Dùng đường dẫn tuyệt đối, chạy claude mcp get netops-ts để kiểm tra |
| Model gọi tool liên tục rồi fail | Handler throw exception | Bắt lỗi, trả isError: true kèm gợi ý sửa |