From bb0ca87da5361d21d277bdb65587427d60c6bb90 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Wed, 26 Aug 2026 16:08:10 +0200 Subject: [PATCH] Clarify McpTransportContext javadoc See #1075 Signed-off-by: Daniel Garnier-Moiroux --- .../client/McpClient.java | 22 ++++++++++++++++--- .../McpAsyncHttpClientRequestCustomizer.java | 12 ++++++++-- .../common/McpTransportContext.java | 17 +++++++++++++- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java index 1af4eea..328566a 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java @@ -494,9 +494,10 @@ public interface McpClient { * calling any client operation. This allows to extract thread-locals and hand * them over to the underlying transport. *

- * There is no direct equivalent in {@link AsyncSpec}. To achieve the same result, - * append {@code contextWrite(McpTransportContext.KEY, context)} to any - * {@link McpAsyncClient} call. + * The supplier is invoked at subscription time, on the calling thread, and the + * resulting context is visible to the transport for every leg of the operation, + * including connections opened on other threads, such as the SSE stream started + * during initialization. * @param contextProvider A supplier to create a context * @return This builder for method chaining */ @@ -580,6 +581,21 @@ public interface McpClient { *

  • Change notification handlers for tools, resources, and prompts *
  • Custom message sampling logic * + * + *

    + * Unlike {@link SyncSpec}, this specification has no + * {@code transportContextProvider}. This is deliberate: in a reactive pipeline the + * caller owns the Reactor context, and whatever the transport needs, such as an + * {@link McpTransportContext}, can be written into it directly. Write it once where + * the reactive chain starts, rather than at every call site, and every + * {@link McpAsyncClient} call downstream inherits it, including the connections + * opened during initialization:

    {@code
    +	 * chain.filter(exchange)
    +	 *     .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context));
    +	 * }
    To bridge thread-locals into the chain, use Reactor's context propagation + * support. + * + * @see SyncSpec#transportContextProvider(Supplier) */ class AsyncSpec { diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java index 756b39c..f2951d8 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpClientRequestCustomizer.java @@ -7,18 +7,26 @@ package io.modelcontextprotocol.client.transport.customizer; import java.net.URI; import java.net.http.HttpRequest; +import io.modelcontextprotocol.client.McpClient.SyncSpec; +import io.modelcontextprotocol.common.McpTransportContext; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.util.annotation.Nullable; -import io.modelcontextprotocol.common.McpTransportContext; - /** * Customize {@link HttpRequest.Builder} before executing the request, in either SSE or * Streamable HTTP transport. *

    * When used in a non-blocking context, implementations MUST be non-blocking. + *

    + * The {@link McpTransportContext} handed to {@code customize} is read from the Reactor + * context, under {@link McpTransportContext#KEY}, and is + * {@link McpTransportContext#EMPTY} when the caller wrote nothing there. Write it once + * where the reactive chain starts, with + * {@code contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context))}, rather than at + * every call site. With a synchronous client, configure + * {@link SyncSpec#transportContextProvider} instead. * * @author Daniel Garnier-Moiroux */ diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java b/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java index 46a2ccf..4691c13 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/common/McpTransportContext.java @@ -11,13 +11,28 @@ import java.util.Map; * Context associated with the transport layer. It allows to add transport-level metadata * for use further down the line. Specifically, it can be beneficial to extract HTTP * request metadata for use in MCP feature implementations. + *

    + * The context travels in the Reactor context, under {@link #KEY}. On the server side, the + * transports populate it from the incoming request. On the client side, writing it is the + * caller's responsibility: + *

      + *
    • with a synchronous client, configure + * {@code McpClient.SyncSpec#transportContextProvider(Supplier)}, which is invoked on the + * calling thread before every operation; + *
    • with an asynchronous client, write it into the Reactor context once, where the + * reactive chain starts, using + * {@code contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context))}. Every client + * call downstream inherits it, so there is no need to repeat it at each call site. + *
    * * @author Dariusz Jędrzejczyk */ public interface McpTransportContext { /** - * Key for use in Reactor Context to transport the context to user land. + * Key for use in Reactor Context to transport the context to user land. Write the + * context under this key to make it visible to the transport, for example + * {@code contextWrite(ctx -> ctx.put(McpTransportContext.KEY, context))}. */ String KEY = "MCP_TRANSPORT_CONTEXT";