Skip to content

Commit 039715d

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 <git@garnier.wf>
1 parent bb87ddf commit 039715d

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ 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");
214216
assertThat(response.body()).contains("notifications/progress");
215217
assertThat(response.body()).contains("\"result\"");
216218
}

‎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 sseResponseIncludesXAccelBufferingHeader() throws Exception {
187+
// https://github.com/modelcontextprotocol/java-sdk/issues/293 - without this
188+
// header, proxies like Nginx buffer the SSE response, breaking real-time
189+
// streaming.
190+
prepareAsyncServerBuilder().build();
191+
192+
var httpClient = HttpClient.newHttpClient();
193+
var sseRequest = HttpRequest.newBuilder()
194+
.uri(URI.create("http://localhost:" + PORT + CUSTOM_SSE_ENDPOINT))
195+
.header("Accept", "text/event-stream")
196+
.GET()
197+
.build();
198+
199+
var response = httpClient.send(sseRequest, HttpResponse.BodyHandlers.ofInputStream());
200+
try {
201+
assertThat(response.headers().firstValue("X-Accel-Buffering")).contains("no");
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: 64 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,6 +213,69 @@ void testMissingHandlerReturnsMethodNotFoundError() {
212213
}
213214
}
214215

216+
/**
217+
* https://github.com/modelcontextprotocol/java-sdk/issues/293 - without this header,
218+
* proxies like Nginx buffer the SSE response, breaking real-time streaming.
219+
*/
220+
@Test
221+
void listeningStreamIncludesXAccelBufferingHeader() throws Exception {
222+
prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
223+
var sessionId = initializeSession(httpClient);
224+
225+
var get = HttpRequest.newBuilder()
226+
.uri(URI.create("http://localhost:" + PORT + MESSAGE_ENDPOINT))
227+
.header("Accept", "text/event-stream")
228+
.header(HttpHeaders.MCP_SESSION_ID, sessionId)
229+
.GET()
230+
.build();
231+
232+
var response = httpClient.send(get, HttpResponse.BodyHandlers.ofInputStream());
233+
try {
234+
assertThat(response.headers().firstValue("X-Accel-Buffering")).contains("no");
235+
}
236+
finally {
237+
response.body().close();
238+
}
239+
}
240+
241+
/**
242+
* https://github.com/modelcontextprotocol/java-sdk/issues/293 - same header is also
243+
* required on the SSE response a POST request opens for a streamed tool call.
244+
*/
245+
@Test
246+
void toolCallSseResponseIncludesXAccelBufferingHeader() throws Exception {
247+
prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
248+
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
249+
.tools(McpServerFeatures.AsyncToolSpecification.builder()
250+
.tool(McpSchema.Tool.builder("echo", EMPTY_JSON_SCHEMA).description("returns immediately").build())
251+
.callHandler((exchange,
252+
request) -> Mono.just(McpSchema.CallToolResult.builder()
253+
.content(List.of(McpSchema.TextContent.builder("ok").build()))
254+
.isError(false)
255+
.build()))
256+
.build())
257+
.build();
258+
var sessionId = initializeSession(httpClient);
259+
260+
var post = HttpRequest.newBuilder()
261+
.uri(URI.create("http://localhost:" + PORT + MESSAGE_ENDPOINT))
262+
.header("Content-Type", "application/json")
263+
.header("Accept", "text/event-stream, application/json")
264+
.header(HttpHeaders.MCP_SESSION_ID, sessionId)
265+
.POST(HttpRequest.BodyPublishers
266+
.ofString("{\"jsonrpc\":\"2.0\",\"id\":\"call-1\",\"method\":\"tools/call\","
267+
+ "\"params\":{\"name\":\"echo\",\"arguments\":{}}}"))
268+
.build();
269+
270+
var response = httpClient.send(post, HttpResponse.BodyHandlers.ofInputStream());
271+
try {
272+
assertThat(response.headers().firstValue("X-Accel-Buffering")).contains("no");
273+
}
274+
finally {
275+
response.body().close();
276+
}
277+
}
278+
215279
@Override
216280
protected void prepareClients(int port, String mcpEndpoint) {
217281
}

0 commit comments

Comments
 (0)