Skip to content

Commit d2487ae

Browse files
DebasishhhKehrlann
authored andcommitted
Standardize SSE response headers
- Extract SSE response preparation into HttpServletResponseUtils - Add X-Accel-Buffering: no header to SSE responses - Add no-transform to the SSE Cache-Control header - Drop Connection: keep-alive from SSE responses (prohibited in HTTP/2) Fixes #293 Signed-off-by: Daniel Garnier-Moiroux <[email protected]>
1 parent bb87ddf commit d2487ae

7 files changed

Lines changed: 129 additions & 13 deletions

File tree

‎mcp-core/src/main/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransport.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ private String eventFrame(JSONRPCMessage message) {
345345
private static PrintWriter startEventStream(HttpServletResponse response) throws IOException {
346346
response.setContentType(TEXT_EVENT_STREAM);
347347
response.setCharacterEncoding(UTF_8);
348-
response.setHeader("Cache-Control", "no-cache");
348+
response.setHeader("Cache-Control", "no-cache, no-transform");
349349
response.setHeader("X-Accel-Buffering", "no");
350350
response.setStatus(HttpServletResponse.SC_OK);
351351
response.flushBuffer();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.server.transport;
6+
7+
import jakarta.servlet.http.HttpServletResponse;
8+
9+
/**
10+
* Utility methods for working with {@link HttpServletResponse}. For internal use only.
11+
*
12+
* @author Daniel Garnier-Moiroux
13+
*/
14+
final class HttpServletResponseUtils {
15+
16+
private static final String TEXT_EVENT_STREAM = "text/event-stream";
17+
18+
private static final String UTF_8 = "UTF-8";
19+
20+
private HttpServletResponseUtils() {
21+
}
22+
23+
/**
24+
* Prepares the response for streaming Server-Sent Events: sets the content type,
25+
* character encoding and the headers that keep intermediaries from caching,
26+
* transforming or buffering the stream.
27+
* @param response The HTTP servlet response
28+
*/
29+
static void prepareSseResponse(HttpServletResponse response) {
30+
response.setContentType(TEXT_EVENT_STREAM);
31+
response.setCharacterEncoding(UTF_8);
32+
response.setHeader("Cache-Control", "no-cache, no-transform");
33+
response.setHeader("X-Accel-Buffering", "no");
34+
}
35+
36+
}

‎mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,10 +300,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
300300
return;
301301
}
302302

303-
response.setContentType("text/event-stream");
304-
response.setCharacterEncoding(UTF_8);
305-
response.setHeader("Cache-Control", "no-cache");
306-
response.setHeader("Connection", "keep-alive");
303+
HttpServletResponseUtils.prepareSseResponse(response);
307304

308305
String sessionId = UUID.randomUUID().toString();
309306
AsyncContext asyncContext = request.startAsync();

‎mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -426,10 +426,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
426426
McpTransportContext transportContext = this.contextExtractor.extract(request);
427427

428428
try {
429-
response.setContentType(TEXT_EVENT_STREAM);
430-
response.setCharacterEncoding(UTF_8);
431-
response.setHeader("Cache-Control", "no-cache");
432-
response.setHeader("Connection", "keep-alive");
429+
HttpServletResponseUtils.prepareSseResponse(response);
433430

434431
AsyncContext asyncContext = request.startAsync();
435432
asyncContext.setTimeout(0);
@@ -601,10 +598,7 @@ else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
601598
}
602599
else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
603600
// For streaming responses, we need to return SSE
604-
response.setContentType(TEXT_EVENT_STREAM);
605-
response.setCharacterEncoding(UTF_8);
606-
response.setHeader("Cache-Control", "no-cache");
607-
response.setHeader("Connection", "keep-alive");
601+
HttpServletResponseUtils.prepareSseResponse(response);
608602

609603
AsyncContext asyncContext = request.startAsync();
610604
asyncContext.setTimeout(0);

‎mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ void streamingToolCallReturnsSse() throws Exception {
211211
assertThat(response.statusCode()).isEqualTo(200);
212212
assertThat(response.headers().firstValue("Content-Type"))
213213
.hasValueSatisfying(v -> assertThat(v).contains("text/event-stream"));
214+
assertThat(response.headers().firstValue("Cache-Control")).contains("no-cache, no-transform");
215+
assertThat(response.headers().firstValue("X-Accel-Buffering")).contains("no");
216+
assertThat(response.headers().firstValue("Connection")).isEmpty();
214217
assertThat(response.body()).contains("notifications/progress");
215218
assertThat(response.body()).contains("\"result\"");
216219
}

‎mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,29 @@ void testRejectsWhenContentLengthHeaderExceedsLimit(String clientType) {
182182
}
183183
}
184184

185+
@Test
186+
void sseResponseHasStreamingHeaders() throws Exception {
187+
prepareAsyncServerBuilder().build();
188+
189+
var httpClient = HttpClient.newHttpClient();
190+
var sseRequest = HttpRequest.newBuilder()
191+
.uri(URI.create("http://localhost:" + PORT + CUSTOM_SSE_ENDPOINT))
192+
.header("Accept", "text/event-stream")
193+
.GET()
194+
.build();
195+
196+
var response = httpClient.send(sseRequest, HttpResponse.BodyHandlers.ofInputStream());
197+
try {
198+
assertThat(response.headers().firstValue("Content-Type")).contains("text/event-stream;charset=UTF-8");
199+
assertThat(response.headers().firstValue("Cache-Control")).contains("no-cache, no-transform");
200+
assertThat(response.headers().firstValue("X-Accel-Buffering")).contains("no");
201+
assertThat(response.headers().firstValue("Connection")).isEmpty();
202+
}
203+
finally {
204+
response.body().close();
205+
}
206+
}
207+
185208
@Test
186209
void rejectsWhenBodyBytesExceedLimitWithoutContentLengthHeader() throws Exception {
187210
var httpClient = HttpClient.newHttpClient();

‎mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.net.http.HttpResponse;
1515
import java.nio.ByteBuffer;
1616
import java.time.Duration;
17+
import java.util.List;
1718
import java.util.Map;
1819
import java.util.Queue;
1920
import java.util.concurrent.CompletableFuture;
@@ -212,10 +213,72 @@ void testMissingHandlerReturnsMethodNotFoundError() {
212213
}
213214
}
214215

216+
@Test
217+
void listeningStreamHasStreamingHeaders() throws Exception {
218+
prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
219+
var sessionId = initializeSession(httpClient);
220+
221+
var get = HttpRequest.newBuilder()
222+
.uri(URI.create("http://localhost:" + PORT + MESSAGE_ENDPOINT))
223+
.header("Accept", "text/event-stream")
224+
.header(HttpHeaders.MCP_SESSION_ID, sessionId)
225+
.GET()
226+
.build();
227+
228+
var response = httpClient.send(get, HttpResponse.BodyHandlers.ofInputStream());
229+
try {
230+
assertSseHeaders(response);
231+
}
232+
finally {
233+
response.body().close();
234+
}
235+
}
236+
237+
@Test
238+
void toolCallSseResponseHasStreamingHeaders() throws Exception {
239+
prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
240+
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
241+
.tools(McpServerFeatures.AsyncToolSpecification.builder()
242+
.tool(McpSchema.Tool.builder("echo", EMPTY_JSON_SCHEMA).description("returns immediately").build())
243+
.callHandler((exchange,
244+
request) -> Mono.just(McpSchema.CallToolResult.builder()
245+
.content(List.of(McpSchema.TextContent.builder("ok").build()))
246+
.isError(false)
247+
.build()))
248+
.build())
249+
.build();
250+
var sessionId = initializeSession(httpClient);
251+
252+
var post = HttpRequest.newBuilder()
253+
.uri(URI.create("http://localhost:" + PORT + MESSAGE_ENDPOINT))
254+
.header("Content-Type", "application/json")
255+
.header("Accept", "text/event-stream, application/json")
256+
.header(HttpHeaders.MCP_SESSION_ID, sessionId)
257+
.POST(HttpRequest.BodyPublishers
258+
.ofString("{\"jsonrpc\":\"2.0\",\"id\":\"call-1\",\"method\":\"tools/call\","
259+
+ "\"params\":{\"name\":\"echo\",\"arguments\":{}}}"))
260+
.build();
261+
262+
var response = httpClient.send(post, HttpResponse.BodyHandlers.ofInputStream());
263+
try {
264+
assertSseHeaders(response);
265+
}
266+
finally {
267+
response.body().close();
268+
}
269+
}
270+
215271
@Override
216272
protected void prepareClients(int port, String mcpEndpoint) {
217273
}
218274

275+
private static void assertSseHeaders(HttpResponse<?> response) {
276+
assertThat(response.headers().firstValue("Content-Type")).contains("text/event-stream;charset=UTF-8");
277+
assertThat(response.headers().firstValue("Cache-Control")).contains("no-cache, no-transform");
278+
assertThat(response.headers().firstValue("X-Accel-Buffering")).contains("no");
279+
assertThat(response.headers().firstValue("Connection")).isEmpty();
280+
}
281+
219282
static McpTransportContextExtractor<HttpServletRequest> TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext
220283
.create(Map.of("important", "value"));
221284

0 commit comments

Comments
 (0)