Add McpTransportContext to McpSyncClient (#522)

* McpSyncClient: introduce McpTransportContext

- McpSyncClient should be considered thread-agnostic, and therefore
  consumers cannot rely on thread locals to propagate "context", e.g.
  pass down the Servlet request reference in a server context.
- This PR introduces a mechanism for populating an McpTransportContext
  before executing client operations, and reworks the HTTP request
  customizers to leverage that McpTransportContext.
- Move McpTransportContext from server to common package for shared client/server usage
- Make McpTransportContext immutable by removing put() and copy() methods
- Add static create() factory method for creating contexts with predefined data
- Update McpTransportContextExtractor to return context instead of modifying one
- Replace DefaultMcpTransportContext mutable implementation with immutable version
- Update all transport implementations to use McpTransportContext.EMPTY as default
- Rename *HttpRequestCustomizer -> *HttpClientRequestCustomizer
- Add end-to-end McpTransportContextIntegrationTests
- This PR introduces a breaking change to the Sync/Async request
  customizers.

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Daniel Garnier-Moiroux 2025-09-02 11:09:40 +02:00 • committed by GitHub
commit 629464b1d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 746 additions and 327 deletions

View file

@ -12,8 +12,7 @@ import java.util.concurrent.ConcurrentHashMap;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.DefaultMcpTransportContext;
import io.modelcontextprotocol.server.McpTransportContext;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpTransportContextExtractor;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
@ -201,7 +200,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
String sseEndpoint, Duration keepAliveInterval) {
this(objectMapper, baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval,
(serverRequest, context) -> context);
(serverRequest) -> McpTransportContext.EMPTY);
}
/**
@ -344,7 +343,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
McpTransportContext transportContext = this.contextExtractor.extract(request);
return ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
@ -401,7 +400,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
.bodyValue(new McpError("Session not found: " + request.queryParam("sessionId").get()));
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
McpTransportContext transportContext = this.contextExtractor.extract(request);
return request.bodyToMono(String.class).flatMap(body -> {
try {
@ -491,7 +490,8 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
private Duration keepAliveInterval;
private McpTransportContextExtractor<ServerRequest> contextExtractor = (serverRequest, context) -> context;
private McpTransportContextExtractor<ServerRequest> contextExtractor = (
serverRequest) -> McpTransportContext.EMPTY;
/**
* Sets the ObjectMapper to use for JSON serialization/deserialization of MCP

View file

@ -5,13 +5,12 @@
package io.modelcontextprotocol.server.transport;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpStatelessServerHandler;
import io.modelcontextprotocol.server.DefaultMcpTransportContext;
import io.modelcontextprotocol.server.McpTransportContextExtractor;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
import io.modelcontextprotocol.server.McpTransportContext;
import io.modelcontextprotocol.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -97,7 +96,7 @@ public class WebFluxStatelessServerTransport implements McpStatelessServerTransp
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
McpTransportContext transportContext = this.contextExtractor.extract(request);
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!(acceptHeaders.contains(MediaType.APPLICATION_JSON)
@ -151,7 +150,8 @@ public class WebFluxStatelessServerTransport implements McpStatelessServerTransp
private String mcpEndpoint = "/mcp";
private McpTransportContextExtractor<ServerRequest> contextExtractor = (serverRequest, context) -> context;
private McpTransportContextExtractor<ServerRequest> contextExtractor = (
serverRequest) -> McpTransportContext.EMPTY;
private Builder() {
// used by a static method

View file

@ -6,7 +6,7 @@ package io.modelcontextprotocol.server.transport;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.DefaultMcpTransportContext;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpTransportContextExtractor;
import io.modelcontextprotocol.spec.HttpHeaders;
import io.modelcontextprotocol.spec.McpError;
@ -15,7 +15,6 @@ import io.modelcontextprotocol.spec.McpStreamableServerSession;
import io.modelcontextprotocol.spec.McpStreamableServerTransport;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.ProtocolVersions;
import io.modelcontextprotocol.server.McpTransportContext;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.KeepAliveScheduler;
@ -166,7 +165,7 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
McpTransportContext transportContext = this.contextExtractor.extract(request);
return Mono.defer(() -> {
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
@ -221,7 +220,7 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
McpTransportContext transportContext = this.contextExtractor.extract(request);
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!(acceptHeaders.contains(MediaType.APPLICATION_JSON)
@ -309,7 +308,7 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
McpTransportContext transportContext = this.contextExtractor.extract(request);
return Mono.defer(() -> {
if (!request.headers().asHttpHeaders().containsKey(HttpHeaders.MCP_SESSION_ID)) {
@ -402,7 +401,8 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
private String mcpEndpoint = "/mcp";
private McpTransportContextExtractor<ServerRequest> contextExtractor = (serverRequest, context) -> context;
private McpTransportContextExtractor<ServerRequest> contextExtractor = (
serverRequest) -> McpTransportContext.EMPTY;
private boolean disallowDelete;

View file

@ -5,6 +5,7 @@
package io.modelcontextprotocol;
import java.time.Duration;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -20,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
import io.modelcontextprotocol.server.McpServer.SingleSessionSyncSpecification;
@ -42,10 +44,8 @@ class WebFluxSseIntegrationTests extends AbstractMcpClientServerIntegrationTests
private WebFluxSseServerTransportProvider mcpServerTransportProvider;
static McpTransportContextExtractor<ServerRequest> TEST_CONTEXT_EXTRACTOR = (r, tc) -> {
tc.put("important", "value");
return tc;
};
static McpTransportContextExtractor<ServerRequest> TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext
.create(Map.of("important", "value"));
@Override
protected void prepareClients(int port, String mcpEndpoint) {

View file

@ -5,6 +5,7 @@
package io.modelcontextprotocol;
import java.time.Duration;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -20,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
import io.modelcontextprotocol.server.McpServer.SyncSpecification;
@ -40,10 +42,8 @@ class WebFluxStreamableIntegrationTests extends AbstractMcpClientServerIntegrati
private WebFluxStreamableServerTransportProvider mcpStreamableServerTransportProvider;
static McpTransportContextExtractor<ServerRequest> TEST_CONTEXT_EXTRACTOR = (r, tc) -> {
tc.put("important", "value");
return tc;
};
static McpTransportContextExtractor<ServerRequest> TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext
.create(Map.of("important", "value"));
@Override
protected void prepareClients(int port, String mcpEndpoint) {