Skip to content

Fix #5239: tolerate non-SSE 200 OK on streamable-http GET /mcp probe#6345

Closed
redinside-dev wants to merge 1 commit into
spring-projects:mainfrom
redinside-dev:fix/issue-5239-streamable-get-probe
Closed

Fix #5239: tolerate non-SSE 200 OK on streamable-http GET /mcp probe#6345
redinside-dev wants to merge 1 commit into
spring-projects:mainfrom
redinside-dev:fix/issue-5239-streamable-get-probe

Conversation

@redinside-dev

Copy link
Copy Markdown

Summary

Fix #5239.

The streamable-http MCP client fails to initialize against MCP servers that return a plain 200 OK with a non-SSE Content-Type (e.g. application/json) on the GET /mcp probe. The spec requires the server to return either Content-Type: text/event-stream or HTTP 405 Method Not Allowed on the GET probe, but real-world servers (ModelScope Model API MCP is the canonical example) instead return 200 OK with a JSON body or no content type to signal that they do not offer a server-initiated stream. The client must treat that as a valid no-stream response and fall back to request-response mode.

Fix

In WebClientStreamableHttpTransport.reconnect's exchangeToFlux handler, add a new branch for "2xx that is not text/event-stream". A new helper isStatelessOk(ClientResponse) returns true for any 2xx that isn't an SSE stream (including 200 with application/json, 200 with no content type, 204, etc.). The branch returns Flux.empty() — the same behavior as the existing 405 path — so the client continues in request-response mode.

else if (isStatelessOk(response)) {
    // Per the MCP spec, the server MUST return either
    // Content-Type: text/event-stream or HTTP 405 Method Not Allowed
    // on the GET probe. Some servers (e.g. ModelScope Model API MCP)
    // return 200 OK with a non-SSE content type (typically
    // application/json) as a no-stream endpoint. Treat that as
    // "stateless server that does not stream" — same as 405 — and
    // fall back to request-response mode rather than failing the
    // initialization. See spring-ai issue #5239.
    logger.debug("The server returned a non-SSE 2xx response to the GET probe; "
            + "treating as a stateless endpoint and using request-response mode.");
    return Flux.empty();
}

Tests

New WebClientStreamableHttpGetProbeIT covers three cases against a local com.sun.net.httpserver.HttpServer:

  • testGetProbeWith200ApplicationJson200 + Content-Type: application/json (fails on the unmodified code, passes with the fix)
  • testGetProbeWith200NoContentType200 with no Content-Type header (fails on the unmodified code, passes with the fix)
  • testGetProbeWith405405 (sanity check; passes before and after)

Each test asserts that (1) the server actually received the GET probe, and (2) the transport's exception handler was not invoked. Without the fix, the default exchangeToFlux else branch (response.createError()) raises an McpTransportException, which the background reconnect loop surfaces via the exception handler.

The existing WebClientStreamableHttpTransportErrorHandlingIT regression tests continue to pass.

Notes

  • This is a non-breaking change: the new branch only adds a "treat 2xx-without-SSE as stateless" mode that strictly broadens the set of servers the client can talk to. Servers that already returned text/event-stream keep going through the original eventStream path.
  • The mcp-core 0.17.1 workaround mentioned in the issue is a different code path (the JDK HTTP-client-based transport uses ResponseSubscribers$SseLineSubscriber directly). That variant does not need this fix because it returns its body to the caller as a single String and does not require an SSE-shaped response on the GET probe. The fix here covers the WebFlux variant, which is the one that fails per the issue body.

The MCP streamable-http spec requires the server to return either
Content-Type: text/event-stream or HTTP 405 Method Not Allowed on the
GET /mcp probe. Some servers (e.g. ModelScope Model API MCP) instead
return a plain 200 OK with a non-SSE content type (typically
application/json or no Content-Type header) to signal that they do not
offer a server-initiated stream. The Spring AI client must treat that
as a valid no-stream response and fall back to request-response mode
rather than failing initialization with an McpTransportException.

The exchangeToFlux handler in WebClientStreamableHttpTransport already
handled isEventStream and isNotAllowed, but fell through to the default
error path for any other 2xx response, which is what triggered
'McpTransportException: Invalid SSE response' (or its WebFlux
equivalent) during the GET probe.

Add an isStatelessOk helper that detects a 2xx response that is not an
SSE stream and treat it the same as 405: return an empty Flux and let
the client continue in request-response mode.

Add WebClientStreamableHttpGetProbeIT with three regression tests:
- 200 OK with Content-Type: application/json
- 200 OK with no Content-Type header
- 405 Method Not Allowed (sanity check that the existing well-formed
  path still works)

The first two tests fail on the unmodified code and pass with the fix;
the third passes both before and after.

Closes spring-projects#5239

Signed-off-by: Anurag Saxena <anuragsaxena.98@gmail.com>
@redinside-dev

Copy link
Copy Markdown
Author

Rebased with Signed-off-by trailer to clear the DCO check (was missing). No code changes — only the commit message trailer was added. All WebFlux transport tests pass locally.

@sdeleuze

Copy link
Copy Markdown
Contributor

Declining this one as we don't accept contribution from AI bots.

@sdeleuze sdeleuze closed this Jun 11, 2026
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.

streamable-http MCP client treats GET /mcp as SSE and fails when server returns JSON (Invalid SSE response)

2 participants