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/sdk
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new Server(
{
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.arguments
const { name, arguments: args } = request.params;
if (name === 'search_files') {
const { pattern, directory = '.' } = args;
// 实际搜索逻辑,这里简化
const results = await searchFiles(pattern, directory);
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2)
}
]
};
}
throw new Error(`Unknown tool: ${name}`);
});
// 启动服务
const transport = new StdioServerTransport();
await server.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 打,否则会污染协议消息
const fs = require('fs'); const log = fs.createWriteStream('/tmp/mcp-debug.log', { flags: 'a' }); log.write(`[${new Date().toISOString()}] 收到请求: ${JSON.stringify(request)}\n`); -
手动模拟请求:用 echo 命令模拟 Claude Code 的请求
echo '{"jsonrpc":"2.0","method":"tools/list","id":"test-001"}' | node mcp-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 网关的集成方案。

1万+

被折叠的 条评论
为什么被折叠?



