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
Description
HttpMcpProxyhas aparseSseResponse()method for handling SSE responses, but routes to it via a strict equality check: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-8to alltext/*media types whenContent-Typeis not set explicitly in the response headers.Starlette MWE
$ 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-8Failing test (MWE)
This test can be added to the existing
HttpMcpProxyTest— it uses the sameHttpServerinfrastructure already in place:Fix
References
Content-Typeare validresponses.py— appends; charset=utf-8to alltext/*types automaticallysse.py—EventSourceResponsedoes not setContent-Typeexplicitly, triggering Starlette's charset append