Refactor McpClient for async and sync interactions

Introduce new builder patterns and enhance type safety

- Add new builder patterns for McpClient and McpServer with sync/async variants
- Introduce dedicated McpClientFeatures and McpServerFeatures classes
- Enhance type safety and reactive support with proper Mono integration
- Add migration guide for 0.6.0 release
- Mark old builder methods as deprecated
- Update documentation to reflect new APIs

BREAKING CHANGE: The old builder patterns using McpClient.using() and McpServer.using()
are now deprecated in favor of new sync() and async() builders that provide
better type safety and clearer sync/async separation. Follow the migration guide.

Resolves #48

Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
Co-authored-by: Christian Tzolov <ctzolov@vmware.com>
This commit is contained in:
Dariusz Jędrzejczyk 2025-01-20 18:33:06 +01:00 • committed by Christian Tzolov
commit 6839d3b740
7 changed files with 3494 additions and 753 deletions

View file

@ -25,6 +25,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.ai.mcp.spec.ClientMcpTransport;
@ -77,10 +78,10 @@ public abstract class AbstractMcpAsyncClientTests {
this.mcpTransport = createMcpTransport();
assertThatCode(() -> {
mcpAsyncClient = McpClient.using(mcpTransport)
mcpAsyncClient = McpClient.async(mcpTransport)
.requestTimeout(TIMEOUT)
.capabilities(ClientCapabilities.builder().roots(true).build())
.async();
.build();
mcpAsyncClient.initialize().block(Duration.ofSeconds(10));
}).doesNotThrowAnyException();
}
@ -96,10 +97,10 @@ public abstract class AbstractMcpAsyncClientTests {
@Test
void testConstructorWithInvalidArguments() {
assertThatThrownBy(() -> McpClient.using(null).sync()).isInstanceOf(IllegalArgumentException.class)
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Transport must not be null");
assertThatThrownBy(() -> McpClient.using(mcpTransport).requestTimeout(null).sync())
assertThatThrownBy(() -> McpClient.sync(mcpTransport).requestTimeout(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Request timeout must not be null");
}
@ -195,10 +196,10 @@ public abstract class AbstractMcpAsyncClientTests {
void testInitializeWithRootsListProviders() {
var transport = createMcpTransport();
var client = McpClient.using(transport)
var client = McpClient.async(transport)
.requestTimeout(TIMEOUT)
.roots(new Root("file:///test/path", "test-root"))
.async();
.build();
assertThatCode(() -> client.initialize().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
@ -277,12 +278,12 @@ public abstract class AbstractMcpAsyncClientTests {
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
var transport = createMcpTransport();
var client = McpClient.using(transport)
var client = McpClient.async(transport)
.requestTimeout(TIMEOUT)
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
.async();
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true)))
.resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true)))
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true)))
.build();
assertThatCode(() -> {
client.initialize().block();
@ -299,9 +300,11 @@ public abstract class AbstractMcpAsyncClientTests {
var capabilities = ClientCapabilities.builder().sampling().build();
var client = McpClient.using(transport).requestTimeout(TIMEOUT).capabilities(capabilities).sampling(request -> {
return CreateMessageResult.builder().message("test").model("test-model").build();
}).async();
var client = McpClient.async(transport)
.requestTimeout(TIMEOUT)
.capabilities(capabilities)
.sampling(request -> Mono.just(CreateMessageResult.builder().message("test").model("test-model").build()))
.build();
assertThatCode(() -> {
client.initialize().block(Duration.ofSeconds(10));
@ -319,14 +322,13 @@ public abstract class AbstractMcpAsyncClientTests {
.sampling()
.build();
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
return CreateMessageResult.builder().message("test").model("test-model").build();
};
var client = McpClient.using(transport)
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
.just(CreateMessageResult.builder().message("test").model("test-model").build());
var client = McpClient.async(transport)
.requestTimeout(TIMEOUT)
.capabilities(capabilities)
.sampling(samplingHandler)
.async();
.build();
assertThatCode(() -> {
var result = client.initialize().block(Duration.ofSeconds(10));
@ -353,10 +355,10 @@ public abstract class AbstractMcpAsyncClientTests {
AtomicBoolean logReceived = new AtomicBoolean(false);
var transport = createMcpTransport();
var client = McpClient.using(transport)
var client = McpClient.async(transport)
.requestTimeout(TIMEOUT)
.loggingConsumer(notification -> logReceived.set(true))
.async();
.loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true)))
.build();
assertThatCode(() -> {
client.initialize().block(Duration.ofSeconds(10));

View file

@ -72,10 +72,10 @@ public abstract class AbstractMcpSyncClientTests {
this.mcpTransport = createMcpTransport();
assertThatCode(() -> {
mcpSyncClient = McpClient.using(mcpTransport)
mcpSyncClient = McpClient.sync(mcpTransport)
.requestTimeout(TIMEOUT)
.capabilities(ClientCapabilities.builder().roots(true).build())
.sync();
.build();
mcpSyncClient.initialize();
}).doesNotThrowAnyException();
}
@ -90,10 +90,10 @@ public abstract class AbstractMcpSyncClientTests {
@Test
void testConstructorWithInvalidArguments() {
assertThatThrownBy(() -> McpClient.using(null).sync()).isInstanceOf(IllegalArgumentException.class)
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Transport must not be null");
assertThatThrownBy(() -> McpClient.using(mcpTransport).requestTimeout(null).sync())
assertThatThrownBy(() -> McpClient.sync(mcpTransport).requestTimeout(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Request timeout must not be null");
}
@ -180,10 +180,10 @@ public abstract class AbstractMcpSyncClientTests {
void testInitializeWithRootsListProviders() {
var transport = createMcpTransport();
var client = McpClient.using(transport)
var client = McpClient.sync(transport)
.requestTimeout(TIMEOUT)
.roots(new Root("file:///test/path", "test-root"))
.sync();
.build();
assertThatCode(() -> {
client.initialize();
@ -262,12 +262,12 @@ public abstract class AbstractMcpSyncClientTests {
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
var transport = createMcpTransport();
var client = McpClient.using(transport)
var client = McpClient.sync(transport)
.requestTimeout(TIMEOUT)
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
.sync();
.build();
assertThatCode(() -> {
client.initialize();
@ -295,10 +295,10 @@ public abstract class AbstractMcpSyncClientTests {
AtomicBoolean logReceived = new AtomicBoolean(false);
var transport = createMcpTransport();
var client = McpClient.using(transport)
var client = McpClient.sync(transport)
.requestTimeout(TIMEOUT)
.loggingConsumer(notification -> logReceived.set(true))
.sync();
.build();
assertThatCode(() -> {
client.initialize();

View file

@ -27,6 +27,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.MockMcpTransport;
import org.springframework.ai.mcp.spec.McpError;
@ -48,12 +49,11 @@ class McpAsyncClientResponseHandlerTests {
List<McpSchema.Tool> receivedTools = new ArrayList<>();
// Create a consumer that will be called when tools change
Consumer<List<McpSchema.Tool>> toolsChangeConsumer = tools -> {
receivedTools.addAll(tools);
};
Function<List<McpSchema.Tool>, Mono<Void>> toolsChangeConsumer = tools -> Mono
.fromRunnable(() -> receivedTools.addAll(tools));
// Create client with tools change consumer
McpAsyncClient asyncMcpClient = McpClient.using(transport).toolsChangeConsumer(toolsChangeConsumer).async();
McpAsyncClient asyncMcpClient = McpClient.async(transport).toolsChangeConsumer(toolsChangeConsumer).build();
// Create a mock tools list that the server will return
Map<String, Object> inputSchema = Map.of("type", "object", "properties", Map.of(), "required", List.of());
@ -88,9 +88,9 @@ class McpAsyncClientResponseHandlerTests {
void testRootsListRequestHandling() {
MockMcpTransport transport = new MockMcpTransport();
McpAsyncClient asyncMcpClient = McpClient.using(transport)
McpAsyncClient asyncMcpClient = McpClient.async(transport)
.roots(new Root("file:///test/path", "test-root"))
.async();
.build();
// Simulate incoming request
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
@ -118,14 +118,13 @@ class McpAsyncClientResponseHandlerTests {
List<McpSchema.Resource> receivedResources = new ArrayList<>();
// Create a consumer that will be called when resources change
Consumer<List<McpSchema.Resource>> resourcesChangeConsumer = resources -> {
receivedResources.addAll(resources);
};
Function<List<McpSchema.Resource>, Mono<Void>> resourcesChangeConsumer = resources -> Mono
.fromRunnable(() -> receivedResources.addAll(resources));
// Create client with resources change consumer
McpAsyncClient asyncMcpClient = McpClient.using(transport)
McpAsyncClient asyncMcpClient = McpClient.async(transport)
.resourcesChangeConsumer(resourcesChangeConsumer)
.async();
.build();
// Create a mock resources list that the server will return
McpSchema.Resource mockResource = new McpSchema.Resource("test://resource", "Test Resource", "A test resource",
@ -165,12 +164,11 @@ class McpAsyncClientResponseHandlerTests {
List<McpSchema.Prompt> receivedPrompts = new ArrayList<>();
// Create a consumer that will be called when prompts change
Consumer<List<McpSchema.Prompt>> promptsChangeConsumer = prompts -> {
receivedPrompts.addAll(prompts);
};
Function<List<McpSchema.Prompt>, Mono<Void>> promptsChangeConsumer = prompts -> Mono
.fromRunnable(() -> receivedPrompts.addAll(prompts));
// Create client with prompts change consumer
McpAsyncClient asyncMcpClient = McpClient.using(transport).promptsChangeConsumer(promptsChangeConsumer).async();
McpAsyncClient asyncMcpClient = McpClient.async(transport).promptsChangeConsumer(promptsChangeConsumer).build();
// Create a mock prompts list that the server will return
McpSchema.Prompt mockPrompt = new McpSchema.Prompt("test-prompt", "Test Prompt Description",
@ -207,17 +205,17 @@ class McpAsyncClientResponseHandlerTests {
MockMcpTransport transport = new MockMcpTransport();
// Create a test sampling handler that echoes back the input
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler = request -> {
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = request -> {
var content = request.messages().get(0).content();
return new McpSchema.CreateMessageResult(McpSchema.Role.ASSISTANT, content, "test-model",
McpSchema.CreateMessageResult.StopReason.END_TURN);
return Mono.just(new McpSchema.CreateMessageResult(McpSchema.Role.ASSISTANT, content, "test-model",
McpSchema.CreateMessageResult.StopReason.END_TURN));
};
// Create client with sampling capability and handler
McpAsyncClient asyncMcpClient = McpClient.using(transport)
McpAsyncClient asyncMcpClient = McpClient.async(transport)
.capabilities(ClientCapabilities.builder().sampling().build())
.sampling(samplingHandler)
.async();
.build();
// Create a mock create message request
var messageRequest = new McpSchema.CreateMessageRequest(
@ -259,9 +257,9 @@ class McpAsyncClientResponseHandlerTests {
MockMcpTransport transport = new MockMcpTransport();
// Create client without sampling capability
McpAsyncClient asyncMcpClient = McpClient.using(transport)
McpAsyncClient asyncMcpClient = McpClient.async(transport)
.capabilities(ClientCapabilities.builder().build()) // No sampling capability
.async();
.build();
// Create a mock create message request
var messageRequest = new McpSchema.CreateMessageRequest(
@ -292,7 +290,7 @@ class McpAsyncClientResponseHandlerTests {
// Create client with sampling capability but null handler
assertThatThrownBy(
() -> McpClient.using(transport).capabilities(ClientCapabilities.builder().sampling().build()).async())
() -> McpClient.async(transport).capabilities(ClientCapabilities.builder().sampling().build()).build())
.isInstanceOf(McpError.class)
.hasMessage("Sampling handler must not be null when client capabilities include sampling");
}

View file

@ -22,11 +22,9 @@ import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.ai.mcp.server.McpServer.PromptRegistration;
import org.springframework.ai.mcp.server.McpServer.ResourceRegistration;
import org.springframework.ai.mcp.server.McpServer.ToolRegistration;
import org.springframework.ai.mcp.spec.McpError;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
@ -81,24 +79,24 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testConstructorWithInvalidArguments() {
assertThatThrownBy(() -> McpServer.using(null)).isInstanceOf(IllegalArgumentException.class)
assertThatThrownBy(() -> McpServer.async(null)).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Transport must not be null");
assertThatThrownBy(() -> McpServer.using(createMcpTransport()).serverInfo((McpSchema.Implementation) null))
assertThatThrownBy(() -> McpServer.async(createMcpTransport()).serverInfo((McpSchema.Implementation) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Server info must not be null");
}
@Test
void testGracefulShutdown() {
var mcpAsyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
StepVerifier.create(mcpAsyncServer.closeGracefully()).verifyComplete();
}
@Test
void testImmediateClose() {
var mcpAsyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpAsyncServer.close()).doesNotThrowAnyException();
}
@ -117,81 +115,80 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testAddTool() {
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.async();
.build();
StepVerifier
.create(mcpAsyncServer.addTool(new ToolRegistration(newTool, args -> new CallToolResult(List.of(), false))))
StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(newTool,
args -> Mono.just(new CallToolResult(List.of(), false)))))
.verifyComplete();
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}
@Test
void testAddDuplicateTool() {
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tool(duplicateTool, args -> new CallToolResult(List.of(), false))
.async();
.tool(duplicateTool, args -> Mono.just(new CallToolResult(List.of(), false)))
.build();
StepVerifier
.create(mcpAsyncServer
.addTool(new ToolRegistration(duplicateTool, args -> new CallToolResult(List.of(), false))))
StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(duplicateTool,
args -> Mono.just(new CallToolResult(List.of(), false)))))
.verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class)
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
});
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}
@Test
void testRemoveTool() {
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tool(too, args -> new CallToolResult(List.of(), false))
.async();
.tool(too, args -> Mono.just(new CallToolResult(List.of(), false)))
.build();
StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete();
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}
@Test
void testRemoveNonexistentTool() {
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.async();
.build();
StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class).hasMessage("Tool with name 'nonexistent-tool' not found");
});
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}
@Test
void testNotifyToolsListChanged() {
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tool(too, args -> new CallToolResult(List.of(), false))
.async();
.tool(too, args -> Mono.just(new CallToolResult(List.of(), false)))
.build();
StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete();
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}
// ---------------------------------------
@ -200,24 +197,24 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testNotifyResourcesListChanged() {
var mcpAsyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
StepVerifier.create(mcpAsyncServer.notifyResourcesListChanged()).verifyComplete();
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}
@Test
void testAddResource() {
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.async();
.build();
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
null);
ResourceRegistration registration = new ResourceRegistration(resource,
req -> new ReadResourceResult(List.of()));
McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration(
resource, req -> Mono.just(new ReadResourceResult(List.of())));
StepVerifier.create(mcpAsyncServer.addResource(registration)).verifyComplete();
@ -226,14 +223,15 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testAddResourceWithNullRegistration() {
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.async();
.build();
StepVerifier.create(mcpAsyncServer.addResource(null)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class).hasMessage("Resource must not be null");
});
StepVerifier.create(mcpAsyncServer.addResource((McpServerFeatures.AsyncResourceRegistration) null))
.verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class).hasMessage("Resource must not be null");
});
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}
@ -241,14 +239,14 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testAddResourceWithoutCapability() {
// Create a server without resource capabilities
McpAsyncServer serverWithoutResources = McpServer.using(createMcpTransport())
McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.async();
.build();
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
null);
ResourceRegistration registration = new ResourceRegistration(resource,
req -> new ReadResourceResult(List.of()));
McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration(
resource, req -> Mono.just(new ReadResourceResult(List.of())));
StepVerifier.create(serverWithoutResources.addResource(registration)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class)
@ -259,9 +257,9 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testRemoveResourceWithoutCapability() {
// Create a server without resource capabilities
McpAsyncServer serverWithoutResources = McpServer.using(createMcpTransport())
McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.async();
.build();
StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class)
@ -275,36 +273,37 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testNotifyPromptsListChanged() {
var mcpAsyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
StepVerifier.create(mcpAsyncServer.notifyPromptsListChanged()).verifyComplete();
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}
@Test
void testAddPromptWithNullRegistration() {
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(false).build())
.async();
.build();
StepVerifier.create(mcpAsyncServer.addPrompt(null)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class).hasMessage("Prompt registration must not be null");
});
StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptRegistration) null))
.verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class).hasMessage("Prompt registration must not be null");
});
}
@Test
void testAddPromptWithoutCapability() {
// Create a server without prompt capabilities
McpAsyncServer serverWithoutPrompts = McpServer.using(createMcpTransport())
McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.async();
.build();
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
"Test prompt description",
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt,
req -> Mono.just(new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));
StepVerifier.create(serverWithoutPrompts.addPrompt(registration)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class)
@ -315,9 +314,9 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testRemovePromptWithoutCapability() {
// Create a server without prompt capabilities
McpAsyncServer serverWithoutPrompts = McpServer.using(createMcpTransport())
McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.async();
.build();
StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class)
@ -330,15 +329,15 @@ public abstract class AbstractMcpAsyncServerTests {
String TEST_PROMPT_NAME_TO_REMOVE = "TEST_PROMPT_NAME678";
Prompt prompt = new Prompt(TEST_PROMPT_NAME_TO_REMOVE, "Test Prompt", List.of());
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
"Test prompt description",
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt,
req -> Mono.just(new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
.prompts(registration)
.async();
.build();
StepVerifier.create(mcpAsyncServer.removePrompt(TEST_PROMPT_NAME_TO_REMOVE)).verifyComplete();
@ -347,10 +346,10 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testRemoveNonexistentPrompt() {
var mcpAsyncServer2 = McpServer.using(createMcpTransport())
var mcpAsyncServer2 = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
.async();
.build();
StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class)
@ -371,15 +370,15 @@ public abstract class AbstractMcpAsyncServerTests {
var rootsReceived = new McpSchema.Root[1];
var consumerCalled = new boolean[1];
var singleConsumerServer = McpServer.using(createMcpTransport())
var singleConsumerServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.rootsChangeConsumers(List.of(roots -> {
.rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> {
consumerCalled[0] = true;
if (!roots.isEmpty()) {
rootsReceived[0] = roots.get(0);
}
}))
.async();
})))
.build();
assertThat(singleConsumerServer).isNotNull();
assertThatCode(() -> singleConsumerServer.closeGracefully().block(Duration.ofSeconds(10)))
@ -391,13 +390,13 @@ public abstract class AbstractMcpAsyncServerTests {
var consumer2Called = new boolean[1];
var rootsContent = new List[1];
var multipleConsumersServer = McpServer.using(createMcpTransport())
var multipleConsumersServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.rootsChangeConsumers(List.of(roots -> {
.rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> {
consumer1Called[0] = true;
rootsContent[0] = roots;
}, roots -> consumer2Called[0] = true))
.async();
}), roots -> Mono.fromRunnable(() -> consumer2Called[0] = true)))
.build();
assertThat(multipleConsumersServer).isNotNull();
assertThatCode(() -> multipleConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
@ -405,12 +404,12 @@ public abstract class AbstractMcpAsyncServerTests {
onClose();
// Test error handling
var errorHandlingServer = McpServer.using(createMcpTransport())
var errorHandlingServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.rootsChangeConsumers(List.of(roots -> {
throw new RuntimeException("Test error");
}))
.async();
.build();
assertThat(errorHandlingServer).isNotNull();
assertThatCode(() -> errorHandlingServer.closeGracefully().block(Duration.ofSeconds(10)))
@ -418,7 +417,7 @@ public abstract class AbstractMcpAsyncServerTests {
onClose();
// Test without consumers
var noConsumersServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
var noConsumersServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThat(noConsumersServer).isNotNull();
assertThatCode(() -> noConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
@ -431,10 +430,10 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testLoggingLevels() {
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().logging().build())
.async();
.build();
// Test all logging levels
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
@ -450,10 +449,10 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testLoggingWithoutCapability() {
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().build()) // No logging capability
.async();
.build();
var notification = McpSchema.LoggingMessageNotification.builder()
.level(McpSchema.LoggingLevel.INFO)
@ -466,10 +465,10 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testLoggingWithNullNotification() {
var mcpAsyncServer = McpServer.using(createMcpTransport())
var mcpAsyncServer = McpServer.async(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().logging().build())
.async();
.build();
StepVerifier.create(mcpAsyncServer.loggingNotification(null)).verifyError(McpError.class);
}

View file

@ -17,15 +17,11 @@
package org.springframework.ai.mcp.server;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ai.mcp.server.McpServer.PromptRegistration;
import org.springframework.ai.mcp.server.McpServer.ResourceRegistration;
import org.springframework.ai.mcp.server.McpServer.ToolRegistration;
import org.springframework.ai.mcp.spec.McpError;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
@ -81,31 +77,31 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testConstructorWithInvalidArguments() {
assertThatThrownBy(() -> McpServer.using(null)).isInstanceOf(IllegalArgumentException.class)
assertThatThrownBy(() -> McpServer.sync(null)).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Transport must not be null");
assertThatThrownBy(() -> McpServer.using(createMcpTransport()).serverInfo((McpSchema.Implementation) null))
assertThatThrownBy(() -> McpServer.sync(createMcpTransport()).serverInfo(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Server info must not be null");
}
@Test
void testGracefulShutdown() {
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}
@Test
void testImmediateClose() {
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException();
}
@Test
void testGetAsyncServer() {
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThat(mcpSyncServer.getAsyncServer()).isNotNull();
@ -126,14 +122,14 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddTool() {
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.sync();
.build();
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
assertThatCode(() -> mcpSyncServer
.addTool(new ToolRegistration(newTool, args -> new CallToolResult(List.of(), false))))
.addTool(new McpServerFeatures.SyncToolRegistration(newTool, args -> new CallToolResult(List.of(), false))))
.doesNotThrowAnyException();
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
@ -143,14 +139,14 @@ public abstract class AbstractMcpSyncServerTests {
void testAddDuplicateTool() {
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tool(duplicateTool, args -> new CallToolResult(List.of(), false))
.sync();
.build();
assertThatThrownBy(() -> mcpSyncServer
.addTool(new ToolRegistration(duplicateTool, args -> new CallToolResult(List.of(), false))))
assertThatThrownBy(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolRegistration(duplicateTool,
args -> new CallToolResult(List.of(), false))))
.isInstanceOf(McpError.class)
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
@ -161,11 +157,11 @@ public abstract class AbstractMcpSyncServerTests {
void testRemoveTool() {
Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema);
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tool(tool, args -> new CallToolResult(List.of(), false))
.sync();
.build();
assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException();
@ -174,10 +170,10 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemoveNonexistentTool() {
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.sync();
.build();
assertThatThrownBy(() -> mcpSyncServer.removeTool("nonexistent-tool")).isInstanceOf(McpError.class)
.hasMessage("Tool with name 'nonexistent-tool' not found");
@ -187,7 +183,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testNotifyToolsListChanged() {
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException();
@ -200,7 +196,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testNotifyResourcesListChanged() {
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException();
@ -209,15 +205,15 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddResource() {
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.sync();
.build();
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
null);
ResourceRegistration registration = new ResourceRegistration(resource,
req -> new ReadResourceResult(List.of()));
McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration(
resource, req -> new ReadResourceResult(List.of()));
assertThatCode(() -> mcpSyncServer.addResource(registration)).doesNotThrowAnyException();
@ -226,12 +222,13 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddResourceWithNullRegistration() {
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.sync();
.build();
assertThatThrownBy(() -> mcpSyncServer.addResource(null)).isInstanceOf(McpError.class)
assertThatThrownBy(() -> mcpSyncServer.addResource((McpServerFeatures.SyncResourceRegistration) null))
.isInstanceOf(McpError.class)
.hasMessage("Resource must not be null");
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
@ -239,12 +236,12 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddResourceWithoutCapability() {
var serverWithoutResources = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var serverWithoutResources = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
null);
ResourceRegistration registration = new ResourceRegistration(resource,
req -> new ReadResourceResult(List.of()));
McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration(
resource, req -> new ReadResourceResult(List.of()));
assertThatThrownBy(() -> serverWithoutResources.addResource(registration)).isInstanceOf(McpError.class)
.hasMessage("Server must be configured with resource capabilities");
@ -252,7 +249,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemoveResourceWithoutCapability() {
var serverWithoutResources = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var serverWithoutResources = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)).isInstanceOf(McpError.class)
.hasMessage("Server must be configured with resource capabilities");
@ -264,7 +261,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testNotifyPromptsListChanged() {
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.notifyPromptsListChanged()).doesNotThrowAnyException();
@ -273,23 +270,24 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddPromptWithNullRegistration() {
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(false).build())
.sync();
.build();
assertThatThrownBy(() -> mcpSyncServer.addPrompt(null)).isInstanceOf(McpError.class)
assertThatThrownBy(() -> mcpSyncServer.addPrompt((McpServerFeatures.SyncPromptRegistration) null))
.isInstanceOf(McpError.class)
.hasMessage("Prompt registration must not be null");
}
@Test
void testAddPromptWithoutCapability() {
var serverWithoutPrompts = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var serverWithoutPrompts = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
"Test prompt description",
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt,
req -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(registration)).isInstanceOf(McpError.class)
.hasMessage("Server must be configured with prompt capabilities");
@ -297,7 +295,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemovePromptWithoutCapability() {
var serverWithoutPrompts = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var serverWithoutPrompts = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class)
.hasMessage("Server must be configured with prompt capabilities");
@ -306,15 +304,15 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemovePrompt() {
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
"Test prompt description",
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt,
req -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
.prompts(registration)
.sync();
.build();
assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException();
@ -323,10 +321,10 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemoveNonexistentPrompt() {
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
.sync();
.build();
assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class)
.hasMessage("Prompt with name 'nonexistent-prompt' not found");
@ -344,7 +342,7 @@ public abstract class AbstractMcpSyncServerTests {
var rootsReceived = new McpSchema.Root[1];
var consumerCalled = new boolean[1];
var singleConsumerServer = McpServer.using(createMcpTransport())
var singleConsumerServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.rootsChangeConsumers(List.of(roots -> {
consumerCalled[0] = true;
@ -352,7 +350,7 @@ public abstract class AbstractMcpSyncServerTests {
rootsReceived[0] = roots.get(0);
}
}))
.sync();
.build();
assertThat(singleConsumerServer).isNotNull();
assertThatCode(() -> singleConsumerServer.closeGracefully()).doesNotThrowAnyException();
@ -363,32 +361,32 @@ public abstract class AbstractMcpSyncServerTests {
var consumer2Called = new boolean[1];
var rootsContent = new List[1];
var multipleConsumersServer = McpServer.using(createMcpTransport())
var multipleConsumersServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.rootsChangeConsumers(List.of(roots -> {
consumer1Called[0] = true;
rootsContent[0] = roots;
}, roots -> consumer2Called[0] = true))
.sync();
.build();
assertThat(multipleConsumersServer).isNotNull();
assertThatCode(() -> multipleConsumersServer.closeGracefully()).doesNotThrowAnyException();
onClose();
// Test error handling
var errorHandlingServer = McpServer.using(createMcpTransport())
var errorHandlingServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.rootsChangeConsumers(List.of(roots -> {
throw new RuntimeException("Test error");
}))
.sync();
.build();
assertThat(errorHandlingServer).isNotNull();
assertThatCode(() -> errorHandlingServer.closeGracefully()).doesNotThrowAnyException();
onClose();
// Test without consumers
var noConsumersServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
var noConsumersServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
assertThat(noConsumersServer).isNotNull();
assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();
@ -400,10 +398,10 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testLoggingLevels() {
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().logging().build())
.sync();
.build();
// Test all logging levels
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
@ -419,10 +417,10 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testLoggingWithoutCapability() {
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().build()) // No logging capability
.sync();
.build();
var notification = McpSchema.LoggingMessageNotification.builder()
.level(McpSchema.LoggingLevel.INFO)
@ -435,10 +433,10 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testLoggingWithNullNotification() {
var mcpSyncServer = McpServer.using(createMcpTransport())
var mcpSyncServer = McpServer.sync(createMcpTransport())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().logging().build())
.sync();
.build();
assertThatThrownBy(() -> mcpSyncServer.loggingNotification(null)).isInstanceOf(McpError.class);
}

View file

@ -36,7 +36,7 @@ import reactor.test.StepVerifier;
import org.springframework.ai.mcp.client.McpClient;
import org.springframework.ai.mcp.client.transport.HttpClientSseClientTransport;
import org.springframework.ai.mcp.server.McpServer;
import org.springframework.ai.mcp.server.McpServer.ToolRegistration;
import org.springframework.ai.mcp.server.McpServerFeatures;
import org.springframework.ai.mcp.spec.McpError;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
@ -63,7 +63,7 @@ public class HttpServletSseServerTransportIntegrationTests {
private HttpServletSseServerTransport mcpServerTransport;
McpClient.Builder clientBuilder;
McpClient.SyncSpec clientBuilder;
private Tomcat tomcat;
@ -99,7 +99,7 @@ public class HttpServletSseServerTransportIntegrationTests {
throw new RuntimeException("Failed to start Tomcat", e);
}
this.clientBuilder = McpClient.using(new HttpClientSseClientTransport("http://localhost:" + PORT));
this.clientBuilder = McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT));
}
@AfterEach
@ -120,7 +120,7 @@ public class HttpServletSseServerTransportIntegrationTests {
@Test
void testCreateMessageWithoutInitialization() {
var mcpAsyncServer = McpServer.using(mcpServerTransport).serverInfo("test-server", "1.0.0").async();
var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build();
var messages = List
.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message")));
@ -137,9 +137,9 @@ public class HttpServletSseServerTransportIntegrationTests {
@Test
void testCreateMessageWithoutSamplingCapabilities() {
var mcpAsyncServer = McpServer.using(mcpServerTransport).serverInfo("test-server", "1.0.0").async();
var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build();
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).sync();
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build();
InitializeResult initResult = client.initialize();
assertThat(initResult).isNotNull();
@ -159,7 +159,7 @@ public class HttpServletSseServerTransportIntegrationTests {
@Test
void testCreateMessageSuccess() {
var mcpAsyncServer = McpServer.using(mcpServerTransport).serverInfo("test-server", "1.0.0").async();
var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build();
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
assertThat(request.messages()).hasSize(1);
@ -172,7 +172,7 @@ public class HttpServletSseServerTransportIntegrationTests {
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
.capabilities(ClientCapabilities.builder().sampling().build())
.sampling(samplingHandler)
.sync();
.build();
InitializeResult initResult = client.initialize();
assertThat(initResult).isNotNull();
@ -199,13 +199,13 @@ public class HttpServletSseServerTransportIntegrationTests {
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
var mcpServer = McpServer.using(mcpServerTransport)
var mcpServer = McpServer.sync(mcpServerTransport)
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
.sync();
.build();
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
.roots(roots)
.sync();
.build();
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
@ -235,8 +235,8 @@ public class HttpServletSseServerTransportIntegrationTests {
@Test
void testToolCallSuccess() {
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
ToolRegistration tool1 = new ToolRegistration(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema),
request -> {
McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
String response = RestClient.create()
.get()
.uri("https://github.com/spring-projects-experimental/spring-ai-mcp/blob/main/README.md")
@ -246,12 +246,12 @@ public class HttpServletSseServerTransportIntegrationTests {
return callResponse;
});
var mcpServer = McpServer.using(mcpServerTransport)
var mcpServer = McpServer.sync(mcpServerTransport)
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool1)
.sync();
.build();
var mcpClient = clientBuilder.sync();
var mcpClient = clientBuilder.build();
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
@ -270,8 +270,8 @@ public class HttpServletSseServerTransportIntegrationTests {
@Test
void testToolListChangeHandlingSuccess() {
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
ToolRegistration tool1 = new ToolRegistration(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema),
request -> {
McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
String response = RestClient.create()
.get()
.uri("https://github.com/spring-projects-experimental/spring-ai-mcp/blob/main/README.md")
@ -281,10 +281,10 @@ public class HttpServletSseServerTransportIntegrationTests {
return callResponse;
});
var mcpServer = McpServer.using(mcpServerTransport)
var mcpServer = McpServer.sync(mcpServerTransport)
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool1)
.sync();
.build();
AtomicReference<List<Tool>> toolsRef = new AtomicReference<>();
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
@ -295,7 +295,7 @@ public class HttpServletSseServerTransportIntegrationTests {
.body(String.class);
assertThat(response).isNotBlank();
toolsRef.set(toolsUpdate);
}).sync();
}).build();
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
@ -316,8 +316,8 @@ public class HttpServletSseServerTransportIntegrationTests {
assertThat(toolsRef.get()).isEmpty();
});
ToolRegistration tool2 = new ToolRegistration(new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema),
request -> callResponse);
McpServerFeatures.SyncToolRegistration tool2 = new McpServerFeatures.SyncToolRegistration(
new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), request -> callResponse);
mcpServer.addTool(tool2);
@ -331,8 +331,8 @@ public class HttpServletSseServerTransportIntegrationTests {
@Test
void testInitialize() {
var mcpServer = McpServer.using(mcpServerTransport).sync();
var mcpClient = clientBuilder.sync();
var mcpServer = McpServer.sync(mcpServerTransport).build();
var mcpClient = clientBuilder.build();
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();

View file

@ -150,7 +150,7 @@ class DefaultMcpSessionTests {
@Test
void testRequestHandling() {
String echoMessage = "Hello MCP!";
Map<String, DefaultMcpSession.RequestHandler> requestHandlers = Map.of(ECHO_METHOD,
Map<String, DefaultMcpSession.RequestHandler<?>> requestHandlers = Map.of(ECHO_METHOD,
params -> Mono.just(params));
transport = new MockMcpTransport();
session = new DefaultMcpSession(TIMEOUT, transport, requestHandlers, Map.of());