refactor: return immutable lists from list methods (#345)

- Wrap the list responses in immutable lists.
 - Add test to verify the immutable responses.
 - Better timeout handling

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov 2025-06-27 21:03:46 +02:00 • committed by GitHub
commit d9006ecb78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 150 additions and 42 deletions

View file

@ -432,12 +432,6 @@ class WebFluxSseIntegrationTests {
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> { Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty(); assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isNotNull(); assertThat(request.requestedSchema()).isNotNull();
try {
TimeUnit.SECONDS.sleep(2);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
}; };
@ -491,14 +485,18 @@ class WebFluxSseIntegrationTests {
@ValueSource(strings = { "httpclient", "webflux" }) @ValueSource(strings = { "httpclient", "webflux" })
void testCreateElicitationWithRequestTimeoutFail(String clientType) { void testCreateElicitationWithRequestTimeoutFail(String clientType) {
var latch = new CountDownLatch(1);
// Client // Client
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> { Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty(); assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isNotNull(); assertThat(request.requestedSchema()).isNotNull();
try { try {
TimeUnit.SECONDS.sleep(2); if (!latch.await(2, TimeUnit.SECONDS)) {
throw new RuntimeException("Timeout waiting for elicitation processing");
}
} }
catch (InterruptedException e) { catch (InterruptedException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -536,7 +534,7 @@ class WebFluxSseIntegrationTests {
var mcpServer = McpServer.async(mcpServerTransportProvider) var mcpServer = McpServer.async(mcpServerTransportProvider)
.serverInfo("test-server", "1.0.0") .serverInfo("test-server", "1.0.0")
.requestTimeout(Duration.ofSeconds(1)) .requestTimeout(Duration.ofSeconds(1)) // 1 second.
.tools(tool) .tools(tool)
.build(); .build();

View file

@ -182,6 +182,20 @@ public abstract class AbstractMcpAsyncClientTests {
}); });
} }
@Test
void testListAllToolsReturnsImmutableList() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools()))
.consumeNextWith(result -> {
assertThat(result.tools()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.tools().add(new Tool("test", "test", "{\"type\":\"object\"}")))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
});
}
@Test @Test
void testPingWithoutInitialization() { void testPingWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server"); verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");
@ -333,6 +347,21 @@ public abstract class AbstractMcpAsyncClientTests {
}); });
} }
@Test
void testListAllResourcesReturnsImmutableList() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources()))
.consumeNextWith(result -> {
assertThat(result.resources()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(
() -> result.resources().add(Resource.builder().uri("test://uri").name("test").build()))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
});
}
@Test @Test
void testMcpAsyncClientState() { void testMcpAsyncClientState() {
withClient(createMcpTransport(), mcpAsyncClient -> { withClient(createMcpTransport(), mcpAsyncClient -> {
@ -384,6 +413,20 @@ public abstract class AbstractMcpAsyncClientTests {
}); });
} }
@Test
void testListAllPromptsReturnsImmutableList() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts()))
.consumeNextWith(result -> {
assertThat(result.prompts()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.prompts().add(new Prompt("test", "test", null)))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
});
}
@Test @Test
void testGetPromptWithoutInitialization() { void testGetPromptWithoutInitialization() {
GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of()); GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of());
@ -553,6 +596,21 @@ public abstract class AbstractMcpAsyncClientTests {
}); });
} }
@Test
void testListAllResourceTemplatesReturnsImmutableList() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates()))
.consumeNextWith(result -> {
assertThat(result.resourceTemplates()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.resourceTemplates()
.add(new McpSchema.ResourceTemplate("test://template", "test", null, null, null)))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
});
}
// @Test // @Test
void testResourceSubscription() { void testResourceSubscription() {
withClient(createMcpTransport(), mcpAsyncClient -> { withClient(createMcpTransport(), mcpAsyncClient -> {

View file

@ -5,6 +5,7 @@ package io.modelcontextprotocol.client;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -677,15 +678,13 @@ public class McpAsyncClient {
* @return A Mono that emits the list of all tools result * @return A Mono that emits the list of all tools result
*/ */
public Mono<McpSchema.ListToolsResult> listTools() { public Mono<McpSchema.ListToolsResult> listTools() {
return this.listTools(McpSchema.FIRST_PAGE).expand(result -> { return this.listTools(McpSchema.FIRST_PAGE)
if (result.nextCursor() != null) { .expand(result -> (result.nextCursor() != null) ? this.listTools(result.nextCursor()) : Mono.empty())
return this.listTools(result.nextCursor()); .reduce(new McpSchema.ListToolsResult(new ArrayList<>(), null), (allToolsResult, result) -> {
} allToolsResult.tools().addAll(result.tools());
return Mono.empty(); return allToolsResult;
}).reduce(new McpSchema.ListToolsResult(new ArrayList<>(), null), (allToolsResult, result) -> { })
allToolsResult.tools().addAll(result.tools()); .map(result -> new McpSchema.ListToolsResult(Collections.unmodifiableList(result.tools()), null));
return allToolsResult;
});
} }
/** /**
@ -739,15 +738,13 @@ public class McpAsyncClient {
* @see #readResource(McpSchema.Resource) * @see #readResource(McpSchema.Resource)
*/ */
public Mono<McpSchema.ListResourcesResult> listResources() { public Mono<McpSchema.ListResourcesResult> listResources() {
return this.listResources(McpSchema.FIRST_PAGE).expand(result -> { return this.listResources(McpSchema.FIRST_PAGE)
if (result.nextCursor() != null) { .expand(result -> (result.nextCursor() != null) ? this.listResources(result.nextCursor()) : Mono.empty())
return this.listResources(result.nextCursor()); .reduce(new McpSchema.ListResourcesResult(new ArrayList<>(), null), (allResourcesResult, result) -> {
} allResourcesResult.resources().addAll(result.resources());
return Mono.empty(); return allResourcesResult;
}).reduce(new McpSchema.ListResourcesResult(new ArrayList<>(), null), (allResourcesResult, result) -> { })
allResourcesResult.resources().addAll(result.resources()); .map(result -> new McpSchema.ListResourcesResult(Collections.unmodifiableList(result.resources()), null));
return allResourcesResult;
});
} }
/** /**
@ -809,17 +806,16 @@ public class McpAsyncClient {
* @see McpSchema.ListResourceTemplatesResult * @see McpSchema.ListResourceTemplatesResult
*/ */
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates() { public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates() {
return this.listResourceTemplates(McpSchema.FIRST_PAGE).expand(result -> { return this.listResourceTemplates(McpSchema.FIRST_PAGE)
if (result.nextCursor() != null) { .expand(result -> (result.nextCursor() != null) ? this.listResourceTemplates(result.nextCursor())
return this.listResourceTemplates(result.nextCursor()); : Mono.empty())
}
return Mono.empty();
})
.reduce(new McpSchema.ListResourceTemplatesResult(new ArrayList<>(), null), .reduce(new McpSchema.ListResourceTemplatesResult(new ArrayList<>(), null),
(allResourceTemplatesResult, result) -> { (allResourceTemplatesResult, result) -> {
allResourceTemplatesResult.resourceTemplates().addAll(result.resourceTemplates()); allResourceTemplatesResult.resourceTemplates().addAll(result.resourceTemplates());
return allResourceTemplatesResult; return allResourceTemplatesResult;
}); })
.map(result -> new McpSchema.ListResourceTemplatesResult(
Collections.unmodifiableList(result.resourceTemplates()), null));
} }
/** /**
@ -914,15 +910,13 @@ public class McpAsyncClient {
* @see #getPrompt(GetPromptRequest) * @see #getPrompt(GetPromptRequest)
*/ */
public Mono<ListPromptsResult> listPrompts() { public Mono<ListPromptsResult> listPrompts() {
return this.listPrompts(McpSchema.FIRST_PAGE).expand(result -> { return this.listPrompts(McpSchema.FIRST_PAGE)
if (result.nextCursor() != null) { .expand(result -> (result.nextCursor() != null) ? this.listPrompts(result.nextCursor()) : Mono.empty())
return this.listPrompts(result.nextCursor()); .reduce(new ListPromptsResult(new ArrayList<>(), null), (allPromptsResult, result) -> {
} allPromptsResult.prompts().addAll(result.prompts());
return Mono.empty(); return allPromptsResult;
}).reduce(new ListPromptsResult(new ArrayList<>(), null), (allPromptsResult, result) -> { })
allPromptsResult.prompts().addAll(result.prompts()); .map(result -> new McpSchema.ListPromptsResult(Collections.unmodifiableList(result.prompts()), null));
return allPromptsResult;
});
} }
/** /**

View file

@ -183,6 +183,20 @@ public abstract class AbstractMcpAsyncClientTests {
}); });
} }
@Test
void testListAllToolsReturnsImmutableList() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools()))
.consumeNextWith(result -> {
assertThat(result.tools()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.tools().add(new Tool("test", "test", "{\"type\":\"object\"}")))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
});
}
@Test @Test
void testPingWithoutInitialization() { void testPingWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server"); verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");
@ -334,6 +348,21 @@ public abstract class AbstractMcpAsyncClientTests {
}); });
} }
@Test
void testListAllResourcesReturnsImmutableList() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources()))
.consumeNextWith(result -> {
assertThat(result.resources()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(
() -> result.resources().add(Resource.builder().uri("test://uri").name("test").build()))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
});
}
@Test @Test
void testMcpAsyncClientState() { void testMcpAsyncClientState() {
withClient(createMcpTransport(), mcpAsyncClient -> { withClient(createMcpTransport(), mcpAsyncClient -> {
@ -385,6 +414,20 @@ public abstract class AbstractMcpAsyncClientTests {
}); });
} }
@Test
void testListAllPromptsReturnsImmutableList() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts()))
.consumeNextWith(result -> {
assertThat(result.prompts()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.prompts().add(new Prompt("test", "test", null)))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
});
}
@Test @Test
void testGetPromptWithoutInitialization() { void testGetPromptWithoutInitialization() {
GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of()); GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of());
@ -554,6 +597,21 @@ public abstract class AbstractMcpAsyncClientTests {
}); });
} }
@Test
void testListAllResourceTemplatesReturnsImmutableList() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates()))
.consumeNextWith(result -> {
assertThat(result.resourceTemplates()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.resourceTemplates()
.add(new McpSchema.ResourceTemplate("test://template", "test", null, null, null)))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
});
}
// @Test // @Test
void testResourceSubscription() { void testResourceSubscription() {
withClient(createMcpTransport(), mcpAsyncClient -> { withClient(createMcpTransport(), mcpAsyncClient -> {