MCP 协议从零实现:手写最简 MCP Server
本文面向想理解 MCP 协议内部原理并动手实现一个 MCP Server 的开发者。预计阅读时间12 分钟最终效果掌握 MCP Server 的创建、Tool 注册、Zod 参数定义、stdio 通信的完整流程。什么是 MCPMCPModel Context Protocol是 Anthropic 提出的开放协议让 AI 助手能够连接外部工具和数据源。你可以把它理解为 AI 世界的 USB 接口——定义一套标准任何工具只要实现这个协议就能被 Claude、Cursor 等客户端调用。协议核心只有三个角色Server提供能力的一方暴露若干 Tool、Resource、PromptClient发起调用的一方Claude Code、Cursor 等Transport通信层负责在 Client 和 Server 之间传递 JSON-RPC 消息本文聚焦最常见的用法用 TypeScript 写一个 MCP Server通过 stdio 传输层暴露自定义工具让 Claude Code 能够调用你写的函数。安装依赖npminit-ynpminstallmodelcontextprotocol/sdk zodnpminstall-Dtypescript types/nodemodelcontextprotocol/sdk是官方 TypeScript SDK内部封装了 JSON-RPC 通信、协议握手、能力协商等细节。zod用于定义工具参数的类型约束——MCP 协议要求工具参数必须有 JSON Schemazod 能自动生成。创建 MCP Server 实例整个 Server 的初始化只需要三行import{McpServer}frommodelcontextprotocol/sdk/server/mcp.js;import{StdioServerTransport}frommodelcontextprotocol/sdk/server/stdio.js;constservernewMcpServer({name:my-first-mcp,version:1.0.0,});McpServer构造函数接收两个参数name和version。这两个值会在协议握手阶段发送给 Client用于标识你的 Server。注册 ToolTool 是 MCP 最核心的能力。每个 Tool 有四个要素要素说明name工具名称Client 通过这个名字调用description自然语言描述告诉 AI 什么时候该用这个工具parameters参数定义用 zod schema 描述handler处理函数收到参数后执行业务逻辑来看一个最简单的例子import{z}fromzod;server.tool(add,Add two numbers together.,{a:z.number().describe(First number),b:z.number().describe(Second number),},async({a,b}){return{content:[{type:text,text:String(ab),}],};},);server.tool()的签名是server.tool(name, description, parameters, handler)parameters 对象的每个字段都是一个 zod schema。zod 不仅做运行时校验SDK 还会自动将其转换为 JSON Schema 传给 Client。.describe()方法写的描述非常重要——AI 会根据这些描述来理解每个参数的含义。handler 函数必须返回一个对象包含content数组。每个 content 项有type和对应的值{ type: text, text: ... }—— 文本结果{ type: image, data: ..., mimeType: ... }—— 图片base64{ type: resource, ... }—— 资源引用大多数场景用text就够了。用 Zod 定义复杂参数简单参数用z.string()、z.number()即可。实际项目中经常遇到复杂结构zod 都能表达// 可选参数 默认值{query:z.string().describe(Search query),limit:z.number().optional().default(10).describe(Max results),}// 枚举{source:z.enum([claude-code,cursor,codex]).describe(Data source),}// 嵌套对象{task:z.object({goal:z.string(),files:z.array(z.string()).optional(),}),}// 数组{tags:z.array(z.string()).describe(Tag list),}ChatCrystal 的recall_for_task工具展示了复杂参数的实际用法。实际代码直接复用独立定义的RecallForTaskRequestShape来自services/memory/schemas.ts而不是在注册时内联展开import{RecallForTaskRequestShape}from../../services/memory/schemas.js;server.tool(recall_for_task,Recall project-first and global-supplement memories for a task.,RecallForTaskRequestShape,async(input){constdataawaitclient.recallForTask(input);return{content:[{type:textasconst,text:JSON.stringify(data,null,2)}],};},);RecallForTaskRequestShape是一个 zod shape 对象包含mode枚举、task嵌套对象含goal、task_kind、project_key等字段、options可选含project_limit、global_limit等。将 schema 提取到独立文件的好处是MCP 注册和 HTTP route handler 可以共用同一套校验逻辑避免重复定义。参数复杂度没有上限zod 支持.transform()、.superRefine()等高级用法可以在校验阶段就完成数据转换。StdioServerTransport标准输入输出通信MCP 协议支持多种 Transport。最常用的是 stdio——通过标准输入输出传递 JSON-RPC 消息consttransportnewStdioServerTransport();awaitserver.connect(transport);为什么用 stdio因为 Claude Code 启动 MCP Server 时会把它当作子进程启动通过 stdin/stdout 与之通信。这意味着Server 不需要监听端口没有端口冲突问题ClientClaude Code控制 Server 的生命周期退出时自动关闭通信内容是 JSON-RPC 2.0 格式每行一个完整的消息server.connect(transport)会完成协议握手Client 发送initialize请求Server 回复自己的能力支持哪些 Tool然后进入正常的消息循环。在 ChatCrystal 中整个启动流程被封装为一个函数exportasyncfunctionstartMcpServer(baseUrl:string){constclientnewCrystalClient(baseUrl);constservernewMcpServer({name:chatcrystal,version:0.2.0,});// 注册 7 个工具...server.tool(search_knowledge,/* ... */);server.tool(get_note,/* ... */);server.tool(list_notes,/* ... */);server.tool(get_relations,/* ... */);server.tool(recall_for_task,/* ... */);server.tool(validate_task_memory,/* ... */);server.tool(write_task_memory,/* ... */);// 启动传输层consttransportnewStdioServerTransport();awaitserver.connect(transport);}注意 handler 是async函数可以做任何异步操作调 HTTP API、查数据库、读文件。ChatCrystal 的 MCP Server 本身不直接访问数据库而是通过CrystalClient调用已有的 REST API这样 MCP 层只是一个薄薄的适配层。配置 Claude Code 连接Server 写好了下一步是让 Claude Code 能找到它。在项目根目录或全局配置中创建.claude/settings.json{mcpServers:{chatcrystal:{command:npx,args:[tsx,src/mcp-server.ts]}}}如果已经发布为 npm 包如 ChatCrystal 的crystalCLI可以直接用命令名{mcpServers:{chatcrystal:{command:crystal,args:[mcp]}}}配置完成后重启 Claude Code输入/mcp可以看到已连接的 Server 和它暴露的 Tool 列表。在对话中Claude 会根据你的问题自动判断是否需要调用这些工具。调试技巧查看原始 JSON-RPC 消息stdio 通信是双向的Client 通过 stdin 发请求Server 通过 stdout 回响应。调试时可以在 Server 代码中加日志但要注意日志必须写到 stderr写到 stdout 会干扰 JSON-RPC 通信console.error(Received:,JSON.stringify(message));MCP Inspector官方提供了modelcontextprotocol/inspector工具可以可视化调试 MCP Servernpx modelcontextprotocol/inspector npx tsx src/mcp-server.ts它会打开一个 Web 界面列出所有 Tool可以手动输入参数调用查看请求和响应的原始 JSON。常见问题Tool 不出现在 Claude Code 中检查server.connect()是否被调用以及settings.json的路径和 command 是否正确。参数校验失败zod schema 的.describe()不是可选的——AI 依赖这些描述来正确填写参数。没有描述的参数AI 可能传入错误的值。超时handler 函数不要做太重的操作。如果需要长时间运行考虑先返回一个处理中的状态用轮询或其他机制获取结果。从 ChatCrystal 学到的设计模式回看 ChatCrystal 的 MCP 实现有几个值得学习的设计决策1. MCP 层不持有状态。CrystalClient是一个纯 HTTP 客户端所有状态都在 Fastify Server 端管理。MCP Server 进程随时可以重启不会丢失数据。2. 参数 schema 复用。RecallForTaskRequestShape这样的 zod shape 对象被提取到独立文件MCP 注册和 HTTP route handler 共用同一套校验逻辑。3. 返回值统一格式。所有工具都返回{ content: [{ type: text, text: JSON.stringify(data) }] }。虽然 MCP 支持多种 content type但 text 是最通用的——AI 能直接读 JSON 文本。4. 用.describe()替代注释。zod 字段的.describe()是给 AI 看的文档不是给人看的注释。写得越清楚AI 调用的准确率越高。完整最小示例把上面的内容合在一起一个可运行的 MCP Server// mcp-server.tsimport{McpServer}frommodelcontextprotocol/sdk/server/mcp.js;import{StdioServerTransport}frommodelcontextprotocol/sdk/server/stdio.js;import{z}fromzod;constservernewMcpServer({name:hello-mcp,version:1.0.0,});server.tool(greet,Greet someone by name.,{name:z.string().describe(Person name),language:z.enum([zh,en]).optional().default(zh).describe(Language),},async({name,language}){constgreetinglanguagezh?你好${name}:Hello,${name}!;return{content:[{type:textasconst,text:greeting}],};},);consttransportnewStdioServerTransport();awaitserver.connect(transport);运行npx tsx mcp-server.ts在 Claude Code 的 settings.json 中添加配置后你就可以在对话中说用 greet 工具跟张三打个招呼Claude 会自动调用你的 Server。下一步到这里你已经掌握了 MCP Server 的核心创建实例、注册工具、定义参数、启动通信。接下来可以探索Resource让 Server 暴露数据文件Client 可以主动读取Prompt预定义的 prompt templateClient 可以列举和选用SSE Transport用 HTTP Server-Sent Events 替代 stdio适合远程部署多工具协作参考 ChatCrystal 的 7 个工具如何配合形成知识检索 记忆写入的闭环MCP 协议规范在 spec.modelcontextprotocol.io 持续更新SDK 文档见 GitHub 仓库。项目地址github.com/ZengLiangYi/ChatCrystal