Summary
A JSON-RPC notification (no id, spec expects a bare 202 empty ack) receives an unlogged,
bodyless 400 Bad Request from the Node StreamableHTTP server transport if — and only if — the
inbound HTTP request's Connection header is literally the string close. Requests with
Connection: keep-alive (or the header omitted) on an otherwise identical connection succeed with
202 as expected.
Reproduced identically on both:
@modelcontextprotocol/sdk 1.30.0 (StreamableHTTPServerTransport, v1)
@modelcontextprotocol/server + @modelcontextprotocol/node 2.0.0 (NodeStreamableHTTPServerTransport, v2)
Both wrap the same @hono/node-server-based Node HTTP transport, which is presumably why the
defect is shared across both SDK major versions.
Impact
Any real MCP client whose HTTP client sends Connection: close on the notifications/initialized
call (observed: claude-code's own MCP client does this on some requests) breaks immediately after
initialize succeeds — one call before tools/list is ever reached. From the client's perspective
this presents as "no tools available" / a silent, unrecoverable connect failure, with nothing useful
logged server-side (not in application code, not in hono's own handleRequestError /
handleFetchError / handleResponseError paths).
This is easy to hit unintentionally for any deployment behind a reverse proxy that forwards the
client's own Connection header to the upstream (e.g. an nginx keepalive upstream block that
declares keepalive N but omits the accompanying proxy_set_header Connection ""; — nginx's own
docs require that pairing, but a server without it silently passes the client's header through
instead of clearing it).
Reproduction (bisected, one variable at a time)
- A normal request with an
id (e.g. tools/list) succeeds fine under identical conditions.
- A bare hono
getRequestListener handler that directly returns new Response(null, {status: 202})
(no MCP SDK layer involved at all) also succeeds fine under identical conditions.
- The only combination that fails: a notification (no
id, 202 expected) and the incoming
request's Connection header equal to "close". This isolates the defect to the SDK's own
notification-response code path, not hono generically and not application code (the MCP server's
own request handlers never run for this case).
Minimal repro (pseudocode — happy to provide a runnable script):
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
Mcp-Session-Id: <valid, already-initialized session or stateless mode>
Connection: close
{"jsonrpc":"2.0","method":"notifications/initialized"}
Expected: HTTP/1.1 202 Accepted (empty body).
Actual: HTTP/1.1 400 Bad Request (empty body, nothing logged).
Same request with Connection: keep-alive (or header omitted) → correct 202.
Environment
- Node 24.18.1
@modelcontextprotocol/sdk 1.30.0, and separately @modelcontextprotocol/server 2.0.0 +
@modelcontextprotocol/node 2.0.0
- Both transports are
StreamableHTTPServerTransport / NodeStreamableHTTPServerTransport
respectively, in stateless mode (sessionIdGenerator: undefined) — also reproduces in stateful
mode with a valid session id, mode is not the differentiator.
Workaround in place
Not a fix — noting for anyone else who hits this. At the reverse-proxy layer, strip/clear the
client's Connection header before forwarding (nginx: proxy_set_header Connection ""; alongside
proxy_http_version 1.1;). This avoids the defect entirely without touching the SDK, but only helps
deployments that sit behind a proxy that can be configured this way — a client connecting directly
to a Node process running the transport with no intervening proxy would still hit this.
Ask
Happy to provide a minimal runnable repro script (Node) and/or a packet capture on request. Filed as
a report only — have not attempted a fix in the SDK's own transport code.
Summary
A JSON-RPC notification (no
id, spec expects a bare202empty ack) receives an unlogged,bodyless
400 Bad Requestfrom the Node StreamableHTTP server transport if — and only if — theinbound HTTP request's
Connectionheader is literally the stringclose. Requests withConnection: keep-alive(or the header omitted) on an otherwise identical connection succeed with202as expected.Reproduced identically on both:
@modelcontextprotocol/sdk1.30.0 (StreamableHTTPServerTransport, v1)@modelcontextprotocol/server+@modelcontextprotocol/node2.0.0 (NodeStreamableHTTPServerTransport, v2)Both wrap the same
@hono/node-server-based Node HTTP transport, which is presumably why thedefect is shared across both SDK major versions.
Impact
Any real MCP client whose HTTP client sends
Connection: closeon thenotifications/initializedcall (observed:
claude-code's own MCP client does this on some requests) breaks immediately afterinitializesucceeds — one call beforetools/listis ever reached. From the client's perspectivethis presents as "no tools available" / a silent, unrecoverable connect failure, with nothing useful
logged server-side (not in application code, not in hono's own
handleRequestError/handleFetchError/handleResponseErrorpaths).This is easy to hit unintentionally for any deployment behind a reverse proxy that forwards the
client's own
Connectionheader to the upstream (e.g. an nginxkeepaliveupstream block thatdeclares
keepalive Nbut omits the accompanyingproxy_set_header Connection "";— nginx's owndocs require that pairing, but a server without it silently passes the client's header through
instead of clearing it).
Reproduction (bisected, one variable at a time)
id(e.g.tools/list) succeeds fine under identical conditions.getRequestListenerhandler that directly returnsnew Response(null, {status: 202})(no MCP SDK layer involved at all) also succeeds fine under identical conditions.
id, 202 expected) and the incomingrequest's
Connectionheader equal to"close". This isolates the defect to the SDK's ownnotification-response code path, not hono generically and not application code (the MCP server's
own request handlers never run for this case).
Minimal repro (pseudocode — happy to provide a runnable script):
Expected:
HTTP/1.1 202 Accepted(empty body).Actual:
HTTP/1.1 400 Bad Request(empty body, nothing logged).Same request with
Connection: keep-alive(or header omitted) → correct202.Environment
@modelcontextprotocol/sdk1.30.0, and separately@modelcontextprotocol/server2.0.0 +@modelcontextprotocol/node2.0.0StreamableHTTPServerTransport/NodeStreamableHTTPServerTransportrespectively, in stateless mode (
sessionIdGenerator: undefined) — also reproduces in statefulmode with a valid session id, mode is not the differentiator.
Workaround in place
Not a fix — noting for anyone else who hits this. At the reverse-proxy layer, strip/clear the
client's
Connectionheader before forwarding (nginx:proxy_set_header Connection "";alongsideproxy_http_version 1.1;). This avoids the defect entirely without touching the SDK, but only helpsdeployments that sit behind a proxy that can be configured this way — a client connecting directly
to a Node process running the transport with no intervening proxy would still hit this.
Ask
Happy to provide a minimal runnable repro script (Node) and/or a packet capture on request. Filed as
a report only — have not attempted a fix in the SDK's own transport code.