Skip to content

Conversation

Kehrlann
Copy link
Contributor

@Kehrlann Kehrlann commented Aug 27, 2025

This PR adds McpTransportContext to MCP Clients, and makes it available to (Async|Sync)HttpRequestCustomizers.

Motivation and Context

The motivation is twofold:

  • It creates a unified API for reading the "context" in which an MCP Client request is issued, regardless of sync/async. It can be consumed by HttpClient-based transports through HttpRequestCustomizer, or through the reactor context in WebClient-based transports.
  • It also addresses limitations with thread local usage in request customizers, see TransportContext for Client side #479

How Has This Been Tested?

Test with Spring AI + Spring Security for passing JWT tokens to MCP Sync clients.

Breaking Changes

Following the move of (Async|Sync)HttpRequestCustomizer to Mcp(Async|Sync)HttpRequestCustomizer, itself a breaking change, I also changed the signatures of those classes.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@Kehrlann Kehrlann force-pushed the dgarnier/client-mcptransportcontext branch 8 times, most recently from 15bb3f9 to 703d363 Compare August 29, 2025 09:47
@Kehrlann Kehrlann changed the title DRAFT: Add McpTransportContext in clients Add McpTransportContext to McpSyncClient Aug 29, 2025
@Kehrlann Kehrlann force-pushed the dgarnier/client-mcptransportcontext branch from 703d363 to 4f7a937 Compare August 29, 2025 10:02
@Kehrlann Kehrlann marked this pull request as ready for review August 29, 2025 10:17
@tzolov tzolov added this to the 0.12.0 milestone Aug 29, 2025
Copy link
Member

@chemicL chemicL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great progress! Left some comments after our sync discussion :)


import io.modelcontextprotocol.server.McpTransportContext;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move McpTransportContext up a level into a new common package. It will be a breaking change, but it's a super recent type.

@@ -183,6 +185,8 @@ class SyncSpec {

private Function<ElicitRequest, ElicitResult> elicitationHandler;

private Supplier<McpTransportContext> contextProvider = McpTransportContext.EMPTY::copy;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to copy here. We should ensure that the copy is made whenever there is an intention for the user to modify the context, e.g. when creating an empty one that is being filled in and used as the provided result.

A couple of ideas:

  • Hide DefaultMcpTransportContext (currently it's public, let's make it package private)
  • Add factory methods to McpTransportContext: empty() which returns the immutable instance; and create() which creates an empty but mutable container.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another note on the McpTransportContext:

We use it already on the server side, where the extractor expects a mutable instance that it can fill in.
With the client we have the opposite where the provider creates an instance and expects to mutate it.

On the read side, however we have the user handlers in server specification that are only supposed to read contents and we should ensure that mutations are not allowed because there is no use of mutating the contents.

On the client end, the customizer is the reader also so it should be disallowed to write something to the context.

Therefore, perhaps let's break the API a little bit:

  1. McpTransportContext#put is removed! The McpTransportContext is just a container of immutable context
  2. McpTransportExtractor - deprecate McpTransportContext extract(T request, McpTransportContext transportContext) or even remove it (BREAKING) and just have McpTransportContext extract(T request) that allows to call McpTransportContext.create(Map) which would accept a Map that is already populated and now the McpTransportContext is only the view.
  3. Client's transport context provider is also creating an instance like the extractor.
  4. Customizer is just supposed to read values using get(K).

* Streamable HTTP transport.
* Streamable HTTP transport. Do not rely on thread-locals in this implementation, instead
* use {@link SyncSpec#transportContextProvider} to extract context, and then consume it
* through {@link McpTransportContext}.
*
* @author Daniel Garnier-Moiroux
*/
public interface McpSyncHttpRequestCustomizer {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not rename it to McpSyncHttpClientRequestCustomizer to reflect that it is tied to JDK's HttpClient.

* @param contextProvider A supplier to create a context
* @return This builder for method chaining
*/
public SyncSpec transportContextProvider(Supplier<McpTransportContext> contextProvider) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's perhaps add a comment so users are not too confused by the lack of this method on the AsyncSpec to clarify that the means to achieve the same is to simply append a contextWrite(McpTransportContext.KEY, context) to any McpAsyncClient API call.

@@ -177,36 +185,43 @@ public boolean closeGracefully() {
public McpSchema.InitializeResult initialize() {
// TODO: block takes no argument here as we assume the async client is
// configured with a requestTimeout at all times
return this.delegate.initialize().block();
var context = this.contextProvider.get();
return this.delegate.initialize().contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context)).block();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding a method like Mono<?> withProvidedContext(...) that wraps all the calls and attaches .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, this.contextProvider.get())

@@ -247,7 +250,9 @@ private Mono<Disposable> reconnect(McpTransportStream<Disposable> stream) {
.header("Cache-Control", "no-cache")
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
.GET();
return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null));
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's perhaps use the innermost Context to be super certain that if at any point we alter the inner chain we do still grab the correct reference here. Replacing Mono.defer on line 236 with Mono.deferContextual(connectionCtx -> { should do the trick.

mcpSyncClient -> {
mcpSyncClient.initialize();

var transportCaptor = ArgumentCaptor.forClass(McpTransportContext.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, perhaps we can add equality overrides to DefaultMcpTransportContext? :)

@Kehrlann Kehrlann force-pushed the dgarnier/client-mcptransportcontext branch from 4f7a937 to 477fbd6 Compare August 29, 2025 14:59
- 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.
- This introduces a breaking change to the Sync/Async request
  customizers.

Signed-off-by: Daniel Garnier-Moiroux <[email protected]>
@Kehrlann Kehrlann force-pushed the dgarnier/client-mcptransportcontext branch from 477fbd6 to 98eb323 Compare August 29, 2025 18:42
@Kehrlann
Copy link
Contributor Author

@chemicL @tzolov I have applied the requested changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants