Skip to content

Commit 7ee277b

Browse files
committed
fix: reject duplicate streamable HTTP initialize requests
1 parent 305e4cb commit 7ee277b

3 files changed

Lines changed: 152 additions & 0 deletions

File tree

disclosure.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This change was submitted despite me reading the rules and understanding AI contribution guidelines.

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,14 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
449449
return;
450450
}
451451

452+
String sessionId = request.getHeader(HttpHeaders.MCP_SESSION_ID);
453+
if (sessionId != null && this.sessions.containsKey(sessionId)) {
454+
this.responseJsonRpcError(response, HttpServletResponse.SC_BAD_REQUEST, jsonrpcRequest.id(),
455+
McpSchema.ErrorCodes.INVALID_REQUEST,
456+
"Duplicate initialize request for active session: " + sessionId);
457+
return;
458+
}
459+
452460
McpSchema.InitializeRequest initializeRequest = jsonMapper.convertValue(jsonrpcRequest.params(),
453461
new TypeRef<McpSchema.InitializeRequest>() {
454462
});
@@ -570,6 +578,18 @@ else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
570578
}
571579
}
572580

581+
private void responseJsonRpcError(HttpServletResponse response, int httpCode, Object requestId, int errorCode,
582+
String message) throws IOException {
583+
response.setContentType(APPLICATION_JSON);
584+
response.setCharacterEncoding(UTF_8);
585+
response.setStatus(httpCode);
586+
McpSchema.JSONRPCResponse errorResponse = McpSchema.JSONRPCResponse.error(requestId,
587+
new McpSchema.JSONRPCResponse.JSONRPCError(errorCode, message));
588+
PrintWriter writer = response.getWriter();
589+
writer.write(this.jsonMapper.writeValueAsString(errorResponse));
590+
writer.flush();
591+
}
592+
573593
/**
574594
* Handles DELETE requests for session deletion.
575595
* @param request The HTTP servlet request
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2024-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.server.transport;
6+
7+
import java.io.ByteArrayInputStream;
8+
import java.io.PrintWriter;
9+
import java.io.StringWriter;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.Collections;
12+
import java.util.concurrent.atomic.AtomicInteger;
13+
14+
import jakarta.servlet.ReadListener;
15+
import jakarta.servlet.ServletInputStream;
16+
import jakarta.servlet.http.HttpServletRequest;
17+
import jakarta.servlet.http.HttpServletResponse;
18+
import org.junit.jupiter.api.Test;
19+
import reactor.core.publisher.Mono;
20+
21+
import io.modelcontextprotocol.json.McpJsonMapper;
22+
import io.modelcontextprotocol.spec.HttpHeaders;
23+
import io.modelcontextprotocol.spec.McpSchema;
24+
import io.modelcontextprotocol.spec.McpStreamableServerSession;
25+
import io.modelcontextprotocol.spec.json.gson.GsonMcpJsonMapper;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.mockito.Mockito.mock;
29+
import static org.mockito.Mockito.verify;
30+
import static org.mockito.Mockito.when;
31+
32+
class HttpServletStreamableServerDuplicateInitializeTests {
33+
34+
private static final String SESSION_ID = "active-session";
35+
36+
private final McpJsonMapper jsonMapper = new GsonMcpJsonMapper();
37+
38+
@Test
39+
void rejectsDuplicateInitializeForActiveSession() throws Exception {
40+
HttpServletStreamableServerTransportProvider provider = HttpServletStreamableServerTransportProvider.builder()
41+
.jsonMapper(this.jsonMapper)
42+
.build();
43+
McpStreamableServerSession session = mock(McpStreamableServerSession.class);
44+
when(session.getId()).thenReturn(SESSION_ID);
45+
AtomicInteger sessionStarts = new AtomicInteger();
46+
provider.setSessionFactory(request -> {
47+
sessionStarts.incrementAndGet();
48+
return new McpStreamableServerSession.McpStreamableServerSessionInit(session,
49+
Mono.just(testInitializeResult()));
50+
});
51+
52+
Exchange initial = initializeExchange(null, "init-1");
53+
provider.doPost(initial.request(), initial.response());
54+
55+
verify(initial.response()).setStatus(HttpServletResponse.SC_OK);
56+
verify(initial.response()).setHeader(HttpHeaders.MCP_SESSION_ID, SESSION_ID);
57+
58+
Exchange duplicate = initializeExchange(SESSION_ID, "init-2");
59+
provider.doPost(duplicate.request(), duplicate.response());
60+
61+
verify(duplicate.response()).setStatus(HttpServletResponse.SC_BAD_REQUEST);
62+
assertThat(duplicate.body().toString()).contains("\"jsonrpc\":\"2.0\"", "\"id\":\"init-2\"", "\"code\":-32600",
63+
"Duplicate initialize request for active session");
64+
assertThat(sessionStarts).hasValue(1);
65+
}
66+
67+
private Exchange initializeExchange(String sessionId, String requestId) throws Exception {
68+
HttpServletRequest request = mock(HttpServletRequest.class);
69+
HttpServletResponse response = mock(HttpServletResponse.class);
70+
StringWriter body = new StringWriter();
71+
when(request.getRequestURI()).thenReturn("/mcp");
72+
when(request.getHeader("Accept")).thenReturn("text/event-stream, application/json");
73+
when(request.getHeader(HttpHeaders.MCP_SESSION_ID)).thenReturn(sessionId);
74+
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
75+
when(request.getInputStream()).thenReturn(servletInputStream(this.jsonMapper
76+
.writeValueAsString(
77+
new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, requestId, testInitializeRequest()))
78+
.getBytes(StandardCharsets.UTF_8)));
79+
when(response.getWriter()).thenReturn(new PrintWriter(body, true));
80+
return new Exchange(request, response, body);
81+
}
82+
83+
private McpSchema.InitializeRequest testInitializeRequest() {
84+
return McpSchema.InitializeRequest
85+
.builder("2025-11-25", new McpSchema.ClientCapabilities(null, null, null, null),
86+
new McpSchema.Implementation("test-client", "1.0.0"))
87+
.build();
88+
}
89+
90+
private McpSchema.InitializeResult testInitializeResult() {
91+
return McpSchema.InitializeResult
92+
.builder("2025-11-25", new McpSchema.ServerCapabilities(null, null, null, null, null, null),
93+
new McpSchema.Implementation("test-server", "1.0.0"))
94+
.build();
95+
}
96+
97+
private static ServletInputStream servletInputStream(byte[] data) {
98+
ByteArrayInputStream delegate = new ByteArrayInputStream(data);
99+
return new ServletInputStream() {
100+
101+
@Override
102+
public boolean isFinished() {
103+
return delegate.available() == 0;
104+
}
105+
106+
@Override
107+
public boolean isReady() {
108+
return true;
109+
}
110+
111+
@Override
112+
public void setReadListener(ReadListener readListener) {
113+
}
114+
115+
@Override
116+
public int read() {
117+
return delegate.read();
118+
}
119+
120+
@Override
121+
public int read(byte[] b, int off, int len) {
122+
return delegate.read(b, off, len);
123+
}
124+
125+
};
126+
}
127+
128+
private record Exchange(HttpServletRequest request, HttpServletResponse response, StringWriter body) {
129+
}
130+
131+
}

0 commit comments

Comments
 (0)