Skip to content

Commit 117d168

Browse files
committed
Preserve init request on reinitialization
1 parent b62d67b commit 117d168

4 files changed

Lines changed: 123 additions & 3 deletions

File tree

docs/client.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ The async client exposes the same overload and returns `Mono<InitializeResult>`.
149149

150150
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.
151151

152-
> **Note:** The custom request is only used for the initial handshake. If the transport session is later re-established (for example after a `McpTransportSessionNotFoundException`), the client re-initializes with the default request and the custom `_meta` is not resent.
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.
153153

154154
## Client Transport
155155

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ class LifecycleInitializer {
9595

9696
private final AtomicReference<DefaultInitialization> initializationRef = new AtomicReference<>();
9797

98+
private final AtomicReference<McpSchema.InitializeRequest> storedCustomInitializeRequest = new AtomicReference<>();
99+
98100
/**
99101
* The max timeout to await for the client-server connection to be initialized.
100102
*/
@@ -259,7 +261,8 @@ public void handleException(Throwable t) {
259261
}
260262
// Providing an empty operation since we are only interested in triggering
261263
// the implicit initialization step.
262-
this.withInitialization("re-initializing", result -> Mono.empty()).subscribe();
264+
this.withInitialization(this.storedCustomInitializeRequest.get(), "re-initializing", result -> Mono.empty())
265+
.subscribe();
263266
}
264267
}
265268

@@ -381,6 +384,10 @@ private Mono<McpSchema.InitializeResult> doInitialize(DefaultInitialization init
381384
}).flatMap(initializeResult -> {
382385
initialization.cacheResult(initializeResult);
383386
return postInitOperation.apply(initialization).thenReturn(initializeResult);
387+
}).doOnNext(initializeResult -> {
388+
if (customRequest != null) {
389+
this.storedCustomInitializeRequest.set(customRequest);
390+
}
384391
}).doOnNext(initialization::complete).onErrorResume(ex -> {
385392
initialization.error(ex);
386393
return Mono.error(ex);
@@ -395,6 +402,7 @@ public void close() {
395402
if (current != null) {
396403
current.close();
397404
}
405+
this.storedCustomInitializeRequest.set(null);
398406
}
399407

400408
/**
@@ -404,6 +412,7 @@ public void close() {
404412
public Mono<?> closeGracefully() {
405413
return Mono.defer(() -> {
406414
DefaultInitialization current = this.initializationRef.getAndSet(null);
415+
this.storedCustomInitializeRequest.set(null);
407416
Mono<?> sessionClose = current != null ? current.closeGracefully() : Mono.empty();
408417
return sessionClose;
409418
});

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,9 @@ public Mono<McpSchema.InitializeResult> initialize() {
477477
* Use this overload to control the full initialize request payload, including the
478478
* optional {@code _meta} field. Call this method before any other client operation to
479479
* ensure the custom request is sent; lazy initialization triggered by other methods
480-
* uses the default request built from client builder settings.
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.
481483
* @param initializeRequest the initialize request to send
482484
* @return the initialize result
483485
* @see #initialize()

mcp-core/src/test/java/io/modelcontextprotocol/client/LifecycleInitializerTests.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package io.modelcontextprotocol.client;
66

77
import java.time.Duration;
8+
import java.util.ArrayList;
89
import java.util.HashMap;
910
import java.util.List;
1011
import java.util.Map;
@@ -391,6 +392,114 @@ void shouldHandleTransportSessionNotFoundException() {
391392
verify(mockSessionSupplier, times(2)).apply(any(ContextView.class));
392393
}
393394

395+
@Test
396+
void shouldReuseCustomInitializeRequestOnReinitialization() {
397+
List<McpSchema.InitializeRequest> capturedRequests = new ArrayList<>();
398+
399+
when(mockClientSession.sendRequest(eq(McpSchema.METHOD_INITIALIZE), any(), any())).thenAnswer(invocation -> {
400+
capturedRequests.add(invocation.getArgument(1));
401+
return Mono.just(MOCK_INIT_RESULT);
402+
});
403+
404+
McpSchema.InitializeRequest customRequest = McpSchema.InitializeRequest
405+
.builder("2.0.0", CLIENT_CAPABILITIES, CLIENT_INFO)
406+
.meta(Map.of("server_id", "proxy-1"))
407+
.build();
408+
409+
StepVerifier
410+
.create(initializer.withInitialization(customRequest, "test", init -> Mono.just(init.initializeResult())))
411+
.expectNext(MOCK_INIT_RESULT)
412+
.verifyComplete();
413+
414+
initializer.handleException(new McpTransportSessionNotFoundException("Session not found"));
415+
416+
assertThat(capturedRequests).hasSize(2);
417+
assertThat(capturedRequests.get(0).meta()).containsEntry("server_id", "proxy-1");
418+
assertThat(capturedRequests.get(1).meta()).containsEntry("server_id", "proxy-1");
419+
verify(mockSessionSupplier, times(2)).apply(any(ContextView.class));
420+
}
421+
422+
@Test
423+
void shouldUseDefaultRequestOnReinitializationWhenNoCustomRequestStored() {
424+
List<McpSchema.InitializeRequest> capturedRequests = new ArrayList<>();
425+
426+
when(mockClientSession.sendRequest(eq(McpSchema.METHOD_INITIALIZE), any(), any())).thenAnswer(invocation -> {
427+
capturedRequests.add(invocation.getArgument(1));
428+
return Mono.just(MOCK_INIT_RESULT);
429+
});
430+
431+
StepVerifier.create(initializer.withInitialization("test", init -> Mono.just(init.initializeResult())))
432+
.expectNext(MOCK_INIT_RESULT)
433+
.verifyComplete();
434+
435+
initializer.handleException(new McpTransportSessionNotFoundException("Session not found"));
436+
437+
assertThat(capturedRequests).hasSize(2);
438+
assertThat(capturedRequests.get(1).protocolVersion()).isEqualTo("2.0.0");
439+
assertThat(capturedRequests.get(1).capabilities()).isEqualTo(CLIENT_CAPABILITIES);
440+
assertThat(capturedRequests.get(1).clientInfo()).isEqualTo(CLIENT_INFO);
441+
assertThat(capturedRequests.get(1).meta()).isNull();
442+
}
443+
444+
@Test
445+
void shouldClearStoredCustomRequestOnClose() {
446+
AtomicReference<McpSchema.InitializeRequest> capturedRequest = new AtomicReference<>();
447+
448+
when(mockClientSession.sendRequest(eq(McpSchema.METHOD_INITIALIZE), any(), any())).thenAnswer(invocation -> {
449+
capturedRequest.set(invocation.getArgument(1));
450+
return Mono.just(MOCK_INIT_RESULT);
451+
});
452+
453+
McpSchema.InitializeRequest customRequest = McpSchema.InitializeRequest
454+
.builder("2.0.0", CLIENT_CAPABILITIES, CLIENT_INFO)
455+
.meta(Map.of("server_id", "proxy-1"))
456+
.build();
457+
458+
StepVerifier
459+
.create(initializer.withInitialization(customRequest, "test", init -> Mono.just(init.initializeResult())))
460+
.expectNext(MOCK_INIT_RESULT)
461+
.verifyComplete();
462+
463+
initializer.close();
464+
465+
StepVerifier
466+
.create(initializer.withInitialization("test after close", init -> Mono.just(init.initializeResult())))
467+
.expectNext(MOCK_INIT_RESULT)
468+
.verifyComplete();
469+
470+
assertThat(capturedRequest.get().meta()).isNull();
471+
assertThat(capturedRequest.get().protocolVersion()).isEqualTo("2.0.0");
472+
}
473+
474+
@Test
475+
void shouldClearStoredCustomRequestOnCloseGracefully() {
476+
AtomicReference<McpSchema.InitializeRequest> capturedRequest = new AtomicReference<>();
477+
478+
when(mockClientSession.sendRequest(eq(McpSchema.METHOD_INITIALIZE), any(), any())).thenAnswer(invocation -> {
479+
capturedRequest.set(invocation.getArgument(1));
480+
return Mono.just(MOCK_INIT_RESULT);
481+
});
482+
483+
McpSchema.InitializeRequest customRequest = McpSchema.InitializeRequest
484+
.builder("2.0.0", CLIENT_CAPABILITIES, CLIENT_INFO)
485+
.meta(Map.of("server_id", "proxy-1"))
486+
.build();
487+
488+
StepVerifier
489+
.create(initializer.withInitialization(customRequest, "test", init -> Mono.just(init.initializeResult())))
490+
.expectNext(MOCK_INIT_RESULT)
491+
.verifyComplete();
492+
493+
StepVerifier.create(initializer.closeGracefully()).verifyComplete();
494+
495+
StepVerifier
496+
.create(initializer.withInitialization("test after close", init -> Mono.just(init.initializeResult())))
497+
.expectNext(MOCK_INIT_RESULT)
498+
.verifyComplete();
499+
500+
assertThat(capturedRequest.get().meta()).isNull();
501+
}
502+
394503
@Test
395504
void shouldHandleOtherExceptions() {
396505
// Simulate a successful initialization first

0 commit comments

Comments
 (0)