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
2 changed files with 3494 additions and 753 deletions

View file

@ -0,0 +1,258 @@
# Spring AI MCP 0.5.0 Migration Guide
This guide outlines the steps required to migrate your code to Spring AI MCP 0.6.0.
## Key Changes
- New builder patterns for `McpClient` and `McpServer`
- Introduction of dedicated feature classes for sync/async operations
- Enhanced type safety and reactive support
- Deprecated methods and classes marked for removal
## Client Migration
### Creating Clients
Before:
```java
// Sync client
McpClient.using(transport)
.requestTimeout(Duration.ofSeconds(5))
.sync();
// Async client
McpClient.using(transport)
.requestTimeout(Duration.ofSeconds(5))
.async();
```
After:
```java
// Sync client
McpClient.sync(transport)
.requestTimeout(Duration.ofSeconds(5))
.build();
// Async client
McpClient.async(transport)
.requestTimeout(Duration.ofSeconds(5))
.build();
```
### Change Consumers
Before:
```java
// Sync client
McpClient.using(transport)
.toolsChangeConsumer(tools -> handleTools(tools))
.resourcesChangeConsumer(resources -> handleResources(resources))
.promptsChangeConsumer(prompts -> handlePrompts(prompts))
.sync();
// Async client
McpClient.using(transport)
.toolsChangeConsumer(tools -> handleTools(tools))
.resourcesChangeConsumer(resources -> handleResources(resources))
.promptsChangeConsumer(prompts -> handlePrompts(prompts))
.async();
```
After:
```java
// Sync client
McpClient.sync(transport)
.toolsChangeConsumer(tools -> handleTools(tools))
.resourcesChangeConsumer(resources -> handleResources(resources))
.promptsChangeConsumer(prompts -> handlePrompts(prompts))
.build();
// Async client
McpClient.async(transport)
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> handleTools(tools)))
.resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> handleResources(resources)))
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> handlePrompts(prompts)))
.build();
```
### Sampling Handlers
Before:
```java
// Sync client
McpClient.using(transport)
.sampling(request -> new CreateMessageResult("response"))
.sync();
// Async client
McpClient.using(transport)
.sampling(request -> new CreateMessageResult("response"))
.async();
```
After:
```java
// Sync client
McpClient.sync(transport)
.sampling(request -> new CreateMessageResult("response"))
.build();
// Async client
McpClient.async(transport)
.sampling(request -> Mono.just(new CreateMessageResult("response")))
.build();
```
## Server Migration
### Creating Servers
Before:
```java
// Sync server
McpServer.using(transport)
.serverInfo("test-server", "1.0.0")
.sync();
// Async server
McpServer.using(transport)
.serverInfo("test-server", "1.0.0")
.async();
```
After:
```java
// Sync server
McpServer.sync(transport)
.serverInfo("test-server", "1.0.0")
.build();
// Async server
McpServer.async(transport)
.serverInfo("test-server", "1.0.0")
.build();
```
### Tool Registration
Before:
```java
// Using ToolRegistration record
new ToolRegistration(
new Tool("calculator", "Performs calculations", schema),
args -> new CallToolResult("result")
);
```
After:
```java
// Sync server
new McpServerFeatures.SyncToolRegistration(
new Tool("calculator", "Performs calculations", schema),
args -> new CallToolResult("result")
);
// Async server
new McpServerFeatures.AsyncToolRegistration(
new Tool("calculator", "Performs calculations", schema),
args -> Mono.just(new CallToolResult("result"))
);
```
### Resource Registration
Before:
```java
// Using ResourceRegistration record
new ResourceRegistration(
new Resource("docs", "Documentation", "text/markdown"),
request -> new ReadResourceResult(content)
);
```
After:
```java
// Sync server
new McpServerFeatures.SyncResourceRegistration(
new Resource("docs", "Documentation", "text/markdown"),
request -> new ReadResourceResult(content)
);
// Async server
new McpServerFeatures.AsyncResourceRegistration(
new Resource("docs", "Documentation", "text/markdown"),
request -> Mono.just(new ReadResourceResult(content))
);
```
### Prompt Registration
Before:
```java
// Using PromptRegistration record
new PromptRegistration(
new Prompt("analyze", "Code analysis"),
request -> new GetPromptResult("result")
);
```
After:
```java
// Sync server
new McpServerFeatures.SyncPromptRegistration(
new Prompt("analyze", "Code analysis"),
request -> new GetPromptResult("result")
);
// Async server
new McpServerFeatures.AsyncPromptRegistration(
new Prompt("analyze", "Code analysis"),
request -> Mono.just(new GetPromptResult("result"))
);
```
## Spring Integration Changes
### Tool Helper Changes
Before:
```java
ToolHelper.toToolRegistration(functionCallback);
ToolHelper.toToolRegistration(functionCallbacks);
```
After:
```java
ToolHelper.toSyncToolRegistration(functionCallback);
ToolHelper.toSyncToolRegistration(functionCallbacks);
```
## Deprecated APIs
The following APIs are deprecated and will be removed in a future release:
- `McpClient.using()` - Use `McpClient.sync()` or `McpClient.async()` instead
- `McpServer.using()` - Use `McpServer.sync()` or `McpServer.async()` instead
- `McpServer.ToolRegistration` - Use `McpServerFeatures.SyncToolRegistration` or `McpServerFeatures.AsyncToolRegistration` instead
- `McpServer.ResourceRegistration` - Use `McpServerFeatures.SyncResourceRegistration` or `McpServerFeatures.AsyncResourceRegistration` instead
- `McpServer.PromptRegistration` - Use `McpServerFeatures.SyncPromptRegistration` or `McpServerFeatures.AsyncPromptRegistration` instead
- `ToolHelper.toToolRegistration()` - Use `ToolHelper.toSyncToolRegistration()` instead
## Benefits of Migration
1. **Improved Type Safety**: The new builder patterns and feature classes provide better compile-time type checking.
2. **Clear Async/Sync Separation**: Distinct builders and features for sync and async operations make the code intent clearer.
3. **Enhanced Reactive Support**: Async operations now properly integrate with Project Reactor's `Mono` type.
4. **Better Error Handling**: More consistent error handling across sync and async operations.
5. **Simplified Configuration**: Builder patterns provide a more intuitive way to configure clients and servers.
## Additional Notes
- The migration primarily focuses on builder patterns and feature organization
- Functional behavior remains the same after migration
- Async operations now properly integrate with Project Reactor
- All deprecated APIs will continue to work but will be removed in a future release

View file

@ -129,14 +129,14 @@ Sync API::
[source,java]
----
// Create a sync client with custom configuration
McpSyncClient client = McpClient.using(transport)
McpSyncClient client = McpClient.sync(transport)
.requestTimeout(Duration.ofSeconds(10))
.capabilities(ClientCapabilities.builder()
.roots(true) // Enable roots capability
.sampling() // Enable sampling capability
.build())
.sampling( request -> new CreateMessageResult(response))
.sync();
.sampling(request -> new CreateMessageResult(response))
.build();
// Initialize connection
client.initialize();
@ -175,23 +175,23 @@ Async API::
[source,java]
----
// Create an async client with custom configuration
McpAsyncClient client = McpClient.using(transport)
McpAsyncClient client = McpClient.async(transport)
.requestTimeout(Duration.ofSeconds(10))
.capabilities(ClientCapabilities.builder()
.roots(true) // Enable roots capability
.sampling() // Enable sampling capability
.build())
.sampling( request -> new CreateMessageResult(response))
.toolsChangeConsumer(tools -> {
.sampling(request -> Mono.just(new CreateMessageResult(response)))
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> {
logger.info("Tools updated: {}", tools);
})
.resourcesChangeConsumer(resources -> {
}))
.resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> {
logger.info("Resources updated: {}", resources);
})
.promptsChangeConsumer(prompts -> {
}))
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> {
logger.info("Prompts updated: {}", prompts);
})
.async();
}))
.build();
// Initialize connection
client.initialize()
@ -361,13 +361,13 @@ Sync API::
[source,java]
----
// Create a server with custom configuration
McpSyncServer syncServer = McpServer.using(transport)
.info("my-server", "1.0.0")
McpSyncServer syncServer = McpServer.sync(transport)
.serverInfo("my-server", "1.0.0")
.capabilities(ServerCapabilities.builder()...build())
.tools(new CalculatorTool())
.resources(resourceRegistration)
.prompts(promptRegistration)
.sync();
.tools(new McpServerFeatures.SyncToolRegistration(calculatorTool, calculatorHandler))
.resources(new McpServerFeatures.SyncResourceRegistration(resource, resourceHandler))
.prompts(new McpServerFeatures.SyncPromptRegistration(prompt, promptHandler))
.build();
// Add a tool handler at runtime
syncServer.addTool(new CalculatorTool());
@ -396,13 +396,13 @@ Async API::
[source,java]
----
// Create an async server with custom configuration
McpAsyncServer asyncServer = McpServer.using(transport)
.info("my-server", "1.0.0")
McpAsyncServer asyncServer = McpServer.async(transport)
.serverInfo("my-server", "1.0.0")
.capabilities(ServerCapabilities.builder()...build())
.tools(new CalculatorTool())
.resources(resourceRegistration)
.prompts(promptRegistration)
.async();
.tools(new McpServerFeatures.AsyncToolRegistration(calculatorTool, args -> Mono.just(calculatorHandler.apply(args))))
.resources(new McpServerFeatures.AsyncResourceRegistration(resource, req -> Mono.just(resourceHandler.apply(req))))
.prompts(new McpServerFeatures.AsyncPromptRegistration(prompt, req -> Mono.just(promptHandler.apply(req))))
.build();
// Add a tool handler at runtime
asyncServer.addTool(new CalculatorTool())
@ -593,7 +593,8 @@ Supported logging levels (in order of increasing severity): DEBUG (0), INFO (1),
[source,java]
----
var toolRegistration = new ToolRegistration(
// Sync tool registration
var syncToolRegistration = new McpServerFeatures.SyncToolRegistration(
new Tool("calculator", "Basic calculator", Map.of(
"operation", "string",
"a", "number",
@ -604,26 +605,50 @@ var toolRegistration = new ToolRegistration(
return new CallToolResult(result, false);
}
);
// Async tool registration
var asyncToolRegistration = new McpServerFeatures.AsyncToolRegistration(
new Tool("calculator", "Basic calculator", Map.of(
"operation", "string",
"a", "number",
"b", "number"
)),
arguments -> {
// Tool implementation
return Mono.just(new CallToolResult(result, false));
}
);
----
==== Resource Registration
[source,java]
----
var resourceRegistration = new ResourceRegistration(
// Sync resource registration
var syncResourceRegistration = new McpServerFeatures.SyncResourceRegistration(
new Resource("custom://resource", "name", "description", "mime-type", null),
request -> {
// Resource read implementation
return new ReadResourceResult(contents);
}
);
// Async resource registration
var asyncResourceRegistration = new McpServerFeatures.AsyncResourceRegistration(
new Resource("custom://resource", "name", "description", "mime-type", null),
request -> {
// Resource read implementation
return Mono.just(new ReadResourceResult(contents));
}
);
----
==== Prompt Registration
[source,java]
----
var promptRegistration = new PromptRegistration(
// Sync prompt registration
var syncPromptRegistration = new McpServerFeatures.SyncPromptRegistration(
new Prompt("greeting", "description", List.of(
new PromptArgument("name", "description", true)
)),
@ -632,6 +657,17 @@ var promptRegistration = new PromptRegistration(
return new GetPromptResult(description, messages);
}
);
// Async prompt registration
var asyncPromptRegistration = new McpServerFeatures.AsyncPromptRegistration(
new Prompt("greeting", "description", List.of(
new PromptArgument("name", "description", true)
)),
request -> {
// Prompt implementation
return Mono.just(new GetPromptResult(description, messages));
}
);
----
== Error Handling