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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions