027、MCP 协议入门:架构设计与第一个 MCP Server
上周五凌晨两点,我盯着终端里一行诡异的报错发呆:
Error: Tool execution failed: Cannot read properties of undefined (reading 'schema')Claude Code 调用我写的自定义工具时,schema 字段丢了。排查半天,发现是 MCP 协议里 tool 定义少了个inputSchema的嵌套层级。这种低级错误,放在白天可能一眼就看出,但凌晨的代码审查总是容易漏掉细节。MCP 协议看着简单,真写起来坑不少。
为什么需要 MCP
先说说背景。Claude Code 的能力扩展靠的是工具(Tool),但早期每个工具都得自己写 HTTP 接口、自己处理认证、自己定义参数格式。不同工具之间没有统一规范,Claude Code 调用时得针对每个工具写不同的适配代码。
MCP(Model Context Protocol)就是来解决这个问题的。它定义了一套标准协议,让 Claude Code 和外部工具之间能通过统一的格式通信。你可以把它理解成 AI 世界的“USB 接口”——不管背后是什么设备,插上就能用。
MCP 的核心架构分三层:
- Transport Layer:负责数据传输,支持 stdio(本地进程通信)和 SSE(Server-Sent Events,远程通信)
- Protocol Layer:定义消息格式、请求/响应模式、错误处理
- Application Layer:具体的能力定义,比如 tool、resource、prompt
Claude Code 用的是 stdio 模式,启动一个子进程,通过标准输入输出交换 JSON 消息。远程场景用 SSE,但开发阶段用 stdio 调试更方便。
协议消息格式
MCP 的消息格式借鉴了 JSON-RPC 2.0,但做了扩展。每条消息必须包含jsonrpc、method、id三个字段:
{"jsonrpc":"2.0","method":"tools/call","id":"req-001","params":{"name":"get_weather","arguments":{"city":"北京"}}}响应格式:
{"jsonrpc":"2.0","id":"req-001","result":{"content":[{"type":"text","text":"北京当前温度:22°C,晴"}]}}注意content是个数组,可以返回多个内容块,支持 text、image、resource 等类型。这里踩过坑:如果只返回一个字符串,Claude Code 会报解析错误,必须包在数组里。
第一个 MCP Server
直接上代码。我用 Node.js 写一个最简单的 MCP Server,实现一个文件搜索工具。
// mcp-server.js// 别这样写:用 express 起 HTTP 服务,MCP 协议不认// 正确姿势:用 @modelcontextprotocol/sdkimport{Server}from'@modelcontextprotocol/sdk/server/index.js';import{StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';constserver=newServer({name:'file-search-server',version:'1.0.0'},{capabilities:{tools:{}// 声明支持工具能力}});// 定义工具列表server.setRequestHandler('tools/list',async()=>{return{tools:[{name:'search_files',description:'在指定目录搜索文件,支持通配符',inputSchema:{type:'object',properties:{pattern:{type:'string',description:'搜索模式,如 *.js 或 **/*.ts'},directory:{type:'string',description:'搜索目录,默认当前目录'}},required:['pattern']}}]};});// 处理工具调用server.setRequestHandler('tools/call',async(request)=>{// 这里踩过坑:request.params 才是参数,不是 request.argumentsconst{name,arguments:args}=request.params;if(name==='search_files'){const{pattern,directory='.'}=args;// 实际搜索逻辑,这里简化constresults=awaitsearchFiles(pattern,directory);return{content:[{type:'text',text:JSON.stringify(results,null,2)}]};}thrownewError(`Unknown tool:${name}`);});// 启动服务consttransport=newStdioServerTransport();awaitserver.connect(transport);关键点:
- capabilities 必须声明:不声明 tools 能力,Claude Code 不会调用你的工具
- inputSchema 嵌套层级:
inputSchema是 tool 对象的属性,不是直接放在 tool 里。我凌晨踩的坑就是这个 - 请求处理区分:
tools/list返回工具列表,tools/call执行具体调用
在 Claude Code 中配置
写好的 MCP Server 需要在 Claude Code 的配置文件中注册。配置文件位置:
- macOS/Linux:
~/.claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{"mcpServers":{"file-search":{"command":"node","args":["/path/to/mcp-server.js"],"env":{"NODE_ENV":"production"}}}}配置完重启 Claude Code,在对话中输入“搜索当前目录下所有 .md 文件”,Claude Code 会自动调用你的工具。
调试技巧
MCP Server 的调试比普通服务麻烦,因为走的是 stdio,没有 HTTP 请求可以抓包。我的调试三板斧:
日志重定向:把日志写到文件,不要往 stdout 打,否则会污染协议消息
constfs=require('fs');constlog=fs.createWriteStream('/tmp/mcp-debug.log',{flags:'a'});log.write(`[${newDate().toISOString()}] 收到请求:${JSON.stringify(request)}\n`);手动模拟请求:用 echo 命令模拟 Claude Code 的请求
echo'{"jsonrpc":"2.0","method":"tools/list","id":"test-001"}'|nodemcp-server.js检查返回格式:Claude Code 对返回格式要求严格,少个字段就报错。写个测试脚本验证所有工具调用的返回格式
常见坑点
- 超时问题:MCP 协议默认超时 60 秒,长时间运行的任务要拆成异步 + 进度通知
- 错误处理:工具执行出错要返回 error 对象,不是直接抛异常
- 参数校验:Claude Code 传的参数可能不符合 schema,服务端要做防御性校验
- 并发调用:Claude Code 可能同时调用多个工具,服务端要做好并发控制
个人经验
MCP 协议的设计思路很清晰——把 AI 和工具的交互标准化。但实际开发中,协议细节的坑不少。我的建议是:
先写一个最简单的工具(比如返回当前时间),跑通整个链路,再逐步加复杂逻辑。调试阶段用 stdio 模式,日志一定要写到文件。schema 定义要严格,Claude Code 的 LLM 有时候会脑补参数,服务端校验能拦住大部分问题。
另外,别想着一次把所有工具都写完。MCP 支持动态注册工具,可以先暴露两三个核心功能,跑起来再迭代。协议版本目前还在快速演进,保持 SDK 更新,关注 breaking changes。
下篇会讲 MCP 的进阶用法:如何实现流式响应、工具链编排、以及和现有 API 网关的集成方案。