先给结论:MCP(Model Context Protocol)的 Server 本质就是一个监听标准输入输出的 JSON-RPC 2.0 程序 ,握手只有三步------initialize → notifications/initialized → tools/list / tools/call。我用 Python 标准库手写了一个最小实现,跑了 7 次往返,把新手最常撞的 3 类报错全部复现了一遍 :-32002(没初始化就调工具)、-32602(参数或工具名不对)、-32601(方法名不存在)。实测在这台机器上,除进程首包 24.8ms 外,之后每次往返只要 0.02~0.15ms。
如果你正在接 AI Agent 的工具调用,或者刚听说 MCP 想搞明白它到底在传什么,这篇可以让你用两个文件、零第三方依赖把它跑通,并知道报错该往哪查。
为什么偏偏是这三个报错
MCP 把"能调工具"拆成了一套有顺序的会话。顺序错了、参数错了、名字错了,都会在 JSON-RPC 层被直接拒绝。协议里最重要的几条:
| 阶段 | 客户端发什么 | 服务端应该怎么应答 |
|---|---|---|
| 1. 握手 | initialize(带 protocolVersion / clientInfo) |
返回支持的 capabilities 和 serverInfo |
| 2. 确认 | notifications/initialized(通知,无 id) |
不返回响应,服务端把状态置为已初始化 |
| 3. 发现 | tools/list |
返回工具数组,每个工具带 inputSchema |
| 4. 调用 | tools/call(name + arguments) |
返回 content 结果,或错误 |
对应的 JSON-RPC 错误码也有明确分工:
| 错误码 | 含义 | 典型触发 |
|---|---|---|
-32700 |
Parse error | 发过去的不是合法 JSON |
-32601 |
Method not found | 方法名拼错,如 foo/bar |
-32602 |
Invalid params | 缺必填参数、类型不对、工具名不存在 |
-32002 |
Server not initialized | 没握手就直接要工具(自定义码,本文实现采用) |
记住这张表,90% 的"连不上/调不通"都能对号入座。
怎么手搓一个最小 MCP Server
说明在先:下面不是官方 SDK ,而是照着 MCP 的 initialize + tools 流程写的最小 JSON-RPC 实现,目的是让你看清协议本身。生产环境仍建议用官方 mcp SDK。
服务端核心逻辑就一个循环:逐行读 JSON,按 method 分发。
python
state = {"initialized": False}
if method == "initialize":
state["initialized"] = False
send({"jsonrpc": "2.0", "id": mid, "result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "mini-mcp", "version": "0.1.0"},
}})
elif method == "notifications/initialized":
state["initialized"] = True
elif method == "tools/list":
if not state["initialized"]:
err(mid, -32002, "Server not initialized")
else:
send({"jsonrpc": "2.0", "id": mid, "result": {"tools": TOOLS}})
工具定义里最关键的是 inputSchema,它决定了客户端能不能"照着 schema 拼参数":
python
TOOLS = [{
"name": "add",
"description": "两数相加,返回和",
"inputSchema": {
"type": "object",
"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
"required": ["a", "b"],
},
}]
调用阶段,服务端必须自己再校验一遍参数,不能迷信客户端:
python
missing = [k for k in ("a", "b") if k not in args]
if missing:
err(mid, -32602, f"Missing required argument(s): {','.join(missing)}")
客户端则是一个简单的子进程管道,发一行、读一行、计时:
python
p = subprocess.Popen([sys.executable, "-X", "utf8", "mini_mcp_server.py"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, encoding="utf-8", bufsize=1)
三类报错:现象、原因、修法(实测)
我把错误场景写进客户端,一次跑出 4 个错误、归并为 3 类:
| # | 现象 | 实测错误码 | 原因 | 修法 |
|---|---|---|---|---|
| 1 | 一上来就 tools/list |
-32002 Server not initialized |
没走 initialize / 没发 notifications/initialized |
先握手,再发通知,然后才请求工具 |
| 2 | tools/call 少传 b |
-32602 Missing required argument(s): b |
参数不符合 inputSchema.required |
按 schema 补全;服务端做必填校验 |
| 2 | 调用不存在的 mul |
-32602 Unknown tool: mul |
工具名写错 | 先 tools/list 拿准确名字 |
| 3 | 调 foo/bar |
-32601 Method not found: foo/bar |
方法名不在协议里 | 对照协议方法表,别自己发明 |
第一类最常见:很多人以为"发了 tools/call 就该有结果",但没握手时服务端连工具清单都没准备好,只能拒绝。第二类多见于自己拼参数时漏字段。第三类则是把方法名当普通函数名随手写。
实测数据
环境:Windows + Python 3.15.2,客户端逐条发请求并计时。
| 步骤 | 耗时(ms) | 结果 |
|---|---|---|
| tools/list before initialize | 24.781 | 错误 -32002 |
| initialize | 0.150 | 成功,返回 mini-mcp 0.1.0 |
| tools/list | 0.134 | 返回 1 个工具 |
| tools/call add(19,23) | 0.064 | 返回 42 |
| tools/call missing b | 0.034 | 错误 -32602 |
| tools/call unknown tool mul | 0.027 | 错误 -32602 |
| unknown method foo/bar | 0.023 | 错误 -32601 |
汇总:7 次往返、捕获 4 个错误(归为 3 类)、最快 0.023ms、最慢 24.781ms、平均 3.602ms。最慢那一次是进程/管道首包,属于一次性开销,不是协议本身慢。
怎么复现
只需要两个文件,零第三方依赖:mini_mcp_server.py 和 mini_mcp_client.py,然后在本机执行:
bash
python -X utf8 mini_mcp_client.py
日志会同时打印到终端并写入 run_log.json,错误码和你看到的表一致。
结论与下一步
- MCP 排障先查三件事:握手顺序、错误码、inputSchema。
- 如果服务端"没反应",先看是不是缺了
notifications/initialized;如果"参数无效",先核对tools/list里的 schema。 - 下一步可以做的:把这三类错误码映射进客户端日志;给
tools/call加超时和重试;再接官方 SDK 做一次对照,看自定义实现和标准实现的行为差异。
先把最小协议跑通,再去接真实工具,比一上来就啃 SDK 快得多。