Skip to content

HttpMcpProxy: SSE response handling fails when Content-Type includes parameters (e.g. charset=utf-8) #1244

Description

@SimplyKnownAsG

Description

HttpMcpProxy has a parseSseResponse() method for handling SSE responses, but routes to it via a strict equality check:

// HttpMcpProxy.java
String contentType = response.body().contentType();
if ("text/event-stream".equals(contentType)) {
    return CompletableFuture.completedFuture(parseSseResponse(response, request));
}

This silently falls through to the JSON deserializer when the server returns Content-Type: text/event-stream; charset=utf-8, which is a valid content-type per RFC 9110 §8.3. Starlette automatically appends ; charset=utf-8 to all text/* media types when Content-Type is not set explicitly in the response headers.

Starlette MWE

# pip install fastapi uvicorn sse-starlette

from fastapi import FastAPI
from sse_starlette.sse import EventSourceResponse
import uvicorn

app = FastAPI()

@app.post("/mcp")
async def mcp():
    async def generator():
        yield {"event": "message", "data": '{"jsonrpc":"2.0","id":1,"result":{}}'}
    # No explicit Content-Type header — Starlette will add ; charset=utf-8
    return EventSourceResponse(generator())

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8080)
$ curl -si -X POST http://localhost:8080/mcp \
    -H "Accept: application/json, text/event-stream" | grep -i content-type
content-type: text/event-stream; charset=utf-8

Failing test (MWE)

This test can be added to the existing HttpMcpProxyTest — it uses the same HttpServer infrastructure already in place:

@Test
void testSseStreamingWithCharsetInContentType() throws IOException {
    mockServer.removeContext("/mcp");
    mockServer.createContext("/mcp", exchange -> {
        String sseResponse = "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"final result\"}\n\n";
        // Simulate Starlette's automatic charset appending (RFC 9110 §8.3 compliant)
        exchange.getResponseHeaders().set("Content-Type", "text/event-stream; charset=utf-8");
        exchange.sendResponseHeaders(200, sseResponse.getBytes(StandardCharsets.UTF_8).length);
        try (OutputStream os = exchange.getResponseBody()) {
            os.write(sseResponse.getBytes(StandardCharsets.UTF_8));
        } finally {
            exchange.close();
        }
    });

    JsonRpcRequest request = JsonRpcRequest.builder()
            .method("test/streaming")
            .id(Document.of(1))
            .jsonrpc("2.0")
            .build();

    JsonRpcResponse response = proxy.rpc(request).join();

    // Without the fix this fails with SerializationException:
    // Unrecognized token 'event': was expecting (JSON String, Number, Array, Object...)
    assertNotNull(response);
    assertNull(response.getError());
    assertEquals("final result", response.getResult().asString());
}

Fix

- if ("text/event-stream".equals(contentType)) {
+ if (contentType != null && contentType.startsWith("text/event-stream")) {

References

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