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:
parent
e1bdc98813
commit
6839d3b740
25 changed files with 3494 additions and 753 deletions
258
mcp-docs/0.6.0-MIGRATION-GUIDE.md
Normal file
258
mcp-docs/0.6.0-MIGRATION-GUIDE.md
Normal 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
|
||||||
|
|
@ -129,14 +129,14 @@ Sync API::
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
// Create a sync client with custom configuration
|
// Create a sync client with custom configuration
|
||||||
McpSyncClient client = McpClient.using(transport)
|
McpSyncClient client = McpClient.sync(transport)
|
||||||
.requestTimeout(Duration.ofSeconds(10))
|
.requestTimeout(Duration.ofSeconds(10))
|
||||||
.capabilities(ClientCapabilities.builder()
|
.capabilities(ClientCapabilities.builder()
|
||||||
.roots(true) // Enable roots capability
|
.roots(true) // Enable roots capability
|
||||||
.sampling() // Enable sampling capability
|
.sampling() // Enable sampling capability
|
||||||
.build())
|
.build())
|
||||||
.sampling( request -> new CreateMessageResult(response))
|
.sampling(request -> new CreateMessageResult(response))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
// Initialize connection
|
// Initialize connection
|
||||||
client.initialize();
|
client.initialize();
|
||||||
|
|
@ -175,23 +175,23 @@ Async API::
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
// Create an async client with custom configuration
|
// Create an async client with custom configuration
|
||||||
McpAsyncClient client = McpClient.using(transport)
|
McpAsyncClient client = McpClient.async(transport)
|
||||||
.requestTimeout(Duration.ofSeconds(10))
|
.requestTimeout(Duration.ofSeconds(10))
|
||||||
.capabilities(ClientCapabilities.builder()
|
.capabilities(ClientCapabilities.builder()
|
||||||
.roots(true) // Enable roots capability
|
.roots(true) // Enable roots capability
|
||||||
.sampling() // Enable sampling capability
|
.sampling() // Enable sampling capability
|
||||||
.build())
|
.build())
|
||||||
.sampling( request -> new CreateMessageResult(response))
|
.sampling(request -> Mono.just(new CreateMessageResult(response)))
|
||||||
.toolsChangeConsumer(tools -> {
|
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> {
|
||||||
logger.info("Tools updated: {}", tools);
|
logger.info("Tools updated: {}", tools);
|
||||||
})
|
}))
|
||||||
.resourcesChangeConsumer(resources -> {
|
.resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> {
|
||||||
logger.info("Resources updated: {}", resources);
|
logger.info("Resources updated: {}", resources);
|
||||||
})
|
}))
|
||||||
.promptsChangeConsumer(prompts -> {
|
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> {
|
||||||
logger.info("Prompts updated: {}", prompts);
|
logger.info("Prompts updated: {}", prompts);
|
||||||
})
|
}))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
// Initialize connection
|
// Initialize connection
|
||||||
client.initialize()
|
client.initialize()
|
||||||
|
|
@ -361,13 +361,13 @@ Sync API::
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
// Create a server with custom configuration
|
// Create a server with custom configuration
|
||||||
McpSyncServer syncServer = McpServer.using(transport)
|
McpSyncServer syncServer = McpServer.sync(transport)
|
||||||
.info("my-server", "1.0.0")
|
.serverInfo("my-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder()...build())
|
.capabilities(ServerCapabilities.builder()...build())
|
||||||
.tools(new CalculatorTool())
|
.tools(new McpServerFeatures.SyncToolRegistration(calculatorTool, calculatorHandler))
|
||||||
.resources(resourceRegistration)
|
.resources(new McpServerFeatures.SyncResourceRegistration(resource, resourceHandler))
|
||||||
.prompts(promptRegistration)
|
.prompts(new McpServerFeatures.SyncPromptRegistration(prompt, promptHandler))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
// Add a tool handler at runtime
|
// Add a tool handler at runtime
|
||||||
syncServer.addTool(new CalculatorTool());
|
syncServer.addTool(new CalculatorTool());
|
||||||
|
|
@ -396,13 +396,13 @@ Async API::
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
// Create an async server with custom configuration
|
// Create an async server with custom configuration
|
||||||
McpAsyncServer asyncServer = McpServer.using(transport)
|
McpAsyncServer asyncServer = McpServer.async(transport)
|
||||||
.info("my-server", "1.0.0")
|
.serverInfo("my-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder()...build())
|
.capabilities(ServerCapabilities.builder()...build())
|
||||||
.tools(new CalculatorTool())
|
.tools(new McpServerFeatures.AsyncToolRegistration(calculatorTool, args -> Mono.just(calculatorHandler.apply(args))))
|
||||||
.resources(resourceRegistration)
|
.resources(new McpServerFeatures.AsyncResourceRegistration(resource, req -> Mono.just(resourceHandler.apply(req))))
|
||||||
.prompts(promptRegistration)
|
.prompts(new McpServerFeatures.AsyncPromptRegistration(prompt, req -> Mono.just(promptHandler.apply(req))))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
// Add a tool handler at runtime
|
// Add a tool handler at runtime
|
||||||
asyncServer.addTool(new CalculatorTool())
|
asyncServer.addTool(new CalculatorTool())
|
||||||
|
|
@ -593,7 +593,8 @@ Supported logging levels (in order of increasing severity): DEBUG (0), INFO (1),
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
var toolRegistration = new ToolRegistration(
|
// Sync tool registration
|
||||||
|
var syncToolRegistration = new McpServerFeatures.SyncToolRegistration(
|
||||||
new Tool("calculator", "Basic calculator", Map.of(
|
new Tool("calculator", "Basic calculator", Map.of(
|
||||||
"operation", "string",
|
"operation", "string",
|
||||||
"a", "number",
|
"a", "number",
|
||||||
|
|
@ -604,26 +605,50 @@ var toolRegistration = new ToolRegistration(
|
||||||
return new CallToolResult(result, false);
|
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
|
==== Resource Registration
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
var resourceRegistration = new ResourceRegistration(
|
// Sync resource registration
|
||||||
|
var syncResourceRegistration = new McpServerFeatures.SyncResourceRegistration(
|
||||||
new Resource("custom://resource", "name", "description", "mime-type", null),
|
new Resource("custom://resource", "name", "description", "mime-type", null),
|
||||||
request -> {
|
request -> {
|
||||||
// Resource read implementation
|
// Resource read implementation
|
||||||
return new ReadResourceResult(contents);
|
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
|
==== Prompt Registration
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
var promptRegistration = new PromptRegistration(
|
// Sync prompt registration
|
||||||
|
var syncPromptRegistration = new McpServerFeatures.SyncPromptRegistration(
|
||||||
new Prompt("greeting", "description", List.of(
|
new Prompt("greeting", "description", List.of(
|
||||||
new PromptArgument("name", "description", true)
|
new PromptArgument("name", "description", true)
|
||||||
)),
|
)),
|
||||||
|
|
@ -632,6 +657,17 @@ var promptRegistration = new PromptRegistration(
|
||||||
return new GetPromptResult(description, messages);
|
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
|
== Error Handling
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
||||||
|
|
@ -77,10 +78,10 @@ public abstract class BlaAbstractMcpAsyncClientTests {
|
||||||
this.mcpTransport = createMcpTransport();
|
this.mcpTransport = createMcpTransport();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
mcpAsyncClient = McpClient.using(mcpTransport)
|
mcpAsyncClient = McpClient.async(mcpTransport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.async();
|
.build();
|
||||||
mcpAsyncClient.initialize().block(Duration.ofSeconds(10));
|
mcpAsyncClient.initialize().block(Duration.ofSeconds(10));
|
||||||
}).doesNotThrowAnyException();
|
}).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
@ -96,10 +97,10 @@ public abstract class BlaAbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstructorWithInvalidArguments() {
|
void testConstructorWithInvalidArguments() {
|
||||||
assertThatThrownBy(() -> McpClient.using(null).sync()).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpClient.using(mcpTransport).requestTimeout(null).sync())
|
assertThatThrownBy(() -> McpClient.sync(mcpTransport).requestTimeout(null).build())
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Request timeout must not be null");
|
.hasMessage("Request timeout must not be null");
|
||||||
}
|
}
|
||||||
|
|
@ -195,10 +196,10 @@ public abstract class BlaAbstractMcpAsyncClientTests {
|
||||||
void testInitializeWithRootsListProviders() {
|
void testInitializeWithRootsListProviders() {
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
|
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.async(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.roots(new Root("file:///test/path", "test-root"))
|
.roots(new Root("file:///test/path", "test-root"))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> client.initialize().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
assertThatCode(() -> client.initialize().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -277,12 +278,12 @@ public abstract class BlaAbstractMcpAsyncClientTests {
|
||||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||||
|
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.async(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true)))
|
||||||
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
.resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true)))
|
||||||
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true)))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize().block();
|
client.initialize().block();
|
||||||
|
|
@ -299,9 +300,11 @@ public abstract class BlaAbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
var capabilities = ClientCapabilities.builder().sampling().build();
|
var capabilities = ClientCapabilities.builder().sampling().build();
|
||||||
|
|
||||||
var client = McpClient.using(transport).requestTimeout(TIMEOUT).capabilities(capabilities).sampling(request -> {
|
var client = McpClient.async(transport)
|
||||||
return CreateMessageResult.builder().message("test").model("test-model").build();
|
.requestTimeout(TIMEOUT)
|
||||||
}).async();
|
.capabilities(capabilities)
|
||||||
|
.sampling(request -> Mono.just(CreateMessageResult.builder().message("test").model("test-model").build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize().block(Duration.ofSeconds(10));
|
client.initialize().block(Duration.ofSeconds(10));
|
||||||
|
|
@ -319,14 +322,13 @@ public abstract class BlaAbstractMcpAsyncClientTests {
|
||||||
.sampling()
|
.sampling()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
||||||
return CreateMessageResult.builder().message("test").model("test-model").build();
|
.just(CreateMessageResult.builder().message("test").model("test-model").build());
|
||||||
};
|
var client = McpClient.async(transport)
|
||||||
var client = McpClient.using(transport)
|
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.capabilities(capabilities)
|
.capabilities(capabilities)
|
||||||
.sampling(samplingHandler)
|
.sampling(samplingHandler)
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
var result = client.initialize().block(Duration.ofSeconds(10));
|
var result = client.initialize().block(Duration.ofSeconds(10));
|
||||||
|
|
@ -353,10 +355,10 @@ public abstract class BlaAbstractMcpAsyncClientTests {
|
||||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
|
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.async(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.loggingConsumer(notification -> logReceived.set(true))
|
.loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true)))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize().block(Duration.ofSeconds(10));
|
client.initialize().block(Duration.ofSeconds(10));
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,10 @@ public abstract class BlaAbstractMcpSyncClientTests {
|
||||||
this.mcpTransport = createMcpTransport();
|
this.mcpTransport = createMcpTransport();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
mcpSyncClient = McpClient.using(mcpTransport)
|
mcpSyncClient = McpClient.sync(mcpTransport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.sync();
|
.build();
|
||||||
mcpSyncClient.initialize();
|
mcpSyncClient.initialize();
|
||||||
}).doesNotThrowAnyException();
|
}).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
@ -90,10 +90,10 @@ public abstract class BlaAbstractMcpSyncClientTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstructorWithInvalidArguments() {
|
void testConstructorWithInvalidArguments() {
|
||||||
assertThatThrownBy(() -> McpClient.using(null).sync()).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpClient.using(mcpTransport).requestTimeout(null).sync())
|
assertThatThrownBy(() -> McpClient.sync(mcpTransport).requestTimeout(null).build())
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Request timeout must not be null");
|
.hasMessage("Request timeout must not be null");
|
||||||
}
|
}
|
||||||
|
|
@ -180,10 +180,10 @@ public abstract class BlaAbstractMcpSyncClientTests {
|
||||||
void testInitializeWithRootsListProviders() {
|
void testInitializeWithRootsListProviders() {
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
|
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.sync(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.roots(new Root("file:///test/path", "test-root"))
|
.roots(new Root("file:///test/path", "test-root"))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize();
|
client.initialize();
|
||||||
|
|
@ -262,12 +262,12 @@ public abstract class BlaAbstractMcpSyncClientTests {
|
||||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||||
|
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.sync(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
||||||
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
||||||
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize();
|
client.initialize();
|
||||||
|
|
@ -295,10 +295,10 @@ public abstract class BlaAbstractMcpSyncClientTests {
|
||||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
|
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.sync(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.loggingConsumer(notification -> logReceived.set(true))
|
.loggingConsumer(notification -> logReceived.set(true))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize();
|
client.initialize();
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,13 @@ package org.springframework.ai.mcp.server;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
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.McpError;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema;
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
|
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
|
||||||
|
|
@ -82,24 +79,24 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstructorWithInvalidArguments() {
|
void testConstructorWithInvalidArguments() {
|
||||||
assertThatThrownBy(() -> McpServer.using(null)).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpServer.async(null)).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.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)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Server info must not be null");
|
.hasMessage("Server info must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGracefulShutdown() {
|
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();
|
StepVerifier.create(mcpAsyncServer.closeGracefully()).verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testImmediateClose() {
|
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();
|
assertThatCode(() -> mcpAsyncServer.close()).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
@ -118,81 +115,80 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testAddTool() {
|
void testAddTool() {
|
||||||
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
|
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")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier
|
StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(newTool,
|
||||||
.create(mcpAsyncServer.addTool(new ToolRegistration(newTool, args -> new CallToolResult(List.of(), false))))
|
args -> Mono.just(new CallToolResult(List.of(), false)))))
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
|
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddDuplicateTool() {
|
void testAddDuplicateTool() {
|
||||||
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
|
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")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(duplicateTool, args -> new CallToolResult(List.of(), false))
|
.tool(duplicateTool, args -> Mono.just(new CallToolResult(List.of(), false)))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier
|
StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(duplicateTool,
|
||||||
.create(mcpAsyncServer
|
args -> Mono.just(new CallToolResult(List.of(), false)))))
|
||||||
.addTool(new ToolRegistration(duplicateTool, args -> new CallToolResult(List.of(), false))))
|
|
||||||
.verifyErrorSatisfies(error -> {
|
.verifyErrorSatisfies(error -> {
|
||||||
assertThat(error).isInstanceOf(McpError.class)
|
assertThat(error).isInstanceOf(McpError.class)
|
||||||
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
|
.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
|
@Test
|
||||||
void testRemoveTool() {
|
void testRemoveTool() {
|
||||||
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
|
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")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(too, args -> new CallToolResult(List.of(), false))
|
.tool(too, args -> Mono.just(new CallToolResult(List.of(), false)))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete();
|
StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete();
|
||||||
|
|
||||||
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
|
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonexistentTool() {
|
void testRemoveNonexistentTool() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyErrorSatisfies(error -> {
|
StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyErrorSatisfies(error -> {
|
||||||
assertThat(error).isInstanceOf(McpError.class).hasMessage("Tool with name 'nonexistent-tool' not found");
|
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
|
@Test
|
||||||
void testNotifyToolsListChanged() {
|
void testNotifyToolsListChanged() {
|
||||||
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
|
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")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(too, args -> new CallToolResult(List.of(), false))
|
.tool(too, args -> Mono.just(new CallToolResult(List.of(), false)))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete();
|
StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete();
|
||||||
|
|
||||||
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
|
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -201,24 +197,24 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyResourcesListChanged() {
|
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();
|
StepVerifier.create(mcpAsyncServer.notifyResourcesListChanged()).verifyComplete();
|
||||||
|
|
||||||
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
|
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddResource() {
|
void testAddResource() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
||||||
null);
|
null);
|
||||||
ResourceRegistration registration = new ResourceRegistration(resource,
|
McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration(
|
||||||
req -> new ReadResourceResult(List.of()));
|
resource, req -> Mono.just(new ReadResourceResult(List.of())));
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.addResource(registration)).verifyComplete();
|
StepVerifier.create(mcpAsyncServer.addResource(registration)).verifyComplete();
|
||||||
|
|
||||||
|
|
@ -227,14 +223,15 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddResourceWithNullRegistration() {
|
void testAddResourceWithNullRegistration() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.addResource(null)).verifyErrorSatisfies(error -> {
|
StepVerifier.create(mcpAsyncServer.addResource((McpServerFeatures.AsyncResourceRegistration) null))
|
||||||
assertThat(error).isInstanceOf(McpError.class).hasMessage("Resource must not be null");
|
.verifyErrorSatisfies(error -> {
|
||||||
});
|
assertThat(error).isInstanceOf(McpError.class).hasMessage("Resource must not be null");
|
||||||
|
});
|
||||||
|
|
||||||
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
@ -242,14 +239,14 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testAddResourceWithoutCapability() {
|
void testAddResourceWithoutCapability() {
|
||||||
// Create a server without resource capabilities
|
// Create a server without resource capabilities
|
||||||
McpAsyncServer serverWithoutResources = McpServer.using(createMcpTransport())
|
McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
||||||
null);
|
null);
|
||||||
ResourceRegistration registration = new ResourceRegistration(resource,
|
McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration(
|
||||||
req -> new ReadResourceResult(List.of()));
|
resource, req -> Mono.just(new ReadResourceResult(List.of())));
|
||||||
|
|
||||||
StepVerifier.create(serverWithoutResources.addResource(registration)).verifyErrorSatisfies(error -> {
|
StepVerifier.create(serverWithoutResources.addResource(registration)).verifyErrorSatisfies(error -> {
|
||||||
assertThat(error).isInstanceOf(McpError.class)
|
assertThat(error).isInstanceOf(McpError.class)
|
||||||
|
|
@ -260,9 +257,9 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testRemoveResourceWithoutCapability() {
|
void testRemoveResourceWithoutCapability() {
|
||||||
// Create a server without resource capabilities
|
// Create a server without resource capabilities
|
||||||
McpAsyncServer serverWithoutResources = McpServer.using(createMcpTransport())
|
McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> {
|
StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> {
|
||||||
assertThat(error).isInstanceOf(McpError.class)
|
assertThat(error).isInstanceOf(McpError.class)
|
||||||
|
|
@ -276,36 +273,37 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyPromptsListChanged() {
|
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();
|
StepVerifier.create(mcpAsyncServer.notifyPromptsListChanged()).verifyComplete();
|
||||||
|
|
||||||
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10)));
|
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddPromptWithNullRegistration() {
|
void testAddPromptWithNullRegistration() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(false).build())
|
.capabilities(ServerCapabilities.builder().prompts(false).build())
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.addPrompt(null)).verifyErrorSatisfies(error -> {
|
StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptRegistration) null))
|
||||||
assertThat(error).isInstanceOf(McpError.class).hasMessage("Prompt registration must not be null");
|
.verifyErrorSatisfies(error -> {
|
||||||
});
|
assertThat(error).isInstanceOf(McpError.class).hasMessage("Prompt registration must not be null");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddPromptWithoutCapability() {
|
void testAddPromptWithoutCapability() {
|
||||||
// Create a server without prompt capabilities
|
// Create a server without prompt capabilities
|
||||||
McpAsyncServer serverWithoutPrompts = McpServer.using(createMcpTransport())
|
McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
|
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
|
||||||
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
|
McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt,
|
||||||
"Test prompt description",
|
req -> Mono.just(new GetPromptResult("Test prompt description", List
|
||||||
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
|
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));
|
||||||
|
|
||||||
StepVerifier.create(serverWithoutPrompts.addPrompt(registration)).verifyErrorSatisfies(error -> {
|
StepVerifier.create(serverWithoutPrompts.addPrompt(registration)).verifyErrorSatisfies(error -> {
|
||||||
assertThat(error).isInstanceOf(McpError.class)
|
assertThat(error).isInstanceOf(McpError.class)
|
||||||
|
|
@ -316,9 +314,9 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testRemovePromptWithoutCapability() {
|
void testRemovePromptWithoutCapability() {
|
||||||
// Create a server without prompt capabilities
|
// Create a server without prompt capabilities
|
||||||
McpAsyncServer serverWithoutPrompts = McpServer.using(createMcpTransport())
|
McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> {
|
StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> {
|
||||||
assertThat(error).isInstanceOf(McpError.class)
|
assertThat(error).isInstanceOf(McpError.class)
|
||||||
|
|
@ -331,15 +329,15 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
String TEST_PROMPT_NAME_TO_REMOVE = "TEST_PROMPT_NAME678";
|
String TEST_PROMPT_NAME_TO_REMOVE = "TEST_PROMPT_NAME678";
|
||||||
|
|
||||||
Prompt prompt = new Prompt(TEST_PROMPT_NAME_TO_REMOVE, "Test Prompt", List.of());
|
Prompt prompt = new Prompt(TEST_PROMPT_NAME_TO_REMOVE, "Test Prompt", List.of());
|
||||||
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
|
McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt,
|
||||||
"Test prompt description",
|
req -> Mono.just(new GetPromptResult("Test prompt description", List
|
||||||
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
|
.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")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
||||||
.prompts(registration)
|
.prompts(registration)
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.removePrompt(TEST_PROMPT_NAME_TO_REMOVE)).verifyComplete();
|
StepVerifier.create(mcpAsyncServer.removePrompt(TEST_PROMPT_NAME_TO_REMOVE)).verifyComplete();
|
||||||
|
|
||||||
|
|
@ -348,10 +346,10 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonexistentPrompt() {
|
void testRemoveNonexistentPrompt() {
|
||||||
var mcpAsyncServer2 = McpServer.using(createMcpTransport())
|
var mcpAsyncServer2 = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyErrorSatisfies(error -> {
|
StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyErrorSatisfies(error -> {
|
||||||
assertThat(error).isInstanceOf(McpError.class)
|
assertThat(error).isInstanceOf(McpError.class)
|
||||||
|
|
@ -372,15 +370,15 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
var rootsReceived = new McpSchema.Root[1];
|
var rootsReceived = new McpSchema.Root[1];
|
||||||
var consumerCalled = new boolean[1];
|
var consumerCalled = new boolean[1];
|
||||||
|
|
||||||
var singleConsumerServer = McpServer.using(createMcpTransport())
|
var singleConsumerServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.rootsChangeConsumers(List.of(roots -> {
|
.rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> {
|
||||||
consumerCalled[0] = true;
|
consumerCalled[0] = true;
|
||||||
if (!roots.isEmpty()) {
|
if (!roots.isEmpty()) {
|
||||||
rootsReceived[0] = roots.get(0);
|
rootsReceived[0] = roots.get(0);
|
||||||
}
|
}
|
||||||
}))
|
})))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThat(singleConsumerServer).isNotNull();
|
assertThat(singleConsumerServer).isNotNull();
|
||||||
assertThatCode(() -> singleConsumerServer.closeGracefully().block(Duration.ofSeconds(10)))
|
assertThatCode(() -> singleConsumerServer.closeGracefully().block(Duration.ofSeconds(10)))
|
||||||
|
|
@ -392,13 +390,13 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
var consumer2Called = new boolean[1];
|
var consumer2Called = new boolean[1];
|
||||||
var rootsContent = new List[1];
|
var rootsContent = new List[1];
|
||||||
|
|
||||||
var multipleConsumersServer = McpServer.using(createMcpTransport())
|
var multipleConsumersServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.rootsChangeConsumers(List.of(roots -> {
|
.rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> {
|
||||||
consumer1Called[0] = true;
|
consumer1Called[0] = true;
|
||||||
rootsContent[0] = roots;
|
rootsContent[0] = roots;
|
||||||
}, roots -> consumer2Called[0] = true))
|
}), roots -> Mono.fromRunnable(() -> consumer2Called[0] = true)))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThat(multipleConsumersServer).isNotNull();
|
assertThat(multipleConsumersServer).isNotNull();
|
||||||
assertThatCode(() -> multipleConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
|
assertThatCode(() -> multipleConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
|
||||||
|
|
@ -406,12 +404,12 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
// Test error handling
|
// Test error handling
|
||||||
var errorHandlingServer = McpServer.using(createMcpTransport())
|
var errorHandlingServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.rootsChangeConsumers(List.of(roots -> {
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
throw new RuntimeException("Test error");
|
throw new RuntimeException("Test error");
|
||||||
}))
|
}))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThat(errorHandlingServer).isNotNull();
|
assertThat(errorHandlingServer).isNotNull();
|
||||||
assertThatCode(() -> errorHandlingServer.closeGracefully().block(Duration.ofSeconds(10)))
|
assertThatCode(() -> errorHandlingServer.closeGracefully().block(Duration.ofSeconds(10)))
|
||||||
|
|
@ -419,7 +417,7 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
// Test without consumers
|
// 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();
|
assertThat(noConsumersServer).isNotNull();
|
||||||
assertThatCode(() -> noConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
|
assertThatCode(() -> noConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
|
||||||
|
|
@ -432,10 +430,10 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().logging().build())
|
.capabilities(ServerCapabilities.builder().logging().build())
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
// Test all logging levels
|
// Test all logging levels
|
||||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
||||||
|
|
@ -451,10 +449,10 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithoutCapability() {
|
void testLoggingWithoutCapability() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().build()) // No logging capability
|
.capabilities(ServerCapabilities.builder().build()) // No logging capability
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
var notification = McpSchema.LoggingMessageNotification.builder()
|
var notification = McpSchema.LoggingMessageNotification.builder()
|
||||||
.level(McpSchema.LoggingLevel.INFO)
|
.level(McpSchema.LoggingLevel.INFO)
|
||||||
|
|
@ -467,10 +465,10 @@ public abstract class BlaAbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithNullNotification() {
|
void testLoggingWithNullNotification() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.async(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().logging().build())
|
.capabilities(ServerCapabilities.builder().logging().build())
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.loggingNotification(null)).verifyError(McpError.class);
|
StepVerifier.create(mcpAsyncServer.loggingNotification(null)).verifyError(McpError.class);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,6 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.McpError;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema;
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
|
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
|
||||||
|
|
@ -81,31 +78,31 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstructorWithInvalidArguments() {
|
void testConstructorWithInvalidArguments() {
|
||||||
assertThatThrownBy(() -> McpServer.using(null)).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpServer.sync(null)).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpServer.using(createMcpTransport()).serverInfo((McpSchema.Implementation) null))
|
assertThatThrownBy(() -> McpServer.sync(createMcpTransport()).serverInfo(null))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Server info must not be null");
|
.hasMessage("Server info must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGracefulShutdown() {
|
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();
|
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testImmediateClose() {
|
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();
|
assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetAsyncServer() {
|
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();
|
assertThat(mcpSyncServer.getAsyncServer()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -126,14 +123,14 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddTool() {
|
void testAddTool() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
|
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
|
||||||
assertThatCode(() -> mcpSyncServer
|
assertThatCode(() -> mcpSyncServer
|
||||||
.addTool(new ToolRegistration(newTool, args -> new CallToolResult(List.of(), false))))
|
.addTool(new McpServerFeatures.SyncToolRegistration(newTool, args -> new CallToolResult(List.of(), false))))
|
||||||
.doesNotThrowAnyException();
|
.doesNotThrowAnyException();
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
|
|
@ -143,14 +140,14 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
void testAddDuplicateTool() {
|
void testAddDuplicateTool() {
|
||||||
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
|
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")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(duplicateTool, args -> new CallToolResult(List.of(), false))
|
.tool(duplicateTool, args -> new CallToolResult(List.of(), false))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatThrownBy(() -> mcpSyncServer
|
assertThatThrownBy(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolRegistration(duplicateTool,
|
||||||
.addTool(new ToolRegistration(duplicateTool, args -> new CallToolResult(List.of(), false))))
|
args -> new CallToolResult(List.of(), false))))
|
||||||
.isInstanceOf(McpError.class)
|
.isInstanceOf(McpError.class)
|
||||||
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
|
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
|
||||||
|
|
||||||
|
|
@ -161,11 +158,11 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
void testRemoveTool() {
|
void testRemoveTool() {
|
||||||
Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema);
|
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")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(tool, args -> new CallToolResult(List.of(), false))
|
.tool(tool, args -> new CallToolResult(List.of(), false))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -174,10 +171,10 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonexistentTool() {
|
void testRemoveNonexistentTool() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatThrownBy(() -> mcpSyncServer.removeTool("nonexistent-tool")).isInstanceOf(McpError.class)
|
assertThatThrownBy(() -> mcpSyncServer.removeTool("nonexistent-tool")).isInstanceOf(McpError.class)
|
||||||
.hasMessage("Tool with name 'nonexistent-tool' not found");
|
.hasMessage("Tool with name 'nonexistent-tool' not found");
|
||||||
|
|
@ -187,7 +184,7 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyToolsListChanged() {
|
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();
|
assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -200,7 +197,7 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyResourcesListChanged() {
|
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();
|
assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -209,15 +206,15 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddResource() {
|
void testAddResource() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
||||||
null);
|
null);
|
||||||
ResourceRegistration registration = new ResourceRegistration(resource,
|
McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration(
|
||||||
req -> new ReadResourceResult(List.of()));
|
resource, req -> new ReadResourceResult(List.of()));
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.addResource(registration)).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.addResource(registration)).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -226,12 +223,13 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddResourceWithNullRegistration() {
|
void testAddResourceWithNullRegistration() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
.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");
|
.hasMessage("Resource must not be null");
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
|
|
@ -239,12 +237,12 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddResourceWithoutCapability() {
|
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",
|
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
||||||
null);
|
null);
|
||||||
ResourceRegistration registration = new ResourceRegistration(resource,
|
McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration(
|
||||||
req -> new ReadResourceResult(List.of()));
|
resource, req -> new ReadResourceResult(List.of()));
|
||||||
|
|
||||||
assertThatThrownBy(() -> serverWithoutResources.addResource(registration)).isInstanceOf(McpError.class)
|
assertThatThrownBy(() -> serverWithoutResources.addResource(registration)).isInstanceOf(McpError.class)
|
||||||
.hasMessage("Server must be configured with resource capabilities");
|
.hasMessage("Server must be configured with resource capabilities");
|
||||||
|
|
@ -252,7 +250,7 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveResourceWithoutCapability() {
|
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)
|
assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)).isInstanceOf(McpError.class)
|
||||||
.hasMessage("Server must be configured with resource capabilities");
|
.hasMessage("Server must be configured with resource capabilities");
|
||||||
|
|
@ -264,7 +262,7 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyPromptsListChanged() {
|
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();
|
assertThatCode(() -> mcpSyncServer.notifyPromptsListChanged()).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -273,23 +271,24 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddPromptWithNullRegistration() {
|
void testAddPromptWithNullRegistration() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(false).build())
|
.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");
|
.hasMessage("Prompt registration must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddPromptWithoutCapability() {
|
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());
|
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
|
||||||
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
|
McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt,
|
||||||
"Test prompt description",
|
req -> new GetPromptResult("Test prompt description", List
|
||||||
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
|
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
|
||||||
|
|
||||||
assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(registration)).isInstanceOf(McpError.class)
|
assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(registration)).isInstanceOf(McpError.class)
|
||||||
.hasMessage("Server must be configured with prompt capabilities");
|
.hasMessage("Server must be configured with prompt capabilities");
|
||||||
|
|
@ -297,7 +296,7 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemovePromptWithoutCapability() {
|
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)
|
assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class)
|
||||||
.hasMessage("Server must be configured with prompt capabilities");
|
.hasMessage("Server must be configured with prompt capabilities");
|
||||||
|
|
@ -306,15 +305,15 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testRemovePrompt() {
|
void testRemovePrompt() {
|
||||||
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
|
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
|
||||||
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
|
McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt,
|
||||||
"Test prompt description",
|
req -> new GetPromptResult("Test prompt description", List
|
||||||
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
|
.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")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
||||||
.prompts(registration)
|
.prompts(registration)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -323,10 +322,10 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonexistentPrompt() {
|
void testRemoveNonexistentPrompt() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class)
|
assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class)
|
||||||
.hasMessage("Prompt with name 'nonexistent-prompt' not found");
|
.hasMessage("Prompt with name 'nonexistent-prompt' not found");
|
||||||
|
|
@ -344,7 +343,7 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
var rootsReceived = new McpSchema.Root[1];
|
var rootsReceived = new McpSchema.Root[1];
|
||||||
var consumerCalled = new boolean[1];
|
var consumerCalled = new boolean[1];
|
||||||
|
|
||||||
var singleConsumerServer = McpServer.using(createMcpTransport())
|
var singleConsumerServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.rootsChangeConsumers(List.of(roots -> {
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
consumerCalled[0] = true;
|
consumerCalled[0] = true;
|
||||||
|
|
@ -352,7 +351,7 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
rootsReceived[0] = roots.get(0);
|
rootsReceived[0] = roots.get(0);
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThat(singleConsumerServer).isNotNull();
|
assertThat(singleConsumerServer).isNotNull();
|
||||||
assertThatCode(() -> singleConsumerServer.closeGracefully()).doesNotThrowAnyException();
|
assertThatCode(() -> singleConsumerServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
|
|
@ -363,32 +362,32 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
var consumer2Called = new boolean[1];
|
var consumer2Called = new boolean[1];
|
||||||
var rootsContent = new List[1];
|
var rootsContent = new List[1];
|
||||||
|
|
||||||
var multipleConsumersServer = McpServer.using(createMcpTransport())
|
var multipleConsumersServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.rootsChangeConsumers(List.of(roots -> {
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
consumer1Called[0] = true;
|
consumer1Called[0] = true;
|
||||||
rootsContent[0] = roots;
|
rootsContent[0] = roots;
|
||||||
}, roots -> consumer2Called[0] = true))
|
}, roots -> consumer2Called[0] = true))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThat(multipleConsumersServer).isNotNull();
|
assertThat(multipleConsumersServer).isNotNull();
|
||||||
assertThatCode(() -> multipleConsumersServer.closeGracefully()).doesNotThrowAnyException();
|
assertThatCode(() -> multipleConsumersServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
// Test error handling
|
// Test error handling
|
||||||
var errorHandlingServer = McpServer.using(createMcpTransport())
|
var errorHandlingServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.rootsChangeConsumers(List.of(roots -> {
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
throw new RuntimeException("Test error");
|
throw new RuntimeException("Test error");
|
||||||
}))
|
}))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThat(errorHandlingServer).isNotNull();
|
assertThat(errorHandlingServer).isNotNull();
|
||||||
assertThatCode(() -> errorHandlingServer.closeGracefully()).doesNotThrowAnyException();
|
assertThatCode(() -> errorHandlingServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
// Test without consumers
|
// 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();
|
assertThat(noConsumersServer).isNotNull();
|
||||||
assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();
|
assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
|
|
@ -400,10 +399,10 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().logging().build())
|
.capabilities(ServerCapabilities.builder().logging().build())
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
// Test all logging levels
|
// Test all logging levels
|
||||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
||||||
|
|
@ -419,10 +418,10 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithoutCapability() {
|
void testLoggingWithoutCapability() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().build()) // No logging capability
|
.capabilities(ServerCapabilities.builder().build()) // No logging capability
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var notification = McpSchema.LoggingMessageNotification.builder()
|
var notification = McpSchema.LoggingMessageNotification.builder()
|
||||||
.level(McpSchema.LoggingLevel.INFO)
|
.level(McpSchema.LoggingLevel.INFO)
|
||||||
|
|
@ -435,10 +434,10 @@ public abstract class BlaAbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithNullNotification() {
|
void testLoggingWithNullNotification() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.sync(createMcpTransport())
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().logging().build())
|
.capabilities(ServerCapabilities.builder().logging().build())
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatThrownBy(() -> mcpSyncServer.loggingNotification(null)).isInstanceOf(McpError.class);
|
assertThatThrownBy(() -> mcpSyncServer.loggingNotification(null)).isInstanceOf(McpError.class);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import org.springframework.ai.mcp.client.McpClient;
|
||||||
import org.springframework.ai.mcp.client.transport.HttpClientSseClientTransport;
|
import org.springframework.ai.mcp.client.transport.HttpClientSseClientTransport;
|
||||||
import org.springframework.ai.mcp.client.transport.WebFluxSseClientTransport;
|
import org.springframework.ai.mcp.client.transport.WebFluxSseClientTransport;
|
||||||
import org.springframework.ai.mcp.server.McpServer;
|
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.server.transport.WebFluxSseServerTransport;
|
import org.springframework.ai.mcp.server.transport.WebFluxSseServerTransport;
|
||||||
import org.springframework.ai.mcp.spec.McpError;
|
import org.springframework.ai.mcp.spec.McpError;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema;
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
|
@ -69,7 +69,7 @@ public class WebFluxSseIntegrationTests {
|
||||||
|
|
||||||
private WebFluxSseServerTransport mcpServerTransport;
|
private WebFluxSseServerTransport mcpServerTransport;
|
||||||
|
|
||||||
ConcurrentHashMap<String, McpClient.Builder> clientBulders = new ConcurrentHashMap<>();
|
ConcurrentHashMap<String, McpClient.SyncSpec> clientBulders = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before() {
|
public void before() {
|
||||||
|
|
@ -80,9 +80,9 @@ public class WebFluxSseIntegrationTests {
|
||||||
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
|
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
|
||||||
this.httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow();
|
this.httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow();
|
||||||
|
|
||||||
clientBulders.put("httpclient", McpClient.using(new HttpClientSseClientTransport("http://localhost:" + PORT)));
|
clientBulders.put("httpclient", McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT)));
|
||||||
clientBulders.put("webflux", McpClient
|
clientBulders.put("webflux",
|
||||||
.using(new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:" + PORT))));
|
McpClient.sync(new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:" + PORT))));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +98,7 @@ public class WebFluxSseIntegrationTests {
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
@Test
|
@Test
|
||||||
void testCreateMessageWithoutInitialization() {
|
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
|
var messages = List
|
||||||
.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message")));
|
.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message")));
|
||||||
|
|
@ -117,11 +117,11 @@ public class WebFluxSseIntegrationTests {
|
||||||
@ValueSource(strings = { "httpclient", "webflux" })
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
void testCreateMessageWithoutSamplingCapabilities(String clientType) {
|
void testCreateMessageWithoutSamplingCapabilities(String clientType) {
|
||||||
|
|
||||||
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 clientBuilder = clientBulders.get(clientType);
|
var clientBuilder = clientBulders.get(clientType);
|
||||||
|
|
||||||
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();
|
InitializeResult initResult = client.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -145,7 +145,7 @@ public class WebFluxSseIntegrationTests {
|
||||||
|
|
||||||
var clientBuilder = clientBulders.get(clientType);
|
var clientBuilder = clientBulders.get(clientType);
|
||||||
|
|
||||||
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 -> {
|
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
||||||
assertThat(request.messages()).hasSize(1);
|
assertThat(request.messages()).hasSize(1);
|
||||||
|
|
@ -158,7 +158,7 @@ public class WebFluxSseIntegrationTests {
|
||||||
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
.capabilities(ClientCapabilities.builder().sampling().build())
|
.capabilities(ClientCapabilities.builder().sampling().build())
|
||||||
.sampling(samplingHandler)
|
.sampling(samplingHandler)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = client.initialize();
|
InitializeResult initResult = client.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -191,13 +191,13 @@ public class WebFluxSseIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -236,14 +236,14 @@ public class WebFluxSseIntegrationTests {
|
||||||
void testRootsWithoutCapability(String clientType) {
|
void testRootsWithoutCapability(String clientType) {
|
||||||
var clientBuilder = clientBulders.get(clientType);
|
var clientBuilder = clientBulders.get(clientType);
|
||||||
|
|
||||||
var mcpServer = McpServer.using(mcpServerTransport).rootsChangeConsumer(rootsUpdate -> {
|
var mcpServer = McpServer.sync(mcpServerTransport).rootsChangeConsumer(rootsUpdate -> {
|
||||||
}).sync();
|
}).build();
|
||||||
|
|
||||||
// Create client without roots capability
|
// Create client without roots capability
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()) // No
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()) // No
|
||||||
// roots
|
// roots
|
||||||
// capability
|
// capability
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -262,13 +262,13 @@ public class WebFluxSseIntegrationTests {
|
||||||
var clientBuilder = clientBulders.get(clientType);
|
var clientBuilder = clientBulders.get(clientType);
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(List.of()) // Empty roots list
|
.roots(List.of()) // Empty roots list
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -293,14 +293,14 @@ public class WebFluxSseIntegrationTests {
|
||||||
AtomicReference<List<Root>> rootsRef1 = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef1 = new AtomicReference<>();
|
||||||
AtomicReference<List<Root>> rootsRef2 = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef2 = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef1.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef1.set(rootsUpdate))
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef2.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef2.set(rootsUpdate))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -325,13 +325,13 @@ public class WebFluxSseIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -368,8 +368,8 @@ public class WebFluxSseIntegrationTests {
|
||||||
var clientBuilder = clientBulders.get(clientType);
|
var clientBuilder = clientBulders.get(clientType);
|
||||||
|
|
||||||
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
|
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
|
||||||
ToolRegistration tool1 = new ToolRegistration(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema),
|
McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
|
||||||
request -> {
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
|
||||||
// perform a blocking call to a remote service
|
// perform a blocking call to a remote service
|
||||||
String response = RestClient.create()
|
String response = RestClient.create()
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -380,12 +380,12 @@ public class WebFluxSseIntegrationTests {
|
||||||
return callResponse;
|
return callResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.sync();
|
var mcpClient = clientBuilder.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -408,8 +408,8 @@ public class WebFluxSseIntegrationTests {
|
||||||
var clientBuilder = clientBulders.get(clientType);
|
var clientBuilder = clientBulders.get(clientType);
|
||||||
|
|
||||||
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
|
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
|
||||||
ToolRegistration tool1 = new ToolRegistration(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema),
|
McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
|
||||||
request -> {
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
|
||||||
// perform a blocking call to a remote service
|
// perform a blocking call to a remote service
|
||||||
String response = RestClient.create()
|
String response = RestClient.create()
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -420,10 +420,10 @@ public class WebFluxSseIntegrationTests {
|
||||||
return callResponse;
|
return callResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
||||||
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
||||||
|
|
@ -435,7 +435,7 @@ public class WebFluxSseIntegrationTests {
|
||||||
.body(String.class);
|
.body(String.class);
|
||||||
assertThat(response).isNotBlank();
|
assertThat(response).isNotBlank();
|
||||||
rootsRef.set(toolsUpdate);
|
rootsRef.set(toolsUpdate);
|
||||||
}).sync();
|
}).build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -458,8 +458,8 @@ public class WebFluxSseIntegrationTests {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a new tool
|
// Add a new tool
|
||||||
ToolRegistration tool2 = new ToolRegistration(new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema),
|
McpServerFeatures.SyncToolRegistration tool2 = new McpServerFeatures.SyncToolRegistration(
|
||||||
request -> callResponse);
|
new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), request -> callResponse);
|
||||||
|
|
||||||
mcpServer.addTool(tool2);
|
mcpServer.addTool(tool2);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import org.springframework.ai.mcp.client.McpClient;
|
import org.springframework.ai.mcp.client.McpClient;
|
||||||
import org.springframework.ai.mcp.client.transport.HttpClientSseClientTransport;
|
import org.springframework.ai.mcp.client.transport.HttpClientSseClientTransport;
|
||||||
import org.springframework.ai.mcp.server.McpServer.ToolRegistration;
|
|
||||||
import org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport;
|
import org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport;
|
||||||
import org.springframework.ai.mcp.spec.McpError;
|
import org.springframework.ai.mcp.spec.McpError;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema;
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
|
@ -71,7 +70,7 @@ public class WebMvcSseIntegrationTests {
|
||||||
|
|
||||||
private WebMvcSseServerTransport mcpServerTransport;
|
private WebMvcSseServerTransport mcpServerTransport;
|
||||||
|
|
||||||
McpClient.Builder clientBuilder;
|
McpClient.SyncSpec clientBuilder;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebMvc
|
@EnableWebMvc
|
||||||
|
|
@ -137,7 +136,7 @@ public class WebMvcSseIntegrationTests {
|
||||||
throw new RuntimeException("Failed to start Tomcat", e);
|
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
|
@AfterEach
|
||||||
|
|
@ -164,7 +163,7 @@ public class WebMvcSseIntegrationTests {
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
@Test
|
@Test
|
||||||
void testCreateMessageWithoutInitialization() {
|
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
|
var messages = List
|
||||||
.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message")));
|
.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message")));
|
||||||
|
|
@ -182,9 +181,9 @@ public class WebMvcSseIntegrationTests {
|
||||||
@Test
|
@Test
|
||||||
void testCreateMessageWithoutSamplingCapabilities() {
|
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();
|
InitializeResult initResult = client.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -205,7 +204,7 @@ public class WebMvcSseIntegrationTests {
|
||||||
@Test
|
@Test
|
||||||
void testCreateMessageSuccess() throws InterruptedException {
|
void testCreateMessageSuccess() throws InterruptedException {
|
||||||
|
|
||||||
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 -> {
|
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
||||||
assertThat(request.messages()).hasSize(1);
|
assertThat(request.messages()).hasSize(1);
|
||||||
|
|
@ -218,7 +217,7 @@ public class WebMvcSseIntegrationTests {
|
||||||
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
.capabilities(ClientCapabilities.builder().sampling().build())
|
.capabilities(ClientCapabilities.builder().sampling().build())
|
||||||
.sampling(samplingHandler)
|
.sampling(samplingHandler)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = client.initialize();
|
InitializeResult initResult = client.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -248,13 +247,13 @@ public class WebMvcSseIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -290,12 +289,12 @@ public class WebMvcSseIntegrationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsWithoutCapability() {
|
void testRootsWithoutCapability() {
|
||||||
var mcpServer = McpServer.using(mcpServerTransport).rootsChangeConsumer(rootsUpdate -> {
|
var mcpServer = McpServer.sync(mcpServerTransport).rootsChangeConsumer(rootsUpdate -> {
|
||||||
}).sync();
|
}).build();
|
||||||
|
|
||||||
// Create client without roots capability
|
// Create client without roots capability
|
||||||
// No roots capability
|
// No roots capability
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).sync();
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -311,13 +310,13 @@ public class WebMvcSseIntegrationTests {
|
||||||
@Test
|
@Test
|
||||||
void testRootsWithEmptyRootsList() {
|
void testRootsWithEmptyRootsList() {
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(List.of()) // Empty roots list
|
.roots(List.of()) // Empty roots list
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -339,14 +338,14 @@ public class WebMvcSseIntegrationTests {
|
||||||
AtomicReference<List<Root>> rootsRef1 = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef1 = new AtomicReference<>();
|
||||||
AtomicReference<List<Root>> rootsRef2 = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef2 = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef1.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef1.set(rootsUpdate))
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef2.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef2.set(rootsUpdate))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -367,13 +366,13 @@ public class WebMvcSseIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
.rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -407,8 +406,8 @@ public class WebMvcSseIntegrationTests {
|
||||||
void testToolCallSuccess() {
|
void testToolCallSuccess() {
|
||||||
|
|
||||||
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
|
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
|
||||||
ToolRegistration tool1 = new ToolRegistration(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema),
|
McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
|
||||||
request -> {
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
|
||||||
// perform a blocking call to a remote service
|
// perform a blocking call to a remote service
|
||||||
String response = RestClient.create()
|
String response = RestClient.create()
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -419,12 +418,12 @@ public class WebMvcSseIntegrationTests {
|
||||||
return callResponse;
|
return callResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.sync();
|
var mcpClient = clientBuilder.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -444,8 +443,8 @@ public class WebMvcSseIntegrationTests {
|
||||||
void testToolListChangeHandlingSuccess() {
|
void testToolListChangeHandlingSuccess() {
|
||||||
|
|
||||||
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
|
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
|
||||||
ToolRegistration tool1 = new ToolRegistration(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema),
|
McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
|
||||||
request -> {
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
|
||||||
// perform a blocking call to a remote service
|
// perform a blocking call to a remote service
|
||||||
String response = RestClient.create()
|
String response = RestClient.create()
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -456,10 +455,10 @@ public class WebMvcSseIntegrationTests {
|
||||||
return callResponse;
|
return callResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
var mcpServer = McpServer.using(mcpServerTransport)
|
var mcpServer = McpServer.sync(mcpServerTransport)
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
||||||
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
||||||
|
|
@ -471,7 +470,7 @@ public class WebMvcSseIntegrationTests {
|
||||||
.body(String.class);
|
.body(String.class);
|
||||||
assertThat(response).isNotBlank();
|
assertThat(response).isNotBlank();
|
||||||
rootsRef.set(toolsUpdate);
|
rootsRef.set(toolsUpdate);
|
||||||
}).sync();
|
}).build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -494,8 +493,8 @@ public class WebMvcSseIntegrationTests {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a new tool
|
// Add a new tool
|
||||||
ToolRegistration tool2 = new ToolRegistration(new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema),
|
McpServerFeatures.SyncToolRegistration tool2 = new McpServerFeatures.SyncToolRegistration(
|
||||||
request -> callResponse);
|
new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), request -> callResponse);
|
||||||
|
|
||||||
mcpServer.addTool(tool2);
|
mcpServer.addTool(tool2);
|
||||||
|
|
||||||
|
|
@ -510,9 +509,9 @@ public class WebMvcSseIntegrationTests {
|
||||||
@Test
|
@Test
|
||||||
void testInitialize() {
|
void testInitialize() {
|
||||||
|
|
||||||
var mcpServer = McpServer.using(mcpServerTransport).sync();
|
var mcpServer = McpServer.sync(mcpServerTransport).build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.sync();
|
var mcpClient = clientBuilder.build();
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import java.util.function.Function;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
|
|
@ -55,10 +56,39 @@ import org.springframework.ai.mcp.util.Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Model Context Protocol (MCP) client implementation that provides asynchronous
|
* The Model Context Protocol (MCP) client implementation that provides asynchronous
|
||||||
* communication with MCP servers.
|
* communication with MCP servers using Project Reactor's Mono and Flux types.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This client implements the MCP specification, enabling AI models to interact with
|
||||||
|
* external tools and resources through a standardized interface. Key features include:
|
||||||
|
* <ul>
|
||||||
|
* <li>Asynchronous communication using reactive programming patterns
|
||||||
|
* <li>Tool discovery and invocation for server-provided functionality
|
||||||
|
* <li>Resource access and management with URI-based addressing
|
||||||
|
* <li>Prompt template handling for standardized AI interactions
|
||||||
|
* <li>Real-time notifications for tools, resources, and prompts changes
|
||||||
|
* <li>Structured logging with configurable severity levels
|
||||||
|
* <li>Message sampling for AI model interactions
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The client follows a lifecycle:
|
||||||
|
* <ol>
|
||||||
|
* <li>Initialization - Establishes connection and negotiates capabilities
|
||||||
|
* <li>Normal Operation - Handles requests and notifications
|
||||||
|
* <li>Graceful Shutdown - Ensures clean connection termination
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation uses Project Reactor for non-blocking operations, making it
|
||||||
|
* suitable for high-throughput scenarios and reactive applications. All operations return
|
||||||
|
* Mono or Flux types that can be composed into reactive pipelines.
|
||||||
*
|
*
|
||||||
* @author Dariusz Jędrzejczyk
|
* @author Dariusz Jędrzejczyk
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
|
* @see McpClient
|
||||||
|
* @see McpSchema
|
||||||
|
* @see DefaultMcpSession
|
||||||
*/
|
*/
|
||||||
public class McpAsyncClient {
|
public class McpAsyncClient {
|
||||||
|
|
||||||
|
|
@ -109,13 +139,96 @@ public class McpAsyncClient {
|
||||||
* request text or image-based interactions and optionally include context from MCP
|
* request text or image-based interactions and optionally include context from MCP
|
||||||
* servers in their prompts.
|
* servers in their prompts.
|
||||||
*/
|
*/
|
||||||
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client transport implementation.
|
* Client transport implementation.
|
||||||
*/
|
*/
|
||||||
private final McpTransport transport;
|
private final McpTransport transport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new McpAsyncClient with the given transport and session request-response
|
||||||
|
* timeout.
|
||||||
|
* @param transport the transport to use.
|
||||||
|
* @param requestTimeout the session request-response timeout.
|
||||||
|
* @param features the MCP Client supported features.
|
||||||
|
*/
|
||||||
|
McpAsyncClient(ClientMcpTransport transport, Duration requestTimeout, McpClientFeatures.Async features) {
|
||||||
|
|
||||||
|
Assert.notNull(transport, "Transport must not be null");
|
||||||
|
Assert.notNull(requestTimeout, "Request timeout must not be null");
|
||||||
|
|
||||||
|
this.clientInfo = features.clientInfo();
|
||||||
|
this.clientCapabilities = features.clientCapabilities();
|
||||||
|
this.transport = transport;
|
||||||
|
this.roots = new ConcurrentHashMap<>(features.roots());
|
||||||
|
|
||||||
|
// Request Handlers
|
||||||
|
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();
|
||||||
|
|
||||||
|
// Roots List Request Handler
|
||||||
|
if (this.clientCapabilities.roots() != null) {
|
||||||
|
requestHandlers.put(McpSchema.METHOD_ROOTS_LIST, rootsListRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sampling Handler
|
||||||
|
if (this.clientCapabilities.sampling() != null) {
|
||||||
|
if (features.samplingHandler() == null) {
|
||||||
|
throw new McpError("Sampling handler must not be null when client capabilities include sampling");
|
||||||
|
}
|
||||||
|
this.samplingHandler = features.samplingHandler();
|
||||||
|
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification Handlers
|
||||||
|
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
|
||||||
|
|
||||||
|
// Tools Change Notification
|
||||||
|
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumersFinal = new ArrayList<>();
|
||||||
|
toolsChangeConsumersFinal
|
||||||
|
.add((notification) -> Mono.fromRunnable(() -> logger.info("Tools changed: {}", notification)));
|
||||||
|
|
||||||
|
if (!Utils.isEmpty(features.toolsChangeConsumers())) {
|
||||||
|
toolsChangeConsumersFinal.addAll(features.toolsChangeConsumers());
|
||||||
|
}
|
||||||
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED,
|
||||||
|
asyncToolsChangeNotificationHandler(toolsChangeConsumersFinal));
|
||||||
|
|
||||||
|
// Resources Change Notification
|
||||||
|
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumersFinal = new ArrayList<>();
|
||||||
|
resourcesChangeConsumersFinal
|
||||||
|
.add((notification) -> Mono.fromRunnable(() -> logger.info("Resources changed: {}", notification)));
|
||||||
|
|
||||||
|
if (!Utils.isEmpty(features.resourcesChangeConsumers())) {
|
||||||
|
resourcesChangeConsumersFinal.addAll(features.resourcesChangeConsumers());
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED,
|
||||||
|
asyncResourcesChangeNotificationHandler(resourcesChangeConsumersFinal));
|
||||||
|
|
||||||
|
// Prompts Change Notification
|
||||||
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumersFinal = new ArrayList<>();
|
||||||
|
promptsChangeConsumersFinal
|
||||||
|
.add((notification) -> Mono.fromRunnable(() -> logger.info("Prompts changed: {}", notification)));
|
||||||
|
if (!Utils.isEmpty(features.promptsChangeConsumers())) {
|
||||||
|
promptsChangeConsumersFinal.addAll(features.promptsChangeConsumers());
|
||||||
|
}
|
||||||
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED,
|
||||||
|
asyncPromptsChangeNotificationHandler(promptsChangeConsumersFinal));
|
||||||
|
|
||||||
|
// Utility Logging Notification
|
||||||
|
List<Function<LoggingMessageNotification, Mono<Void>>> loggingConsumersFinal = new ArrayList<>();
|
||||||
|
loggingConsumersFinal.add((notification) -> Mono.fromRunnable(() -> logger.info("Logging: {}", notification)));
|
||||||
|
if (!Utils.isEmpty(features.loggingConsumers())) {
|
||||||
|
loggingConsumersFinal.addAll(features.loggingConsumers());
|
||||||
|
}
|
||||||
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_MESSAGE,
|
||||||
|
asyncLoggingNotificationHandler(loggingConsumersFinal));
|
||||||
|
|
||||||
|
this.mcpSession = new DefaultMcpSession(requestTimeout, transport, requestHandlers, notificationHandlers);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new McpAsyncClient with the given transport and session request-response
|
* Create a new McpAsyncClient with the given transport and session request-response
|
||||||
* timeout.
|
* timeout.
|
||||||
|
|
@ -129,7 +242,9 @@ public class McpAsyncClient {
|
||||||
* @param promptsChangeConsumers the prompts change consumers.
|
* @param promptsChangeConsumers the prompts change consumers.
|
||||||
* @param loggingConsumers the logging consumers.
|
* @param loggingConsumers the logging consumers.
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
|
* @deprecated Use {@link McpClient#async(ClientMcpTransport)} to obtain an instance.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public McpAsyncClient(ClientMcpTransport transport, Duration requestTimeout, Implementation clientInfo,
|
public McpAsyncClient(ClientMcpTransport transport, Duration requestTimeout, Implementation clientInfo,
|
||||||
ClientCapabilities clientCapabilities, Map<String, Root> roots,
|
ClientCapabilities clientCapabilities, Map<String, Root> roots,
|
||||||
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||||
|
|
@ -153,11 +268,11 @@ public class McpAsyncClient {
|
||||||
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// Request Handlers
|
// Request Handlers
|
||||||
Map<String, RequestHandler> requestHanlers = new HashMap<>();
|
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();
|
||||||
|
|
||||||
// Roots List Request Handler
|
// Roots List Request Handler
|
||||||
if (this.roots != null && this.clientCapabilities.roots() != null) {
|
if (this.clientCapabilities.roots() != null) {
|
||||||
requestHanlers.put(McpSchema.METHOD_ROOTS_LIST, rootsListRequestHandler());
|
requestHandlers.put(McpSchema.METHOD_ROOTS_LIST, rootsListRequestHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sampling Handler
|
// Sampling Handler
|
||||||
|
|
@ -165,8 +280,9 @@ public class McpAsyncClient {
|
||||||
if (samplingHandler == null) {
|
if (samplingHandler == null) {
|
||||||
throw new McpError("Sampling handler must not be null when client capabilities include sampling");
|
throw new McpError("Sampling handler must not be null when client capabilities include sampling");
|
||||||
}
|
}
|
||||||
this.samplingHandler = samplingHandler;
|
this.samplingHandler = r -> Mono.fromCallable(() -> samplingHandler.apply(r))
|
||||||
requestHanlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification Handlers
|
// Notification Handlers
|
||||||
|
|
@ -208,7 +324,7 @@ public class McpAsyncClient {
|
||||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_MESSAGE,
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_MESSAGE,
|
||||||
loggingNotificationHandler(loggingConsumersFinal));
|
loggingNotificationHandler(loggingConsumersFinal));
|
||||||
|
|
||||||
this.mcpSession = new DefaultMcpSession(requestTimeout, transport, requestHanlers, notificationHandlers);
|
this.mcpSession = new DefaultMcpSession(requestTimeout, transport, requestHandlers, notificationHandlers);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,15 +532,13 @@ public class McpAsyncClient {
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// Sampling
|
// Sampling
|
||||||
// --------------------------
|
// --------------------------
|
||||||
private RequestHandler samplingCreateMessageHandler() {
|
private RequestHandler<CreateMessageResult> samplingCreateMessageHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
McpSchema.CreateMessageRequest request = transport.unmarshalFrom(params,
|
McpSchema.CreateMessageRequest request = transport.unmarshalFrom(params,
|
||||||
new TypeReference<McpSchema.CreateMessageRequest>() {
|
new TypeReference<McpSchema.CreateMessageRequest>() {
|
||||||
});
|
});
|
||||||
|
|
||||||
CreateMessageResult response = this.samplingHandler.apply(request);
|
return this.samplingHandler.apply(request);
|
||||||
|
|
||||||
return Mono.just(response);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,6 +614,26 @@ public class McpAsyncClient {
|
||||||
}).then(); // Convert to Mono<Void>
|
}).then(); // Convert to Mono<Void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a notification handler for tools/list_changed notifications from the
|
||||||
|
* server. When the server's available tools change, it sends a notification to inform
|
||||||
|
* connected clients. This handler automatically fetches the updated tool list and
|
||||||
|
* distributes it to all registered consumers.
|
||||||
|
* @param toolsChangeConsumers List of consumers that will be notified when the tools
|
||||||
|
* list changes. Each consumer receives the complete updated list of tools.
|
||||||
|
* @return A NotificationHandler that processes tools/list_changed notifications by:
|
||||||
|
* 1. Fetching the current list of tools from the server 2. Distributing the updated
|
||||||
|
* list to all registered consumers 3. Handling any errors that occur during this
|
||||||
|
* process
|
||||||
|
*/
|
||||||
|
private NotificationHandler asyncToolsChangeNotificationHandler(
|
||||||
|
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers) {
|
||||||
|
// TODO: params are not used yet
|
||||||
|
return params -> listTools().flatMap(listToolsResult -> Flux.fromIterable(toolsChangeConsumers)
|
||||||
|
.flatMap(consumer -> consumer.apply(listToolsResult.tools()))
|
||||||
|
.then());
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// Resources
|
// Resources
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
|
@ -621,6 +755,13 @@ public class McpAsyncClient {
|
||||||
}).then();
|
}).then();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NotificationHandler asyncResourcesChangeNotificationHandler(
|
||||||
|
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers) {
|
||||||
|
return params -> listResources().flatMap(listResourcesResult -> Flux.fromIterable(resourcesChangeConsumers)
|
||||||
|
.flatMap(consumer -> consumer.apply(listResourcesResult.resources()))
|
||||||
|
.then());
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// Prompts
|
// Prompts
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
|
@ -682,6 +823,13 @@ public class McpAsyncClient {
|
||||||
}; // @formatter:on
|
}; // @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NotificationHandler asyncPromptsChangeNotificationHandler(
|
||||||
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers) {
|
||||||
|
return params -> listPrompts().flatMap(listPromptsResult -> Flux.fromIterable(promptsChangeConsumers)
|
||||||
|
.flatMap(consumer -> consumer.apply(listPromptsResult.prompts()))
|
||||||
|
.then());
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// Logging
|
// Logging
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
|
@ -711,6 +859,28 @@ public class McpAsyncClient {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a notification handler for logging notifications from the server. This
|
||||||
|
* handler automatically distributes logging messages to all registered consumers.
|
||||||
|
* @param loggingConsumers List of consumers that will be notified when a logging
|
||||||
|
* message is received. Each consumer receives the logging message notification.
|
||||||
|
* @return A NotificationHandler that processes log notifications by distributing the
|
||||||
|
* message to all registered consumers
|
||||||
|
*/
|
||||||
|
private NotificationHandler asyncLoggingNotificationHandler(
|
||||||
|
List<Function<LoggingMessageNotification, Mono<Void>>> loggingConsumers) {
|
||||||
|
|
||||||
|
return params -> {
|
||||||
|
McpSchema.LoggingMessageNotification loggingMessageNotification = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.LoggingMessageNotification>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
return Flux.fromIterable(loggingConsumers)
|
||||||
|
.flatMap(consumer -> consumer.apply(loggingMessageNotification))
|
||||||
|
.then();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client can set the minimum logging level it wants to receive from the server.
|
* Client can set the minimum logging level it wants to receive from the server.
|
||||||
* @param loggingLevel the min logging level
|
* @param loggingLevel the min logging level
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema;
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
|
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
|
||||||
|
|
@ -58,24 +60,30 @@ import org.springframework.ai.mcp.util.Assert;
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Example of creating a basic client: <pre>{@code
|
* Example of creating a basic synchronous client: <pre>{@code
|
||||||
* McpClient.using(transport)
|
* McpClient.sync(transport)
|
||||||
* .requestTimeout(Duration.ofSeconds(5))
|
* .requestTimeout(Duration.ofSeconds(5))
|
||||||
* .sync(); // or .async()
|
* .build();
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* Example of creating a basic asynchronous client: <pre>{@code
|
||||||
|
* McpClient.async(transport)
|
||||||
|
* .requestTimeout(Duration.ofSeconds(5))
|
||||||
|
* .build();
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Example with advanced configuration: <pre>{@code
|
* Example with advanced asynchronous configuration: <pre>{@code
|
||||||
* McpClient.using(transport)
|
* McpClient.async(transport)
|
||||||
* .requestTimeout(Duration.ofSeconds(10))
|
* .requestTimeout(Duration.ofSeconds(10))
|
||||||
* .capabilities(new ClientCapabilities(...))
|
* .capabilities(new ClientCapabilities(...))
|
||||||
* .clientInfo(new Implementation("My Client", "1.0.0"))
|
* .clientInfo(new Implementation("My Client", "1.0.0"))
|
||||||
* .roots(new Root("file://workspace", "Workspace Files"))
|
* .roots(new Root("file://workspace", "Workspace Files"))
|
||||||
* .toolsChangeConsumer(tools -> System.out.println("Tools updated: " + tools))
|
* .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> System.out.println("Tools updated: " + tools)))
|
||||||
* .resourcesChangeConsumer(resources -> System.out.println("Resources updated: " + resources))
|
* .resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> System.out.println("Resources updated: " + resources)))
|
||||||
* .promptsChangeConsumer(prompts -> System.out.println("Prompts updated: " + prompts))
|
* .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> System.out.println("Prompts updated: " + prompts)))
|
||||||
* .loggingConsumer(message -> System.out.println("Log message: " + message))
|
* .loggingConsumer(message -> Mono.fromRunnable(() -> System.out.println("Log message: " + message)))
|
||||||
* .async();
|
* .build();
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
|
|
@ -115,7 +123,11 @@ public interface McpClient {
|
||||||
* and {@code SseClientTransport} for SSE-based communication.
|
* and {@code SseClientTransport} for SSE-based communication.
|
||||||
* @return A new builder instance for configuring the client
|
* @return A new builder instance for configuring the client
|
||||||
* @throws IllegalArgumentException if transport is null
|
* @throws IllegalArgumentException if transport is null
|
||||||
|
* @deprecated Use {@link #sync(ClientMcpTransport)} or
|
||||||
|
* {@link #async(ClientMcpTransport)} specification builder to configure the client
|
||||||
|
* and build an instance.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public static Builder using(ClientMcpTransport transport) {
|
public static Builder using(ClientMcpTransport transport) {
|
||||||
return new Builder(transport);
|
return new Builder(transport);
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +148,12 @@ public interface McpClient {
|
||||||
* <li>Change notification handlers for tools, resources, and prompts
|
* <li>Change notification handlers for tools, resources, and prompts
|
||||||
* <li>Custom message sampling logic
|
* <li>Custom message sampling logic
|
||||||
* </ul>
|
* </ul>
|
||||||
|
*
|
||||||
|
* @deprecated Use {@link #sync(ClientMcpTransport)} or
|
||||||
|
* {@link #async(ClientMcpTransport)} specification builder to instantiate an
|
||||||
|
* instance.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private final ClientMcpTransport transport;
|
private final ClientMcpTransport transport;
|
||||||
|
|
@ -353,4 +370,431 @@ public interface McpClient {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous client specification.
|
||||||
|
*/
|
||||||
|
class SyncSpec {
|
||||||
|
|
||||||
|
private final ClientMcpTransport transport;
|
||||||
|
|
||||||
|
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
|
||||||
|
|
||||||
|
private ClientCapabilities capabilities;
|
||||||
|
|
||||||
|
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
|
||||||
|
|
||||||
|
private Map<String, Root> roots = new HashMap<>();
|
||||||
|
|
||||||
|
private List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
||||||
|
|
||||||
|
private SyncSpec(ClientMcpTransport transport) {
|
||||||
|
this.transport = transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the duration to wait for server responses before timing out requests. This
|
||||||
|
* timeout applies to all requests made through the client, including tool calls,
|
||||||
|
* resource access, and prompt operations.
|
||||||
|
* @param requestTimeout The duration to wait before timing out requests. Must not
|
||||||
|
* be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if requestTimeout is null
|
||||||
|
*/
|
||||||
|
public SyncSpec requestTimeout(Duration requestTimeout) {
|
||||||
|
Assert.notNull(requestTimeout, "Request timeout must not be null");
|
||||||
|
this.requestTimeout = requestTimeout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client capabilities that will be advertised to the server during
|
||||||
|
* connection initialization. Capabilities define what features the client
|
||||||
|
* supports, such as tool execution, resource access, and prompt handling.
|
||||||
|
* @param capabilities The client capabilities configuration. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if capabilities is null
|
||||||
|
*/
|
||||||
|
public SyncSpec capabilities(ClientCapabilities capabilities) {
|
||||||
|
Assert.notNull(capabilities, "Capabilities must not be null");
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client implementation information that will be shared with the server
|
||||||
|
* during connection initialization. This helps with version compatibility and
|
||||||
|
* debugging.
|
||||||
|
* @param clientInfo The client implementation details including name and version.
|
||||||
|
* Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if clientInfo is null
|
||||||
|
*/
|
||||||
|
public SyncSpec clientInfo(Implementation clientInfo) {
|
||||||
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
|
this.clientInfo = clientInfo;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the root URIs that this client can access. Roots define the base URIs for
|
||||||
|
* resources that the client can request from the server. For example, a root
|
||||||
|
* might be "file://workspace" for accessing workspace files.
|
||||||
|
* @param roots A list of root definitions. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if roots is null
|
||||||
|
*/
|
||||||
|
public SyncSpec roots(List<Root> roots) {
|
||||||
|
Assert.notNull(roots, "Roots must not be null");
|
||||||
|
for (Root root : roots) {
|
||||||
|
this.roots.put(root.uri(), root);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the root URIs that this client can access, using a varargs parameter for
|
||||||
|
* convenience. This is an alternative to {@link #roots(List)}.
|
||||||
|
* @param roots An array of root definitions. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if roots is null
|
||||||
|
* @see #roots(List)
|
||||||
|
*/
|
||||||
|
public SyncSpec roots(Root... roots) {
|
||||||
|
Assert.notNull(roots, "Roots must not be null");
|
||||||
|
for (Root root : roots) {
|
||||||
|
this.roots.put(root.uri(), root);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom sampling handler for processing message creation requests. The
|
||||||
|
* sampling handler can modify or validate messages before they are sent to the
|
||||||
|
* server, enabling custom processing logic.
|
||||||
|
* @param samplingHandler A function that processes message requests and returns
|
||||||
|
* results. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if samplingHandler is null
|
||||||
|
*/
|
||||||
|
public SyncSpec sampling(Function<CreateMessageRequest, CreateMessageResult> samplingHandler) {
|
||||||
|
Assert.notNull(samplingHandler, "Sampling handler must not be null");
|
||||||
|
this.samplingHandler = samplingHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified when the available tools change. This allows the
|
||||||
|
* client to react to changes in the server's tool capabilities, such as tools
|
||||||
|
* being added or removed.
|
||||||
|
* @param toolsChangeConsumer A consumer that receives the updated list of
|
||||||
|
* available tools. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if toolsChangeConsumer is null
|
||||||
|
*/
|
||||||
|
public SyncSpec toolsChangeConsumer(Consumer<List<McpSchema.Tool>> toolsChangeConsumer) {
|
||||||
|
Assert.notNull(toolsChangeConsumer, "Tools change consumer must not be null");
|
||||||
|
this.toolsChangeConsumers.add(toolsChangeConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified when the available resources change. This allows
|
||||||
|
* the client to react to changes in the server's resource availability, such as
|
||||||
|
* files being added or removed.
|
||||||
|
* @param resourcesChangeConsumer A consumer that receives the updated list of
|
||||||
|
* available resources. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if resourcesChangeConsumer is null
|
||||||
|
*/
|
||||||
|
public SyncSpec resourcesChangeConsumer(Consumer<List<McpSchema.Resource>> resourcesChangeConsumer) {
|
||||||
|
Assert.notNull(resourcesChangeConsumer, "Resources change consumer must not be null");
|
||||||
|
this.resourcesChangeConsumers.add(resourcesChangeConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified when the available prompts change. This allows
|
||||||
|
* the client to react to changes in the server's prompt templates, such as new
|
||||||
|
* templates being added or existing ones being modified.
|
||||||
|
* @param promptsChangeConsumer A consumer that receives the updated list of
|
||||||
|
* available prompts. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if promptsChangeConsumer is null
|
||||||
|
*/
|
||||||
|
public SyncSpec promptsChangeConsumer(Consumer<List<McpSchema.Prompt>> promptsChangeConsumer) {
|
||||||
|
Assert.notNull(promptsChangeConsumer, "Prompts change consumer must not be null");
|
||||||
|
this.promptsChangeConsumers.add(promptsChangeConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified when logging messages are received from the
|
||||||
|
* server. This allows the client to react to log messages, such as warnings or
|
||||||
|
* errors, that are sent by the server.
|
||||||
|
* @param loggingConsumer A consumer that receives logging messages. Must not be
|
||||||
|
* null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
*/
|
||||||
|
public SyncSpec loggingConsumer(Consumer<McpSchema.LoggingMessageNotification> loggingConsumer) {
|
||||||
|
Assert.notNull(loggingConsumer, "Logging consumer must not be null");
|
||||||
|
this.loggingConsumers.add(loggingConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple consumers to be notified when logging messages are received from
|
||||||
|
* the server. This allows the client to react to log messages, such as warnings
|
||||||
|
* or errors, that are sent by the server.
|
||||||
|
* @param loggingConsumers A list of consumers that receive logging messages. Must
|
||||||
|
* not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
*/
|
||||||
|
public SyncSpec loggingConsumers(List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers) {
|
||||||
|
Assert.notNull(loggingConsumers, "Logging consumers must not be null");
|
||||||
|
this.loggingConsumers.addAll(loggingConsumers);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of {@link McpSyncClient} with the provided configurations or
|
||||||
|
* sensible defaults.
|
||||||
|
* @return a new instance of {@link McpSyncClient}.
|
||||||
|
*/
|
||||||
|
public McpSyncClient build() {
|
||||||
|
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
||||||
|
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
|
||||||
|
this.loggingConsumers, this.samplingHandler);
|
||||||
|
|
||||||
|
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
|
||||||
|
|
||||||
|
return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, asyncFeatures));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous client specification.
|
||||||
|
*/
|
||||||
|
class AsyncSpec {
|
||||||
|
|
||||||
|
private final ClientMcpTransport transport;
|
||||||
|
|
||||||
|
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
|
||||||
|
|
||||||
|
private ClientCapabilities capabilities;
|
||||||
|
|
||||||
|
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
|
||||||
|
|
||||||
|
private Map<String, Root> roots = new HashMap<>();
|
||||||
|
|
||||||
|
private List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
||||||
|
|
||||||
|
private AsyncSpec(ClientMcpTransport transport) {
|
||||||
|
this.transport = transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the duration to wait for server responses before timing out requests. This
|
||||||
|
* timeout applies to all requests made through the client, including tool calls,
|
||||||
|
* resource access, and prompt operations.
|
||||||
|
* @param requestTimeout The duration to wait before timing out requests. Must not
|
||||||
|
* be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if requestTimeout is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec requestTimeout(Duration requestTimeout) {
|
||||||
|
Assert.notNull(requestTimeout, "Request timeout must not be null");
|
||||||
|
this.requestTimeout = requestTimeout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client capabilities that will be advertised to the server during
|
||||||
|
* connection initialization. Capabilities define what features the client
|
||||||
|
* supports, such as tool execution, resource access, and prompt handling.
|
||||||
|
* @param capabilities The client capabilities configuration. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if capabilities is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec capabilities(ClientCapabilities capabilities) {
|
||||||
|
Assert.notNull(capabilities, "Capabilities must not be null");
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client implementation information that will be shared with the server
|
||||||
|
* during connection initialization. This helps with version compatibility and
|
||||||
|
* debugging.
|
||||||
|
* @param clientInfo The client implementation details including name and version.
|
||||||
|
* Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if clientInfo is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec clientInfo(Implementation clientInfo) {
|
||||||
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
|
this.clientInfo = clientInfo;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the root URIs that this client can access. Roots define the base URIs for
|
||||||
|
* resources that the client can request from the server. For example, a root
|
||||||
|
* might be "file://workspace" for accessing workspace files.
|
||||||
|
* @param roots A list of root definitions. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if roots is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec roots(List<Root> roots) {
|
||||||
|
Assert.notNull(roots, "Roots must not be null");
|
||||||
|
for (Root root : roots) {
|
||||||
|
this.roots.put(root.uri(), root);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the root URIs that this client can access, using a varargs parameter for
|
||||||
|
* convenience. This is an alternative to {@link #roots(List)}.
|
||||||
|
* @param roots An array of root definitions. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if roots is null
|
||||||
|
* @see #roots(List)
|
||||||
|
*/
|
||||||
|
public AsyncSpec roots(Root... roots) {
|
||||||
|
Assert.notNull(roots, "Roots must not be null");
|
||||||
|
for (Root root : roots) {
|
||||||
|
this.roots.put(root.uri(), root);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom sampling handler for processing message creation requests. The
|
||||||
|
* sampling handler can modify or validate messages before they are sent to the
|
||||||
|
* server, enabling custom processing logic.
|
||||||
|
* @param samplingHandler A function that processes message requests and returns
|
||||||
|
* results. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if samplingHandler is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec sampling(Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler) {
|
||||||
|
Assert.notNull(samplingHandler, "Sampling handler must not be null");
|
||||||
|
this.samplingHandler = samplingHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified when the available tools change. This allows the
|
||||||
|
* client to react to changes in the server's tool capabilities, such as tools
|
||||||
|
* being added or removed.
|
||||||
|
* @param toolsChangeConsumer A consumer that receives the updated list of
|
||||||
|
* available tools. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if toolsChangeConsumer is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec toolsChangeConsumer(Function<List<McpSchema.Tool>, Mono<Void>> toolsChangeConsumer) {
|
||||||
|
Assert.notNull(toolsChangeConsumer, "Tools change consumer must not be null");
|
||||||
|
this.toolsChangeConsumers.add(toolsChangeConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified when the available resources change. This allows
|
||||||
|
* the client to react to changes in the server's resource availability, such as
|
||||||
|
* files being added or removed.
|
||||||
|
* @param resourcesChangeConsumer A consumer that receives the updated list of
|
||||||
|
* available resources. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if resourcesChangeConsumer is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec resourcesChangeConsumer(
|
||||||
|
Function<List<McpSchema.Resource>, Mono<Void>> resourcesChangeConsumer) {
|
||||||
|
Assert.notNull(resourcesChangeConsumer, "Resources change consumer must not be null");
|
||||||
|
this.resourcesChangeConsumers.add(resourcesChangeConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified when the available prompts change. This allows
|
||||||
|
* the client to react to changes in the server's prompt templates, such as new
|
||||||
|
* templates being added or existing ones being modified.
|
||||||
|
* @param promptsChangeConsumer A consumer that receives the updated list of
|
||||||
|
* available prompts. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if promptsChangeConsumer is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec promptsChangeConsumer(Function<List<McpSchema.Prompt>, Mono<Void>> promptsChangeConsumer) {
|
||||||
|
Assert.notNull(promptsChangeConsumer, "Prompts change consumer must not be null");
|
||||||
|
this.promptsChangeConsumers.add(promptsChangeConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified when logging messages are received from the
|
||||||
|
* server. This allows the client to react to log messages, such as warnings or
|
||||||
|
* errors, that are sent by the server.
|
||||||
|
* @param loggingConsumer A consumer that receives logging messages. Must not be
|
||||||
|
* null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
*/
|
||||||
|
public AsyncSpec loggingConsumer(Function<McpSchema.LoggingMessageNotification, Mono<Void>> loggingConsumer) {
|
||||||
|
Assert.notNull(loggingConsumer, "Logging consumer must not be null");
|
||||||
|
this.loggingConsumers.add(loggingConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple consumers to be notified when logging messages are received from
|
||||||
|
* the server. This allows the client to react to log messages, such as warnings
|
||||||
|
* or errors, that are sent by the server.
|
||||||
|
* @param loggingConsumers A list of consumers that receive logging messages. Must
|
||||||
|
* not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
*/
|
||||||
|
public AsyncSpec loggingConsumers(
|
||||||
|
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers) {
|
||||||
|
Assert.notNull(loggingConsumers, "Logging consumers must not be null");
|
||||||
|
this.loggingConsumers.addAll(loggingConsumers);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of {@link McpAsyncClient} with the provided configurations
|
||||||
|
* or sensible defaults.
|
||||||
|
* @return a new instance of {@link McpAsyncClient}.
|
||||||
|
*/
|
||||||
|
public McpAsyncClient build() {
|
||||||
|
return new McpAsyncClient(this.transport, this.requestTimeout,
|
||||||
|
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
||||||
|
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
|
||||||
|
this.loggingConsumers, this.samplingHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static SyncSpec sync(ClientMcpTransport transport) {
|
||||||
|
return new SyncSpec(transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
static AsyncSpec async(ClientMcpTransport transport) {
|
||||||
|
return new AsyncSpec(transport);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
package org.springframework.ai.mcp.client;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
import org.springframework.ai.mcp.util.Assert;
|
||||||
|
import org.springframework.ai.mcp.util.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal representation of features and capabilities for Model Context Protocol (MCP)
|
||||||
|
* clients. This class provides two record types for managing client features:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link Async} for non-blocking operations with Project Reactor's Mono responses
|
||||||
|
* <li>{@link Sync} for blocking operations with direct responses
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Each feature specification includes:
|
||||||
|
* <ul>
|
||||||
|
* <li>Client implementation information and capabilities
|
||||||
|
* <li>Root URI mappings for resource access
|
||||||
|
* <li>Change notification handlers for tools, resources, and prompts
|
||||||
|
* <li>Logging message consumers
|
||||||
|
* <li>Message sampling handlers for request processing
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The class supports conversion between synchronous and asynchronous specifications
|
||||||
|
* through the {@link Async#fromSync} method, which ensures proper handling of blocking
|
||||||
|
* operations in non-blocking contexts by scheduling them on a bounded elastic scheduler.
|
||||||
|
*
|
||||||
|
* @see McpClient
|
||||||
|
* @see McpSchema.Implementation
|
||||||
|
* @see McpSchema.ClientCapabilities
|
||||||
|
*/
|
||||||
|
class McpClientFeatures {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous client features specification providing the capabilities and request
|
||||||
|
* and notification handlers.
|
||||||
|
*
|
||||||
|
* @param clientInfo the client implementation information.
|
||||||
|
* @param clientCapabilities the client capabilities.
|
||||||
|
* @param roots the roots.
|
||||||
|
* @param toolsChangeConsumers the tools change consumers.
|
||||||
|
* @param resourcesChangeConsumers the resources change consumers.
|
||||||
|
* @param promptsChangeConsumers the prompts change consumers.
|
||||||
|
* @param loggingConsumers the logging consumers.
|
||||||
|
* @param samplingHandler the sampling handler.
|
||||||
|
*/
|
||||||
|
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
|
Map<String, McpSchema.Root> roots, List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
|
||||||
|
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
|
||||||
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||||
|
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||||
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance and validate the arguments.
|
||||||
|
* @param clientInfo the client implementation information.
|
||||||
|
* @param clientCapabilities the client capabilities.
|
||||||
|
* @param roots the roots.
|
||||||
|
* @param toolsChangeConsumers the tools change consumers.
|
||||||
|
* @param resourcesChangeConsumers the resources change consumers.
|
||||||
|
* @param promptsChangeConsumers the prompts change consumers.
|
||||||
|
* @param loggingConsumers the logging consumers.
|
||||||
|
* @param samplingHandler the sampling handler.
|
||||||
|
*/
|
||||||
|
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
|
Map<String, McpSchema.Root> roots,
|
||||||
|
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
|
||||||
|
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
|
||||||
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||||
|
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||||
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler) {
|
||||||
|
|
||||||
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
|
|
||||||
|
this.clientInfo = clientInfo;
|
||||||
|
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
||||||
|
: new McpSchema.ClientCapabilities(null,
|
||||||
|
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
||||||
|
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null);
|
||||||
|
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
this.toolsChangeConsumers = toolsChangeConsumers;
|
||||||
|
this.resourcesChangeConsumers = resourcesChangeConsumers;
|
||||||
|
this.promptsChangeConsumers = promptsChangeConsumers;
|
||||||
|
this.loggingConsumers = loggingConsumers;
|
||||||
|
this.samplingHandler = samplingHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a synchronous specification into an asynchronous one and provide
|
||||||
|
* blocking code offloading to prevent accidental blocking of the non-blocking
|
||||||
|
* transport.
|
||||||
|
* @param syncSpec a potentially blocking, synchronous specification.
|
||||||
|
* @return a specification which is protected from blocking calls specified by the
|
||||||
|
* user.
|
||||||
|
*/
|
||||||
|
public static Async fromSync(Sync syncSpec) {
|
||||||
|
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers = new ArrayList<>();
|
||||||
|
for (Consumer<List<McpSchema.Tool>> consumer : syncSpec.toolsChangeConsumers()) {
|
||||||
|
toolsChangeConsumers.add(t -> Mono.<Void>fromRunnable(() -> consumer.accept(t))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers = new ArrayList<>();
|
||||||
|
for (Consumer<List<McpSchema.Resource>> consumer : syncSpec.resourcesChangeConsumers()) {
|
||||||
|
resourcesChangeConsumers.add(r -> Mono.<Void>fromRunnable(() -> consumer.accept(r))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Consumer<List<McpSchema.Prompt>> consumer : syncSpec.promptsChangeConsumers()) {
|
||||||
|
promptsChangeConsumers.add(p -> Mono.<Void>fromRunnable(() -> consumer.accept(p))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers = new ArrayList<>();
|
||||||
|
for (Consumer<McpSchema.LoggingMessageNotification> consumer : syncSpec.loggingConsumers()) {
|
||||||
|
loggingConsumers.add(l -> Mono.<Void>fromRunnable(() -> consumer.accept(l))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
|
||||||
|
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
|
||||||
|
toolsChangeConsumers, resourcesChangeConsumers, promptsChangeConsumers, loggingConsumers,
|
||||||
|
samplingHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous client features specification providing the capabilities and request
|
||||||
|
* and notification handlers.
|
||||||
|
*
|
||||||
|
* @param clientInfo the client implementation information.
|
||||||
|
* @param clientCapabilities the client capabilities.
|
||||||
|
* @param roots the roots.
|
||||||
|
* @param toolsChangeConsumers the tools change consumers.
|
||||||
|
* @param resourcesChangeConsumers the resources change consumers.
|
||||||
|
* @param promptsChangeConsumers the prompts change consumers.
|
||||||
|
* @param loggingConsumers the logging consumers.
|
||||||
|
* @param samplingHandler the sampling handler.
|
||||||
|
*/
|
||||||
|
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
|
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||||
|
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
||||||
|
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||||
|
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||||
|
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance and validate the arguments.
|
||||||
|
* @param clientInfo the client implementation information.
|
||||||
|
* @param clientCapabilities the client capabilities.
|
||||||
|
* @param roots the roots.
|
||||||
|
* @param toolsChangeConsumers the tools change consumers.
|
||||||
|
* @param resourcesChangeConsumers the resources change consumers.
|
||||||
|
* @param promptsChangeConsumers the prompts change consumers.
|
||||||
|
* @param loggingConsumers the logging consumers.
|
||||||
|
* @param samplingHandler the sampling handler.
|
||||||
|
*/
|
||||||
|
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
|
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||||
|
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
||||||
|
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||||
|
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||||
|
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler) {
|
||||||
|
|
||||||
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
|
|
||||||
|
this.clientInfo = clientInfo;
|
||||||
|
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
||||||
|
: new McpSchema.ClientCapabilities(null,
|
||||||
|
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
||||||
|
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null);
|
||||||
|
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
|
||||||
|
|
||||||
|
this.toolsChangeConsumers = toolsChangeConsumers;
|
||||||
|
this.resourcesChangeConsumers = resourcesChangeConsumers;
|
||||||
|
this.promptsChangeConsumers = promptsChangeConsumers;
|
||||||
|
this.loggingConsumers = loggingConsumers;
|
||||||
|
this.samplingHandler = samplingHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -29,10 +29,39 @@ import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult;
|
||||||
import org.springframework.ai.mcp.util.Assert;
|
import org.springframework.ai.mcp.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A synchronous client for the Model Context Protocol (MCP).
|
* A synchronous client implementation for the Model Context Protocol (MCP) that wraps an
|
||||||
|
* {@link McpAsyncClient} to provide blocking operations.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This client implements the MCP specification by delegating to an asynchronous client
|
||||||
|
* and blocking on the results. Key features include:
|
||||||
|
* <ul>
|
||||||
|
* <li>Synchronous, blocking API for simpler integration in non-reactive applications
|
||||||
|
* <li>Tool discovery and invocation for server-provided functionality
|
||||||
|
* <li>Resource access and management with URI-based addressing
|
||||||
|
* <li>Prompt template handling for standardized AI interactions
|
||||||
|
* <li>Real-time notifications for tools, resources, and prompts changes
|
||||||
|
* <li>Structured logging with configurable severity levels
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The client follows the same lifecycle as its async counterpart:
|
||||||
|
* <ol>
|
||||||
|
* <li>Initialization - Establishes connection and negotiates capabilities
|
||||||
|
* <li>Normal Operation - Handles requests and notifications
|
||||||
|
* <li>Graceful Shutdown - Ensures clean connection termination
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation implements {@link AutoCloseable} for resource cleanup and provides
|
||||||
|
* both immediate and graceful shutdown options. All operations block until completion or
|
||||||
|
* timeout, making it suitable for traditional synchronous programming models.
|
||||||
*
|
*
|
||||||
* @author Dariusz Jędrzejczyk
|
* @author Dariusz Jędrzejczyk
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
|
* @see McpClient
|
||||||
|
* @see McpAsyncClient
|
||||||
|
* @see McpSchema
|
||||||
*/
|
*/
|
||||||
public class McpSyncClient implements AutoCloseable {
|
public class McpSyncClient implements AutoCloseable {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,12 @@ import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
|
|
@ -48,9 +50,43 @@ import org.springframework.ai.mcp.util.Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Model Context Protocol (MCP) server implementation that provides asynchronous
|
* The Model Context Protocol (MCP) server implementation that provides asynchronous
|
||||||
* communication.
|
* communication using Project Reactor's Mono and Flux types.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This server implements the MCP specification, enabling AI models to expose tools,
|
||||||
|
* resources, and prompts through a standardized interface. Key features include:
|
||||||
|
* <ul>
|
||||||
|
* <li>Asynchronous communication using reactive programming patterns
|
||||||
|
* <li>Dynamic tool registration and management
|
||||||
|
* <li>Resource handling with URI-based addressing
|
||||||
|
* <li>Prompt template management
|
||||||
|
* <li>Real-time client notifications for state changes
|
||||||
|
* <li>Structured logging with configurable severity levels
|
||||||
|
* <li>Support for client-side AI model sampling
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The server follows a lifecycle:
|
||||||
|
* <ol>
|
||||||
|
* <li>Initialization - Accepts client connections and negotiates capabilities
|
||||||
|
* <li>Normal Operation - Handles client requests and sends notifications
|
||||||
|
* <li>Graceful Shutdown - Ensures clean connection termination
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation uses Project Reactor for non-blocking operations, making it
|
||||||
|
* suitable for high-throughput scenarios and reactive applications. All operations return
|
||||||
|
* Mono or Flux types that can be composed into reactive pipelines.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The server supports runtime modification of its capabilities through methods like
|
||||||
|
* {@link #addTool}, {@link #addResource}, and {@link #addPrompt}, automatically notifying
|
||||||
|
* connected clients of changes when configured to do so.
|
||||||
*
|
*
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
|
* @see McpServer
|
||||||
|
* @see McpSchema
|
||||||
|
* @see DefaultMcpSession
|
||||||
*/
|
*/
|
||||||
public class McpAsyncServer {
|
public class McpAsyncServer {
|
||||||
|
|
||||||
|
|
@ -75,16 +111,84 @@ public class McpAsyncServer {
|
||||||
/**
|
/**
|
||||||
* Thread-safe list of tool handlers that can be modified at runtime.
|
* Thread-safe list of tool handlers that can be modified at runtime.
|
||||||
*/
|
*/
|
||||||
private final CopyOnWriteArrayList<ToolRegistration> tools;
|
private final CopyOnWriteArrayList<McpServerFeatures.AsyncToolRegistration> tools = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
private final CopyOnWriteArrayList<McpSchema.ResourceTemplate> resourceTemplates;
|
private final CopyOnWriteArrayList<McpSchema.ResourceTemplate> resourceTemplates = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, ResourceRegistration> resources;
|
private final ConcurrentHashMap<String, McpServerFeatures.AsyncResourceRegistration> resources = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, PromptRegistration> prompts;
|
private final ConcurrentHashMap<String, McpServerFeatures.AsyncPromptRegistration> prompts = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
|
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new McpAsyncServer with the given transport and capabilities.
|
||||||
|
* @param mcpTransport The transport layer implementation for MCP communication.
|
||||||
|
* @param features The MCP server supported features.
|
||||||
|
*/
|
||||||
|
McpAsyncServer(ServerMcpTransport mcpTransport, McpServerFeatures.Async features) {
|
||||||
|
this.serverInfo = features.serverInfo();
|
||||||
|
this.serverCapabilities = features.serverCapabilities();
|
||||||
|
this.tools.addAll(features.tools());
|
||||||
|
this.resources.putAll(features.resources());
|
||||||
|
this.resourceTemplates.addAll(features.resourceTemplates());
|
||||||
|
this.prompts.putAll(features.prompts());
|
||||||
|
|
||||||
|
Map<String, DefaultMcpSession.RequestHandler<?>> requestHandlers = new HashMap<>();
|
||||||
|
|
||||||
|
// Initialize request handlers for standard MCP methods
|
||||||
|
requestHandlers.put(McpSchema.METHOD_INITIALIZE, initializeRequestHandler());
|
||||||
|
|
||||||
|
// Ping MUST respond with an empty data, but not NULL response.
|
||||||
|
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.just(""));
|
||||||
|
|
||||||
|
// Add tools API handlers if the tool capability is enabled
|
||||||
|
if (this.serverCapabilities.tools() != null) {
|
||||||
|
requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler());
|
||||||
|
requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resources API handlers if provided
|
||||||
|
if (!Utils.isEmpty(this.resources)) {
|
||||||
|
requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler());
|
||||||
|
requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resource templates API handlers if provided.
|
||||||
|
if (!Utils.isEmpty(this.resourceTemplates)) {
|
||||||
|
requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add prompts API handlers if provider exists
|
||||||
|
if (!Utils.isEmpty(this.prompts)) {
|
||||||
|
requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler());
|
||||||
|
requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add logging API handlers if the logging capability is enabled
|
||||||
|
if (this.serverCapabilities.logging() != null) {
|
||||||
|
requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
|
||||||
|
|
||||||
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
|
||||||
|
|
||||||
|
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers = features.rootsChangeConsumers();
|
||||||
|
|
||||||
|
if (Utils.isEmpty(rootsChangeConsumers)) {
|
||||||
|
rootsChangeConsumers = List.of((roots) -> Mono.fromRunnable(() -> logger
|
||||||
|
.warn("Roots list changed notification, but no consumers provided. Roots list changed: {}", roots)));
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
|
||||||
|
asyncRootsListChangedNotificationHandler(rootsChangeConsumers));
|
||||||
|
|
||||||
|
this.transport = mcpTransport;
|
||||||
|
this.mcpSession = new DefaultMcpSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
|
||||||
|
notificationHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new McpAsyncServer with the given transport and capabilities.
|
* Create a new McpAsyncServer with the given transport and capabilities.
|
||||||
* @param mcpTransport The transport layer implementation for MCP communication
|
* @param mcpTransport The transport layer implementation for MCP communication
|
||||||
|
|
@ -96,18 +200,28 @@ public class McpAsyncServer {
|
||||||
* @param prompts The map of prompt registrations
|
* @param prompts The map of prompt registrations
|
||||||
* @param rootsChangeConsumers The list of consumers that will be notified when the
|
* @param rootsChangeConsumers The list of consumers that will be notified when the
|
||||||
* roots list changes
|
* roots list changes
|
||||||
|
* @deprecated Use {@link McpServer#sync(ServerMcpTransport)} or
|
||||||
|
* {@link McpServer#async(ServerMcpTransport)} to create a new server instance.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public McpAsyncServer(ServerMcpTransport mcpTransport, McpSchema.Implementation serverInfo,
|
public McpAsyncServer(ServerMcpTransport mcpTransport, McpSchema.Implementation serverInfo,
|
||||||
McpSchema.ServerCapabilities serverCapabilities, List<ToolRegistration> tools,
|
McpSchema.ServerCapabilities serverCapabilities, List<ToolRegistration> tools,
|
||||||
Map<String, ResourceRegistration> resources, List<McpSchema.ResourceTemplate> resourceTemplates,
|
Map<String, ResourceRegistration> resources, List<McpSchema.ResourceTemplate> resourceTemplates,
|
||||||
Map<String, PromptRegistration> prompts, List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
Map<String, PromptRegistration> prompts, List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
||||||
|
|
||||||
this.serverInfo = serverInfo;
|
this.serverInfo = serverInfo;
|
||||||
this.tools = new CopyOnWriteArrayList<>(tools != null ? tools : List.of());
|
if (!Utils.isEmpty(tools)) {
|
||||||
this.resources = !Utils.isEmpty(resources) ? new ConcurrentHashMap<>(resources) : new ConcurrentHashMap<>();
|
this.tools.addAll(McpServer.mapDeprecatedTools(tools));
|
||||||
this.resourceTemplates = !Utils.isEmpty(resourceTemplates) ? new CopyOnWriteArrayList<>(resourceTemplates)
|
}
|
||||||
: new CopyOnWriteArrayList<>();
|
if (!Utils.isEmpty(resources)) {
|
||||||
this.prompts = !Utils.isEmpty(prompts) ? new ConcurrentHashMap<>(prompts) : new ConcurrentHashMap<>();
|
this.resources.putAll(McpServer.mapDeprecatedResources(resources));
|
||||||
|
}
|
||||||
|
if (!Utils.isEmpty(resourceTemplates)) {
|
||||||
|
this.resourceTemplates.addAll(resourceTemplates);
|
||||||
|
}
|
||||||
|
if (!Utils.isEmpty(prompts)) {
|
||||||
|
this.prompts.putAll(McpServer.mapDeprecatedPrompts(prompts));
|
||||||
|
}
|
||||||
|
|
||||||
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities : new McpSchema.ServerCapabilities(
|
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities : new McpSchema.ServerCapabilities(
|
||||||
null, // experimental
|
null, // experimental
|
||||||
|
|
@ -118,13 +232,13 @@ public class McpAsyncServer {
|
||||||
: null,
|
: null,
|
||||||
!Utils.isEmpty(this.tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
|
!Utils.isEmpty(this.tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
|
||||||
|
|
||||||
Map<String, DefaultMcpSession.RequestHandler> requestHandlers = new HashMap<>();
|
Map<String, DefaultMcpSession.RequestHandler<?>> requestHandlers = new HashMap<>();
|
||||||
|
|
||||||
// Initialize request handlers for standard MCP methods
|
// Initialize request handlers for standard MCP methods
|
||||||
requestHandlers.put(McpSchema.METHOD_INITIALIZE, initializeRequestHandler());
|
requestHandlers.put(McpSchema.METHOD_INITIALIZE, initializeRequestHandler());
|
||||||
|
|
||||||
// Ping MUST respond with an empty data, but not NULL response.
|
// Ping MUST respond with an empty data, but not NULL response.
|
||||||
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.<Object>just(""));
|
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.just(""));
|
||||||
|
|
||||||
// Add tools API handlers if the tool capability is enabled
|
// Add tools API handlers if the tool capability is enabled
|
||||||
if (this.serverCapabilities.tools() != null) {
|
if (this.serverCapabilities.tools() != null) {
|
||||||
|
|
@ -173,7 +287,7 @@ public class McpAsyncServer {
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Lifecycle Management
|
// Lifecycle Management
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
private DefaultMcpSession.RequestHandler initializeRequestHandler() {
|
private DefaultMcpSession.RequestHandler<McpSchema.InitializeResult> initializeRequestHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params,
|
McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params,
|
||||||
new TypeReference<McpSchema.InitializeRequest>() {
|
new TypeReference<McpSchema.InitializeRequest>() {
|
||||||
|
|
@ -187,14 +301,13 @@ public class McpAsyncServer {
|
||||||
initializeRequest.clientInfo());
|
initializeRequest.clientInfo());
|
||||||
|
|
||||||
if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeRequest.protocolVersion())) {
|
if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeRequest.protocolVersion())) {
|
||||||
return Mono
|
return Mono.<McpSchema.InitializeResult>error(new McpError(
|
||||||
.<Object>error(new McpError(
|
"Unsupported protocol version from client: " + initializeRequest.protocolVersion()))
|
||||||
"Unsupported protocol version from client: " + initializeRequest.protocolVersion()))
|
|
||||||
.publishOn(Schedulers.boundedElastic());
|
.publishOn(Schedulers.boundedElastic());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Mono
|
return Mono
|
||||||
.<Object>just(new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, this.serverCapabilities,
|
.just(new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, this.serverCapabilities,
|
||||||
this.serverInfo, null))
|
this.serverInfo, null))
|
||||||
.publishOn(Schedulers.boundedElastic());
|
.publishOn(Schedulers.boundedElastic());
|
||||||
};
|
};
|
||||||
|
|
@ -268,6 +381,7 @@ public class McpAsyncServer {
|
||||||
LIST_ROOTS_RESULT_TYPE_REF);
|
LIST_ROOTS_RESULT_TYPE_REF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
private NotificationHandler rootsListChnagedNotificationHandler(
|
private NotificationHandler rootsListChnagedNotificationHandler(
|
||||||
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
||||||
|
|
||||||
|
|
@ -281,6 +395,13 @@ public class McpAsyncServer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NotificationHandler asyncRootsListChangedNotificationHandler(
|
||||||
|
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
|
||||||
|
return params -> listRoots().flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
|
||||||
|
.flatMap(consumer -> consumer.apply(listRootsResult.roots()))
|
||||||
|
.then());
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Tool Management
|
// Tool Management
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -290,6 +411,44 @@ public class McpAsyncServer {
|
||||||
* @param toolRegistration The tool registration to add
|
* @param toolRegistration The tool registration to add
|
||||||
* @return Mono that completes when clients have been notified of the change
|
* @return Mono that completes when clients have been notified of the change
|
||||||
*/
|
*/
|
||||||
|
public Mono<Void> addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) {
|
||||||
|
if (toolRegistration == null) {
|
||||||
|
return Mono.error(new McpError("Tool registration must not be null"));
|
||||||
|
}
|
||||||
|
if (toolRegistration.tool() == null) {
|
||||||
|
return Mono.error(new McpError("Tool must not be null"));
|
||||||
|
}
|
||||||
|
if (toolRegistration.call() == null) {
|
||||||
|
return Mono.error(new McpError("Tool call handler must not be null"));
|
||||||
|
}
|
||||||
|
if (this.serverCapabilities.tools() == null) {
|
||||||
|
return Mono.error(new McpError("Server must be configured with tool capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
// Check for duplicate tool names
|
||||||
|
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolRegistration.tool().name()))) {
|
||||||
|
return Mono
|
||||||
|
.error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tools.add(toolRegistration);
|
||||||
|
logger.info("Added tool handler: {}", toolRegistration.tool().name());
|
||||||
|
|
||||||
|
if (this.serverCapabilities.tools().listChanged()) {
|
||||||
|
return notifyToolsListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new tool registration at runtime.
|
||||||
|
* @param toolRegistration The tool registration to add
|
||||||
|
* @return Mono that completes when clients have been notified of the change
|
||||||
|
* @deprecated Use {@link #addTool(McpServerFeatures.AsyncToolRegistration)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public Mono<Void> addTool(ToolRegistration toolRegistration) {
|
public Mono<Void> addTool(ToolRegistration toolRegistration) {
|
||||||
if (toolRegistration == null) {
|
if (toolRegistration == null) {
|
||||||
return Mono.error(new McpError("Tool registration must not be null"));
|
return Mono.error(new McpError("Tool registration must not be null"));
|
||||||
|
|
@ -309,7 +468,7 @@ public class McpAsyncServer {
|
||||||
return Mono.error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists"));
|
return Mono.error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists"));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tools.add(toolRegistration);
|
this.tools.add(McpServer.mapDeprecatedTool(toolRegistration));
|
||||||
logger.info("Added tool handler: {}", toolRegistration.tool().name());
|
logger.info("Added tool handler: {}", toolRegistration.tool().name());
|
||||||
if (this.serverCapabilities.tools().listChanged()) {
|
if (this.serverCapabilities.tools().listChanged()) {
|
||||||
return notifyToolsListChanged();
|
return notifyToolsListChanged();
|
||||||
|
|
@ -330,15 +489,17 @@ public class McpAsyncServer {
|
||||||
return Mono.error(new McpError("Server must be configured with tool capabilities"));
|
return Mono.error(new McpError("Server must be configured with tool capabilities"));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean removed = this.tools.removeIf(toolRegistration -> toolRegistration.tool().name().equals(toolName));
|
return Mono.defer(() -> {
|
||||||
if (removed) {
|
boolean removed = this.tools.removeIf(toolRegistration -> toolRegistration.tool().name().equals(toolName));
|
||||||
logger.info("Removed tool handler: {}", toolName);
|
if (removed) {
|
||||||
if (this.serverCapabilities.tools().listChanged()) {
|
logger.info("Removed tool handler: {}", toolName);
|
||||||
return notifyToolsListChanged();
|
if (this.serverCapabilities.tools().listChanged()) {
|
||||||
|
return notifyToolsListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
return Mono.empty();
|
return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
|
||||||
}
|
});
|
||||||
return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -349,32 +510,30 @@ public class McpAsyncServer {
|
||||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
|
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultMcpSession.RequestHandler toolsListRequestHandler() {
|
private DefaultMcpSession.RequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
|
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolRegistration::tool).toList();
|
||||||
List<Tool> tools = this.tools.stream().map(ToolRegistration::tool).toList();
|
|
||||||
|
|
||||||
return Mono.just(new McpSchema.ListToolsResult(tools, null));
|
return Mono.just(new McpSchema.ListToolsResult(tools, null));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultMcpSession.RequestHandler toolsCallRequestHandler() {
|
private DefaultMcpSession.RequestHandler<CallToolResult> toolsCallRequestHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
McpSchema.CallToolRequest callToolRequest = transport.unmarshalFrom(params,
|
McpSchema.CallToolRequest callToolRequest = transport.unmarshalFrom(params,
|
||||||
new TypeReference<McpSchema.CallToolRequest>() {
|
new TypeReference<McpSchema.CallToolRequest>() {
|
||||||
});
|
});
|
||||||
|
|
||||||
Optional<ToolRegistration> toolRegistration = this.tools.stream()
|
Optional<McpServerFeatures.AsyncToolRegistration> toolRegistration = this.tools.stream()
|
||||||
.filter(tr -> callToolRequest.name().equals(tr.tool().name()))
|
.filter(tr -> callToolRequest.name().equals(tr.tool().name()))
|
||||||
.findAny();
|
.findAny();
|
||||||
|
|
||||||
if (toolRegistration.isEmpty()) {
|
if (toolRegistration.isEmpty()) {
|
||||||
return Mono.<Object>error(new McpError("Tool not found: " + callToolRequest.name()));
|
return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Mono.fromCallable(() -> toolRegistration.get().call().apply(callToolRequest.arguments()))
|
return toolRegistration.map(tool -> tool.call().apply(callToolRequest.arguments()))
|
||||||
.map(result -> (Object) result)
|
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -387,6 +546,35 @@ public class McpAsyncServer {
|
||||||
* @param resourceHandler The resource handler to add
|
* @param resourceHandler The resource handler to add
|
||||||
* @return Mono that completes when clients have been notified of the change
|
* @return Mono that completes when clients have been notified of the change
|
||||||
*/
|
*/
|
||||||
|
public Mono<Void> addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) {
|
||||||
|
if (resourceHandler == null || resourceHandler.resource() == null) {
|
||||||
|
return Mono.error(new McpError("Resource must not be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.serverCapabilities.resources() == null) {
|
||||||
|
return Mono.error(new McpError("Server must be configured with resource capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
if (this.resources.putIfAbsent(resourceHandler.resource().uri(), resourceHandler) != null) {
|
||||||
|
return Mono
|
||||||
|
.error(new McpError("Resource with URI '" + resourceHandler.resource().uri() + "' already exists"));
|
||||||
|
}
|
||||||
|
logger.info("Added resource handler: {}", resourceHandler.resource().uri());
|
||||||
|
if (this.serverCapabilities.resources().listChanged()) {
|
||||||
|
return notifyResourcesListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new resource handler at runtime.
|
||||||
|
* @param resourceHandler The resource handler to add
|
||||||
|
* @return Mono that completes when clients have been notified of the change
|
||||||
|
* @deprecated Use {@link #addResource(McpServerFeatures.AsyncResourceRegistration)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public Mono<Void> addResource(ResourceRegistration resourceHandler) {
|
public Mono<Void> addResource(ResourceRegistration resourceHandler) {
|
||||||
if (resourceHandler == null || resourceHandler.resource() == null) {
|
if (resourceHandler == null || resourceHandler.resource() == null) {
|
||||||
return Mono.error(new McpError("Resource must not be null"));
|
return Mono.error(new McpError("Resource must not be null"));
|
||||||
|
|
@ -401,7 +589,7 @@ public class McpAsyncServer {
|
||||||
.error(new McpError("Resource with URI '" + resourceHandler.resource().uri() + "' already exists"));
|
.error(new McpError("Resource with URI '" + resourceHandler.resource().uri() + "' already exists"));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resources.put(resourceHandler.resource().uri(), resourceHandler);
|
this.resources.put(resourceHandler.resource().uri(), McpServer.mapDeprecatedResource(resourceHandler));
|
||||||
logger.info("Added resource handler: {}", resourceHandler.resource().uri());
|
logger.info("Added resource handler: {}", resourceHandler.resource().uri());
|
||||||
if (this.serverCapabilities.resources().listChanged()) {
|
if (this.serverCapabilities.resources().listChanged()) {
|
||||||
return notifyResourcesListChanged();
|
return notifyResourcesListChanged();
|
||||||
|
|
@ -422,15 +610,17 @@ public class McpAsyncServer {
|
||||||
return Mono.error(new McpError("Server must be configured with resource capabilities"));
|
return Mono.error(new McpError("Server must be configured with resource capabilities"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ResourceRegistration removed = this.resources.remove(resourceUri);
|
return Mono.defer(() -> {
|
||||||
if (removed != null) {
|
McpServerFeatures.AsyncResourceRegistration removed = this.resources.remove(resourceUri);
|
||||||
logger.info("Removed resource handler: {}", resourceUri);
|
if (removed != null) {
|
||||||
if (this.serverCapabilities.resources().listChanged()) {
|
logger.info("Removed resource handler: {}", resourceUri);
|
||||||
return notifyResourcesListChanged();
|
if (this.serverCapabilities.resources().listChanged()) {
|
||||||
|
return notifyResourcesListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
return Mono.empty();
|
return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
|
||||||
}
|
});
|
||||||
return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -441,28 +631,30 @@ public class McpAsyncServer {
|
||||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null);
|
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultMcpSession.RequestHandler resourcesListRequestHandler() {
|
private DefaultMcpSession.RequestHandler<McpSchema.ListResourcesResult> resourcesListRequestHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
var resourceList = this.resources.values().stream().map(ResourceRegistration::resource).toList();
|
var resourceList = this.resources.values()
|
||||||
|
.stream()
|
||||||
|
.map(McpServerFeatures.AsyncResourceRegistration::resource)
|
||||||
|
.toList();
|
||||||
return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
|
return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultMcpSession.RequestHandler resourceTemplateListRequestHandler() {
|
private DefaultMcpSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
|
||||||
return params -> Mono.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
|
return params -> Mono.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultMcpSession.RequestHandler resourcesReadRequestHandler() {
|
private DefaultMcpSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
McpSchema.ReadResourceRequest resourceRequest = transport.unmarshalFrom(params,
|
McpSchema.ReadResourceRequest resourceRequest = transport.unmarshalFrom(params,
|
||||||
new TypeReference<McpSchema.ReadResourceRequest>() {
|
new TypeReference<McpSchema.ReadResourceRequest>() {
|
||||||
});
|
});
|
||||||
var resourceUri = resourceRequest.uri();
|
var resourceUri = resourceRequest.uri();
|
||||||
if (this.resources.containsKey(resourceUri)) {
|
McpServerFeatures.AsyncResourceRegistration registration = this.resources.get(resourceUri);
|
||||||
return Mono.fromCallable(() -> this.resources.get(resourceUri).readHandler().apply(resourceRequest))
|
if (registration != null) {
|
||||||
.map(result -> (Object) result)
|
return this.resources.get(resourceUri).readHandler().apply(resourceRequest);
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
|
||||||
}
|
}
|
||||||
return Mono.error(new McpError("Resource not found: " + resourceUri));
|
return Mono.error(new McpError("Resource not found: " + resourceUri));
|
||||||
};
|
};
|
||||||
|
|
@ -477,6 +669,41 @@ public class McpAsyncServer {
|
||||||
* @param promptRegistration The prompt handler to add
|
* @param promptRegistration The prompt handler to add
|
||||||
* @return Mono that completes when clients have been notified of the change
|
* @return Mono that completes when clients have been notified of the change
|
||||||
*/
|
*/
|
||||||
|
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) {
|
||||||
|
if (promptRegistration == null) {
|
||||||
|
return Mono.error(new McpError("Prompt registration must not be null"));
|
||||||
|
}
|
||||||
|
if (this.serverCapabilities.prompts() == null) {
|
||||||
|
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
McpServerFeatures.AsyncPromptRegistration registration = this.prompts
|
||||||
|
.putIfAbsent(promptRegistration.prompt().name(), promptRegistration);
|
||||||
|
if (registration != null) {
|
||||||
|
return Mono.error(
|
||||||
|
new McpError("Prompt with name '" + promptRegistration.prompt().name() + "' already exists"));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Added prompt handler: {}", promptRegistration.prompt().name());
|
||||||
|
|
||||||
|
// Servers that declared the listChanged capability SHOULD send a
|
||||||
|
// notification,
|
||||||
|
// when the list of available prompts changes
|
||||||
|
if (this.serverCapabilities.prompts().listChanged()) {
|
||||||
|
return notifyPromptsListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new prompt handler at runtime.
|
||||||
|
* @param promptRegistration The prompt handler to add
|
||||||
|
* @return Mono that completes when clients have been notified of the change
|
||||||
|
* @deprecated Use {@link #addPrompt(McpServerFeatures.AsyncPromptRegistration)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public Mono<Void> addPrompt(PromptRegistration promptRegistration) {
|
public Mono<Void> addPrompt(PromptRegistration promptRegistration) {
|
||||||
if (promptRegistration == null) {
|
if (promptRegistration == null) {
|
||||||
return Mono.error(new McpError("Prompt registration must not be null"));
|
return Mono.error(new McpError("Prompt registration must not be null"));
|
||||||
|
|
@ -490,7 +717,7 @@ public class McpAsyncServer {
|
||||||
.error(new McpError("Prompt with name '" + promptRegistration.prompt().name() + "' already exists"));
|
.error(new McpError("Prompt with name '" + promptRegistration.prompt().name() + "' already exists"));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.prompts.put(promptRegistration.prompt().name(), promptRegistration);
|
this.prompts.put(promptRegistration.prompt().name(), McpServer.mapDeprecatedPrompt(promptRegistration));
|
||||||
|
|
||||||
logger.info("Added prompt handler: {}", promptRegistration.prompt().name());
|
logger.info("Added prompt handler: {}", promptRegistration.prompt().name());
|
||||||
|
|
||||||
|
|
@ -515,18 +742,20 @@ public class McpAsyncServer {
|
||||||
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
|
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
|
||||||
}
|
}
|
||||||
|
|
||||||
PromptRegistration removed = this.prompts.remove(promptName);
|
return Mono.defer(() -> {
|
||||||
|
McpServerFeatures.AsyncPromptRegistration removed = this.prompts.remove(promptName);
|
||||||
|
|
||||||
if (removed != null) {
|
if (removed != null) {
|
||||||
logger.info("Removed prompt handler: {}", promptName);
|
logger.info("Removed prompt handler: {}", promptName);
|
||||||
// Servers that declared the listChanged capability SHOULD send a
|
// Servers that declared the listChanged capability SHOULD send a
|
||||||
// notification, when the list of available prompts changes
|
// notification, when the list of available prompts changes
|
||||||
if (this.serverCapabilities.prompts().listChanged()) {
|
if (this.serverCapabilities.prompts().listChanged()) {
|
||||||
return this.notifyPromptsListChanged();
|
return this.notifyPromptsListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
return Mono.empty();
|
return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
|
||||||
}
|
});
|
||||||
return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -537,34 +766,35 @@ public class McpAsyncServer {
|
||||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
|
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultMcpSession.RequestHandler promptsListRequestHandler() {
|
private DefaultMcpSession.RequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
// TODO: Implement pagination
|
// TODO: Implement pagination
|
||||||
// McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
// McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||||
// new TypeReference<McpSchema.PaginatedRequest>() {
|
// new TypeReference<McpSchema.PaginatedRequest>() {
|
||||||
// });
|
// });
|
||||||
|
|
||||||
var promptList = this.prompts.values().stream().map(PromptRegistration::prompt).toList();
|
var promptList = this.prompts.values()
|
||||||
|
.stream()
|
||||||
|
.map(McpServerFeatures.AsyncPromptRegistration::prompt)
|
||||||
|
.toList();
|
||||||
|
|
||||||
return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
|
return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefaultMcpSession.RequestHandler promptsGetRequestHandler() {
|
private DefaultMcpSession.RequestHandler<McpSchema.GetPromptResult> promptsGetRequestHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
McpSchema.GetPromptRequest promptRequest = transport.unmarshalFrom(params,
|
McpSchema.GetPromptRequest promptRequest = transport.unmarshalFrom(params,
|
||||||
new TypeReference<McpSchema.GetPromptRequest>() {
|
new TypeReference<McpSchema.GetPromptRequest>() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Implement prompt retrieval logic here
|
// Implement prompt retrieval logic here
|
||||||
if (this.prompts.containsKey(promptRequest.name())) {
|
McpServerFeatures.AsyncPromptRegistration registration = this.prompts.get(promptRequest.name());
|
||||||
return Mono
|
if (registration == null) {
|
||||||
.fromCallable(() -> this.prompts.get(promptRequest.name()).promptHandler().apply(promptRequest))
|
return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
|
||||||
.map(result -> (Object) result)
|
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
|
return registration.promptHandler().apply(promptRequest);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -600,13 +830,10 @@ public class McpAsyncServer {
|
||||||
* not be sent.
|
* not be sent.
|
||||||
* @return A handler that processes logging level change requests
|
* @return A handler that processes logging level change requests
|
||||||
*/
|
*/
|
||||||
private DefaultMcpSession.RequestHandler setLoggerRequestHandler() {
|
private DefaultMcpSession.RequestHandler<Void> setLoggerRequestHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
McpSchema.LoggingLevel setLoggerRequest = transport.unmarshalFrom(params,
|
this.minLoggingLevel = transport.unmarshalFrom(params, new TypeReference<LoggingLevel>() {
|
||||||
new TypeReference<McpSchema.LoggingLevel>() {
|
});
|
||||||
});
|
|
||||||
|
|
||||||
this.minLoggingLevel = setLoggerRequest;
|
|
||||||
|
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
};
|
};
|
||||||
|
|
@ -615,7 +842,7 @@ public class McpAsyncServer {
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Sampling
|
// Sampling
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
private static TypeReference<McpSchema.CreateMessageResult> CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() {
|
private static final TypeReference<McpSchema.CreateMessageResult> CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,399 @@
|
||||||
|
package org.springframework.ai.mcp.server;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
import org.springframework.ai.mcp.util.Assert;
|
||||||
|
import org.springframework.ai.mcp.util.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP server features specification that a particular server can choose to support.
|
||||||
|
*/
|
||||||
|
public class McpServerFeatures {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous server features specification.
|
||||||
|
*
|
||||||
|
* @param serverInfo The server implementation details
|
||||||
|
* @param serverCapabilities The server capabilities
|
||||||
|
* @param tools The list of tool registrations
|
||||||
|
* @param resources The map of resource registrations
|
||||||
|
* @param resourceTemplates The list of resource templates
|
||||||
|
* @param prompts The map of prompt registrations
|
||||||
|
* @param rootsChangeConsumers The list of consumers that will be notified when the
|
||||||
|
* roots list changes
|
||||||
|
*/
|
||||||
|
record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
|
||||||
|
List<McpServerFeatures.AsyncToolRegistration> tools, Map<String, AsyncResourceRegistration> resources,
|
||||||
|
List<McpSchema.ResourceTemplate> resourceTemplates,
|
||||||
|
Map<String, McpServerFeatures.AsyncPromptRegistration> prompts,
|
||||||
|
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance and validate the arguments.
|
||||||
|
* @param serverInfo The server implementation details
|
||||||
|
* @param serverCapabilities The server capabilities
|
||||||
|
* @param tools The list of tool registrations
|
||||||
|
* @param resources The map of resource registrations
|
||||||
|
* @param resourceTemplates The list of resource templates
|
||||||
|
* @param prompts The map of prompt registrations
|
||||||
|
* @param rootsChangeConsumers The list of consumers that will be notified when
|
||||||
|
* the roots list changes
|
||||||
|
*/
|
||||||
|
Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
|
||||||
|
List<McpServerFeatures.AsyncToolRegistration> tools, Map<String, AsyncResourceRegistration> resources,
|
||||||
|
List<McpSchema.ResourceTemplate> resourceTemplates,
|
||||||
|
Map<String, McpServerFeatures.AsyncPromptRegistration> prompts,
|
||||||
|
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
|
||||||
|
|
||||||
|
Assert.notNull(serverInfo, "Server info must not be null");
|
||||||
|
|
||||||
|
this.serverInfo = serverInfo;
|
||||||
|
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities
|
||||||
|
: new McpSchema.ServerCapabilities(null, // experimental
|
||||||
|
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable
|
||||||
|
// logging
|
||||||
|
// by
|
||||||
|
// default
|
||||||
|
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
|
||||||
|
!Utils.isEmpty(resources)
|
||||||
|
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
|
||||||
|
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
|
||||||
|
|
||||||
|
this.tools = (tools != null) ? tools : List.of();
|
||||||
|
this.resources = (resources != null) ? resources : Map.of();
|
||||||
|
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : List.of();
|
||||||
|
this.prompts = (prompts != null) ? prompts : Map.of();
|
||||||
|
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a synchronous specification into an asynchronous one and provide
|
||||||
|
* blocking code offloading to prevent accidental blocking of the non-blocking
|
||||||
|
* transport.
|
||||||
|
* @param syncSpec a potentially blocking, synchronous specification.
|
||||||
|
* @return a specification which is protected from blocking calls specified by the
|
||||||
|
* user.
|
||||||
|
*/
|
||||||
|
static Async fromSync(Sync syncSpec) {
|
||||||
|
List<McpServerFeatures.AsyncToolRegistration> tools = new ArrayList<>();
|
||||||
|
for (var tool : syncSpec.tools()) {
|
||||||
|
tools.add(AsyncToolRegistration.fromSync(tool));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, AsyncResourceRegistration> resources = new HashMap<>();
|
||||||
|
syncSpec.resources().forEach((key, resource) -> {
|
||||||
|
resources.put(key, AsyncResourceRegistration.fromSync(resource));
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, AsyncPromptRegistration> prompts = new HashMap<>();
|
||||||
|
syncSpec.prompts().forEach((key, prompt) -> {
|
||||||
|
prompts.put(key, AsyncPromptRegistration.fromSync(prompt));
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Function<List<McpSchema.Root>, Mono<Void>>> rootChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
for (var rootChangeConsumer : syncSpec.rootsChangeConsumers()) {
|
||||||
|
rootChangeConsumers.add(list -> Mono.<Void>fromRunnable(() -> rootChangeConsumer.accept(list))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, resources,
|
||||||
|
syncSpec.resourceTemplates(), prompts, rootChangeConsumers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous server features specification.
|
||||||
|
*
|
||||||
|
* @param serverInfo The server implementation details
|
||||||
|
* @param serverCapabilities The server capabilities
|
||||||
|
* @param tools The list of tool registrations
|
||||||
|
* @param resources The map of resource registrations
|
||||||
|
* @param resourceTemplates The list of resource templates
|
||||||
|
* @param prompts The map of prompt registrations
|
||||||
|
* @param rootsChangeConsumers The list of consumers that will be notified when the
|
||||||
|
* roots list changes
|
||||||
|
*/
|
||||||
|
record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
|
||||||
|
List<McpServerFeatures.SyncToolRegistration> tools,
|
||||||
|
Map<String, McpServerFeatures.SyncResourceRegistration> resources,
|
||||||
|
List<McpSchema.ResourceTemplate> resourceTemplates,
|
||||||
|
Map<String, McpServerFeatures.SyncPromptRegistration> prompts,
|
||||||
|
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance and validate the arguments.
|
||||||
|
* @param serverInfo The server implementation details
|
||||||
|
* @param serverCapabilities The server capabilities
|
||||||
|
* @param tools The list of tool registrations
|
||||||
|
* @param resources The map of resource registrations
|
||||||
|
* @param resourceTemplates The list of resource templates
|
||||||
|
* @param prompts The map of prompt registrations
|
||||||
|
* @param rootsChangeConsumers The list of consumers that will be notified when
|
||||||
|
* the roots list changes
|
||||||
|
*/
|
||||||
|
Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
|
||||||
|
List<McpServerFeatures.SyncToolRegistration> tools,
|
||||||
|
Map<String, McpServerFeatures.SyncResourceRegistration> resources,
|
||||||
|
List<McpSchema.ResourceTemplate> resourceTemplates,
|
||||||
|
Map<String, McpServerFeatures.SyncPromptRegistration> prompts,
|
||||||
|
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
||||||
|
|
||||||
|
Assert.notNull(serverInfo, "Server info must not be null");
|
||||||
|
|
||||||
|
this.serverInfo = serverInfo;
|
||||||
|
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities
|
||||||
|
: new McpSchema.ServerCapabilities(null, // experimental
|
||||||
|
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable
|
||||||
|
// logging
|
||||||
|
// by
|
||||||
|
// default
|
||||||
|
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
|
||||||
|
!Utils.isEmpty(resources)
|
||||||
|
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
|
||||||
|
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
|
||||||
|
|
||||||
|
this.tools = (tools != null) ? tools : new ArrayList<>();
|
||||||
|
this.resources = (resources != null) ? resources : new HashMap<>();
|
||||||
|
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : new ArrayList<>();
|
||||||
|
this.prompts = (prompts != null) ? prompts : new HashMap<>();
|
||||||
|
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration of a tool with its asynchronous handler function. Tools are the
|
||||||
|
* primary way for MCP servers to expose functionality to AI models. Each tool
|
||||||
|
* represents a specific capability, such as:
|
||||||
|
* <ul>
|
||||||
|
* <li>Performing calculations
|
||||||
|
* <li>Accessing external APIs
|
||||||
|
* <li>Querying databases
|
||||||
|
* <li>Manipulating files
|
||||||
|
* <li>Executing system commands
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example tool registration: <pre>{@code
|
||||||
|
* new McpServerFeatures.AsyncToolRegistration(
|
||||||
|
* new Tool(
|
||||||
|
* "calculator",
|
||||||
|
* "Performs mathematical calculations",
|
||||||
|
* new JsonSchemaObject()
|
||||||
|
* .required("expression")
|
||||||
|
* .property("expression", JsonSchemaType.STRING)
|
||||||
|
* ),
|
||||||
|
* args -> {
|
||||||
|
* String expr = (String) args.get("expression");
|
||||||
|
* return Mono.just(new CallToolResult("Result: " + evaluate(expr)));
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param tool The tool definition including name, description, and parameter schema
|
||||||
|
* @param call The function that implements the tool's logic, receiving arguments and
|
||||||
|
* returning results
|
||||||
|
*/
|
||||||
|
public record AsyncToolRegistration(McpSchema.Tool tool,
|
||||||
|
Function<Map<String, Object>, Mono<McpSchema.CallToolResult>> call) {
|
||||||
|
|
||||||
|
static AsyncToolRegistration fromSync(SyncToolRegistration tool) {
|
||||||
|
// FIXME: This is temporary, proper validation should be implemented
|
||||||
|
if (tool == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new AsyncToolRegistration(tool.tool(),
|
||||||
|
map -> Mono.fromCallable(() -> tool.call().apply(map)).subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration of a resource with its asynchronous handler function. Resources
|
||||||
|
* provide context to AI models by exposing data such as:
|
||||||
|
* <ul>
|
||||||
|
* <li>File contents
|
||||||
|
* <li>Database records
|
||||||
|
* <li>API responses
|
||||||
|
* <li>System information
|
||||||
|
* <li>Application state
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example resource registration: <pre>{@code
|
||||||
|
* new McpServerFeatures.AsyncResourceRegistration(
|
||||||
|
* new Resource("docs", "Documentation files", "text/markdown"),
|
||||||
|
* request -> {
|
||||||
|
* String content = readFile(request.getPath());
|
||||||
|
* return Mono.just(new ReadResourceResult(content));
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param resource The resource definition including name, description, and MIME type
|
||||||
|
* @param readHandler The function that handles resource read requests
|
||||||
|
*/
|
||||||
|
public record AsyncResourceRegistration(McpSchema.Resource resource,
|
||||||
|
Function<McpSchema.ReadResourceRequest, Mono<McpSchema.ReadResourceResult>> readHandler) {
|
||||||
|
|
||||||
|
static AsyncResourceRegistration fromSync(SyncResourceRegistration resource) {
|
||||||
|
// FIXME: This is temporary, proper validation should be implemented
|
||||||
|
if (resource == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new AsyncResourceRegistration(resource.resource(),
|
||||||
|
req -> Mono.fromCallable(() -> resource.readHandler().apply(req))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration of a prompt template with its asynchronous handler function. Prompts
|
||||||
|
* provide structured templates for AI model interactions, supporting:
|
||||||
|
* <ul>
|
||||||
|
* <li>Consistent message formatting
|
||||||
|
* <li>Parameter substitution
|
||||||
|
* <li>Context injection
|
||||||
|
* <li>Response formatting
|
||||||
|
* <li>Instruction templating
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example prompt registration: <pre>{@code
|
||||||
|
* new McpServerFeatures.AsyncPromptRegistration(
|
||||||
|
* new Prompt("analyze", "Code analysis template"),
|
||||||
|
* request -> {
|
||||||
|
* String code = request.getArguments().get("code");
|
||||||
|
* return Mono.just(new GetPromptResult(
|
||||||
|
* "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
|
||||||
|
* ));
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param prompt The prompt definition including name and description
|
||||||
|
* @param promptHandler The function that processes prompt requests and returns
|
||||||
|
* formatted templates
|
||||||
|
*/
|
||||||
|
public record AsyncPromptRegistration(McpSchema.Prompt prompt,
|
||||||
|
Function<McpSchema.GetPromptRequest, Mono<McpSchema.GetPromptResult>> promptHandler) {
|
||||||
|
|
||||||
|
static AsyncPromptRegistration fromSync(SyncPromptRegistration prompt) {
|
||||||
|
// FIXME: This is temporary, proper validation should be implemented
|
||||||
|
if (prompt == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new AsyncPromptRegistration(prompt.prompt(),
|
||||||
|
req -> Mono.fromCallable(() -> prompt.promptHandler().apply(req))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration of a tool with its synchronous handler function. Tools are the primary
|
||||||
|
* way for MCP servers to expose functionality to AI models. Each tool represents a
|
||||||
|
* specific capability, such as:
|
||||||
|
* <ul>
|
||||||
|
* <li>Performing calculations
|
||||||
|
* <li>Accessing external APIs
|
||||||
|
* <li>Querying databases
|
||||||
|
* <li>Manipulating files
|
||||||
|
* <li>Executing system commands
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example tool registration: <pre>{@code
|
||||||
|
* new McpServerFeatures.SyncToolRegistration(
|
||||||
|
* new Tool(
|
||||||
|
* "calculator",
|
||||||
|
* "Performs mathematical calculations",
|
||||||
|
* new JsonSchemaObject()
|
||||||
|
* .required("expression")
|
||||||
|
* .property("expression", JsonSchemaType.STRING)
|
||||||
|
* ),
|
||||||
|
* args -> {
|
||||||
|
* String expr = (String) args.get("expression");
|
||||||
|
* return new CallToolResult("Result: " + evaluate(expr));
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param tool The tool definition including name, description, and parameter schema
|
||||||
|
* @param call The function that implements the tool's logic, receiving arguments and
|
||||||
|
* returning results
|
||||||
|
*/
|
||||||
|
public record SyncToolRegistration(McpSchema.Tool tool,
|
||||||
|
Function<Map<String, Object>, McpSchema.CallToolResult> call) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration of a resource with its synchronous handler function. Resources provide
|
||||||
|
* context to AI models by exposing data such as:
|
||||||
|
* <ul>
|
||||||
|
* <li>File contents
|
||||||
|
* <li>Database records
|
||||||
|
* <li>API responses
|
||||||
|
* <li>System information
|
||||||
|
* <li>Application state
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example resource registration: <pre>{@code
|
||||||
|
* new McpServerFeatures.SyncResourceRegistration(
|
||||||
|
* new Resource("docs", "Documentation files", "text/markdown"),
|
||||||
|
* request -> {
|
||||||
|
* String content = readFile(request.getPath());
|
||||||
|
* return new ReadResourceResult(content);
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param resource The resource definition including name, description, and MIME type
|
||||||
|
* @param readHandler The function that handles resource read requests
|
||||||
|
*/
|
||||||
|
public record SyncResourceRegistration(McpSchema.Resource resource,
|
||||||
|
Function<McpSchema.ReadResourceRequest, McpSchema.ReadResourceResult> readHandler) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration of a prompt template with its synchronous handler function. Prompts
|
||||||
|
* provide structured templates for AI model interactions, supporting:
|
||||||
|
* <ul>
|
||||||
|
* <li>Consistent message formatting
|
||||||
|
* <li>Parameter substitution
|
||||||
|
* <li>Context injection
|
||||||
|
* <li>Response formatting
|
||||||
|
* <li>Instruction templating
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example prompt registration: <pre>{@code
|
||||||
|
* new McpServerFeatures.SyncPromptRegistration(
|
||||||
|
* new Prompt("analyze", "Code analysis template"),
|
||||||
|
* request -> {
|
||||||
|
* String code = request.getArguments().get("code");
|
||||||
|
* return new GetPromptResult(
|
||||||
|
* "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param prompt The prompt definition including name and description
|
||||||
|
* @param promptHandler The function that processes prompt requests and returns
|
||||||
|
* formatted templates
|
||||||
|
*/
|
||||||
|
public record SyncPromptRegistration(McpSchema.Prompt prompt,
|
||||||
|
Function<McpSchema.GetPromptRequest, McpSchema.GetPromptResult> promptHandler) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -26,11 +26,42 @@ import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
|
||||||
import org.springframework.ai.mcp.util.Assert;
|
import org.springframework.ai.mcp.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronous wrapper around {@link McpAsyncServer} that provides blocking operations.
|
* A synchronous implementation of the Model Context Protocol (MCP) server that wraps
|
||||||
* This class delegates all operations to an underlying async server instance while
|
* {@link McpAsyncServer} to provide blocking operations. This class delegates all
|
||||||
* providing a synchronous API.
|
* operations to an underlying async server instance while providing a simpler,
|
||||||
|
* synchronous API for scenarios where reactive programming is not required.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The MCP server enables AI models to expose tools, resources, and prompts through a
|
||||||
|
* standardized interface. Key features available through this synchronous API include:
|
||||||
|
* <ul>
|
||||||
|
* <li>Tool registration and management for extending AI model capabilities
|
||||||
|
* <li>Resource handling with URI-based addressing for providing context
|
||||||
|
* <li>Prompt template management for standardized interactions
|
||||||
|
* <li>Real-time client notifications for state changes
|
||||||
|
* <li>Structured logging with configurable severity levels
|
||||||
|
* <li>Support for client-side AI model sampling
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* While {@link McpAsyncServer} uses Project Reactor's Mono and Flux types for
|
||||||
|
* non-blocking operations, this class converts those into blocking calls, making it more
|
||||||
|
* suitable for:
|
||||||
|
* <ul>
|
||||||
|
* <li>Traditional synchronous applications
|
||||||
|
* <li>Simple scripting scenarios
|
||||||
|
* <li>Testing and debugging
|
||||||
|
* <li>Cases where reactive programming adds unnecessary complexity
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The server supports runtime modification of its capabilities through methods like
|
||||||
|
* {@link #addTool}, {@link #addResource}, and {@link #addPrompt}, automatically notifying
|
||||||
|
* connected clients of changes when configured to do so.
|
||||||
*
|
*
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
|
* @see McpAsyncServer
|
||||||
|
* @see McpSchema
|
||||||
*/
|
*/
|
||||||
public class McpSyncServer {
|
public class McpSyncServer {
|
||||||
|
|
||||||
|
|
@ -69,6 +100,16 @@ public class McpSyncServer {
|
||||||
* Add a new tool handler.
|
* Add a new tool handler.
|
||||||
* @param toolHandler The tool handler to add
|
* @param toolHandler The tool handler to add
|
||||||
*/
|
*/
|
||||||
|
public void addTool(McpServerFeatures.SyncToolRegistration toolHandler) {
|
||||||
|
this.asyncServer.addTool(McpServerFeatures.AsyncToolRegistration.fromSync(toolHandler)).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new tool handler.
|
||||||
|
* @param toolHandler The tool handler to add
|
||||||
|
* @deprecated Use {@link #addTool(McpServerFeatures.SyncToolRegistration)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public void addTool(ToolRegistration toolHandler) {
|
public void addTool(ToolRegistration toolHandler) {
|
||||||
this.asyncServer.addTool(toolHandler).block();
|
this.asyncServer.addTool(toolHandler).block();
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +126,16 @@ public class McpSyncServer {
|
||||||
* Add a new resource handler.
|
* Add a new resource handler.
|
||||||
* @param resourceHandler The resource handler to add
|
* @param resourceHandler The resource handler to add
|
||||||
*/
|
*/
|
||||||
|
public void addResource(McpServerFeatures.SyncResourceRegistration resourceHandler) {
|
||||||
|
this.asyncServer.addResource(McpServerFeatures.AsyncResourceRegistration.fromSync(resourceHandler)).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new resource handler.
|
||||||
|
* @param resourceHandler The resource handler to add
|
||||||
|
* @deprecated Use {@link #addResource(McpServerFeatures.SyncResourceRegistration)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public void addResource(ResourceRegistration resourceHandler) {
|
public void addResource(ResourceRegistration resourceHandler) {
|
||||||
this.asyncServer.addResource(resourceHandler).block();
|
this.asyncServer.addResource(resourceHandler).block();
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +152,16 @@ public class McpSyncServer {
|
||||||
* Add a new prompt handler.
|
* Add a new prompt handler.
|
||||||
* @param promptRegistration The prompt registration to add
|
* @param promptRegistration The prompt registration to add
|
||||||
*/
|
*/
|
||||||
|
public void addPrompt(McpServerFeatures.SyncPromptRegistration promptRegistration) {
|
||||||
|
this.asyncServer.addPrompt(McpServerFeatures.AsyncPromptRegistration.fromSync(promptRegistration)).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new prompt handler.
|
||||||
|
* @param promptRegistration The prompt registration to add
|
||||||
|
* @deprecated Use {@link #addPrompt(McpServerFeatures.SyncPromptRegistration)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public void addPrompt(PromptRegistration promptRegistration) {
|
public void addPrompt(PromptRegistration promptRegistration) {
|
||||||
this.asyncServer.addPrompt(promptRegistration).block();
|
this.asyncServer.addPrompt(promptRegistration).block();
|
||||||
}
|
}
|
||||||
|
|
@ -120,28 +181,6 @@ public class McpSyncServer {
|
||||||
this.asyncServer.notifyToolsListChanged().block();
|
this.asyncServer.notifyToolsListChanged().block();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify clients that the list of available resources has changed.
|
|
||||||
*/
|
|
||||||
public void notifyResourcesListChanged() {
|
|
||||||
this.asyncServer.notifyResourcesListChanged().block();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify clients that the list of available prompts has changed.
|
|
||||||
*/
|
|
||||||
public void notifyPromptsListChanged() {
|
|
||||||
this.asyncServer.notifyPromptsListChanged().block();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a logging message notification to all clients.
|
|
||||||
* @param loggingMessageNotification The logging message notification to send
|
|
||||||
*/
|
|
||||||
public void loggingNotification(LoggingMessageNotification loggingMessageNotification) {
|
|
||||||
this.asyncServer.loggingNotification(loggingMessageNotification).block();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the server capabilities that define the supported features and functionality.
|
* Get the server capabilities that define the supported features and functionality.
|
||||||
* @return The server capabilities
|
* @return The server capabilities
|
||||||
|
|
@ -174,6 +213,28 @@ public class McpSyncServer {
|
||||||
return this.asyncServer.getClientInfo();
|
return this.asyncServer.getClientInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify clients that the list of available resources has changed.
|
||||||
|
*/
|
||||||
|
public void notifyResourcesListChanged() {
|
||||||
|
this.asyncServer.notifyResourcesListChanged().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify clients that the list of available prompts has changed.
|
||||||
|
*/
|
||||||
|
public void notifyPromptsListChanged() {
|
||||||
|
this.asyncServer.notifyPromptsListChanged().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a logging message notification to all clients.
|
||||||
|
* @param loggingMessageNotification The logging message notification to send
|
||||||
|
*/
|
||||||
|
public void loggingNotification(LoggingMessageNotification loggingMessageNotification) {
|
||||||
|
this.asyncServer.loggingNotification(loggingMessageNotification).block();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the server gracefully.
|
* Close the server gracefully.
|
||||||
*/
|
*/
|
||||||
|
|
@ -199,13 +260,19 @@ public class McpSyncServer {
|
||||||
/**
|
/**
|
||||||
* Create a new message using the sampling capabilities of the client. The Model
|
* Create a new message using the sampling capabilities of the client. The Model
|
||||||
* Context Protocol (MCP) provides a standardized way for servers to request LLM
|
* Context Protocol (MCP) provides a standardized way for servers to request LLM
|
||||||
* sampling (“completions” or “generations”) from language models via clients. This
|
* sampling ("completions" or "generations") from language models via clients.
|
||||||
* flow allows clients to maintain control over model access, selection, and
|
*
|
||||||
|
* <p>
|
||||||
|
* This flow allows clients to maintain control over model access, selection, and
|
||||||
* permissions while enabling servers to leverage AI capabilities—with no server API
|
* permissions while enabling servers to leverage AI capabilities—with no server API
|
||||||
* keys necessary. Servers can request text or image-based interactions and optionally
|
* keys necessary. Servers can request text or image-based interactions and optionally
|
||||||
* include context from MCP servers in their prompts.
|
* include context from MCP servers in their prompts.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Unlike its async counterpart, this method blocks until the message creation is
|
||||||
|
* complete, making it easier to use in synchronous code paths.
|
||||||
* @param createMessageRequest The request to create a new message
|
* @param createMessageRequest The request to create a new message
|
||||||
* @return A Mono that completes when the message has been created
|
* @return The result of the message creation
|
||||||
* @throws McpError if the client has not been initialized or does not support
|
* @throws McpError if the client has not been initialized or does not support
|
||||||
* sampling capabilities
|
* sampling capabilities
|
||||||
* @throws McpError if the client does not support the createMessage method
|
* @throws McpError if the client does not support the createMessage method
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ public class DefaultMcpSession implements McpSession {
|
||||||
private final ConcurrentHashMap<Object, MonoSink<McpSchema.JSONRPCResponse>> pendingResponses = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Object, MonoSink<McpSchema.JSONRPCResponse>> pendingResponses = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/** Map of request handlers keyed by method name */
|
/** Map of request handlers keyed by method name */
|
||||||
private final ConcurrentHashMap<String, RequestHandler> requestHandlers = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, RequestHandler<?>> requestHandlers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/** Map of notification handlers keyed by method name */
|
/** Map of notification handlers keyed by method name */
|
||||||
private final ConcurrentHashMap<String, NotificationHandler> notificationHandlers = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, NotificationHandler> notificationHandlers = new ConcurrentHashMap<>();
|
||||||
|
|
@ -79,16 +79,18 @@ public class DefaultMcpSession implements McpSession {
|
||||||
/**
|
/**
|
||||||
* Functional interface for handling incoming JSON-RPC requests. Implementations
|
* Functional interface for handling incoming JSON-RPC requests. Implementations
|
||||||
* should process the request parameters and return a response.
|
* should process the request parameters and return a response.
|
||||||
|
*
|
||||||
|
* @param <T> Response type
|
||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface RequestHandler {
|
public interface RequestHandler<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles an incoming request with the given parameters.
|
* Handles an incoming request with the given parameters.
|
||||||
* @param params The request parameters
|
* @param params The request parameters
|
||||||
* @return A Mono containing the response object
|
* @return A Mono containing the response object
|
||||||
*/
|
*/
|
||||||
Mono<Object> handle(Object params);
|
Mono<T> handle(Object params);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,7 +118,7 @@ public class DefaultMcpSession implements McpSession {
|
||||||
* @param notificationHandlers Map of method names to notification handlers
|
* @param notificationHandlers Map of method names to notification handlers
|
||||||
*/
|
*/
|
||||||
public DefaultMcpSession(Duration requestTimeout, McpTransport transport,
|
public DefaultMcpSession(Duration requestTimeout, McpTransport transport,
|
||||||
Map<String, RequestHandler> requestHandlers, Map<String, NotificationHandler> notificationHandlers) {
|
Map<String, RequestHandler<?>> requestHandlers, Map<String, NotificationHandler> notificationHandlers) {
|
||||||
|
|
||||||
Assert.notNull(requestTimeout, "The requstTimeout can not be null");
|
Assert.notNull(requestTimeout, "The requstTimeout can not be null");
|
||||||
Assert.notNull(transport, "The transport can not be null");
|
Assert.notNull(transport, "The transport can not be null");
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
||||||
|
|
@ -77,10 +78,10 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
this.mcpTransport = createMcpTransport();
|
this.mcpTransport = createMcpTransport();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
mcpAsyncClient = McpClient.using(mcpTransport)
|
mcpAsyncClient = McpClient.async(mcpTransport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.async();
|
.build();
|
||||||
mcpAsyncClient.initialize().block(Duration.ofSeconds(10));
|
mcpAsyncClient.initialize().block(Duration.ofSeconds(10));
|
||||||
}).doesNotThrowAnyException();
|
}).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
@ -96,10 +97,10 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstructorWithInvalidArguments() {
|
void testConstructorWithInvalidArguments() {
|
||||||
assertThatThrownBy(() -> McpClient.using(null).sync()).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpClient.using(mcpTransport).requestTimeout(null).sync())
|
assertThatThrownBy(() -> McpClient.sync(mcpTransport).requestTimeout(null).build())
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Request timeout must not be null");
|
.hasMessage("Request timeout must not be null");
|
||||||
}
|
}
|
||||||
|
|
@ -195,10 +196,10 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
void testInitializeWithRootsListProviders() {
|
void testInitializeWithRootsListProviders() {
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
|
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.async(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.roots(new Root("file:///test/path", "test-root"))
|
.roots(new Root("file:///test/path", "test-root"))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> client.initialize().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
assertThatCode(() -> client.initialize().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -277,12 +278,12 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||||
|
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.async(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true)))
|
||||||
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
.resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true)))
|
||||||
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true)))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize().block();
|
client.initialize().block();
|
||||||
|
|
@ -299,9 +300,11 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
var capabilities = ClientCapabilities.builder().sampling().build();
|
var capabilities = ClientCapabilities.builder().sampling().build();
|
||||||
|
|
||||||
var client = McpClient.using(transport).requestTimeout(TIMEOUT).capabilities(capabilities).sampling(request -> {
|
var client = McpClient.async(transport)
|
||||||
return CreateMessageResult.builder().message("test").model("test-model").build();
|
.requestTimeout(TIMEOUT)
|
||||||
}).async();
|
.capabilities(capabilities)
|
||||||
|
.sampling(request -> Mono.just(CreateMessageResult.builder().message("test").model("test-model").build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize().block(Duration.ofSeconds(10));
|
client.initialize().block(Duration.ofSeconds(10));
|
||||||
|
|
@ -319,14 +322,13 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
.sampling()
|
.sampling()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
||||||
return CreateMessageResult.builder().message("test").model("test-model").build();
|
.just(CreateMessageResult.builder().message("test").model("test-model").build());
|
||||||
};
|
var client = McpClient.async(transport)
|
||||||
var client = McpClient.using(transport)
|
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.capabilities(capabilities)
|
.capabilities(capabilities)
|
||||||
.sampling(samplingHandler)
|
.sampling(samplingHandler)
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
var result = client.initialize().block(Duration.ofSeconds(10));
|
var result = client.initialize().block(Duration.ofSeconds(10));
|
||||||
|
|
@ -353,10 +355,10 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
|
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.async(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.loggingConsumer(notification -> logReceived.set(true))
|
.loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true)))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize().block(Duration.ofSeconds(10));
|
client.initialize().block(Duration.ofSeconds(10));
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,10 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
this.mcpTransport = createMcpTransport();
|
this.mcpTransport = createMcpTransport();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
mcpSyncClient = McpClient.using(mcpTransport)
|
mcpSyncClient = McpClient.sync(mcpTransport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.sync();
|
.build();
|
||||||
mcpSyncClient.initialize();
|
mcpSyncClient.initialize();
|
||||||
}).doesNotThrowAnyException();
|
}).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
@ -90,10 +90,10 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstructorWithInvalidArguments() {
|
void testConstructorWithInvalidArguments() {
|
||||||
assertThatThrownBy(() -> McpClient.using(null).sync()).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpClient.using(mcpTransport).requestTimeout(null).sync())
|
assertThatThrownBy(() -> McpClient.sync(mcpTransport).requestTimeout(null).build())
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Request timeout must not be null");
|
.hasMessage("Request timeout must not be null");
|
||||||
}
|
}
|
||||||
|
|
@ -180,10 +180,10 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
void testInitializeWithRootsListProviders() {
|
void testInitializeWithRootsListProviders() {
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
|
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.sync(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.roots(new Root("file:///test/path", "test-root"))
|
.roots(new Root("file:///test/path", "test-root"))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize();
|
client.initialize();
|
||||||
|
|
@ -262,12 +262,12 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||||
|
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.sync(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
||||||
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
||||||
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize();
|
client.initialize();
|
||||||
|
|
@ -295,10 +295,10 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||||
var transport = createMcpTransport();
|
var transport = createMcpTransport();
|
||||||
|
|
||||||
var client = McpClient.using(transport)
|
var client = McpClient.sync(transport)
|
||||||
.requestTimeout(TIMEOUT)
|
.requestTimeout(TIMEOUT)
|
||||||
.loggingConsumer(notification -> logReceived.set(true))
|
.loggingConsumer(notification -> logReceived.set(true))
|
||||||
.sync();
|
.build();
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize();
|
client.initialize();
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.ai.mcp.MockMcpTransport;
|
import org.springframework.ai.mcp.MockMcpTransport;
|
||||||
import org.springframework.ai.mcp.spec.McpError;
|
import org.springframework.ai.mcp.spec.McpError;
|
||||||
|
|
@ -48,12 +49,11 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
List<McpSchema.Tool> receivedTools = new ArrayList<>();
|
List<McpSchema.Tool> receivedTools = new ArrayList<>();
|
||||||
|
|
||||||
// Create a consumer that will be called when tools change
|
// Create a consumer that will be called when tools change
|
||||||
Consumer<List<McpSchema.Tool>> toolsChangeConsumer = tools -> {
|
Function<List<McpSchema.Tool>, Mono<Void>> toolsChangeConsumer = tools -> Mono
|
||||||
receivedTools.addAll(tools);
|
.fromRunnable(() -> receivedTools.addAll(tools));
|
||||||
};
|
|
||||||
|
|
||||||
// Create client with tools change consumer
|
// 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
|
// Create a mock tools list that the server will return
|
||||||
Map<String, Object> inputSchema = Map.of("type", "object", "properties", Map.of(), "required", List.of());
|
Map<String, Object> inputSchema = Map.of("type", "object", "properties", Map.of(), "required", List.of());
|
||||||
|
|
@ -88,9 +88,9 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
void testRootsListRequestHandling() {
|
void testRootsListRequestHandling() {
|
||||||
MockMcpTransport transport = new MockMcpTransport();
|
MockMcpTransport transport = new MockMcpTransport();
|
||||||
|
|
||||||
McpAsyncClient asyncMcpClient = McpClient.using(transport)
|
McpAsyncClient asyncMcpClient = McpClient.async(transport)
|
||||||
.roots(new Root("file:///test/path", "test-root"))
|
.roots(new Root("file:///test/path", "test-root"))
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
// Simulate incoming request
|
// Simulate incoming request
|
||||||
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
|
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
|
||||||
|
|
@ -118,14 +118,13 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
List<McpSchema.Resource> receivedResources = new ArrayList<>();
|
List<McpSchema.Resource> receivedResources = new ArrayList<>();
|
||||||
|
|
||||||
// Create a consumer that will be called when resources change
|
// Create a consumer that will be called when resources change
|
||||||
Consumer<List<McpSchema.Resource>> resourcesChangeConsumer = resources -> {
|
Function<List<McpSchema.Resource>, Mono<Void>> resourcesChangeConsumer = resources -> Mono
|
||||||
receivedResources.addAll(resources);
|
.fromRunnable(() -> receivedResources.addAll(resources));
|
||||||
};
|
|
||||||
|
|
||||||
// Create client with resources change consumer
|
// Create client with resources change consumer
|
||||||
McpAsyncClient asyncMcpClient = McpClient.using(transport)
|
McpAsyncClient asyncMcpClient = McpClient.async(transport)
|
||||||
.resourcesChangeConsumer(resourcesChangeConsumer)
|
.resourcesChangeConsumer(resourcesChangeConsumer)
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
// Create a mock resources list that the server will return
|
// Create a mock resources list that the server will return
|
||||||
McpSchema.Resource mockResource = new McpSchema.Resource("test://resource", "Test Resource", "A test resource",
|
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<>();
|
List<McpSchema.Prompt> receivedPrompts = new ArrayList<>();
|
||||||
|
|
||||||
// Create a consumer that will be called when prompts change
|
// Create a consumer that will be called when prompts change
|
||||||
Consumer<List<McpSchema.Prompt>> promptsChangeConsumer = prompts -> {
|
Function<List<McpSchema.Prompt>, Mono<Void>> promptsChangeConsumer = prompts -> Mono
|
||||||
receivedPrompts.addAll(prompts);
|
.fromRunnable(() -> receivedPrompts.addAll(prompts));
|
||||||
};
|
|
||||||
|
|
||||||
// Create client with prompts change consumer
|
// 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
|
// Create a mock prompts list that the server will return
|
||||||
McpSchema.Prompt mockPrompt = new McpSchema.Prompt("test-prompt", "Test Prompt Description",
|
McpSchema.Prompt mockPrompt = new McpSchema.Prompt("test-prompt", "Test Prompt Description",
|
||||||
|
|
@ -207,17 +205,17 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
MockMcpTransport transport = new MockMcpTransport();
|
MockMcpTransport transport = new MockMcpTransport();
|
||||||
|
|
||||||
// Create a test sampling handler that echoes back the input
|
// 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();
|
var content = request.messages().get(0).content();
|
||||||
return new McpSchema.CreateMessageResult(McpSchema.Role.ASSISTANT, content, "test-model",
|
return Mono.just(new McpSchema.CreateMessageResult(McpSchema.Role.ASSISTANT, content, "test-model",
|
||||||
McpSchema.CreateMessageResult.StopReason.END_TURN);
|
McpSchema.CreateMessageResult.StopReason.END_TURN));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create client with sampling capability and handler
|
// Create client with sampling capability and handler
|
||||||
McpAsyncClient asyncMcpClient = McpClient.using(transport)
|
McpAsyncClient asyncMcpClient = McpClient.async(transport)
|
||||||
.capabilities(ClientCapabilities.builder().sampling().build())
|
.capabilities(ClientCapabilities.builder().sampling().build())
|
||||||
.sampling(samplingHandler)
|
.sampling(samplingHandler)
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
// Create a mock create message request
|
// Create a mock create message request
|
||||||
var messageRequest = new McpSchema.CreateMessageRequest(
|
var messageRequest = new McpSchema.CreateMessageRequest(
|
||||||
|
|
@ -259,9 +257,9 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
MockMcpTransport transport = new MockMcpTransport();
|
MockMcpTransport transport = new MockMcpTransport();
|
||||||
|
|
||||||
// Create client without sampling capability
|
// Create client without sampling capability
|
||||||
McpAsyncClient asyncMcpClient = McpClient.using(transport)
|
McpAsyncClient asyncMcpClient = McpClient.async(transport)
|
||||||
.capabilities(ClientCapabilities.builder().build()) // No sampling capability
|
.capabilities(ClientCapabilities.builder().build()) // No sampling capability
|
||||||
.async();
|
.build();
|
||||||
|
|
||||||
// Create a mock create message request
|
// Create a mock create message request
|
||||||
var messageRequest = new McpSchema.CreateMessageRequest(
|
var messageRequest = new McpSchema.CreateMessageRequest(
|
||||||
|
|
@ -292,7 +290,7 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
|
|
||||||
// Create client with sampling capability but null handler
|
// Create client with sampling capability but null handler
|
||||||
assertThatThrownBy(
|
assertThatThrownBy(
|
||||||
() -> McpClient.using(transport).capabilities(ClientCapabilities.builder().sampling().build()).async())
|
() -> McpClient.async(transport).capabilities(ClientCapabilities.builder().sampling().build()).build())
|
||||||
.isInstanceOf(McpError.class)
|
.isInstanceOf(McpError.class)
|
||||||
.hasMessage("Sampling handler must not be null when client capabilities include sampling");
|
.hasMessage("Sampling handler must not be null when client capabilities include sampling");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue