Skip to content

Commit f1cec19

Browse files
Send Mcp-Method header on Streamable HTTP client requests (SEP-2243)
The streamable HTTP client transport now mirrors the JSON-RPC method of outgoing requests and notifications into the Mcp-Method HTTP header, as required by SEP-2243. Responses do not carry a method and are sent without the header. Signed-off-by: Nikita Kibitkin <nikita.n.kibitkin@gmail.com>
1 parent d1ef187 commit f1cec19

3 files changed

Lines changed: 113 additions & 0 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,13 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
503503
transportSession.sessionId().get());
504504
}
505505

506+
if (sentMessage instanceof McpSchema.JSONRPCRequest request) {
507+
requestBuilder = requestBuilder.header(HttpHeaders.MCP_METHOD, request.method());
508+
}
509+
else if (sentMessage instanceof McpSchema.JSONRPCNotification notification) {
510+
requestBuilder = requestBuilder.header(HttpHeaders.MCP_METHOD, notification.method());
511+
}
512+
506513
var builder = requestBuilder.uri(uri)
507514
.header(HttpHeaders.ACCEPT, APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
508515
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_UTF8)

mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ public interface HttpHeaders {
2626
*/
2727
String PROTOCOL_VERSION = "MCP-Protocol-Version";
2828

29+
/**
30+
* Mirrors the JSON-RPC method of the request or notification carried in the body.
31+
* @see <a href=
32+
* "https://modelcontextprotocol.io/seps/2243-http-standardization">SEP-2243</a>
33+
*/
34+
String MCP_METHOD = "Mcp-Method";
35+
2936
/**
3037
* The HTTP Content-Length header.
3138
* @see <a href=

mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,27 @@
77
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
88
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
99
import io.modelcontextprotocol.common.McpTransportContext;
10+
import io.modelcontextprotocol.spec.HttpHeaders;
1011
import io.modelcontextprotocol.spec.McpSchema;
1112
import io.modelcontextprotocol.spec.McpTransportSessionClosedException;
1213
import io.modelcontextprotocol.spec.ProtocolVersions;
1314
import java.net.URI;
1415
import java.net.URISyntaxException;
16+
import java.net.http.HttpRequest;
1517
import java.util.Map;
1618
import java.util.function.Consumer;
1719
import java.util.function.Function;
1820

1921
import org.junit.jupiter.api.AfterAll;
2022
import org.junit.jupiter.api.BeforeAll;
2123
import org.junit.jupiter.api.Test;
24+
import org.mockito.ArgumentCaptor;
2225
import org.testcontainers.containers.GenericContainer;
2326
import org.testcontainers.containers.wait.strategy.Wait;
2427
import reactor.core.publisher.Mono;
2528
import reactor.test.StepVerifier;
2629

30+
import static org.assertj.core.api.Assertions.assertThat;
2731
import static org.mockito.ArgumentMatchers.any;
2832
import static org.mockito.ArgumentMatchers.eq;
2933
import static org.mockito.Mockito.atLeastOnce;
@@ -166,4 +170,99 @@ void testCloseInitialized() {
166170
.verify();
167171
}
168172

173+
@Test
174+
void testMcpMethodHeaderOnRequest() throws URISyntaxException {
175+
var uri = new URI(host + "/mcp");
176+
var mockRequestCustomizer = mock(McpSyncHttpClientRequestCustomizer.class);
177+
178+
var transport = HttpClientStreamableHttpTransport.builder(host)
179+
.httpRequestCustomizer(mockRequestCustomizer)
180+
.build();
181+
182+
withTransport(transport, (t) -> {
183+
var initializeRequest = McpSchema.InitializeRequest
184+
.builder(ProtocolVersions.MCP_2025_11_25, McpSchema.ClientCapabilities.builder().roots(true).build(),
185+
McpSchema.Implementation.builder("MCP Client", "0.3.1").build())
186+
.build();
187+
var testMessage = new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, "test-id", initializeRequest);
188+
189+
StepVerifier
190+
.create(t.sendMessage(testMessage).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context)))
191+
.verifyComplete();
192+
193+
var requestCaptor = ArgumentCaptor.forClass(HttpRequest.Builder.class);
194+
verify(mockRequestCustomizer, atLeastOnce()).customize(requestCaptor.capture(), eq("POST"), eq(uri), any(),
195+
eq(context));
196+
assertThat(requestCaptor.getValue().build().headers().firstValue(HttpHeaders.MCP_METHOD))
197+
.hasValue(McpSchema.METHOD_INITIALIZE);
198+
});
199+
}
200+
201+
@Test
202+
void testMcpMethodHeaderOnNotification() throws URISyntaxException {
203+
var uri = new URI(host + "/mcp");
204+
var mockRequestCustomizer = mock(McpSyncHttpClientRequestCustomizer.class);
205+
206+
var transport = HttpClientStreamableHttpTransport.builder(host)
207+
.httpRequestCustomizer(mockRequestCustomizer)
208+
.build();
209+
210+
withTransport(transport, (t) -> {
211+
var initializeRequest = McpSchema.InitializeRequest
212+
.builder(ProtocolVersions.MCP_2025_11_25, McpSchema.ClientCapabilities.builder().roots(true).build(),
213+
McpSchema.Implementation.builder("MCP Client", "0.3.1").build())
214+
.build();
215+
var initializeMessage = new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, "test-id",
216+
initializeRequest);
217+
StepVerifier
218+
.create(t.sendMessage(initializeMessage).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context)))
219+
.verifyComplete();
220+
221+
var testMessage = new McpSchema.JSONRPCNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED);
222+
223+
StepVerifier
224+
.create(t.sendMessage(testMessage).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context)))
225+
.verifyComplete();
226+
227+
var requestCaptor = ArgumentCaptor.forClass(HttpRequest.Builder.class);
228+
verify(mockRequestCustomizer, atLeastOnce()).customize(requestCaptor.capture(), eq("POST"), eq(uri), any(),
229+
eq(context));
230+
assertThat(requestCaptor.getValue().build().headers().firstValue(HttpHeaders.MCP_METHOD))
231+
.hasValue(McpSchema.METHOD_NOTIFICATION_INITIALIZED);
232+
});
233+
}
234+
235+
@Test
236+
void testNoMcpMethodHeaderOnResponse() throws URISyntaxException {
237+
var uri = new URI(host + "/mcp");
238+
var mockRequestCustomizer = mock(McpSyncHttpClientRequestCustomizer.class);
239+
240+
var transport = HttpClientStreamableHttpTransport.builder(host)
241+
.httpRequestCustomizer(mockRequestCustomizer)
242+
.build();
243+
244+
withTransport(transport, (t) -> {
245+
var initializeRequest = McpSchema.InitializeRequest
246+
.builder(ProtocolVersions.MCP_2025_11_25, McpSchema.ClientCapabilities.builder().roots(true).build(),
247+
McpSchema.Implementation.builder("MCP Client", "0.3.1").build())
248+
.build();
249+
var initializeMessage = new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, "test-id",
250+
initializeRequest);
251+
StepVerifier
252+
.create(t.sendMessage(initializeMessage).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context)))
253+
.verifyComplete();
254+
255+
var testMessage = McpSchema.JSONRPCResponse.result("test-id-2", Map.of());
256+
257+
StepVerifier
258+
.create(t.sendMessage(testMessage).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context)))
259+
.verifyComplete();
260+
261+
var requestCaptor = ArgumentCaptor.forClass(HttpRequest.Builder.class);
262+
verify(mockRequestCustomizer, atLeastOnce()).customize(requestCaptor.capture(), eq("POST"), eq(uri), any(),
263+
eq(context));
264+
assertThat(requestCaptor.getValue().build().headers().firstValue(HttpHeaders.MCP_METHOD)).isEmpty();
265+
});
266+
}
267+
169268
}

0 commit comments

Comments
 (0)