菜单

上线之前,怎么测试一个 MCP server

2026-10-10MCP Checkup

你的 MCP server 在编辑器里跑得好好的。这个结果是真的,但范围很窄:编辑器跑在你自己的机器上,只说一种协议版本,而且很宽容。一个从公网连进来的 agent,这几点都不具备。

这篇文章按外部客户端遇到你的方式来测:一层一层往上,从 TLS 证书一直到工具描述里的每个字。每一步都有一条可以直接粘贴的 curl 命令,把 https://your-server.example.com/mcp 换成你的地址就行。

开始之前:你的 server 说的是哪一代协议?

MCP 的形态变过。到 2025-11-25 为止,客户端要先用 initialize 握手建立会话。现在的版本 2026-07-28 去掉了握手和会话:每个请求都在 _meta 里自带协议版本和客户端能力(客户端信息建议带,但不是必需),Streamable HTTP 传输还会把其中一部分复制到请求头里。

两代协议现在都在用。2026-10-09,我们目录里一共 1,902 台服务器,其中能识别出协议版本的有 1,806 台:

协议版本 服务器数
2025-06-18 1,185
2026-07-28 513
2024-11-05 39
2025-03-26 38
2025-11-25 31

所以你实现的是哪一代就测哪一代;两代都实现了,就两代都测。下面各步在两代有差别的地方会分开写。

第 1 层:客户端到底连不连得上?

curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' -v https://your-server.example.com/mcp 2>&1 | grep -E '^\* (SSL|Server certificate|subject|expire|issuer)|^[0-9]{3} '

看三件事:

  • 证书:没过期;证书里的名字(SAN)覆盖你的主机名;证书链完整。缺一张中间证书时,桌面浏览器往往能从缓存里补上,看起来没事,但一个全新的客户端会直接失败。
  • 重定向:如果 /mcp 会跳转,看它跳到哪里。一旦跳到别的主机,客户端就可能丢掉请求头,包括 Authorization;很多客户端干脆不跟。
  • 耗时:记下总耗时。agent 是在对话中间调用工具的,首字节慢,屏幕前等着的人能感觉到。

GET 返回 405 Method Not Allowed 在这里是正常的:2026-07-28 只要求端点接受 POST;规范还说,只支持这一版的服务器应该对 GET 回 405。

第 2 层:握手能不能走通?

每个 POST 都必须带 Accept 头,而且同时列出 application/json 和 text/event-stream。只列了其中一个的话,很多服务器框架会直接回 406 Not Acceptable。一个 server 在这个客户端里好好的、换个客户端就连不上,这是常见原因。

**当前版本(2026-07-28):server/discover。**服务器必须实现它,一次就能返回支持的版本、能力和身份:

curl -sS -i https://your-server.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"0.1"},
        "io.modelcontextprotocol/clientCapabilities":{}}}}'

应该得到 200,里面有 supportedVersions、capabilities,最好还有 _meta 里的 serverInfo。有两种失败要认得:

  • 400,错误码 -32020(HeaderMismatch):请求头和请求体对不上,或者缺了必需的头。MCP-Protocol-Version 和 Mcp-Method 每个请求都要带。
  • 400,错误码 -32022(UnsupportedProtocolVersion):响应体会列出服务器真正支持的版本。只要列表是真的,这就是正确行为,不是 bug。

更早的版本:initialize。

curl -sS -i https://your-server.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18","capabilities":{},
        "clientInfo":{"name":"curl-test","version":"0.1"}}}'

返回结果里会写明服务器同意的 protocolVersion。如果响应带了 Mcp-Session-Id 头,之后每个请求都要把它带回去;从 2025-06-18 起,还要带上 MCP-Protocol-Version。然后告诉服务器你准备好了:

curl -sS -i https://your-server.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-06-18' \
  -H "Mcp-Session-Id: $SESSION" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

这条通知应该得到 202 Accepted,没有响应体。

**怎么读响应。**服务器可以回普通 JSON,也可以回一段很短的 Server-Sent Events 流。两种都合规,客户端必须都能处理。看到 Content-Type: text/event-stream 时,JSON 在 data: 开头的那几行里:

... | sed -n 's/^data: //p'

第 3 层:工具清单是不是 agent 需要的样子?

# 2026-07-28
curl -sS https://your-server.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/list' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"0.1"},
        "io.modelcontextprotocol/clientCapabilities":{}}}}'

(更早的版本:params 写成 {},并带上你的会话头。)

逐个工具检查:

  • 有 name,有 inputSchema,而且 inputSchema 是一个 JSON Schema 对象,不能是 null。没有参数的工具也应该写成 {"type":"object","additionalProperties":false}。
  • 名字 1–128 个字符,只用 A–Z a–z 0–9 _ - .,不带空格。客户端会把工具名放进请求头或提示词,其他字符都容易出问题。
  • 工具没变时,每次返回的顺序都一样。客户端会缓存这个列表,模型的提示词缓存也会。

然后检查分页。结果里有 nextCursor,说明列表还没完。把它放进 params 的 "cursor" 再调一次,直到不再返回 nextCursor。只读第一页的客户端会悄无声息地丢掉后面的工具。我们在自己的报告页上也发现过这个问题。

第 4 层:错误的形状对不对?

错误里说清楚了哪里不对,agent 就能恢复;没说清楚,它就放弃。MCP 把错误分成两类:

  • 协议错误是 JSON-RPC 错误:工具不存在、请求格式不对、服务器内部故障。工具不存在是 -32602。方法不存在是 -32601,在 2026-07-28 下还要配上 HTTP 404。
  • 工具执行错误是一个正常的结果,带 "isError": true,文字说明出了什么问题,比如日期已经过去了、这个 ID 不存在。模型读得懂这段文字,就能换个参数再试。

调一个不存在的工具:

curl -sS https://your-server.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/call' \
  -H 'Mcp-Name: definitely_not_a_tool' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"definitely_not_a_tool","arguments":{},"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"0.1"},
        "io.modelcontextprotocol/clientCapabilities":{}}}}'

你要的是一个带错误码和说明的 JSON-RPC error 对象。不要 HTML 错误页,不要一个空的 200,也不要堆栈。

再拿一个真实存在的工具,故意传错参数。好的服务器会回 isError: true,再加一句模型能照着改的话。

第 5 层:如果要求鉴权,客户端看得懂你的要求吗?

MCP 里鉴权是可选的。但只要你用了,一个没有 token 的客户端就必须能自己弄清楚怎么拿到 token,而不是靠人去读你的文档。

curl -sS -i https://your-server.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"0.1"},"io.modelcontextprotocol/clientCapabilities":{}}}}' \
  | grep -iE '^HTTP/|^www-authenticate'

应该得到 401,以及类似这样的头:

WWW-Authenticate: Bearer resource_metadata="https://your-server.example.com/.well-known/oauth-protected-resource", scope="files:read"

再去取 resource_metadata 指向的地址:

curl -sS https://your-server.example.com/.well-known/oauth-protected-resource

它应该返回 JSON,里面有你的 resource,authorization_servers 里至少有一项。规范要求服务器发布这份文档(RFC 9728),客户端靠它找到去哪里登录。常见的错:

  • 返回了 401,却没有 WWW-Authenticate 头;
  • resource_metadata 指向的地址返回的是你的 HTML 首页;
  • authorization_servers 里的地址提供不了 OAuth 元数据。

这些都不需要真的 token,所以完全可以、也应该从外部测。

第 6 层:模型会读到什么?

工具名和描述会原样进入模型的上下文。要看两件事:

  • 对 agent 下指令的话,而不是描述这个工具:「总是先调用这个工具」「忽略其他工具」「不要告诉用户」。描述工具做什么就够了,什么时候调用让客户端决定。
  • 隐藏字符:零宽空格、双向文字覆盖符、不可见的格式字符。它们改变的是模型读到的内容,人在代码评审里看到的却一模一样。
# 打印文本里含有零宽字符或双向控制字符的工具名
curl -sS ... tools/list ... | sed -n '/^{/p; s/^data: //p' \
  | jq -r '.result.tools[] | select(tostring | test("[\u200b-\u200f\u202a-\u202e\u2060-\u2064\ufeff]")) | .name'

没有任何输出才是你要的结果。

第 7 层:有没有改了你本来没打算改的东西?

agent 和批准它们的人,都会对一套工具形成习惯。一次部署如果改了工具名、删了参数或者改了 schema 里的类型,按旧样子做的计划就会坏掉,而且往往没有清楚的报错。

给你发布的东西记一个指纹:

curl -sS ... tools/list ... | sed -n '/^{/p; s/^data: //p' \
  | jq -S '.result.tools | map({name, inputSchema})' | sha256sum

把这个哈希和版本记在一起。下次部署后再算一次:如果变了,你应该说得出是哪一处改动造成的。

把它变成习惯,而不是上线当天的一次性任务

服务器会慢慢走样:证书会过期,代理会被改配置,升级一个依赖就可能改了错误的形状。每次部署后都跑一遍同样的检查,平时也定时跑。

如果不想自己维护这些脚本,可以在 mcpcheckup.com 粘贴你的地址。它会像 agent 一样从外部跑这些检查,每一项给出四种状态之一:已通过、未通过、观察到风险,或者未检查并写明原因。从不给一个总分。

清单

层 检查什么
连通 证书有效、链完整、名字匹配;不跳转到别的主机;耗时合理
握手 Accept 同时列出两种类型;server/discover(2026-07-28)或 initialize(更早版本)走得通;请求头和请求体一致
工具 每个工具都有名字和对象形式的 inputSchema;名字只用允许的字符;nextCursor 一直翻到底
错误 不存在的工具 → JSON-RPC 错误;参数错误 → isError: true,并附一句能照着改的说明
鉴权 401 带着含 resource_metadata 的 WWW-Authenticate;元数据文档里列出了授权服务器
描述 没有对 agent 下指令的话;没有隐藏字符
变化 每次发布都记下工具集指纹,每次部署后比对