MCP 最小 Server 实测:3 个 JSON-RPC 报错与修法

10 阅读5分钟

先给结论: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 错误码也有明确分工:

错误码含义典型触发
-32700Parse error发过去的不是合法 JSON
-32601Method not found方法名拼错,如 foo/bar
-32602Invalid params缺必填参数、类型不对、工具名不存在
-32002Server not initialized没握手就直接要工具(自定义码,本文实现采用)

记住这张表,90% 的"连不上/调不通"都能对号入座。

怎么手搓一个最小 MCP Server

说明在先:下面不是官方 SDK,而是照着 MCP 的 initialize + tools 流程写的最小 JSON-RPC 实现,目的是让你看清协议本身。生产环境仍建议用官方 mcp SDK。

服务端核心逻辑就一个循环:逐行读 JSON,按 method 分发。

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 拼参数":

TOOLS = [{
    "name": "add",
    "description": "两数相加,返回和",
    "inputSchema": {
        "type": "object",
        "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
        "required": ["a", "b"],
    },
}]

调用阶段,服务端必须自己再校验一遍参数,不能迷信客户端:

missing = [k for k in ("a", "b") if k not in args]
if missing:
    err(mid, -32602, f"Missing required argument(s): {','.join(missing)}")

客户端则是一个简单的子进程管道,发一行、读一行、计时:

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先握手,再发通知,然后才请求工具
2tools/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 initialize24.781错误 -32002
initialize0.150成功,返回 mini-mcp 0.1.0
tools/list0.134返回 1 个工具
tools/call add(19,23)0.064返回 42
tools/call missing b0.034错误 -32602
tools/call unknown tool mul0.027错误 -32602
unknown method foo/bar0.023错误 -32601

汇总:7 次往返、捕获 4 个错误(归为 3 类)、最快 0.023ms、最慢 24.781ms、平均 3.602ms。最慢那一次是进程/管道首包,属于一次性开销,不是协议本身慢。

怎么复现

只需要两个文件,零第三方依赖:mini_mcp_server.py 和 mini_mcp_client.py,然后在本机执行:

python -X utf8 mini_mcp_client.py

日志会同时打印到终端并写入 run_log.json,错误码和你看到的表一致。

结论与下一步

  • MCP 排障先查三件事:握手顺序、错误码、inputSchema。
  • 如果服务端"没反应",先看是不是缺了 notifications/initialized;如果"参数无效",先核对 tools/list 里的 schema。
  • 下一步可以做的:把这三类错误码映射进客户端日志;给 tools/call 加超时和重试;再接官方 SDK 做一次对照,看自定义实现和标准实现的行为差异。

先把最小协议跑通,再去接真实工具,比一上来就啃 SDK 快得多。