上篇 MCP 协议详解 讲了 MCP 是什么、Client/Server 架构、以及它为什么是「AI 工具的 USB 接口」。那篇偏理论,这篇我们撸起袖子写代码——从零构建一个能跑、能用的 MCP Server。
最终效果:Claude Code 通过 MCP 协议调用你写的 Server,读取本地数据、执行自定义工具。
前置准备
开始前确保环境就绪:
- Node.js 18+(MCP SDK 要求 ESM)
- Claude Code 或支持 MCP 的客户端(用于最终测试)
- 一个空目录,比如
my-first-mcp-server
1 2
| mkdir my-first-mcp-server && cd my-first-mcp-server npm init -y
|
修改 package.json,加上 "type": "module"(MCP SDK 纯 ESM):
1 2 3 4 5 6 7 8 9
| { "name": "my-first-mcp-server", "version": "1.0.0", "type": "module", "scripts": { "dev": "tsx src/index.ts", "build": "tsc" } }
|
安装依赖:
1 2
| npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node tsx
|
项目结构
1 2 3 4 5 6 7 8
| my-first-mcp-server/ ├── package.json ├── tsconfig.json └── src/ ├── index.ts # 入口,创建 Server 并启动 └── tools/ ├── file-tools.ts # 文件操作工具 └── weather.ts # 天气查询工具(模拟)
|
Hello World:最简 MCP Server
先从最小的可运行版本开始。不用管工具和资源,先让 Server 跑起来:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({ name: "my-first-mcp-server", version: "1.0.0", });
const transport = new StdioServerTransport(); await server.connect(transport);
console.error("MCP Server is running on stdio");
|
StdioServerTransport 通过标准输入输出通信——这是 MCP 最常用的传输方式,不需要网络端口,完全在本地机器上运行。
用 console.error 而不是 console.log 是因为 stdout 是 MCP 协议的通信通道,普通日志必须走 stderr。
运行测试——Server 启动后等待 JSON-RPC 消息,你手动输入不会被 echo 回来,这是正常的:
Ctrl+C 退出。能跑就行。
Tool 是 MCP Server 的核心——它暴露函数给 AI 调用。我们来写一个计算两个日期之间相差天数的工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { z } from "zod";
server.tool( "date-diff", "计算两个日期之间相差的天数", { start: z.string().describe("起始日期,格式 YYYY-MM-DD"), end: z.string().describe("结束日期,格式 YYYY-MM-DD"), }, async ({ start, end }) => { const startDate = new Date(start); const endDate = new Date(end); const diffMs = endDate.getTime() - startDate.getTime(); const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
return { content: [ { type: "text", text: `${start} 到 ${end} 相差 ${diffDays} 天`, }, ], }; } );
|
关键点:
- 第一个参数是 tool name,AI 用它来识别和调用
- 第二个参数是 description,AI 读这个来决定何时调用
zod schema 定义了参数类型,MCP SDK 用它做参数校验- 返回值必须是
{ content: [{ type: "text", text: "..." }] } 格式
Description 写得好不好直接影响 AI 调用准确率。写清楚它做什么、输入什么、输出什么。
添加文件读取工具
更实用的例子——让 AI 能读取本地文件目录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import { z } from "zod"; import { readdir, readFile } from "node:fs/promises"; import { join } from "node:path";
export function registerFileTools(server: import("@modelcontextprotocol/sdk/server/mcp.js").McpServer) { server.tool( "list-files", "列出指定目录下的所有文件", { path: z.string().describe("目录路径(绝对路径或相对于当前目录的路径)"), }, async ({ path }) => { const files = await readdir(path); const fileList = files.map((f, i) => `${i + 1}. ${f}`).join("\n");
return { content: [ { type: "text", text: fileList || "目录为空", }, ], }; } );
server.tool( "read-file", "读取指定文件的内容", { path: z.string().describe("文件路径"), }, async ({ path }) => { const content = await readFile(path, "utf-8"); const preview = content.slice(0, 5000);
return { content: [ { type: "text", text: preview + (content.length > 5000 ? "\n...(内容过长,已截断)" : ""), }, ], }; } ); }
|
在 index.ts 中注册:
1 2 3
| import { registerFileTools } from "./tools/file-tools.js";
registerFileTools(server);
|
写完后让 AI 能浏览文件系统——但注意安全:生产环境一定要限制可访问路径,否则相当于给 AI 开了文件系统的后门。
添加外部 API 工具
模拟一个天气查询——实际项目中替换成真实 API 调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { z } from "zod";
export function registerWeatherTool(server: any) { server.tool( "get-weather", "查询指定城市的天气信息", { city: z.string().describe("城市名称,如 Beijing、Shanghai"), }, async ({ city }: { city: string }) => { const weatherData: Record<string, string> = { beijing: "北京:晴,25°C,湿度 40%,风力 2 级", shanghai: "上海:多云,28°C,湿度 65%,风力 3 级", };
const key = city.toLowerCase(); const result = weatherData[key] || `${city}:暂无天气数据`;
return { content: [{ type: "text", text: result }], }; } ); }
|
真实项目中,把模拟数据替换成高德天气 API 或 OpenWeatherMap。关键不变——Tool 定义、参数校验、返回格式。
暴露 Resource
Tool 是「让 AI 做事」,Resource 是「让 AI 看数据」。我们来暴露一个 Resource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| server.resource( "project-info", "project://info", { description: "项目基本信息,包括名称、版本、描述", }, async () => { return { contents: [ { uri: "project://info", mimeType: "application/json", text: JSON.stringify( { name: "my-first-mcp-server", version: "1.0.0", description: "一个 MCP Server 开发示例", tools: ["date-diff", "list-files", "read-file", "get-weather"], resources: ["project://info"], }, null, 2 ), }, ], }; } );
|
Resource 用 URI 标识,AI 用 resources/read 方法获取。对于静态配置、文档、schema 等场景非常合适。
本地测试
写完了,怎么测?两步。
第一步:用 MCP Inspector
1
| npx @modelcontextprotocol/inspector tsx src/index.ts
|
Inspector 打开浏览器页面,你可以:
- 查看 Server 暴露了哪些 Tools 和 Resources
- 手动调用 Tool 看返回结果
- 检查 JSON-RPC 通信日志
第二步:接入 Claude Code
在 Claude Code 的 mcp.json 中注册(位置:项目根目录或 ~/.claude/mcp.json):
1 2 3 4 5 6 7 8
| { "mcpServers": { "my-tools": { "command": "npx", "args": ["tsx", "/absolute/path/to/my-first-mcp-server/src/index.ts"] } } }
|
重启 Claude Code(如果你用 IntelliJ IDEA,CC GUI 插件 可以让你在 IDE 里直接操作 Claude Code,不用切终端),问一句「你能用 my-tools 帮我做什么」:
1 2 3 4 5
| 我能使用以下工具: 1. date-diff - 计算两个日期之间相差的天数 2. list-files - 列出指定目录下的所有文件 3. read-file - 读取指定文件的内容 4. get-weather - 查询指定城市的天气信息
|
然后直接对话调用:帮我算一下 2026-01-01 到 2026-05-22 有几天
文件读取工具要限制路径范围。生产环境中对 path 参数做白名单校验,防止 AI 被 prompt injection 利用去读取 /etc/passwd 或 .env。
完整代码
把所有代码拼在一起,完整可运行的 src/index.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { registerFileTools } from "./tools/file-tools.js"; import { registerWeatherTool } from "./tools/weather.js";
const server = new McpServer({ name: "my-first-mcp-server", version: "1.0.0", });
server.tool( "date-diff", "计算两个日期之间相差的天数", { start: z.string().describe("起始日期,格式 YYYY-MM-DD"), end: z.string().describe("结束日期,格式 YYYY-MM-DD"), }, async ({ start, end }) => { const diffMs = new Date(end).getTime() - new Date(start).getTime(); const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)); return { content: [{ type: "text", text: `${start} 到 ${end} 相差 ${diffDays} 天` }], }; } );
server.resource( "project-info", "project://info", { description: "项目基本信息" }, async () => ({ contents: [{ uri: "project://info", mimeType: "application/json", text: JSON.stringify({ name: "my-first-mcp-server", version: "1.0.0", tools: ["date-diff", "list-files", "read-file", "get-weather"] }, null, 2), }], }) );
registerFileTools(server); registerWeatherTool(server);
const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Server running on stdio");
|
进阶方向
这个 Demo 完成了,但 MCP 能做的远不止这些:
- 多 Transport:除了 stdio,MCP 还支持 HTTP/SSE,适合远程 Server 场景
- Prompts 模板:Server 可以暴露预定义的 Prompt 模板,让 AI 通过
prompts/get 获取 - Streamable HTTP:MCP 2025 规范新增,支持真正的流式响应
- 认证机制:OAuth 2.0 集成,保护敏感工具的访问权限
总结
MCP Server 开发的核心流程:
- 用
McpServer 创建实例 - 用
server.tool() 定义工具(name + description + zod schema + handler) - 用
server.resource() 暴露数据 - 用
StdioServerTransport 通过 stdin/stdout 通信 - 在客户端 mcp.json 中注册,AI 就能自动发现和调用
MCP 把 AI 的能力边界从「内置工具」扩展到了「无限自定义工具」。你写的每一行代码都可以变成 AI 的操作能力——这才是 MCP 的真正价值。
从这个角度看,MCP Server 本质上就是 AI Agent 的「技能系统」。如果你对 Agent 开发的完整流程感兴趣,AI Agent 开发完整指南 涵盖了从工具定义到上线的全部环节。