Skip to content

Commit 3d063e3

Browse files
committed
Add initialize(InitializeRequest) overload to MCP client
1 parent 30f1adf commit 3d063e3

7 files changed

Lines changed: 438 additions & 11 deletions

File tree

docs/client.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,26 @@ The client provides both synchronous and asynchronous APIs for flexibility in di
131131
.subscribe();
132132
```
133133

134+
### Custom Initialize Request
135+
136+
By default, `initialize()` builds the request from client builder settings (protocol version, capabilities, client info). To control the full initialize payload — including the optional `_meta` field — use the `initialize(InitializeRequest)` overload:
137+
138+
```java
139+
InitializeRequest request = InitializeRequest
140+
.builder(ProtocolVersions.MCP_2025_11_25, client.getClientCapabilities(), client.getClientInfo())
141+
.meta(Map.of("server_id", "proxy-1", "invocation_id", "abc-123"))
142+
.build();
143+
144+
// Call before any other client operation so the custom request is sent.
145+
client.initialize(request);
146+
```
147+
148+
The async client exposes the same overload and returns `Mono<InitializeResult>`.
149+
150+
If another client method triggers lazy initialization first, the default request is sent instead. Call `initialize(request)` before `listTools()`, `callTool()`, or similar operations when custom initialize metadata is required.
151+
152+
After a successful `initialize(InitializeRequest)`, the client remembers that request and resends it when the transport session is re-established (for example after a `McpTransportSessionNotFoundException`). The stored request is cleared when the client is closed.
153+
134154
## Client Transport
135155

136156
The transport layer handles the communication between MCP clients and servers, providing different implementations for various use cases. The client transport manages message serialization, connection establishment, and protocol-specific communication patterns.

mcp-core/src/main/java/io/modelcontextprotocol/client/LifecycleInitializer.java

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import java.time.Duration;
88
import java.util.ArrayList;
99
import java.util.Collections;
10+
import java.util.HashMap;
1011
import java.util.List;
12+
import java.util.Map;
1113
import java.util.concurrent.atomic.AtomicReference;
1214
import java.util.function.Function;
1315

@@ -93,6 +95,8 @@ class LifecycleInitializer {
9395

9496
private final AtomicReference<DefaultInitialization> initializationRef = new AtomicReference<>();
9597

98+
private final AtomicReference<McpSchema.InitializeRequest> storedCustomInitializeRequest = new AtomicReference<>();
99+
96100
/**
97101
* The max timeout to await for the client-server connection to be initialized.
98102
*/
@@ -257,7 +261,8 @@ public void handleException(Throwable t) {
257261
}
258262
// Providing an empty operation since we are only interested in triggering
259263
// the implicit initialization step.
260-
this.withInitialization("re-initializing", result -> Mono.empty()).subscribe();
264+
this.withInitialization(this.storedCustomInitializeRequest.get(), "re-initializing", result -> Mono.empty())
265+
.subscribe();
261266
}
262267
}
263268

@@ -270,15 +275,43 @@ public void handleException(Throwable t) {
270275
* @return A Mono that completes with the result of the operation
271276
*/
272277
public <T> Mono<T> withInitialization(String actionName, Function<Initialization, Mono<T>> operation) {
278+
return this.withInitialization(null, actionName, operation);
279+
}
280+
281+
/**
282+
* Utility method to ensure the initialization is established before executing an
283+
* operation, using a caller-provided initialize request.
284+
* @param <T> The type of the result Mono
285+
* @param initializeRequest The initialize request to send; may be null to use the
286+
* default. Only applied when this call triggers the initialization, otherwise
287+
* ignored.
288+
* @param actionName The action to perform when the client is initialized
289+
* @param operation The operation to execute when the client is initialized
290+
* @return A Mono that completes with the result of the operation
291+
*/
292+
public <T> Mono<T> withInitialization(McpSchema.InitializeRequest initializeRequest, String actionName,
293+
Function<Initialization, Mono<T>> operation) {
294+
// Snapshot eagerly so mutating the source _meta map before subscription cannot
295+
// change what is sent.
296+
McpSchema.InitializeRequest sanitizedRequest = initializeRequest != null
297+
? sanitizeInitializeRequest(initializeRequest) : null;
273298
return Mono.deferContextual(ctx -> {
274299
DefaultInitialization newInit = new DefaultInitialization();
275300
DefaultInitialization previous = this.initializationRef.compareAndExchange(null, newInit);
276301

277302
boolean needsToInitialize = previous == null;
278303
logger.debug(needsToInitialize ? "Initialization process started" : "Joining previous initialization");
279304

280-
Mono<McpSchema.InitializeResult> initializationJob = needsToInitialize
281-
? this.doInitialize(newInit, this.postInitializationHook, ctx) : previous.await();
305+
Mono<McpSchema.InitializeResult> initializationJob;
306+
if (needsToInitialize) {
307+
initializationJob = this.doInitialize(newInit, sanitizedRequest, this.postInitializationHook, ctx);
308+
}
309+
else {
310+
if (sanitizedRequest != null) {
311+
logger.debug("Custom initialize request ignored; client is already initialized");
312+
}
313+
initializationJob = previous.await();
314+
}
282315

283316
return initializationJob.map(initializeResult -> this.initializationRef.get())
284317
.timeout(this.initializationTimeout)
@@ -292,19 +325,43 @@ public <T> Mono<T> withInitialization(String actionName, Function<Initialization
292325
});
293326
}
294327

328+
private static McpSchema.InitializeRequest sanitizeInitializeRequest(McpSchema.InitializeRequest request) {
329+
if (request.meta() == null) {
330+
return request;
331+
}
332+
return McpSchema.InitializeRequest
333+
.builder(request.protocolVersion(), request.capabilities(), request.clientInfo())
334+
.meta(Collections.unmodifiableMap(new HashMap<>(request.meta())))
335+
.build();
336+
}
337+
338+
private McpSchema.InitializeRequest buildInitializeRequest(McpSchema.InitializeRequest customRequest) {
339+
if (customRequest != null) {
340+
return customRequest;
341+
}
342+
String latestVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
343+
return McpSchema.InitializeRequest.builder(latestVersion, this.clientCapabilities, this.clientInfo).build();
344+
}
345+
295346
private Mono<McpSchema.InitializeResult> doInitialize(DefaultInitialization initialization,
296-
Function<Initialization, Mono<Void>> postInitOperation, ContextView ctx) {
347+
McpSchema.InitializeRequest customRequest, Function<Initialization, Mono<Void>> postInitOperation,
348+
ContextView ctx) {
349+
350+
McpSchema.InitializeRequest initializeRequest = this.buildInitializeRequest(customRequest);
351+
352+
if (!this.protocolVersions.contains(initializeRequest.protocolVersion())) {
353+
McpError error = McpError.builder(-32602)
354+
.message("Unsupported protocol version")
355+
.data("Unsupported protocol version in initialize request: " + initializeRequest.protocolVersion())
356+
.build();
357+
initialization.error(error);
358+
return Mono.error(error);
359+
}
297360

298361
initialization.setMcpClientSession(this.sessionSupplier.apply(ctx));
299362

300363
McpClientSession mcpClientSession = initialization.mcpSession();
301364

302-
String latestVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
303-
304-
McpSchema.InitializeRequest initializeRequest = McpSchema.InitializeRequest
305-
.builder(latestVersion, this.clientCapabilities, this.clientInfo)
306-
.build();
307-
308365
Mono<McpSchema.InitializeResult> result = mcpClientSession.sendRequest(McpSchema.METHOD_INITIALIZE,
309366
initializeRequest, McpAsyncClient.INITIALIZE_RESULT_TYPE_REF);
310367

@@ -327,6 +384,10 @@ private Mono<McpSchema.InitializeResult> doInitialize(DefaultInitialization init
327384
}).flatMap(initializeResult -> {
328385
initialization.cacheResult(initializeResult);
329386
return postInitOperation.apply(initialization).thenReturn(initializeResult);
387+
}).doOnNext(initializeResult -> {
388+
if (customRequest != null) {
389+
this.storedCustomInitializeRequest.set(customRequest);
390+
}
330391
}).doOnNext(initialization::complete).onErrorResume(ex -> {
331392
initialization.error(ex);
332393
return Mono.error(ex);
@@ -341,6 +402,7 @@ public void close() {
341402
if (current != null) {
342403
current.close();
343404
}
405+
this.storedCustomInitializeRequest.set(null);
344406
}
345407

346408
/**
@@ -350,9 +412,10 @@ public void close() {
350412
public Mono<?> closeGracefully() {
351413
return Mono.defer(() -> {
352414
DefaultInitialization current = this.initializationRef.getAndSet(null);
415+
this.storedCustomInitializeRequest.set(null);
353416
Mono<?> sessionClose = current != null ? current.closeGracefully() : Mono.empty();
354417
return sessionClose;
355418
});
356419
}
357420

358-
}
421+
}

mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,25 @@ public Mono<McpSchema.InitializeResult> initialize() {
471471
return this.initializer.withInitialization("by explicit API call", init -> Mono.just(init.initializeResult()));
472472
}
473473

474+
/**
475+
* Initializes the client using a caller-provided initialize request.
476+
* <p>
477+
* Use this overload to control the full initialize request payload, including the
478+
* optional {@code _meta} field. Call this method before any other client operation to
479+
* ensure the custom request is sent; lazy initialization triggered by other methods
480+
* uses the default request built from client builder settings. After a successful
481+
* call, the custom request is remembered and resent on transport session recovery; it
482+
* is cleared when the client is closed.
483+
* @param initializeRequest the initialize request to send
484+
* @return the initialize result
485+
* @see #initialize()
486+
*/
487+
public Mono<McpSchema.InitializeResult> initialize(McpSchema.InitializeRequest initializeRequest) {
488+
Assert.notNull(initializeRequest, "InitializeRequest must not be null");
489+
return this.initializer.withInitialization(initializeRequest, "by explicit API call with custom request",
490+
init -> Mono.just(init.initializeResult()));
491+
}
492+
474493
// --------------------------
475494
// Basic Utilities
476495
// --------------------------

mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,23 @@ public McpSchema.InitializeResult initialize() {
190190
return withProvidedContext(this.delegate.initialize()).block();
191191
}
192192

193+
/**
194+
* Initializes the client using a caller-provided initialize request.
195+
* <p>
196+
* Use this overload to control the full initialize request payload, including the
197+
* optional {@code _meta} field. Call this method before any other client operation to
198+
* ensure the custom request is sent; lazy initialization triggered by other methods
199+
* uses the default request built from client builder settings. After a successful
200+
* call, the custom request is remembered and resent on transport session recovery; it
201+
* is cleared when the client is closed.
202+
* @param initializeRequest the initialize request to send
203+
* @return the initialize result
204+
* @see #initialize()
205+
*/
206+
public McpSchema.InitializeResult initialize(McpSchema.InitializeRequest initializeRequest) {
207+
return withProvidedContext(this.delegate.initialize(initializeRequest)).block();
208+
}
209+
193210
/**
194211
* Send a roots/list_changed notification.
195212
*/

0 commit comments

Comments
 (0)