菜单

MCP server 返回 406 Not Acceptable:是什么意思,怎么修

2026-10-16MCP Checkup

你把客户端指向自己的 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 一样从外部连接,每一项检查给出四种状态之一:已通过、未通过、观察到风险,或者未检查并写明原因。从不给一个总分。