Lab 5:构建 MCP 服务器(stdio)并在 Agent 中使用
系列:Claude Code 完全指南 V2 · 第 19 篇实战 Lab
前置:完成 Lab 2;理解工具在 API 中的声明方式。
学习目标
- 使用
@modelcontextprotocol/sdk搭建一个最小 MCP Server,通过 stdio 与客户端通信。 - 实现一个演示工具
get_weather(可返回模拟 JSON,不必调用真实气象 API)。 - 在 Node Agent 中使用
@anthropic-ai/sdk之外的 MCP 客户端(或用child_process启动 server 并用 MCP 客户端库连接)。本 Lab 采用:独立mcp-server子包 + Agent 内@modelcontextprotocol/sdkClient + StdioClientTransport。 - 将 MCP 暴露的工具动态合并进
messages.create的tools列表,并把执行路由到 MCPcallTool。
架构总览
步骤 1:工作区布局
lab5-mcp/
├── package.json
├── tsconfig.json
├── src/
│ ├── agent/
│ │ └── main.ts
│ └── mcp-server/
│ └── index.ts根 package.json:
json
{
"name": "lab5-mcp",
"type": "module",
"scripts": {
"mcp": "tsx src/mcp-server/index.ts",
"agent": "tsx src/agent/main.ts"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}步骤 2:MCP Server(stdio)
当前 npm 上的 @modelcontextprotocol/sdk(v1.x) 推荐使用高层 McpServer + registerTool,并需安装 peer 依赖 zod(可与 SDK 一样使用 zod/v4 或 zod/v3,见 SDK README)。
src/mcp-server/index.ts:
typescript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as z from "zod/v4";
const server = new McpServer({ name: "weather-demo", version: "0.1.0" });
server.registerTool(
"get_weather",
{
description: "Get fake weather for a city (demo only).",
inputSchema: {
city: z.string().describe("City name"),
},
},
async ({ city }) => {
const payload = {
city,
unit: "C",
condition: "sunny",
temperature: 22,
note: "This is simulated data for Lab 5.",
};
return {
content: [{ type: "text", text: JSON.stringify(payload) }],
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP weather server (stdio) ready.");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});注意:MCP server 进程应只通过 stdout 写协议数据;调试请用
console.error打到 stderr,避免破坏 JSON-RPC 流。
若你使用的是已拆分的 v2 预览包(@modelcontextprotocol/server),请将 import 改为该包的 server/mcp 与 server/stdio 路径,逻辑保持一致即可。
步骤 3:Agent 连接 MCP 并桥接到 Claude
src/agent/main.ts(教学版:启动时 spawn 子进程运行同一套 tsx mcp-server,再用 StdioClientTransport 连接;为缩短篇幅,内置工具可省略,仅演示 MCP):
typescript
import { spawn } from "node:child_process";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const MODEL = "claude-sonnet-4-20250514";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = path.resolve(__dirname, "../..");
async function startMcpClient(): Promise<{
client: Client;
transport: StdioClientTransport;
}> {
const serverPath = path.join(root, "src/mcp-server/index.ts");
const transport = new StdioClientTransport({
command: "npx",
args: ["tsx", serverPath],
cwd: root,
});
const client = new Client({ name: "lab5-agent", version: "0.1.0" });
await client.connect(transport);
return { client, transport };
}
function mcpToolToAnthropic(t: {
name: string;
description?: string;
inputSchema?: unknown;
}) {
return {
name: t.name,
description: t.description ?? "",
input_schema: t.inputSchema as Record<string, unknown>,
};
}
async function main() {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) throw new Error("ANTHROPIC_API_KEY required");
const { client: mcp, transport } = await startMcpClient();
const listed = await mcp.listTools();
const anthropicTools = listed.tools.map(mcpToolToAnthropic);
const anthropic = new Anthropic({ apiKey });
const userPrompt =
"请用 get_weather 查询 Beijing 的天气,并用一句话中文总结。";
let messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: userPrompt },
];
while (true) {
const msg = await anthropic.messages.create({
model: MODEL,
max_tokens: 1024,
tools: anthropicTools,
messages: messages,
});
messages.push({ role: "assistant", content: msg.content });
const toolUses = msg.content.filter((b) => b.type === "tool_use");
if (toolUses.length === 0) {
const text = msg.content
.filter((b) => b.type === "text")
.map((b) => b.text)
.join("\n");
console.log("助手:", text);
break;
}
const results: Anthropic.Messages.ToolResultBlockParam[] = [];
for (const u of toolUses) {
const res = await mcp.callTool({
name: u.name,
arguments: (u.input ?? {}) as Record<string, unknown>,
});
const text = res.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n");
results.push({
type: "tool_result",
tool_use_id: u.id,
content: text,
});
}
messages.push({ role: "user", content: results });
}
await mcp.close();
await transport.close();
}
main().catch((e) => {
console.error(e);
process.exit(1);
});运行方式
终端 1(可选手动调试 server):
bash
npm run mcp运行 Agent(会自动拉起子进程 server):
bash
export ANTHROPIC_API_KEY=...
npm run agentMCP 与 Anthropic tools 字段映射
常见问题
npx tsx找不到:在lab5-mcp根目录先npm install。- Windows 路径:
StdioClientTransport的command可改为node+ 预编译dist。 - 与 Claude Code 对比:真实 Claude Code 通过内置 MCP 配置管理多 server;本 Lab 为单 server 硬编码演示。
扩展练习
- 把 Lab 2 的
read_file与 MCPget_weather合并到同一tools数组,路由器根据name前缀或注册表分流。 - 增加
resources/prompts能力(SDK 支持),体验完整 MCP。
下一 Lab
Lab 6:多 Agent 协调 将实现主从 Agent:子 Agent 只读探索,主 Agent 决策。