MCP 协议从零实现:手写最简 MCP Server
本文面向:想理解 MCP 协议内部原理并动手实现一个 MCP Server 的开发者。
预计阅读时间:12 分钟
最终效果:掌握 MCP Server 的创建、Tool 注册、Zod 参数定义、stdio 通信的完整流程。
什么是 MCP
MCP(Model Context Protocol)是 Anthropic 提出的开放协议,让 AI 助手能够连接外部工具和数据源。你可以把它理解为 AI 世界的 USB 接口——定义一套标准,任何工具只要实现这个协议,就能被 Claude、Cursor 等客户端调用。
协议核心只有三个角色:
- Server:提供能力的一方,暴露若干 Tool、Resource、Prompt
- Client:发起调用的一方(Claude Code、Cursor 等)
- Transport:通信层,负责在 Client 和 Server 之间传递 JSON-RPC 消息
本文聚焦最常见的用法:用 TypeScript 写一个 MCP Server,通过 stdio 传输层暴露自定义工具,让 Claude Code 能够调用你写的函数。
安装依赖
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
@modelcontextprotocol/sdk 是官方 TypeScript SDK,内部封装了 JSON-RPC 通信、协议握手、能力协商等细节。zod 用于定义工具参数的类型约束——MCP 协议要求工具参数必须有 JSON Schema,zod 能自动生成。
创建 MCP Server 实例
整个 Server 的初始化只需要三行:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer({
name: 'my-first-mcp',
version: '1.0.0',
});
McpServer 构造函数接收两个参数:name 和 version。这两个值会在协议握手阶段发送给 Client,用于标识你的 Server。
注册 Tool
Tool 是 MCP 最核心的能力。每个 Tool 有四个要素:
| 要素 | 说明 |
|---|---|
| name | 工具名称,Client 通过这个名字调用 |
| description | 自然语言描述,告诉 AI 什么时候该用这个工具 |
| parameters | 参数定义,用 zod schema 描述 |
| handler | 处理函数,收到参数后执行业务逻辑 |
来看一个最简单的例子:
import { z } from 'zod';
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(a + b),
}],
};
},
);
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) => {
const data = await client.recallForTask(input);
return {
content: [{ type: 'text' as const, 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 消息:
const transport = new StdioServerTransport();
await server.connect(transport);
为什么用 stdio?因为 Claude Code 启动 MCP Server 时,会把它当作子进程启动,通过 stdin/stdout 与之通信。这意味着:
- Server 不需要监听端口,没有端口冲突问题
- Client(Claude Code)控制 Server 的生命周期,退出时自动关闭
- 通信内容是 JSON-RPC 2.0 格式,每行一个完整的消息
server.connect(transport) 会完成协议握手:Client 发送 initialize 请求,Server 回复自己的能力(支持哪些 Tool),然后进入正常的消息循环。
在 ChatCrystal 中,整个启动流程被封装为一个函数:
export async function startMcpServer(baseUrl: string) {
const client = new CrystalClient(baseUrl);
const server = new McpServer({
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', /* ... */);
// 启动传输层
const transport = new StdioServerTransport();
await server.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 的 crystal CLI),可以直接用命令名:
{
"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 Server:
npx @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.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({
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 }) => {
const greeting = language === 'zh'
? `你好,${name}!`
: `Hello, ${name}!`;
return {
content: [{ type: 'text' as const, text: greeting }],
};
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
运行:npx tsx mcp-server.ts
在 Claude Code 的 settings.json 中添加配置后,你就可以在对话中说"用 greet 工具跟张三打个招呼",Claude 会自动调用你的 Server。
下一步
到这里你已经掌握了 MCP Server 的核心:创建实例、注册工具、定义参数、启动通信。接下来可以探索:
- Resource:让 Server 暴露数据文件,Client 可以主动读取
- Prompt:预定义的 prompt template,Client 可以列举和选用
- SSE Transport:用 HTTP Server-Sent Events 替代 stdio,适合远程部署
- 多工具协作:参考 ChatCrystal 的 7 个工具如何配合,形成知识检索 + 记忆写入的闭环
MCP 协议规范在 spec.modelcontextprotocol.io 持续更新,SDK 文档见 GitHub 仓库。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)