Skip to content

Commit 32331f9

Browse files
Kehrlanntzolov
authored andcommitted
Java SDK docs: document hooks for customizing HTTP requests
Signed-off-by: Daniel Garnier-Moiroux <[email protected]>
1 parent 6444639 commit 32331f9

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed

docs/sdk/java/mcp-client.mdx

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,71 @@ The transport layer handles the communication between MCP clients and servers, p
186186
```
187187
</Tab>
188188
</Tabs>
189+
190+
**HttpClient: Customizing HTTP requests**
191+
192+
To customize the base HTTP request builder used for every request, provide a custom `HttpRequestBuilder`.
193+
This is available in both Streamable HTTP and SSE transports.
194+
When using a custom `HttpRequest.Builder`, every HTTP request issued by the transport
195+
will use the hardcoded configuration from the builder.
196+
For example, this is useful for providing a never-expiring security token in a header.
197+
To add a `X-Custom-Header` header to every request, use:
198+
199+
```java
200+
var requestBuilder = HttpRequest
201+
.newBuilder()
202+
.header("X-Custom-Header", "some header value");
203+
204+
HttpClientStreamableHttpTransport
205+
.builder("https://mcp.example.com")
206+
.requestBuilder(requestBuilder)
207+
.build();
208+
```
209+
210+
To dynamically modify HTTP request before they are issued, implement either `McpSyncHttpClientRequestCustomizer` or `McpAsyncHttpClientRequestCustomizer`.
211+
Choose the request customizer matching the MCP Client type, sync or async.
212+
Note that thread-locals may not be available in the customizers, context-related information
213+
must be accessed through `McpTransportContext` (see [adding context information](#adding-context-information)).
214+
Example implementations:
215+
216+
```java
217+
// Sync
218+
class MyRequestCustomizer implements McpSyncHttpClientRequestCustomizer {
219+
@Override
220+
public void customize(HttpRequest.Builder builder, String method,
221+
URI endpoint, String body, McpTransportContext context) {
222+
// ... custom logic ...
223+
var token = obtainAccessToken(context);
224+
builder.header("Authorization", "Bearer " + token);
225+
}
226+
}
227+
228+
// Async
229+
class MyAsyncRequestCustomizer implements McpAsyncHttpClientRequestCustomizer {
230+
@Override
231+
public Publisher<HttpRequest.Builder> customize(HttpRequest.Builder builder,
232+
String method, URI endpoint, String body, McpTransportContext context) {
233+
// ... custom logic ...
234+
Mono<String> token = obtainAccessToken(context);
235+
return token.map(t ->
236+
builder.copy()
237+
.header("Authorization", "Bearer " + t)
238+
);
239+
}
240+
}
241+
```
242+
243+
The transports, both Streamable HTTP and SSE, can be configured to use a single customizer.
244+
245+
```java
246+
HttpClientStreamableHttpTransport
247+
.builder("https://mcp.example.com")
248+
.httpRequestCustomizer(new MyRequestCustomizer()) // sync
249+
.asyncHttpRequestCustomizer(new MyAsyncRequestCustomizer()) // OR async
250+
.build();
251+
```
252+
253+
To compose multiple customizers, use `DelegatingMcpSyncHttpClientRequestCustomizer` or `DelegatingMcpAsyncHttpClientRequestCustomizer`.
189254
</Tab>
190255
<Tab title="WebClient">
191256
<p>WebClient-based client transport. Requires the `mcp-spring-webflux` dependency.</p>
@@ -214,6 +279,49 @@ The transport layer handles the communication between MCP clients and servers, p
214279

215280
</Tab>
216281
</Tabs>
282+
283+
**WebClient: Customizing HTTP requests**
284+
285+
To customize outgoing HTTP requests, provide a custom `WebClient.Builder`.
286+
When using a custom builder, every request sent by the transport will be customized.
287+
For example, this is useful for providing a never-expiring security token in a header.
288+
To add a `X-Custom-Header` header to every request, use:
289+
290+
```java
291+
var webClientBuilder = WebClient.builder()
292+
.defaultHeader("X-Custom-Header", "some header value");
293+
294+
McpTransport transport = WebClientStreamableHttpTransport
295+
.builder(webClientBuilder)
296+
.build();
297+
```
298+
299+
To dynamically modify HTTP request, the builder has a dedicated API, `ExchangeFilterFunction`.
300+
In that function, the new request can be computed at run time, and additional information can
301+
be obtained from the [Reactor context](https://projectreactor.io/docs/core/release/reference/advancedFeatures/context.html).
302+
It is common to store context information in an `McpTransportContext` within the Reactor context,
303+
typically in McpSyncClient, but any context key can be used. For example:
304+
305+
```java
306+
WebClient.builder()
307+
.filter((request, next) -> {
308+
return Mono.deferContextual(ctx -> {
309+
var transportContext = ctx.get(McpTransportContext.KEY);
310+
var otherInfo = ctx.get("your-context-key");
311+
Mono<String> token = obtainAccessToken(transportContext, otherInfo);
312+
return token.map(t -> {
313+
var newRequest = ClientRequest.from(request)
314+
.header("Authorization", "Bearer " + t)
315+
.build();
316+
// Ensure you call next.exchange to execute the HTTP request
317+
return next.exchange(newRequest);
318+
});
319+
});
320+
});
321+
```
322+
323+
To learn how to populate context information, see [adding context information](#adding-context-information).
324+
217325
</Tab>
218326

219327
</Tabs>
@@ -525,3 +633,72 @@ Mono<CompleteResult> result = mcpClient.completeCompletion(request);
525633

526634
</Tab>
527635
</Tabs>
636+
637+
### Adding context information
638+
639+
HTTP request sent through SSE or Streamable HTTP transport can be customized with
640+
dedicated APIs (see [client transport](#client-transport)). These customizers may need
641+
additional context-specific information that must be injected at the client level.
642+
643+
<Tabs>
644+
<Tab title="Sync API">
645+
646+
The `McpSyncClient` is used in a blocking environment, and may rely on thread-locals to share information.
647+
For example, some frameworks store the current server request or security tokens in a thread-local.
648+
To make this type of information available to underlying transports, use `SyncSpec#transportContextProvider`:
649+
650+
```java
651+
McpClient.sync(transport)
652+
.transportContextProvider(() -> {
653+
var data = obtainDataFromThreadLocals();
654+
return McpTransportContext.create(
655+
Map.of("some-data", data)
656+
);
657+
})
658+
.build();
659+
```
660+
661+
This `McpTransportContext` will be available in HttpClient-based `McpSyncHttpClientRequestCustomizer`
662+
and WebClient-based `ExchangeFilterFunction`.
663+
664+
</Tab>
665+
666+
<Tab title="Async API">
667+
668+
The `McpAsyncClient` is used in a reactive environment.
669+
Information is passed through the reactive chain using the [Reactor context](https://projectreactor.io/docs/core/release/reference/advancedFeatures/context.html).
670+
To insert data in the Reactor context, use `contextWrite` when calling client operations:
671+
672+
```java
673+
var client = McpClient.async(transport)
674+
// ...
675+
.build();
676+
677+
Mono<McpSchema.ListToolsResult> toolList = client
678+
.listTools()
679+
.contextWrite(ctx -> {
680+
var transportContext = McpTransportContext.create(
681+
Map.of("some-key", "some value")
682+
);
683+
return ctx.put(McpTransportContext.KEY, transportContext)
684+
.put("reactor-context-key", "some reactor value");
685+
});
686+
```
687+
688+
With this, the `McpTransportContext` will be available in HttpClient-based `McpAsyncHttpClientRequestCustomizer`.
689+
The entire Reactor context is also available in both `McpAsyncHttpClientRequestCustomizer`
690+
and WebClient's `ExchangeFilterFunction`, through `Mono.deferContextual`:
691+
692+
```java
693+
WebClient.builder()
694+
.filter((request, next) -> {
695+
return Mono.deferContextual(ctx -> {
696+
var transportContext = ctx.get(McpTransportContext.KEY);
697+
var someReactorValue = ctx.get("reactor-context-key");
698+
// ... use context values ...
699+
});
700+
});
701+
```
702+
703+
</Tab>
704+
</Tabs>

docs/sdk/java/mcp-server.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ The server supports both synchronous and asynchronous APIs, allowing for flexibl
5757
syncServer.close();
5858
```
5959

60+
### Preserving thread-locals in handlers
61+
62+
`McpSyncServer` delegates execution to an underlying `McpAsyncServer`.
63+
Execution of handlers for tools, resources, etc, may not happen on the thread calling the method.
64+
In that case, thread-locals are lost, which may break certain features in frameworks relying on thread-bound work.
65+
To ensure execution happens on the calling thread, and that thread-locals remain available, set `McpServer.sync(...).immediateExecution(true)`.
66+
67+
<Note>
68+
This is only relevant to Sync servers. Async servers use the reactive stack and should not rely on thread-locals.
69+
</Note>
70+
6071
</Tab>
6172

6273
<Tab title="Async API">

0 commit comments

Comments
 (0)