Skip to content

StreamableHTTP server transport returns bare 400 for a notification when the request has Connection: close #2730

Description

@amirtafreshi

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions