菜单

远程 MCP server 上线清单

2026-10-13MCP Checkup

一个在编辑器里跑得好好的 MCP server,只通过了一项测试:你自己的客户端,在你自己的机器上。这份清单讲的是,当别人的 agent 从公网连进来时,有哪些事情会变。每一项都写了为什么重要、怎么检查。

下面的命令按当前版本 2026-07-28 来写;早期版本有差别的地方,条目里会说明。先在 bash 里把这几个变量设好,之后的命令直接粘贴就能用:

URL=https://your-server.example.com/mcp
H=(-H 'Content-Type: application/json'
   -H 'Accept: application/json, text/event-stream'
   -H 'MCP-Protocol-Version: 2026-07-28')
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"0.1"},"io.modelcontextprotocol/clientCapabilities":{}}'
DISCOVER='{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{'"$META"'}}'

如果你的服务器说的是 2025-11-25 或更早的版本,第一步改用 initialize 请求,不用 server/discover;之后每个请求都要带上 MCP-Protocol-Version,以及它给你的 Mcp-Session-Id(如果有的话)。

1. 域名和 TLS

  • **为什么重要:**agent 是从你控制不了的机器上连进来的。证书过期、证书没覆盖你的主机名、缺一张中间证书,在那些机器上都会直接失败,哪怕你自己的浏览器看起来没问题。
  • **怎么检查:**看证书校验结果和有效期;确认端点不会跳转到别的主机,因为跳到别的主机时,客户端可能丢掉 Authorization 头,或者干脆不跟。
openssl s_client -connect your-server.example.com:443 -servername your-server.example.com </dev/null 2>/dev/null \
  | grep -E 'Verify return code|NotAfter'
curl -sS -o /dev/null -w '%{http_code} redirect=[%{redirect_url}]\n' "$URL" "${H[@]}" \
  -H 'Mcp-Method: server/discover' -d "$DISCOVER"

你要看到 Verify return code: 0 (ok),以及空的 redirect=[]。

2. 端点和传输方式

  • **为什么重要:**客户端期待的是一个接受 POST 的 HTTPS 地址,也就是 2025-03-26 引入的 Streamable HTTP 传输。2024-11-05 那套老的 HTTP+SSE 传输从那时起就已弃用,2026-07-28 又按规范的功能生命周期政策把它正式列为 Deprecated,新服务器不应该再采用。
  • **怎么检查:**对外只公布一个确切的地址(想好结尾要不要斜杠),这里的每条命令都用这同一个地址。只支持 2026-07-28 的服务器,应该对 GET 回 405。在 2025-03-26 到 2025-11-25 下,GET 要么打开一条 SSE 流,要么回 405。
curl -sS -o /dev/null -w 'GET %{http_code}\n' -H 'Accept: text/event-stream' "$URL"

3. Accept 和 Content-Type

  • **为什么重要:**你的服务器每收到一个请求,都会自己决定是回一个 JSON 对象还是一条 SSE 流。所以每一版 Streamable HTTP 都要求客户端在 Accept 里同时列出 application/json 和 text/event-stream,并要求你回复的 Content-Type 正好是这两者之一。如果你的服务器或代理执行了 Accept 规则,只列一种类型的客户端会拿到 406 Not Acceptable。
  • **怎么检查:**完整的请求应该成功,内容类型是两者之一。只列 application/json 的请求,要么得到正常回复,要么得到一个响应体写明缺了哪种类型的 406,而不是一个光秃秃的错误页。
curl -sS -D - -o /dev/null "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' -d "$DISCOVER" | grep -i '^content-type'
curl -sS -i "$URL" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: server/discover' -d "$DISCOVER"

4. 无状态还是有会话

  • 为什么重要:2026-07-28 没有握手,也没有会话。每个请求都在 _meta 里带上自己的协议版本和客户端能力,服务器不能依赖之前的请求。到 2025-11-25 为止,服务器可以在 initialize 时下发 Mcp-Session-Id。如果你为老客户端保留了会话,每个实例都得找得到它们;你已经结束的会话必须回 404,客户端正是靠这个知道要重新开始。
  • **怎么检查:**新版请求在一条全新的连接上作为第一个请求就能成功,而且响应里没有会话头。
curl -sS -D - -o /dev/null "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' -d "$DISCOVER" \
  | grep -i '^mcp-session-id' || echo 'no session header'

5. 校验 Origin

  • **为什么重要:**浏览器会带上 Origin 头,而 DNS 重绑定攻击可能让一个网页访问到它本不该访问的服务器。每一版 HTTP 传输都规定服务器必须校验 Origin。从 2025-11-25 起,Origin 存在但不合法的请求,必须回 403 Forbidden。
  • **怎么检查:**发一个你不允许的来源,应该得到 403。不带 Origin 头时(curl 默认就不带),同一个请求应该正常。
curl -sS -o /dev/null -w '%{http_code}\n' "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' \
  -H 'Origin: https://other-site.example.com' -d "$DISCOVER"

6. 鉴权发现

  • **为什么重要:**MCP 里鉴权是可选的。但如果你要求鉴权,一个没有 token 的客户端就得能自己弄清楚去哪里拿 token,而不是靠读你的文档。在 2026-07-28 下,你的服务器必须发布 OAuth 2.0 受保护资源元数据(RFC 9728),里面至少列出一个授权服务器,并且至少用以下两种方式之一让客户端找到它:在 401 的 WWW-Authenticate 头里给出 resource_metadata 地址,或者放在 well-known 地址上。(2025-06-18 要求必须用这个头。)两种都做,成本很低。无效或过期的 token 也必须得到 401。
  • **怎么检查:**不带 token 时,应该得到 401,并带有 WWW-Authenticate: Bearer resource_metadata="..."。那个地址,或者 well-known 地址,应该返回含有 resource 和 authorization_servers 的 JSON。客户端会先试带路径的 well-known 地址,再试根路径的那个。
curl -sS -i "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' -d "$DISCOVER" | grep -iE '^HTTP/|^www-authenticate'
curl -sS https://your-server.example.com/.well-known/oauth-protected-resource/mcp
curl -sS https://your-server.example.com/.well-known/oauth-protected-resource

7. 分页

  • **为什么重要:**每页多大由服务器决定。如果 tools/list 返回了 nextCursor,说明列表还没完;只读第一页的客户端会悄无声息地丢掉后面的工具。游标是不透明的,空字符串也是一个合法的游标。无效的游标应该得到错误码 -32602。
  • **怎么检查:**顺着 nextCursor 一直翻到底,数一数工具有多少个。这个循环还会写出 tools.jsonl,第 8 项和第 13 项会用到。
cursor=null
while :; do
  body=$(jq -nc --argjson c "$cursor" --argjson m "{$META}" \
    '{jsonrpc:"2.0", id:2, method:"tools/list", params:($m + (if $c == null then {} else {cursor:$c} end))}')
  page=$(curl -sS "$URL" "${H[@]}" -H 'Mcp-Method: tools/list' -d "$body" | sed -n '/^{/p; s/^data: //p')
  echo "$page" | jq -c '.result.tools[]'
  cursor=$(echo "$page" | jq -c '.result.nextCursor // null')
  [ "$cursor" = null ] && break
done > tools.jsonl
wc -l < tools.jsonl

8. 工具描述只做描述

  • **为什么重要:**工具名和描述会原样进入模型的上下文。对 agent 说话的文字,比如「总是先调用这个工具」「忽略其他工具」「不要告诉用户」,是在操纵它,而不是在告诉它这个工具做什么;这也正是排查提示词注入的人会去搜的东西。描述工具做什么就够了,什么时候调用,让客户端决定。
  • **怎么检查:**按模型看到的样子把每条描述读一遍,再搜一搜有没有命令式的话、有没有不可见字符。第一条命令打印出来的要逐条看,有些是无害的,比如「never returns null」;第二条命令没有输出,才是你要的结果。
jq -r '"\(.name): \(.description)"' tools.jsonl \
  | grep -inE 'always|never|ignore|instead of|do not tell|before (any|other)'
jq -r 'select(tostring | test("[\u200b-\u200f\u202a-\u202e\u2060-\u2064\ufeff]")) | .name' tools.jsonl

9. 错误的形状

  • **为什么重要:**错误里说清楚了哪里不对,agent 才能恢复。MCP 把协议错误和工具执行错误分开:协议错误是 JSON-RPC 的 error 对象(工具不存在是 -32602,方法不存在是 -32601);工具执行错误是一个正常的结果,带 isError: true,再加一句模型能照着改的话。在 2026-07-28 下,HTTP 状态码也有讲究:方法不存在回 404;缺请求头或请求头对不上回 400 加 -32020;不支持的版本回 400 加 -32022(UnsupportedProtocolVersion),并列出你支持的版本。同时支持两代协议的客户端会读这些响应体,来判断你的服务器是新版还是老版,所以一个空的 400 或 404 可能把它引到错的路上。
  • **怎么检查:**第一条命令应该得到 404 和 -32601,第二条应该得到 400 和 -32020。然后拿一个真实存在的工具,故意传错参数,应该得到 isError: true 和一句模型用得上的话。
curl -sS -i "$URL" "${H[@]}" -H 'Mcp-Method: no/such_method' \
  -d '{"jsonrpc":"2.0","id":3,"method":"no/such_method","params":{'"$META"'}}'
curl -sS -i "$URL" "${H[@]}" -H 'Mcp-Method: tools/list' -d "$DISCOVER"

10. 超时和长任务

  • **为什么重要:**每一次工具调用,都有人在等。规范说,实现方应该给发出的每个请求设超时,收到进度通知时可以重置计时,但仍然应该有一个上限。在 2026-07-28 下,关闭一个请求的响应流就是取消这个请求,服务器应该停下这项工作。在 2025-11-25 下,连接断开不应该被当作取消,客户端要取消时发 notifications/cancelled。对于一个请求撑不住的长时间工作,可以用可选的 Tasks 扩展:它返回一个句柄,由客户端轮询。
  • **怎么检查:**通过公网地址调用你最慢的工具,带上进度 token。看进度事件是不是实时到达,以及中间有没有哪一层先把流掐断了。做缓冲的代理会把所有事件攒到最后才放出来,这也是 2026-07-28 要求服务器在 SSE 响应上带 X-Accel-Buffering: no 的原因。
time curl -sS -N "$URL" "${H[@]}" -H 'Mcp-Method: tools/call' -H 'Mcp-Name: your_slowest_tool' \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"your_slowest_tool","arguments":{},"_meta":{
        "progressToken":"t1",
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientCapabilities":{}}}}'

11. 限流

  • 为什么重要:2026-07-28 的工具一页把「对工具调用限流」列为服务器必须做的事之一。一个陷在重试循环里的 agent,几秒钟内就可能把同一个工具调用很多次。
  • **怎么检查:**只对你自己的服务器,发一小串连续请求,看多出来的那些得到什么。清楚的错误,比如带 Retry-After 头的 429,能让客户端退避;卡住不动或者回 500 就不行。
for i in $(seq 1 30); do
  curl -sS -o /dev/null -w '%{http_code} ' "$URL" "${H[@]}" -H 'Mcp-Method: server/discover' -d "$DISCOVER"
done; echo

12. 日志里不留凭据

  • **为什么重要:**token 会出现在 Authorization 头里,有时也会出现在工具参数里。把它们记下来的请求日志,等于把能用的凭据留给了所有能读日志的人。
  • **怎么检查:**用一个测试 token 跑一遍这份清单,然后在日志里搜它。计数应该是零。
grep -c "$TEST_TOKEN" your-server.log

13. 每次发布记一个工具集指纹

  • **为什么重要:**批准 agent 使用哪些工具的人,以及 agent 本身,都会围绕一套工具来做计划。一次部署如果改了工具名、删了参数或者改了 schema 里的类型,这些计划就会坏掉,而且往往没有清楚的报错。
  • **怎么检查:**每次发布后,对工具名和输入 schema 算一个哈希,和这次发布记在一起,每次部署后再比对。如果变了,你应该说得出是哪一处改动造成的。
jq -S -s 'map({name, inputSchema}) | sort_by(.name)' tools.jsonl | sha256sum

上线之后

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

在 mcpcheckup.com 粘贴你的地址,看看 agent 会看到什么。它会像 agent 一样从外部连接,每一项检查给出四种状态之一:已通过、未通过、观察到风险,或者未检查并写明原因。从不给一个总分。