MCP server returns 406 Not Acceptable: what it means and how to fix it
You point a client at your MCP server and the first request comes back 406 Not Acceptable. None of your tool code ran. The request was turned away at the door because of one header: Accept.
This guide explains where that rule comes from, which spec revisions state it, how to reproduce the error with curl, and how to fix it in the client, in the server, and in whatever sits between them. Replace https://your-server.example.com/mcp with your endpoint.
What 406 means here
406 Not Acceptable is a plain HTTP status (RFC 9110, section 15.5.7). It means: you told me which response types you can read, and I can't answer in any of them.
In MCP's Streamable HTTP transport, the server decides per request whether to answer with a single JSON object (Content-Type: application/json) or with a Server-Sent Events stream (Content-Type: text/event-stream). The client can't know in advance which one it will get, so the spec makes the client promise to handle both: every POST must carry an Accept header that lists both application/json and text/event-stream.
That rule binds the client. The spec does not say what a server must do when a client breaks it, and it never mentions 406. So a 406 means some layer chose to enforce the client rule and used the standard HTTP status for it: your server, the SDK transport it runs on, a web framework, or a proxy. The fix is usually on the sending side, but where exactly depends on which layer answered.
Which revisions require what
| Revision | HTTP transport | What the spec says about Accept |
|---|---|---|
| 2024-11-05 | HTTP+SSE: a GET stream plus a separate POST endpoint | No Accept rule |
| 2025-03-26, 2025-06-18, 2025-11-25 | Streamable HTTP: one endpoint for POST and GET | A POST must list both types. A GET, which opens an optional stream, must list text/event-stream |
| 2026-07-28 | Streamable HTTP: one endpoint, POST only | A POST must list both types. There is no GET stream, and a server that speaks only this revision should answer GET with 405 |
The server's side is the same in every Streamable HTTP revision: the reply to a request must be application/json or text/event-stream, and the client must handle either.
Reproduce it
First, see what your tool sends when you don't set the header. curl, for one, sends Accept: */*:
curl -sS -v -o /dev/null https://your-server.example.com/mcp -d '{}' 2>&1 | grep -i '^> accept'
Under HTTP rules */* covers both types (RFC 9110, section 12.5.1). Some servers still reject it, because they look for the two type names as literal text. That is why a quick curl -d against an MCP server can get a 406 that a real client would not.
Servers on 2025-03-26 to 2025-11-25. Send initialize with only one type listed:
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"}}}'
A server that enforces the rule answers 406. Now list both:
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"}}}'
This should return 200, with either a JSON body or a short SSE stream whose data: line holds the result.
Servers on 2026-07-28. There is no initialize. Use server/discover, which every server must implement, and send the headers this revision requires, so that a missing header doesn't give you a different error:
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":{}}}}'
Then run it again with -H 'Accept: application/json' in place of the full Accept line. If the first call works and the second gets 406, the Accept check is the cause.
A server that doesn't enforce the rule may answer 200 both times. That is allowed: the requirement is on the client.
Who answered: your server, or something in front of it?
Look at the body of the 406.
- A JSON-RPC
errorobject came from the MCP layer: your server, or the SDK transport it is built on. - An HTML page, a line of plain text or an empty body came from something before it: a framework's own content negotiation, an API gateway, a CDN or a reverse proxy.
Then take the proxy out of the picture. Send the same two requests to the public URL and straight to your origin, and compare:
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
(For a 2026-07-28 server, use the server/discover request and headers from above.) Both URLs should give the same codes. If they don't, the difference lives in the layer between them. It also helps to log the Accept header as your application receives it, not as the client sent it: those two can differ.
Fix it in the client
- Send
Accept: application/json, text/event-streamon every POST: requests, notifications and retries alike. HTTP libraries often add a default of their own, so set it explicitly. - Handle both reply types. When
Content-Typeistext/event-stream, read the JSON from thedata:lines; the final JSON-RPC response arrives on that stream. - For servers on 2025-03-26 to 2025-11-25, a GET that opens the optional stream must list
text/event-stream. Under 2026-07-28 there is no GET stream to open. - If your client sits on top of an HTTP stack you didn't write, check that no middleware rewrites
Acceptafter you set it.
Fix it in the server
Whether to enforce the rule is your call. Making the failure readable is what lets a client, and the person debugging it, recover.
- Put a JSON-RPC error in the body of the 406 that names what is missing, for example "client must accept both application/json and text/event-stream". A bare 406 with an empty body explains nothing.
- Decide how you treat wildcards. Under HTTP rules,
*/*covers both types,application/*covers JSON andtext/*covers event streams. A strict reading of the MCP rule wants both types named. Either choice is defensible; say which one you made in the error. - If you are lenient, answer in a type the client listed. A server that only ever replies with JSON can accept a request whose
Acceptcoversapplication/json, but it must not then open an SSE stream for that client. - Apply the same check on every route that reaches your MCP endpoint, so the behaviour doesn't depend on which hostname or path a client used.
Proxies and frameworks: causing it and hiding it
A layer in front of your server can produce a 406 your code never saw:
- A gateway, edge function or firewall rule that rewrites, drops or normalises
Acceptbefore forwarding. The client sent both types; your server received one, or none. - A framework route declared to produce a single content type, whose built-in content negotiation rejects the request before your handler runs. The body is then the framework's error, not JSON-RPC.
The same layers can hide one:
- A proxy that replaces upstream error bodies with its own error page. You still see a 406, or a 502, but the JSON-RPC message that explained it is gone.
- A gateway that adds a default
Acceptwhen the client sent none. Requests through the gateway work, the same client fails against a host without that gateway, and the bug looks random.
One more problem lives in the same layer, even though it isn't a 406: response buffering. A proxy that buffers holds back SSE events until the stream ends. Under 2026-07-28 the spec says servers should send X-Accel-Buffering: no when they open an SSE stream, to ask proxies such as nginx not to buffer it.
Quick reference
| You see | Most likely source | Next step |
|---|---|---|
| 406 with a JSON-RPC error body | Your MCP server or its SDK | Fix the client's Accept, or relax the server's check |
| 406 with HTML or an empty body | A framework, gateway or proxy | Compare the public URL with the origin; look for header rewriting |
| 406 only through the public URL | The proxy layer | Pass Accept through unchanged |
| 200 with only one type listed | A lenient server | Allowed; the client should still send both |
See what an agent sees
A 406 is one of the first things a client can hit, and it stops the client before your tools are even listed. If you want to see your server the way an outside client meets it, paste your address at mcpcheckup.com. It connects from outside, the way an agent does, and reports each check in one of four states: verified, failed, observed risk, or not checked with a stated reason. You never get a single score.