Skip to content

fix(client): skip the SSE fallback when the server/discover probe is rejected with 400/404 - #1855

Open
scottt732 wants to merge 1 commit into
modelcontextprotocol:mainfrom
scottt732:skip-sse-fallback-on-discover-probe
Open

fix(client): skip the SSE fallback when the server/discover probe is rejected with 400/404#1855
scottt732 wants to merge 1 commit into
modelcontextprotocol:mainfrom
scottt732:skip-sse-fallback-on-discover-probe

Conversation

@scottt732

Copy link
Copy Markdown

Summary

AutoDetectingClientSessionTransport attempts the SSE fallback when the SEP-2575 server/discover probe is rejected with 400 or 404. Neither status says anything about which transport the peer speaks: a server predating SEP-2575 rejects the session-less server/discover POST the same way whether it is Streamable-HTTP-only or SSE-only (400 when it cannot parse the request, 404 when it requires Mcp-Session-Id on every non-initialize POST).

McpClientImpl.ConnectAsync already reads exactly these two statuses as "this server requires the initialize handshake" and retries with initialize on the same transport — and that attempt still falls back to SSE. So the SSE GET spent during the probe is always discarded:

  • a Streamable-HTTP-only server predating SEP-2575 pays a wasted GET on every connect, and logs streamable HTTP transport failed with status code NotFound, falling back to SSE transport at Information for what is settled protocol negotiation rather than a failure;
  • an SSE-only server is reached either way — one POST later instead of one GET earlier.

This skips the fallback for that one case, scoped to the server/discover probe and to the two statuses ConnectAsync acts on. Every other failure keeps the existing behavior.

Observable behavior this addresses

Datadog's managed MCP server (datadog-mcp 1.0.0, https://mcp.us5.datadoghq.com/api/unstable/mcp-server/mcp) is a Streamable-HTTP-only server predating SEP-2575:

Request Response
POST server/discover (no session) 404, non-JSON-RPC body
GET 405
POST initialize 200

Every connect currently costs discover-POST -> SSE-GET (405) -> initialize-POST, and logs the misleading fallback line. With this change it is discover-POST -> initialize-POST.

Relationship to existing work

Verification

--filter 'FullyQualifiedName~HttpClientTransportAutoDetectTests': 13 passed on both net9.0 and net10.0 (9 pre-existing + 4 new). net8.0 could not run locally — no .NET 8 runtime on this machine.

Full ModelContextProtocol.Tests on net10.0: 2361 passed, 2 failed — both DockerEverythingServerTests, which fail identically on unmodified main in this environment (container startup, not transport). ModelContextProtocol.AspNetCore.Tests on net10.0: 615 passed, 0 failed.

DockerEverythingServerTests.ConnectAndReceiveMessage_EverythingServerWithSse — a real SSE-only server reached over AutoDetect — passes with the change, and the second new test pins that same path at the transport level.

New tests:

  • AutoDetectMode_SkipsSseFallback_WhenDiscoverProbeIsRejected (Theory: 404, 400) — no GET is issued, the original status surfaces, and no fallback line is logged.
  • AutoDetectMode_StillFallsBackToSse_WhenInitializeFollowsRejectedDiscoverProbe — an SSE-only server is still reached, via the initialize retry.
  • AutoDetectMode_FallsBackToSse_WhenDiscoverProbeFailsWithUnrelatedStatus (415) — the skip stays scoped to 400/404.

AI assistance disclosure: AI was used to discover this opportunity and draft the change and text. The submission was checked against the prepared artifact and recorded verification evidence.

…rejected

A server predating SEP-2575 rejects the session-less server/discover POST at the
HTTP layer with 400 (cannot parse the request) or 404 (requires Mcp-Session-Id on
every non-initialize POST). AutoDetectingClientSessionTransport treated that like
any other non-JSON-RPC error and attempted the SSE fallback, but those two statuses
say nothing about which transport the peer speaks -- an SSE-only server rejects the
probe exactly the same way.

McpClientImpl.ConnectAsync already reads 400/404 from the probe as 'this server
requires the initialize handshake' and retries with initialize on the same transport,
and that retry still falls back to SSE. So the SSE GET spent during the probe is
always discarded: a Streamable-HTTP-only server that predates SEP-2575 pays a wasted
round trip on every connect and logs 'falling back to SSE transport' for settled
protocol negotiation, while an SSE-only server is reached one POST later either way.

Scoped to the discover probe and to the two statuses ConnectAsync acts on, so every
other failure keeps the existing fallback.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant