MCP server 返回 406 Not Acceptable:是什么意思,怎么修
你把客户端指向自己的 MCP server,第一个请求就回来一个 406 Not Acceptable。你的工具代码一行都没跑,请求在门口就被挡回来了,原因只是一个请求头:Accept。
这篇文章讲清楚这条规则从哪里来、哪些规范版本写了它、怎么用 curl 复现,以及在客户端、服务端和两者之间的那一层分别怎么修。把 https://your-server.example.com/mcp 换成你的地址就行。
406 在这里是什么意思
406 Not Acceptable 是一个普通的 HTTP 状态码(RFC 9110 第 15.5.7 节),意思是:你告诉我你能读哪些响应类型,而我没法用其中任何一种来回答。
在 MCP 的 Streamable HTTP 传输里,服务器每收到一个请求,都会自己决定是回一个 JSON 对象(Content-Type: application/json),还是回一段 Server-Sent Events 流(Content-Type: text/event-stream)。客户端事先不知道会拿到哪一种,所以规范要求客户端承诺两种都能处理:每个 POST 都必须带 Accept 头,并且同时列出 application/json 和 text/event-stream。
这条规则约束的是客户端。规范没有规定客户端违反它时服务器该怎么做,也从来没有提到 406。所以看到 406,说明某一层选择了替规范执行这条客户端规则,并用了标准的 HTTP 状态码:可能是你的服务器,可能是它所用的 SDK 传输层,可能是 Web 框架,也可能是代理。修的地方通常在发送方,但具体在哪,要看是哪一层回的。
哪个版本要求什么
| 版本 | HTTP 传输 | 规范对 Accept 的要求 |
|---|---|---|
| 2024-11-05 | HTTP+SSE:一条 GET 流,加一个单独的 POST 端点 | 没有 Accept 规则 |
| 2025-03-26、2025-06-18、2025-11-25 | Streamable HTTP:一个端点,支持 POST 和 GET | POST 必须同时列出两种类型。GET 用来打开一条可选的流,必须列出 text/event-stream |
| 2026-07-28 | Streamable HTTP:一个端点,只用 POST | POST 必须同时列出两种类型。没有 GET 流;只支持这一版的服务器应该对 GET 回 405 |
在所有 Streamable HTTP 版本里,服务器这一侧的要求都一样:对请求的回复必须是 application/json 或 text/event-stream,客户端两种都得能处理。
复现
先看看你不设这个头时,工具实际发的是什么。以 curl 为例,它默认发 Accept: */*:
curl -sS -v -o /dev/null https://your-server.example.com/mcp -d '{}' 2>&1 | grep -i '^> accept'
按 HTTP 的规则,*/* 两种类型都覆盖了(RFC 9110 第 12.5.1 节)。但有些服务器照样拒绝,因为它们是按字面去找这两个类型名的。这就是为什么随手对 MCP server 跑一条 curl -d 会拿到 406,而真正的客户端不会。
**2025-03-26 到 2025-11-25 的服务器。**发一个只列了一种类型的 initialize:
curl -sS -i https://your-server.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"curl-test","version":"0.1"}}}'
执行这条规则的服务器会回 406。再把两种都列上:
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"}}}'
这次应该得到 200,响应体要么是 JSON,要么是一段很短的 SSE 流,结果在 data: 那一行里。
**2026-07-28 的服务器。**这一版没有 initialize。改用 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":{}}}}'
然后把完整的 Accept 那一行换成 -H 'Accept: application/json' 再跑一次。第一次成功、第二次 406,原因就是 Accept 检查。
不执行这条规则的服务器,两次可能都回 200。这是允许的:这条要求是对客户端提的。
是谁回的:你的服务器,还是它前面的东西?
看 406 的响应体。
- 如果是一个 JSON-RPC
error对象,它来自 MCP 这一层:你的服务器,或者它所基于的 SDK 传输层。 - 如果是一个 HTML 页面、一行纯文本,或者干脆是空的,它来自更前面的某一层:框架自带的内容协商、API 网关、CDN 或反向代理。
然后把代理排除掉。同样两个请求,一个发到公网地址,一个直接发到源站,对比结果:
ORIGIN_URL=http://localhost:8080/mcp # your server, bypassing the proxy
for url in https://your-server.example.com/mcp "$ORIGIN_URL"; do
for accept in 'application/json' 'application/json, text/event-stream'; do
printf '%s [%s] ' "$url" "$accept"
curl -sS -o /dev/null -w '%{http_code}\n' "$url" \
-H 'Content-Type: application/json' -H "Accept: $accept" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl-test","version":"0.1"}}}'
done
done
(2026-07-28 的服务器,换成上面那个 server/discover 请求和对应的请求头。)两个地址应该给出一样的状态码。不一样的话,差别就出在两者之间的那一层。另外,最好把应用实际收到的 Accept 记进日志,而不是客户端发出的那个:这两者可能不一样。
在客户端修
- 每个 POST 都带上
Accept: application/json, text/event-stream:请求、通知、重试,一个都不能少。HTTP 库常常会自己加一个默认值,所以要显式设置。 - 两种回复类型都要处理。
Content-Type是text/event-stream时,从data:行里读 JSON;最终的 JSON-RPC 响应就在这条流上。 - 对 2025-03-26 到 2025-11-25 的服务器,用来打开可选流的 GET 必须列出
text/event-stream。2026-07-28 下没有 GET 流可开。 - 如果你的客户端建在一个不是你写的 HTTP 栈上,确认没有中间件在你设置之后又改写了
Accept。
在服务端修
执不执行这条规则由你决定。让失败信息看得懂,才能让客户端和排错的人恢复过来。
- 在 406 的响应体里放一个 JSON-RPC 错误,写明缺了什么,比如 "client must accept both application/json and text/event-stream"。一个空响应体的 406 什么也说明不了。
- 想清楚怎么对待通配符。按 HTTP 规则,
*/*覆盖两种类型,application/*覆盖 JSON,text/*覆盖事件流。而严格按 MCP 的字面,要求两种类型都写出来。两种做法都说得通;选了哪种,就在错误里说明。 - 如果你选择宽松,就用客户端列出的类型来回复。一个永远只回 JSON 的服务器,可以接受
Accept覆盖了application/json的请求,但接下来就不能给这个客户端开 SSE 流。 - 能到达你 MCP 端点的每一条路由都用同一套检查,这样结果不会因为客户端用了哪个主机名或路径而不同。
代理和框架:会制造它,也会掩盖它
你服务器前面的某一层,可能产生一个你的代码根本没见过的 406:
- 网关、边缘函数或防火墙规则在转发前改写、删掉或规范化了
Accept。客户端发了两种类型,你的服务器只收到一种,甚至一种都没有。 - 某个框架路由声明只产出一种内容类型,它自带的内容协商在你的处理函数运行之前就拒绝了请求。这时响应体是框架的错误信息,不是 JSON-RPC。
同样这些层,也可能把它藏起来:
- 代理用自己的错误页替换了上游的错误响应体。你还是看到 406(或者 502),但原本解释原因的那条 JSON-RPC 消息没了。
- 网关在客户端没发
Accept时自动补一个默认值。经过网关的请求都正常,同一个客户端换到一台没有这层网关的主机上就失败,看起来像随机出错。
还有一个问题也出在这一层,虽然它不是 406:响应缓冲。做缓冲的代理会把 SSE 事件一直攒到流结束才放出来。2026-07-28 规定,服务器打开 SSE 流时应该带上 X-Accel-Buffering: no,请 nginx 这类代理不要缓冲。
速查表
| 你看到的 | 最可能的来源 | 下一步 |
|---|---|---|
| 406,响应体是 JSON-RPC 错误 | 你的 MCP server 或它的 SDK | 修客户端的 Accept,或者放宽服务端的检查 |
| 406,响应体是 HTML 或空的 | 框架、网关或代理 | 对比公网地址和源站;查有没有改写请求头 |
| 只有经过公网地址时才 406 | 代理层 | 让 Accept 原样透传 |
| 只列一种类型也回 200 | 宽松的服务器 | 允许;客户端仍然应该两种都发 |
看看 agent 看到的是什么
406 是客户端最早可能撞上的错误之一,它会让客户端在你的工具被列出来之前就停下。想像外部客户端那样看一眼你的服务器,可以在 mcpcheckup.com 粘贴你的地址。它会像 agent 一样从外部连接,每一项检查给出四种状态之一:已通过、未通过、观察到风险,或者未检查并写明原因。从不给一个总分。