refactor: Split transport implementations into modules
Split the transport implementations into dedicated modules to improve modularity and reduce core dependencies. Added shared test module for common test utilities. Key changes: - Create mcp-transport/mcp-webflux-sse-transport module - Move SseClientTransport → WebFluxSseClientTransport - Move SseServerTransport → WebFluxSseServerTransport - Move related tests and utilities - Create mcp-transport/mcp-webmvc-sse-transport module - Add new WebMvcSseServerTransport implementation - Add corresponding tests and integration tests - Add framework-independent SSE client implementations to core module - Add FlowSseClient using java.net.http - Add HttpClientSseClientTransport using java.net.http - Keep StdioClientTransport/StdioServerTransport in core (no external deps) - Create mcp-test module for shared test infrastructure - Move abstract test base classes - Move shared test utilities and mocks - Update test dependencies The modular approach allows users to choose between: - Framework-specific implementations (WebFlux/WebMVC) - Pure Java implementations (java.net.http) - Basic implementations (STDIO) Add breaking changes readme: 0.5.0-BREAKING-CHANGES.md Resolves #54
This commit is contained in:
parent
85c4fb8713
commit
611f3258d6
63 changed files with 5002 additions and 1588 deletions
105
0.5.0-BREAKING-CHANGES.md
Normal file
105
0.5.0-BREAKING-CHANGES.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Breaking Changes for Spring AI MCP 0.5.0-SNAPSHOT
|
||||
|
||||
## Major Changes
|
||||
|
||||
1. Transport Implementation Modularization
|
||||
- SSE transport implementations have been moved to dedicated modules:
|
||||
- `mcp-webflux-sse-transport`: WebFlux-based SSE transport
|
||||
- `mcp-webmvc-sse-transport`: WebMVC-based SSE transport
|
||||
- Base MCP module no longer includes transport implementations
|
||||
|
||||
2. Class Renames
|
||||
- `SseClientTransport` → `WebFluxSseClientTransport`
|
||||
- `SseServerTransport` → `WebFluxSseServerTransport`
|
||||
|
||||
3. Project Structure
|
||||
- Test utilities moved to new `mcp-test` module
|
||||
- Sample application moved to `samples/` directory
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### 1. Update Dependencies
|
||||
|
||||
If using SSE transport, add the appropriate transport module to your pom.xml:
|
||||
|
||||
For WebFlux SSE transport:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webflux-sse-transport</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
For WebMVC SSE transport:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webmvc-sse-transport</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 2. Update Transport Class References
|
||||
|
||||
Replace:
|
||||
```java
|
||||
import org.springframework.ai.mcp.client.transport.SseClientTransport;
|
||||
import org.springframework.ai.mcp.server.transport.SseServerTransport;
|
||||
```
|
||||
|
||||
With:
|
||||
```java
|
||||
// For WebFlux
|
||||
import org.springframework.ai.mcp.client.transport.WebFluxSseClientTransport;
|
||||
import org.springframework.ai.mcp.server.transport.WebFluxSseServerTransport;
|
||||
|
||||
// Or for WebMVC
|
||||
import org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport;
|
||||
```
|
||||
|
||||
### 3. Update Transport Instantiation
|
||||
|
||||
Replace:
|
||||
```java
|
||||
var transport = new SseClientTransport(webClientBuilder);
|
||||
```
|
||||
|
||||
With:
|
||||
```java
|
||||
var transport = new WebFluxSseClientTransport(webClientBuilder);
|
||||
```
|
||||
|
||||
Replace:
|
||||
```java
|
||||
var serverTransport = new SseServerTransport(objectMapper, messageEndpoint);
|
||||
```
|
||||
|
||||
With:
|
||||
```java
|
||||
// For WebFlux
|
||||
var serverTransport = new WebFluxSseServerTransport(objectMapper, messageEndpoint);
|
||||
|
||||
// Or for WebMVC
|
||||
var serverTransport = new WebMvcSseServerTransport(objectMapper, messageEndpoint);
|
||||
```
|
||||
|
||||
### 4. Test Dependencies
|
||||
|
||||
If you're using MCP test utilities, add the test module dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-test</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## Other Changes
|
||||
|
||||
- Spring AI dependency updated to 1.0.0-M5
|
||||
- Improved dependency management and reduced transitive dependencies
|
||||
- Sample application restructured under samples/ directory
|
||||
- New test utilities and base test classes in mcp-test module
|
||||
|
|
@ -68,7 +68,7 @@ Explore these MCP examples in the [spring-ai-examples/model-context-protocol](ht
|
|||
- [SQLite Chatbot](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/sqlite/chatbot) - Interactive chatbot with SQLite database interaction
|
||||
- [Filesystem](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/filesystem) - Enables LLM interaction with local filesystem folders and files
|
||||
- [Brave](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/brave) - Enables natural language interactions with Brave Search, allowing you to perform internet searches.
|
||||
- [Spring-ai-mcp-sample](./spring-ai-mcp-sample/) - Showcases how to create and use MCP servers and clients with different transport modes and capabilities.
|
||||
- [Spring-ai-mcp-sample](./samples/spring-ai-mcp-sample/) - Showcases how to create and use MCP servers and clients with different transport modes and capabilities.
|
||||
## Documentation
|
||||
|
||||
- [Java MCP SDK documentation](mcp/README.md)
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
# SSE Server Transport Implementation for MCP
|
||||
|
||||
## Overview
|
||||
|
||||
The `SseClientTransport` class implements the Model Context Protocol (MCP) HTTP with SSE transport specification, providing a bidirectional communication channel between clients and servers. This implementation is part of the Spring AI MCP Core library and follows the [MCP HTTP with SSE Transport Specification](https://spec.modelcontextprotocol.io/specification/basic/transports/#http-with-sse).
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Bidirectional Communication**:
|
||||
- Inbound: Receives messages through SSE connection
|
||||
- Outbound: Sends messages via HTTP POST requests
|
||||
|
||||
- **Error Handling**:
|
||||
- Automatic reconnection for transient failures
|
||||
- Graceful shutdown capabilities
|
||||
- Comprehensive error reporting
|
||||
|
||||
- **Message Flow**:
|
||||
1. Client establishes SSE connection to `/sse` endpoint
|
||||
2. Server sends 'endpoint' event with message URI
|
||||
3. Client uses provided URI for outbound messages
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Transport Architecture
|
||||
|
||||
The transport consists of two main processing pipelines:
|
||||
|
||||
1. **Inbound Processing**:
|
||||
- Establishes SSE connection to `/sse` endpoint
|
||||
- Handles two types of events:
|
||||
- `message`: JSON-RPC protocol messages
|
||||
- `endpoint`: Server-provided URI for outbound messages
|
||||
- Implements automatic retry logic for connection failures
|
||||
|
||||
2. **Outbound Processing**:
|
||||
- Sends messages via HTTP POST to server-provided endpoint
|
||||
- Uses dedicated scheduler for sequential message processing
|
||||
- Handles JSON serialization and HTTP communication
|
||||
|
||||
### Key Components
|
||||
|
||||
- **WebClient**: Handles HTTP communications for both SSE and POST requests
|
||||
- **ObjectMapper**: Manages JSON serialization/deserialization
|
||||
- **Scheduler**: Ensures sequential outbound message processing
|
||||
- **Message Endpoint Sink**: Manages server-provided endpoint URI
|
||||
|
||||
## Usage Example
|
||||
|
||||
```java
|
||||
// Create WebClient builder with base URL
|
||||
WebClient.Builder webClientBuilder = WebClient.builder()
|
||||
.baseUrl("http://localhost:3001");
|
||||
|
||||
// Initialize transport
|
||||
SseClientTransport transport = new SseClientTransport(webClientBuilder);
|
||||
|
||||
// Create a JSON-RPC request
|
||||
JSONRPCRequest request = new JSONRPCRequest(
|
||||
McpSchema.JSONRPC_VERSION,
|
||||
"method-name",
|
||||
"request-id",
|
||||
Map.of("key", "value")
|
||||
);
|
||||
|
||||
// Send message
|
||||
transport.sendMessage(request)
|
||||
.subscribe(
|
||||
success -> System.out.println("Message sent successfully"),
|
||||
error -> System.err.println("Error sending message: " + error)
|
||||
);
|
||||
|
||||
// Graceful shutdown when done
|
||||
transport.closeGracefully()
|
||||
.subscribe();
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Message Processing
|
||||
|
||||
1. **Inbound Messages**:
|
||||
- Received through SSE connection
|
||||
- Automatically deserialized from JSON
|
||||
- Routed to appropriate handlers based on event type
|
||||
|
||||
2. **Outbound Messages**:
|
||||
- Serialized to JSON
|
||||
- Sent via HTTP POST
|
||||
- Processed sequentially on dedicated thread
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Connection Failures**:
|
||||
- Automatic retry for transient failures
|
||||
- Configurable retry policies
|
||||
- Graceful handling of permanent failures
|
||||
|
||||
- **Message Processing Errors**:
|
||||
- Comprehensive error reporting
|
||||
- Clean failure handling
|
||||
- Maintains system stability
|
||||
|
||||
### Shutdown Process
|
||||
|
||||
The transport implements a graceful shutdown mechanism that:
|
||||
1. Marks transport as closing
|
||||
2. Disposes of all subscriptions
|
||||
3. Cleans up schedulers
|
||||
4. Completes message endpoint sink
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
- **Protocol**: HTTP with Server-Sent Events (SSE)
|
||||
- **Message Format**: JSON-RPC 2.0
|
||||
- **Content Type**: application/json for outbound, text/event-stream for inbound
|
||||
- **Endpoints**:
|
||||
- SSE Connection: `/sse`
|
||||
- Message Endpoint: Dynamically provided by server
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Spring WebFlux for reactive HTTP client
|
||||
- Jackson for JSON processing
|
||||
- Project Reactor for reactive streams
|
||||
- SLF4J for logging
|
||||
|
||||
## Thread Safety
|
||||
|
||||
The implementation is thread-safe and designed for concurrent use:
|
||||
- Inbound processing uses SSE event loop
|
||||
- Outbound processing uses dedicated single-threaded scheduler
|
||||
- State management uses thread-safe constructs
|
||||
|
||||
## Testing
|
||||
|
||||
The implementation includes comprehensive tests covering:
|
||||
- Message processing
|
||||
- Error handling
|
||||
- Connection management
|
||||
- Shutdown procedures
|
||||
- Edge cases and failure scenarios
|
||||
|
||||
For detailed test examples, see `SseClientTransportTests.java`.
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
# StdioClientTransport
|
||||
|
||||
The `StdioClientTransport` class implements the [MCP Stdio Transport](https://spec.modelcontextprotocol.io/specification/basic/transports/#stdio) specification, providing communication with MCP servers over standard input/output streams.
|
||||
|
||||
## Overview
|
||||
|
||||
The Stdio transport uses standard input/output streams for bidirectional communication between the client and server:
|
||||
- Messages are sent to the server via stdout
|
||||
- Messages are received from the server via stdin
|
||||
- Error and debug information is sent via stderr
|
||||
|
||||
## Message Format
|
||||
|
||||
Messages are exchanged as newline-delimited JSON-RPC messages:
|
||||
- Each message is a single line of JSON text
|
||||
- Messages are terminated with a newline character (`\n`)
|
||||
- Messages follow the JSON-RPC 2.0 specification
|
||||
- UTF-8 encoding is used for all messages
|
||||
|
||||
### Message Types
|
||||
|
||||
The transport handles three types of JSON-RPC messages:
|
||||
|
||||
1. **Requests** - Messages with both `method` and `id` fields:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "example.method",
|
||||
"params": {}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Notifications** - Messages with `method` but no `id` field:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "example.notification",
|
||||
"params": {}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Responses** - Messages with either `result` or `error` fields:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Process Management
|
||||
|
||||
The transport manages a server process with:
|
||||
- Command and arguments specified via `ServerParameters`
|
||||
- Environment variables for configuration
|
||||
- Proper process cleanup on shutdown
|
||||
|
||||
### Threading Model
|
||||
|
||||
The implementation uses three dedicated threads:
|
||||
1. **Inbound** - Reads and processes messages from stdin
|
||||
2. **Outbound** - Writes messages to stdout
|
||||
3. **Error** - Handles stderr messages
|
||||
|
||||
### Error Handling
|
||||
|
||||
Errors are handled at multiple levels:
|
||||
- Process startup failures
|
||||
- Stream I/O errors
|
||||
- Message parsing errors
|
||||
- Process termination handling
|
||||
|
||||
## Usage Example
|
||||
|
||||
```java
|
||||
// Create server parameters
|
||||
ServerParameters params = ServerParameters.builder()
|
||||
.command("path/to/server")
|
||||
.args(List.of("--arg1", "value1"))
|
||||
.env(Map.of("ENV_VAR", "value"))
|
||||
.build();
|
||||
|
||||
// Create transport
|
||||
StdioClientTransport transport = new StdioClientTransport(params);
|
||||
|
||||
// Start transport
|
||||
transport.start();
|
||||
|
||||
// Send message
|
||||
JSONRPCRequest request = new JSONRPCRequest();
|
||||
request.setId(1);
|
||||
request.setMethod("example.method");
|
||||
request.setParams(new HashMap<>());
|
||||
transport.send(request);
|
||||
|
||||
// Close transport
|
||||
transport.closeGracefully().block();
|
||||
```
|
||||
|
||||
## Error Messages
|
||||
|
||||
Error messages from the server process are:
|
||||
- Written to stderr
|
||||
- Logged at ERROR level
|
||||
- Emitted through the error sink for handling
|
||||
- UTF-8 encoded
|
||||
|
||||
## Debugging
|
||||
|
||||
For debugging purposes:
|
||||
1. Enable DEBUG level logging to see detailed message flow
|
||||
2. Monitor stderr for server process errors
|
||||
3. Check process exit codes for abnormal termination
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The transport ensures thread safety through dedicated schedulers
|
||||
- Messages are processed asynchronously using Project Reactor
|
||||
- Resources are properly cleaned up on shutdown
|
||||
- The implementation is resilient to various failure modes
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
@startuml Core Components
|
||||
|
||||
' Core Interfaces
|
||||
interface McpTransport {
|
||||
+Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> handler)
|
||||
+Mono<Void> sendMessage(JSONRPCMessage message)
|
||||
+void close()
|
||||
+Mono<Void> closeGracefully()
|
||||
+<T> T unmarshalFrom(Object data, TypeReference<T> typeRef)
|
||||
}
|
||||
|
||||
interface McpSession {
|
||||
+<T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef)
|
||||
+Mono<Void> sendNotification(String method, Map<String, Object> params)
|
||||
+Mono<Void> closeGracefully()
|
||||
+void close()
|
||||
}
|
||||
|
||||
' Core Implementation Classes
|
||||
class DefaultMcpSession {
|
||||
-McpTransport transport
|
||||
-ObjectMapper objectMapper
|
||||
+interface RequestHandler
|
||||
+interface NotificationHandler
|
||||
}
|
||||
|
||||
' Client Classes
|
||||
class McpClient {
|
||||
+{static} Builder using(McpTransport transport)
|
||||
+{static} McpAsyncClient async(McpTransport transport)
|
||||
+{static} McpSyncClient sync(McpTransport transport)
|
||||
}
|
||||
|
||||
class McpAsyncClient {
|
||||
+Mono<InitializeResult> initialize()
|
||||
+Mono<CallToolResult> callTool(CallToolRequest request)
|
||||
+Mono<ListToolsResult> listTools()
|
||||
+Mono<ListResourcesResult> listResources()
|
||||
+Mono<ReadResourceResult> readResource(ReadResourceRequest request)
|
||||
+Mono<ListResourceTemplatesResult> listResourceTemplates()
|
||||
+Mono<ListPromptsResult> listPrompts()
|
||||
+Mono<GetPromptResult> getPrompt(GetPromptRequest request)
|
||||
}
|
||||
|
||||
class McpSyncClient {
|
||||
+InitializeResult initialize()
|
||||
+CallToolResult callTool(CallToolRequest request)
|
||||
+ListToolsResult listTools()
|
||||
+ListResourcesResult listResources()
|
||||
+ReadResourceResult readResource(ReadResourceRequest request)
|
||||
+ListResourceTemplatesResult listResourceTemplates()
|
||||
+ListPromptsResult listPrompts()
|
||||
+GetPromptResult getPrompt(GetPromptRequest request)
|
||||
}
|
||||
|
||||
' Server Classes
|
||||
class McpServer {
|
||||
+{static} Builder using(McpTransport transport)
|
||||
}
|
||||
|
||||
class McpAsyncServer {
|
||||
+Mono<Void> addTool(ToolHandler toolHandler)
|
||||
+Mono<Void> removeTool(String toolName)
|
||||
+Mono<Void> notifyToolsListChanged()
|
||||
+Mono<Void> notifyResourcesListChanged()
|
||||
+Mono<Void> notifyPromptsListChanged()
|
||||
+interface ToolHandler
|
||||
}
|
||||
|
||||
class McpSyncServer {
|
||||
+void addTool(ToolHandler toolHandler)
|
||||
+void removeTool(String toolName)
|
||||
+void notifyToolsListChanged()
|
||||
+void notifyResourcesListChanged()
|
||||
+void notifyPromptsListChanged()
|
||||
+McpAsyncServer getAsyncServer()
|
||||
}
|
||||
|
||||
' Transport Implementations
|
||||
class StdioClientTransport {
|
||||
-Process serverProcess
|
||||
-ServerParameters parameters
|
||||
}
|
||||
|
||||
class SseClientTransport {
|
||||
-WebClient webClient
|
||||
}
|
||||
|
||||
class StdioServerTransport {
|
||||
}
|
||||
|
||||
class SseServerTransport {
|
||||
}
|
||||
|
||||
' Schema and Error Classes
|
||||
class McpSchema {
|
||||
+class ErrorCodes
|
||||
+interface Request
|
||||
+interface JSONRPCMessage
|
||||
+interface ResourceContents
|
||||
+interface Content
|
||||
+interface ServerCapabilities
|
||||
+{static} JSONRPCMessage deserializeJsonRpcMessage()
|
||||
}
|
||||
|
||||
class McpError {
|
||||
-int code
|
||||
-String message
|
||||
}
|
||||
|
||||
' Relationships
|
||||
McpTransport <|.. StdioClientTransport
|
||||
McpTransport <|.. SseClientTransport
|
||||
McpTransport <|.. StdioServerTransport
|
||||
McpTransport <|.. SseServerTransport
|
||||
|
||||
McpSession <|.. DefaultMcpSession
|
||||
DefaultMcpSession --o McpAsyncClient
|
||||
DefaultMcpSession --o McpAsyncServer
|
||||
|
||||
McpClient ..> McpAsyncClient : creates
|
||||
McpClient ..> McpSyncClient : creates
|
||||
McpSyncClient o-- McpAsyncClient
|
||||
|
||||
McpServer ..> McpAsyncServer : creates
|
||||
McpServer ..> McpSyncServer : creates
|
||||
McpSyncServer o-- McpAsyncServer
|
||||
|
||||
DefaultMcpSession o-- McpTransport
|
||||
McpSchema <.. McpSession : uses
|
||||
McpError ..> McpSession : throws
|
||||
|
||||
@enduml
|
||||
|
||||
@startuml Message Flow
|
||||
|
||||
package "MCP Schema" {
|
||||
interface JSONRPCMessage {
|
||||
+String jsonrpc()
|
||||
}
|
||||
|
||||
interface Request {
|
||||
}
|
||||
|
||||
class InitializeRequest
|
||||
class CallToolRequest
|
||||
class ListToolsRequest
|
||||
class ListResourcesRequest
|
||||
class ReadResourceRequest
|
||||
class ListResourceTemplatesRequest
|
||||
class ListPromptsRequest
|
||||
class GetPromptRequest
|
||||
}
|
||||
|
||||
package "Resource Types" {
|
||||
interface ResourceContents {
|
||||
+String uri()
|
||||
+String mimeType()
|
||||
}
|
||||
|
||||
class TextResourceContents
|
||||
class BlobResourceContents
|
||||
|
||||
interface Content {
|
||||
+String type()
|
||||
}
|
||||
|
||||
class TextContent
|
||||
class ImageContent
|
||||
class EmbeddedResource
|
||||
|
||||
interface Annotated {
|
||||
+Annotations annotations()
|
||||
}
|
||||
|
||||
interface PromptOrResourceReference {
|
||||
+String type()
|
||||
}
|
||||
|
||||
class PromptReference
|
||||
class ResourceReference
|
||||
}
|
||||
|
||||
JSONRPCMessage <|.. Request
|
||||
Request <|.. InitializeRequest
|
||||
Request <|.. CallToolRequest
|
||||
Request <|.. ListToolsRequest
|
||||
Request <|.. ListResourcesRequest
|
||||
Request <|.. ReadResourceRequest
|
||||
Request <|.. ListResourceTemplatesRequest
|
||||
Request <|.. ListPromptsRequest
|
||||
Request <|.. GetPromptRequest
|
||||
|
||||
ResourceContents <|.. TextResourceContents
|
||||
ResourceContents <|.. BlobResourceContents
|
||||
|
||||
Content <|.. TextContent
|
||||
Content <|.. ImageContent
|
||||
Content <|.. EmbeddedResource
|
||||
|
||||
PromptOrResourceReference <|.. PromptReference
|
||||
PromptOrResourceReference <|.. ResourceReference
|
||||
|
||||
@enduml
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
# MCP Client Reference
|
||||
|
||||
## McpClient Factory
|
||||
|
||||
`org.springframework.ai.mcp.client.McpClient`
|
||||
|
||||
The `McpClient` class serves as a factory for creating Model Context Protocol (MCP) clients. It provides the main entry point for establishing connections with MCP servers, offering both synchronous and asynchronous client implementations.
|
||||
|
||||
### Overview
|
||||
|
||||
The client factory uses a builder pattern for flexible configuration, allowing you to customize various aspects of the client including timeout durations, notification handlers, and more.
|
||||
|
||||
### Client Types
|
||||
|
||||
The factory can create two types of clients:
|
||||
- `McpAsyncClient` for non-blocking operations
|
||||
- `McpSyncClient` for blocking operations
|
||||
|
||||
### Builder Configuration
|
||||
|
||||
#### Basic Usage
|
||||
```java
|
||||
McpClient.using(transport)
|
||||
.requestTimeout(Duration.ofSeconds(5))
|
||||
.sync(); // or .async()
|
||||
```
|
||||
|
||||
#### Builder Methods
|
||||
|
||||
##### using(McpTransport transport)
|
||||
```java
|
||||
public static Builder using(McpTransport transport)
|
||||
```
|
||||
Start building an MCP client with the specified transport.
|
||||
|
||||
##### requestTimeout(Duration requestTimeout)
|
||||
```java
|
||||
public Builder requestTimeout(Duration requestTimeout)
|
||||
```
|
||||
Set the request timeout duration (default is 20 seconds).
|
||||
|
||||
##### capabilities(ClientCapabilities capabilities)
|
||||
```java
|
||||
public Builder capabilities(ClientCapabilities capabilities)
|
||||
```
|
||||
Set the client capabilities that define what features the client supports.
|
||||
|
||||
##### clientInfo(Implementation clientInfo)
|
||||
```java
|
||||
public Builder clientInfo(Implementation clientInfo)
|
||||
```
|
||||
Set the client implementation information (name and version).
|
||||
|
||||
##### roots(List<Root> roots)
|
||||
```java
|
||||
public Builder roots(List<Root> roots)
|
||||
```
|
||||
Add a list of roots that define filesystem boundaries for server operations.
|
||||
|
||||
##### roots(Root... roots)
|
||||
```java
|
||||
public Builder roots(Root... roots)
|
||||
```
|
||||
Add roots that define filesystem boundaries for server operations.
|
||||
|
||||
##### toolsChangeConsumer(Consumer<List<Tool>> consumer)
|
||||
```java
|
||||
public Builder toolsChangeConsumer(Consumer<List<McpSchema.Tool>> toolsChangeConsumer)
|
||||
```
|
||||
Add a consumer for tool changes notifications.
|
||||
|
||||
##### resourcesChangeConsumer(Consumer<List<Resource>> consumer)
|
||||
```java
|
||||
public Builder resourcesChangeConsumer(Consumer<List<McpSchema.Resource>> resourcesChangeConsumer)
|
||||
```
|
||||
Add a consumer for resource changes notifications.
|
||||
|
||||
##### promptsChangeConsumer(Consumer<List<Prompt>> consumer)
|
||||
```java
|
||||
public Builder promptsChangeConsumer(Consumer<List<McpSchema.Prompt>> promptsChangeConsumer)
|
||||
```
|
||||
Add a consumer for prompt changes notifications.
|
||||
|
||||
##### sync()
|
||||
```java
|
||||
public McpSyncClient sync()
|
||||
```
|
||||
Build and return a synchronous MCP client.
|
||||
|
||||
##### async()
|
||||
```java
|
||||
public McpAsyncClient async()
|
||||
```
|
||||
Build and return an asynchronous MCP client.
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Creating a Synchronous Client
|
||||
```java
|
||||
McpTransport transport = // obtain transport instance
|
||||
|
||||
// Configure client capabilities
|
||||
ClientCapabilities capabilities = ClientCapabilities.builder()
|
||||
.experimental(Map.of("feature", "value"))
|
||||
.roots(true)
|
||||
.sampling()
|
||||
.build();
|
||||
|
||||
McpSyncClient client = McpClient.using(transport)
|
||||
.requestTimeout(Duration.ofSeconds(10))
|
||||
.rootsListChangedNotification(true)
|
||||
.toolsChangeConsumer(tools -> {
|
||||
System.out.println("Tools updated: " + tools);
|
||||
})
|
||||
.sync();
|
||||
|
||||
// Initialize client with capabilities
|
||||
client.initialize(LATEST_PROTOCOL_VERSION, capabilities,
|
||||
new Implementation("client-name", "1.0.0"));
|
||||
```
|
||||
|
||||
#### Creating an Asynchronous Client
|
||||
```java
|
||||
McpTransport transport = // obtain transport instance
|
||||
McpAsyncClient client = McpClient.using(transport)
|
||||
.requestTimeout(Duration.ofSeconds(5))
|
||||
.resourcesChangeConsumer(resources -> {
|
||||
System.out.println("Resources updated: " + resources);
|
||||
})
|
||||
.async();
|
||||
```
|
||||
|
||||
#### Using Multiple Change Consumers
|
||||
```java
|
||||
McpClient.using(transport)
|
||||
.toolsChangeConsumer(tools -> {
|
||||
// Handle tool changes
|
||||
})
|
||||
.toolsChangeConsumer(tools -> {
|
||||
// Additional tool change handling
|
||||
})
|
||||
.resourcesChangeConsumer(resources -> {
|
||||
// Handle resource changes
|
||||
})
|
||||
.sync();
|
||||
```
|
||||
|
||||
### Client Capabilities
|
||||
|
||||
The `ClientCapabilities` class represents features that clients can implement to enrich connected MCP servers. It uses a builder pattern for easy configuration.
|
||||
|
||||
#### ClientCapabilities Builder
|
||||
|
||||
```java
|
||||
ClientCapabilities capabilities = ClientCapabilities.builder()
|
||||
.experimental(experimentalMap)
|
||||
.roots(true)
|
||||
.sampling()
|
||||
.build();
|
||||
```
|
||||
|
||||
##### Builder Methods
|
||||
|
||||
###### experimental(Map<String, Object> experimental)
|
||||
```java
|
||||
public Builder experimental(Map<String, Object> experimental)
|
||||
```
|
||||
Set experimental features map for work-in-progress capabilities.
|
||||
|
||||
###### roots(Boolean listChanged)
|
||||
```java
|
||||
public Builder roots(Boolean listChanged)
|
||||
```
|
||||
Configure root capabilities with listChanged parameter. Roots define the boundaries of where servers can operate within the filesystem.
|
||||
|
||||
###### sampling()
|
||||
```java
|
||||
public Builder sampling()
|
||||
```
|
||||
Add sampling capabilities that allow servers to request LLM sampling from language models via clients.
|
||||
|
||||
### Client Operations
|
||||
|
||||
#### Root Management
|
||||
|
||||
Both synchronous and asynchronous clients provide methods for managing filesystem roots:
|
||||
|
||||
##### Synchronous API
|
||||
```java
|
||||
void addRoot(Root root) // Add a new root
|
||||
void removeRoot(String rootUri) // Remove a root by URI
|
||||
void rootsListChangedNotification() // Notify about roots list changes
|
||||
```
|
||||
|
||||
##### Asynchronous API
|
||||
```java
|
||||
Mono<Void> addRoot(Root root) // Add a new root
|
||||
Mono<Void> removeRoot(String rootUri) // Remove a root by URI
|
||||
Mono<Void> rootsListChangedNotification() // Notify about roots list changes
|
||||
```
|
||||
|
||||
#### Tools API
|
||||
|
||||
Methods for interacting with server tools:
|
||||
|
||||
##### Synchronous API
|
||||
```java
|
||||
CallToolResult callTool(CallToolRequest request) // Execute a tool
|
||||
ListToolsResult listTools() // List all available tools
|
||||
ListToolsResult listTools(String cursor) // List tools with pagination
|
||||
```
|
||||
|
||||
##### Asynchronous API
|
||||
```java
|
||||
Mono<CallToolResult> callTool(CallToolRequest request)
|
||||
Mono<ListToolsResult> listTools()
|
||||
Mono<ListToolsResult> listTools(String cursor)
|
||||
```
|
||||
|
||||
#### Resources API
|
||||
|
||||
Methods for working with server resources:
|
||||
|
||||
##### Synchronous API
|
||||
```java
|
||||
ListResourcesResult listResources() // List all resources
|
||||
ListResourcesResult listResources(String cursor) // List resources with pagination
|
||||
ReadResourceResult readResource(Resource resource) // Read a resource
|
||||
ReadResourceResult readResource(ReadResourceRequest request) // Read a resource by request
|
||||
ListResourceTemplatesResult listResourceTemplates() // List resource templates
|
||||
void subscribeResource(SubscribeRequest request) // Subscribe to resource updates
|
||||
void unsubscribeResource(UnsubscribeRequest request) // Unsubscribe from updates
|
||||
void sendResourcesListChanged() // Notify about resource changes
|
||||
```
|
||||
|
||||
##### Asynchronous API
|
||||
```java
|
||||
Mono<ListResourcesResult> listResources()
|
||||
Mono<ListResourcesResult> listResources(String cursor)
|
||||
Mono<ReadResourceResult> readResource(Resource resource)
|
||||
Mono<ReadResourceResult> readResource(ReadResourceRequest request)
|
||||
Mono<ListResourceTemplatesResult> listResourceTemplates()
|
||||
Mono<Void> subscribeResource(SubscribeRequest request)
|
||||
Mono<Void> unsubscribeResource(UnsubscribeRequest request)
|
||||
Mono<Void> sendResourcesListChanged()
|
||||
```
|
||||
|
||||
#### Prompts API
|
||||
|
||||
Methods for working with server prompts:
|
||||
|
||||
##### Synchronous API
|
||||
```java
|
||||
ListPromptsResult listPrompts() // List all prompts
|
||||
ListPromptsResult listPrompts(String cursor) // List prompts with pagination
|
||||
GetPromptResult getPrompt(GetPromptRequest request) // Get a specific prompt
|
||||
void promptListChangedNotification() // Notify about prompt changes
|
||||
```
|
||||
|
||||
##### Asynchronous API
|
||||
```java
|
||||
Mono<ListPromptsResult> listPrompts()
|
||||
Mono<ListPromptsResult> listPrompts(String cursor)
|
||||
Mono<GetPromptResult> getPrompt(GetPromptRequest request)
|
||||
Mono<Void> promptListChangedNotification()
|
||||
```
|
||||
|
||||
#### Lifecycle Management
|
||||
|
||||
Methods for managing client lifecycle:
|
||||
|
||||
##### Synchronous API
|
||||
```java
|
||||
InitializeResult initialize() // Initialize client connection
|
||||
void close() // Close client immediately
|
||||
boolean closeGracefully() // Close client with graceful shutdown
|
||||
Object ping() // Send ping request
|
||||
```
|
||||
|
||||
##### Asynchronous API
|
||||
```java
|
||||
Mono<InitializeResult> initialize()
|
||||
void close()
|
||||
Mono<Void> closeGracefully()
|
||||
Mono<Object> ping()
|
||||
```
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- The client factory uses a builder pattern for configuration
|
||||
- Default request timeout is 20 seconds
|
||||
- Multiple consumers can be registered for tools, resources, and prompts changes
|
||||
- The builder validates that the transport is not null
|
||||
- Deprecated static factory methods are available but using the builder pattern is recommended
|
||||
- The synchronous client is implemented as a wrapper around the asynchronous client
|
||||
- ClientCapabilities uses a builder pattern for configuring client features
|
||||
- The synchronous client blocks on async operations with the configured request timeout
|
||||
- Resource subscriptions require server support for the subscribe capability
|
||||
- Root management requires client to be configured with root capabilities
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
# MCP Error Handling Reference
|
||||
|
||||
## McpError Class
|
||||
|
||||
`org.springframework.ai.mcp.spec.McpError`
|
||||
|
||||
The `McpError` class is a runtime exception that represents errors that occur during Model Context Protocol (MCP) operations. It extends `RuntimeException` and provides a way to encapsulate error information from the MCP protocol.
|
||||
|
||||
### Overview
|
||||
|
||||
The McpError class is designed to handle various error scenarios that can occur during MCP communication, including protocol errors, transport errors, and application-specific errors.
|
||||
|
||||
### Class Definition
|
||||
|
||||
```java
|
||||
public class McpError extends RuntimeException {
|
||||
public McpError(Object error) {
|
||||
super(error.toString());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Error Scenarios
|
||||
|
||||
1. **Transport Errors**
|
||||
- Connection failures
|
||||
- Timeout errors
|
||||
- Protocol violations
|
||||
|
||||
2. **Protocol Errors**
|
||||
- Invalid JSON-RPC messages
|
||||
- Missing required fields
|
||||
- Invalid method names
|
||||
|
||||
3. **Application Errors**
|
||||
- Tool execution failures
|
||||
- Resource access errors
|
||||
- Invalid parameters
|
||||
|
||||
### Error Handling Best Practices
|
||||
|
||||
#### Client-Side Error Handling
|
||||
|
||||
```java
|
||||
McpClient client = McpClient.using(transport)
|
||||
.async();
|
||||
|
||||
client.sendRequest("method", params)
|
||||
.onErrorResume(McpError.class, error -> {
|
||||
// Handle MCP-specific errors
|
||||
logger.error("MCP error occurred: {}", error.getMessage());
|
||||
return Mono.empty();
|
||||
})
|
||||
.onErrorResume(Exception.class, error -> {
|
||||
// Handle other exceptions
|
||||
logger.error("Unexpected error: {}", error.getMessage());
|
||||
return Mono.empty();
|
||||
});
|
||||
```
|
||||
|
||||
#### Server-Side Error Handling
|
||||
|
||||
```java
|
||||
McpServer.using(transport)
|
||||
.tool(new McpSchema.Tool("example", "Example tool", inputSchema),
|
||||
args -> {
|
||||
try {
|
||||
// Tool implementation
|
||||
return new McpSchema.CallToolResult(result);
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Convert to MCP error
|
||||
throw new McpError("Tool execution failed: " + e.getMessage());
|
||||
}
|
||||
})
|
||||
.async();
|
||||
```
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- McpError is a runtime exception, so it doesn't require explicit declaration in method signatures
|
||||
- The error message is created by calling toString() on the provided error object
|
||||
- Error handling should be implemented at both client and server sides
|
||||
- Reactive error handling using Project Reactor's error operators is recommended
|
||||
- Consider logging errors for debugging and monitoring purposes
|
||||
|
||||
### Error Recovery Strategies
|
||||
|
||||
1. **Retry Logic**
|
||||
```java
|
||||
client.sendRequest("method", params)
|
||||
.retry(3) // Retry up to 3 times
|
||||
.onErrorResume(McpError.class, error -> {
|
||||
// Handle failure after retries
|
||||
return Mono.empty();
|
||||
});
|
||||
```
|
||||
|
||||
2. **Fallback Values**
|
||||
```java
|
||||
client.sendRequest("method", params)
|
||||
.onErrorReturn(McpError.class, fallbackValue);
|
||||
```
|
||||
|
||||
3. **Circuit Breaking**
|
||||
```java
|
||||
// Using Resilience4j or similar library
|
||||
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("mcpClient");
|
||||
client.sendRequest("method", params)
|
||||
.transform(CircuitBreakerOperator.of(circuitBreaker));
|
||||
```
|
||||
|
||||
### Error Logging and Monitoring
|
||||
|
||||
```java
|
||||
client.sendRequest("method", params)
|
||||
.doOnError(McpError.class, error -> {
|
||||
// Log error details
|
||||
logger.error("MCP error: {}", error.getMessage());
|
||||
// Update metrics
|
||||
errorCounter.increment();
|
||||
});
|
||||
```
|
||||
|
||||
### Integration with Spring Error Handling
|
||||
|
||||
```java
|
||||
@ControllerAdvice
|
||||
public class McpErrorHandler {
|
||||
@ExceptionHandler(McpError.class)
|
||||
public ResponseEntity<ErrorResponse> handleMcpError(McpError error) {
|
||||
ErrorResponse response = new ErrorResponse(
|
||||
"MCP_ERROR",
|
||||
error.getMessage()
|
||||
);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Error Scenarios
|
||||
|
||||
```java
|
||||
@Test
|
||||
void testMcpError() {
|
||||
McpError error = new McpError("Test error");
|
||||
assertEquals("Test error", error.getMessage());
|
||||
|
||||
assertThrows(McpError.class, () -> {
|
||||
throw new McpError("Test error");
|
||||
});
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
# Spring AI MCP Reference Documentation
|
||||
|
||||
This documentation covers the Spring AI implementation of the Model Context Protocol (MCP) specification. The implementation follows the [official MCP specification](https://spec.modelcontextprotocol.io/specification/).
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
### 1. Transport Layer
|
||||
[Transport](ref-transport.md)
|
||||
- McpTransport Interface
|
||||
- Transport Implementations (SSE, STDIO)
|
||||
- Message Serialization/Deserialization
|
||||
|
||||
### 2. Session Layer
|
||||
[Session Management](ref-session.md)
|
||||
- McpSession Interface
|
||||
- DefaultMcpSession Implementation
|
||||
- Uses McpTransport for communication
|
||||
|
||||
### 3. High-Level Components
|
||||
Both components operate at the same level, using McpSession internally:
|
||||
|
||||
[Client](ref-client.md)
|
||||
- McpClient Factory
|
||||
- Synchronous and Asynchronous Implementations
|
||||
- Client Configuration and Builder Pattern
|
||||
|
||||
[Server](ref-server.md)
|
||||
- McpServer Factory
|
||||
- Synchronous and Asynchronous Implementations
|
||||
- Server Configuration and Builder Pattern
|
||||
|
||||
### 4. Supporting Components
|
||||
|
||||
[Error Handling](ref-errors.md)
|
||||
- McpError
|
||||
- Error Codes and Handling
|
||||
- Exception Hierarchy
|
||||
|
||||
|
||||
## Component Relationships
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ McpClient │ │ McpServer │
|
||||
└─────────────┘ └─────────────┘
|
||||
│ │
|
||||
└─────────┬────────┘
|
||||
│
|
||||
┌───────────────┐
|
||||
│ McpSession │
|
||||
└───────────────┘
|
||||
│
|
||||
┌───────────────┐
|
||||
│ McpTransport │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
## Package Organization
|
||||
|
||||
The implementation is organized under the `org.springframework.ai.mcp` package with the following structure:
|
||||
|
||||
- `org.springframework.ai.mcp.spec` - Core interfaces and classes
|
||||
- McpTransport
|
||||
- McpSession
|
||||
- McpSchema
|
||||
- McpError
|
||||
- `org.springframework.ai.mcp.client` - Client implementations
|
||||
- McpClient
|
||||
- McpAsyncClient
|
||||
- McpSyncClient
|
||||
- Client-specific transports
|
||||
- `org.springframework.ai.mcp.server` - Server implementations
|
||||
- McpServer
|
||||
- McpAsyncServer
|
||||
- McpSyncServer
|
||||
- Server-specific transports
|
||||
- `org.springframework.ai.mcp.util` - Utility classes
|
||||
- Assert
|
||||
- Utils
|
||||
|
||||
## Key Features
|
||||
|
||||
- Synchronous and Asynchronous implementations
|
||||
- Multiple transport options (SSE, STDIO)
|
||||
- Comprehensive error handling
|
||||
- Builder pattern for configuration
|
||||
- Reactive programming support with Project Reactor
|
||||
- Type-safe message handling
|
||||
- Resource and prompt management
|
||||
- Tool discovery and execution
|
||||
- Change notification system
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
# MCP Server Reference
|
||||
|
||||
## McpServer Factory
|
||||
|
||||
`org.springframework.ai.mcp.server.McpServer`
|
||||
|
||||
The `McpServer` class serves as a factory for creating Model Context Protocol (MCP) servers. It provides the main entry point for establishing MCP servers, offering both synchronous and asynchronous server implementations.
|
||||
|
||||
### Overview
|
||||
|
||||
The server factory uses a builder pattern for flexible configuration, allowing you to customize various aspects of the server including server information, capabilities, tools, resource providers, and prompt providers.
|
||||
|
||||
### Server Types
|
||||
|
||||
The factory can create two types of servers:
|
||||
- `McpAsyncServer` for non-blocking operations
|
||||
- `McpSyncServer` for blocking operations
|
||||
|
||||
### Builder Configuration
|
||||
|
||||
#### Basic Usage
|
||||
```java
|
||||
McpServer.using(transport)
|
||||
.serverInfo("my-server", "1.0.0")
|
||||
.addTool(new MyToolHandler())
|
||||
.async(); // or .sync()
|
||||
```
|
||||
|
||||
#### Builder Methods
|
||||
|
||||
##### using(McpTransport transport)
|
||||
```java
|
||||
public static Builder using(McpTransport transport)
|
||||
```
|
||||
Start building an MCP server with the specified transport.
|
||||
|
||||
##### serverInfo(Implementation serverInfo)
|
||||
```java
|
||||
public Builder serverInfo(McpSchema.Implementation serverInfo)
|
||||
```
|
||||
Set the server implementation information.
|
||||
|
||||
##### serverInfo(String name, String version)
|
||||
```java
|
||||
public Builder serverInfo(String name, String version)
|
||||
```
|
||||
Set the server implementation information using name and version.
|
||||
|
||||
##### serverCapabilities(ServerCapabilities capabilities)
|
||||
```java
|
||||
public Builder serverCapabilities(McpSchema.ServerCapabilities serverCapabilities)
|
||||
```
|
||||
Set the server capabilities configuration.
|
||||
|
||||
##### addTool(ToolHandler handler)
|
||||
```java
|
||||
public <T extends McpAsyncServer.ToolHandler> Builder addTool(T toolHandler)
|
||||
```
|
||||
Add a tool handler to the server.
|
||||
|
||||
##### tool(Tool tool, Function<Map<String, Object>, CallToolResult> handler)
|
||||
```java
|
||||
public Builder tool(McpSchema.Tool tool, Function<Map<String, Object>, McpSchema.CallToolResult> handler)
|
||||
```
|
||||
Add a tool with its handler function.
|
||||
|
||||
##### addTools(List<ToolHandler> handlers)
|
||||
```java
|
||||
public Builder addTools(List<ToolHandler> toolHandlers)
|
||||
```
|
||||
Add multiple tool handlers to the server.
|
||||
|
||||
##### resourcesProvider(Function<String, List<Resource>> provider)
|
||||
```java
|
||||
public Builder resourcesProvider(Function<String, List<McpSchema.Resource>> resourcesProvider)
|
||||
```
|
||||
Set the resources provider function.
|
||||
|
||||
##### promptsProvider(Function<String, List<Prompt>> provider)
|
||||
```java
|
||||
public Builder promptsProvider(Function<String, List<McpSchema.Prompt>> promptsProvider)
|
||||
```
|
||||
Set the prompts provider function.
|
||||
|
||||
##### sync()
|
||||
```java
|
||||
public McpSyncServer sync()
|
||||
```
|
||||
Build and return a synchronous MCP server.
|
||||
|
||||
##### async()
|
||||
```java
|
||||
public AsyncServerBuilder async()
|
||||
```
|
||||
Build and return an asynchronous server builder.
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Creating a Basic Server
|
||||
```java
|
||||
McpTransport transport = // obtain transport instance
|
||||
McpServer.using(transport)
|
||||
.serverInfo("my-server", "1.0.0")
|
||||
.sync();
|
||||
```
|
||||
|
||||
#### Creating a Server with Tools
|
||||
```java
|
||||
McpServer.using(transport)
|
||||
.serverInfo("calculation-server", "1.0.0")
|
||||
.tool(
|
||||
new McpSchema.Tool("calculate", "Performs calculations", Map.of(
|
||||
"operation", Map.of("type", "string"),
|
||||
"numbers", Map.of("type", "array")
|
||||
)),
|
||||
args -> {
|
||||
// Handle calculation
|
||||
return new McpSchema.CallToolResult(/* result */);
|
||||
}
|
||||
)
|
||||
.async();
|
||||
```
|
||||
|
||||
#### Creating a Server with Resources and Prompts
|
||||
```java
|
||||
McpServer.using(transport)
|
||||
.serverInfo("content-server", "1.0.0")
|
||||
.resourcesProvider(cursor -> {
|
||||
// Return list of resources based on cursor
|
||||
return List.of(/* resources */);
|
||||
})
|
||||
.promptsProvider(cursor -> {
|
||||
// Return list of prompts based on cursor
|
||||
return List.of(/* prompts */);
|
||||
})
|
||||
.sync();
|
||||
```
|
||||
|
||||
- Tool handlers must be thread-safe as they may be called concurrently
|
||||
- The synchronous server is implemented as a wrapper around the asynchronous server
|
||||
- Resource and prompt providers use cursor-based pagination
|
||||
- The AsyncServerBuilder allows adding tools after server creation
|
||||
|
||||
### Tool Handler Interface
|
||||
|
||||
```java
|
||||
public interface ToolHandler {
|
||||
String getName();
|
||||
String getDescription();
|
||||
Map<String, Object> getInputSchema();
|
||||
McpSchema.CallToolResult call(Map<String, Object> arguments);
|
||||
}
|
||||
```
|
||||
|
||||
Tool handlers implement this interface to define:
|
||||
- Tool name and description
|
||||
- Input schema for validation
|
||||
- Execution logic in the call method
|
||||
|
||||
### Logging Capabilities
|
||||
|
||||
The MCP Server supports logging functionality that allows sending log messages with different severity levels to the Client. This feature can be enabled through server capabilities configuration.
|
||||
|
||||
#### Enabling Logging
|
||||
|
||||
```java
|
||||
McpServer.using(transport)
|
||||
.serverInfo("my-server", "1.0.0")
|
||||
.capabilities(ServerCapabilities.builder().logging().build())
|
||||
.sync(); // or .async()
|
||||
```
|
||||
|
||||
#### Logging Levels
|
||||
|
||||
The server supports the following logging levels:
|
||||
- TRACE
|
||||
- DEBUG
|
||||
- INFO
|
||||
- WARN
|
||||
- ERROR
|
||||
- FATAL
|
||||
|
||||
#### Sending Log Messages
|
||||
|
||||
For synchronous servers:
|
||||
```java
|
||||
LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.INFO)
|
||||
.logger("my-logger")
|
||||
.data("Log message")
|
||||
.build();
|
||||
|
||||
mcpSyncServer.loggingNotification(notification);
|
||||
```
|
||||
|
||||
For asynchronous servers:
|
||||
```java
|
||||
LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.INFO)
|
||||
.logger("my-logger")
|
||||
.data("Log message")
|
||||
.build();
|
||||
|
||||
mcpAsyncServer.loggingNotification(notification)
|
||||
.subscribe();
|
||||
```
|
||||
|
||||
- Logging works even if the logging capability is not enabled
|
||||
- Null notifications are rejected with a McpError
|
||||
- Log messages include:
|
||||
- Level: The severity level of the message
|
||||
- Logger: The name of the logger (typically identifies the source)
|
||||
- Data: The actual log message content
|
||||
- The server implementation is transport-agnostic, allowing different transport mechanisms to handle the log messages
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
# MCP Session Reference
|
||||
|
||||
## McpSession Interface
|
||||
|
||||
`org.springframework.ai.mcp.spec.McpSession`
|
||||
|
||||
The MCP (Model Context Protocol) session components implement the [MCP specification](https://spec.modelcontextprotocol.io/) for managing bidirectional JSON-RPC communication between clients and model servers.
|
||||
The implementation provides a foundation for request-response patterns and notification handling, as well as managing the session lifecycle.
|
||||
|
||||
### Overview
|
||||
|
||||
The session operates asynchronously using Project Reactor's `Mono` type for non-blocking operations. It supports both request-response patterns and one-way notifications.
|
||||
|
||||
### Methods
|
||||
|
||||
#### Request-Response
|
||||
|
||||
```java
|
||||
<T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef)
|
||||
```
|
||||
|
||||
Sends a request to the model server and expects a response of type T.
|
||||
|
||||
**Parameters:**
|
||||
- `method`: The name of the method to be called on the server
|
||||
- `requestParams`: The parameters to be sent with the request
|
||||
- `typeRef`: The TypeReference describing the expected response type
|
||||
|
||||
**Returns:**
|
||||
- A `Mono` that will emit the response when received
|
||||
|
||||
#### Notification
|
||||
|
||||
```java
|
||||
Mono<Void> sendNotification(String method, Map<String, Object> params)
|
||||
```
|
||||
|
||||
Sends a notification to the model server with parameters.
|
||||
|
||||
**Parameters:**
|
||||
- `method`: The name of the notification method to be called on the server
|
||||
- `params`: A map of parameters to be sent with the notification
|
||||
|
||||
**Returns:**
|
||||
- A `Mono` that completes when the notification has been sent
|
||||
|
||||
#### Close Gracefully
|
||||
```java
|
||||
Mono<Void> closeGracefully()
|
||||
```
|
||||
|
||||
Closes the session and releases any associated resources asynchronously.
|
||||
|
||||
**Returns:**
|
||||
- A `Mono` that completes when the session has been closed
|
||||
|
||||
#### Close
|
||||
```java
|
||||
void close()
|
||||
```
|
||||
|
||||
Closes the session and releases any associated resources synchronously.
|
||||
|
||||
### DefaultMcpSession Implementation
|
||||
|
||||
The `DefaultMcpSession` provides the concrete implementation of the MCP session with the following key features:
|
||||
|
||||
#### 1. Message Correlation
|
||||
- Uses unique request IDs combining session UUID prefix and atomic counter
|
||||
- Maintains thread-safe tracking of pending responses
|
||||
- Implements timeout management for requests
|
||||
|
||||
#### 2. Handler Registration
|
||||
```java
|
||||
public interface RequestHandler {
|
||||
Mono<Object> handle(Object params);
|
||||
}
|
||||
|
||||
public interface NotificationHandler {
|
||||
Mono<Void> handle(Object params);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Usage Example
|
||||
|
||||
```java
|
||||
McpSession session = // obtain session instance
|
||||
|
||||
// Send a request
|
||||
TypeReference<ResponseType> typeRef = new TypeReference<ResponseType>() {};
|
||||
Mono<ResponseType> response = session.sendRequest("method.name", requestParams, typeRef);
|
||||
|
||||
// Send a notification without parameters
|
||||
Mono<Void> notificationResult = session.sendNotification("notification.method");
|
||||
|
||||
// Send a notification with parameters
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("key", "value");
|
||||
Mono<Void> paramNotificationResult = session.sendNotification("notification.method", params);
|
||||
|
||||
// Close the session gracefully
|
||||
session.closeGracefully()
|
||||
.subscribe(
|
||||
null,
|
||||
error -> System.err.println("Error closing session: " + error),
|
||||
() -> System.out.println("Session closed successfully")
|
||||
);
|
||||
```
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- The interface is designed to be transport-agnostic, allowing different transport implementations (SSE, STDIO) to be used.
|
||||
- All operations except `close()` are non-blocking and return `Mono` instances.
|
||||
- The `sendRequest` method uses Jackson's `TypeReference` for type-safe deserialization of responses.
|
||||
- The default implementation of `sendNotification(String)` delegates to `sendNotification(String, Map)` with null parameters.
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
# MCP Transport Reference
|
||||
|
||||
## McpTransport Interface
|
||||
|
||||
`org.springframework.ai.mcp.spec.McpTransport`
|
||||
|
||||
The `McpTransport` interface defines the asynchronous transport layer for the Model Context Protocol (MCP). It provides the foundation for implementing custom transport mechanisms, handling bidirectional communication between client and server components using JSON-RPC format.
|
||||
|
||||
### Overview
|
||||
|
||||
The transport layer is designed to be protocol-agnostic, allowing for various implementations such as WebSocket, HTTP, or custom protocols. It manages the lifecycle of connections and handles message exchange between components.
|
||||
|
||||
### Key Responsibilities
|
||||
|
||||
- Managing the lifecycle of the transport connection
|
||||
- Handling incoming messages and errors from the server
|
||||
- Sending outbound messages to the server
|
||||
- Message serialization and deserialization
|
||||
|
||||
### Methods
|
||||
|
||||
#### Connect
|
||||
```java
|
||||
Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> handler)
|
||||
```
|
||||
|
||||
Initializes and starts the transport connection.
|
||||
|
||||
**Parameters:**
|
||||
- `handler`: A function that processes incoming messages and returns response messages
|
||||
|
||||
**Returns:**
|
||||
- A `Mono<Void>` that completes when the connection is established
|
||||
|
||||
#### Send Message
|
||||
```java
|
||||
Mono<Void> sendMessage(JSONRPCMessage message)
|
||||
```
|
||||
|
||||
Sends a message to the server asynchronously.
|
||||
|
||||
**Parameters:**
|
||||
- `message`: The JSON-RPC message to be sent to the server
|
||||
|
||||
**Returns:**
|
||||
- A `Mono<Void>` that completes when the message has been sent
|
||||
|
||||
#### Close Gracefully
|
||||
```java
|
||||
Mono<Void> closeGracefully()
|
||||
```
|
||||
|
||||
Closes the transport connection and releases any associated resources asynchronously.
|
||||
|
||||
**Returns:**
|
||||
- A `Mono<Void>` that completes when the connection has been closed
|
||||
|
||||
#### Close
|
||||
```java
|
||||
default void close()
|
||||
```
|
||||
|
||||
Synchronously closes the transport connection. By default, this method delegates to `closeGracefully()` and subscribes to the result.
|
||||
|
||||
#### Unmarshal From
|
||||
```java
|
||||
<T> T unmarshalFrom(Object data, TypeReference<T> typeRef)
|
||||
```
|
||||
|
||||
Deserializes data into the specified type.
|
||||
|
||||
**Parameters:**
|
||||
- `data`: The data to deserialize
|
||||
- `typeRef`: The TypeReference describing the target type
|
||||
|
||||
**Returns:**
|
||||
- The deserialized object of type T
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- The transport layer uses Project Reactor's `Mono` type for non-blocking operations
|
||||
- Messages are exchanged using the JSON-RPC format as specified by the MCP protocol
|
||||
- The interface is designed to be extensible, allowing for different transport implementations
|
||||
- Current implementations include:
|
||||
- SSE (Server-Sent Events) Transport
|
||||
- STDIO Transport (Standard Input/Output)
|
||||
|
||||
### Usage Example
|
||||
|
||||
```java
|
||||
McpTransport transport = // obtain transport instance
|
||||
|
||||
// Connect and handle messages
|
||||
transport.connect(message -> {
|
||||
// Process incoming message
|
||||
return processMessage(message);
|
||||
}).subscribe();
|
||||
|
||||
// Send a message
|
||||
JSONRPCMessage message = new JSONRPCMessage();
|
||||
// ... configure message
|
||||
transport.sendMessage(message)
|
||||
.subscribe(
|
||||
null,
|
||||
error -> System.err.println("Error sending message: " + error),
|
||||
() -> System.out.println("Message sent successfully")
|
||||
);
|
||||
|
||||
// Close the transport
|
||||
transport.closeGracefully()
|
||||
.subscribe(
|
||||
null,
|
||||
error -> System.err.println("Error closing transport: " + error),
|
||||
() -> System.out.println("Transport closed successfully")
|
||||
);
|
||||
```
|
||||
|
||||
### Transport Implementations
|
||||
|
||||
#### SSE Transport
|
||||
The SSE (Server-Sent Events) transport implementation provides a unidirectional communication channel from the server to the client using HTTP streaming.
|
||||
|
||||
#### STDIO Transport
|
||||
The STDIO transport implementation uses standard input/output streams for communication, making it suitable for command-line applications and process-based communication.
|
||||
|
|
@ -149,16 +149,27 @@ class StdioClientTransport implements ClientMcpTransport {
|
|||
+Sinks.Many<String> getErrorSink()
|
||||
}
|
||||
|
||||
class SseClientTransport implements ClientMcpTransport {
|
||||
}
|
||||
|
||||
class StdioServerTransport implements ServerMcpTransport {
|
||||
}
|
||||
|
||||
class SseServerTransport implements ServerMcpTransport {
|
||||
|
||||
class HttpClientSseClientTransport implements ClientMcpTransport {
|
||||
}
|
||||
|
||||
|
||||
class WebFluxSseClientTransport implements ClientMcpTransport {
|
||||
}
|
||||
|
||||
|
||||
class WebFluxSseServerTransport implements ServerMcpTransport {
|
||||
+RouterFunction<?> getRouterFunction()
|
||||
}
|
||||
|
||||
class WebMvcSseServerTransport implements ServerMcpTransport {
|
||||
+RouterFunction<?> getRouterFunction()
|
||||
}
|
||||
|
||||
|
||||
' Schema and Error Classes
|
||||
class McpSchema {
|
||||
+class ErrorCodes
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 65 KiB |
|
|
@ -1,25 +1,35 @@
|
|||
= Java MCP SDK
|
||||
|
||||
Java SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools.
|
||||
Java SDK implementation of the link:https://modelcontextprotocol.io/introduction[Model Context Protocol], enabling seamless integration with language models and AI tools.
|
||||
|
||||
== Features
|
||||
|
||||
* Synchronous and Asynchronous MCP Client and MCP Server implementations
|
||||
* Standard MCP operations support:
|
||||
** Protocol version compatibility negotiation
|
||||
** Tool discovery, execution, list change notifications
|
||||
** Resource management with URI templates
|
||||
** Roots list management and notifications
|
||||
** Prompt handling and management
|
||||
** Smampling support for AI model interactions
|
||||
** Protocol link:https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#initialization[version compatibility negotiation]
|
||||
** link:https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/[Tool] discovery, execution, list change notifications
|
||||
** link:https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/[Resource] management with URI templates
|
||||
** link:https://spec.modelcontextprotocol.io/specification/2024-11-05/client/roots/[Roots] list management and notifications
|
||||
** link:https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/[Prompt] handling and management
|
||||
** link:https://spec.modelcontextprotocol.io/specification/2024-11-05/client/sampling/[Sampling] support for AI model interactions
|
||||
* Multiple transport implementations:
|
||||
** Stdio-based transport for process-based communication
|
||||
** SSE-based transport for HTTP streaming
|
||||
** Core transports:
|
||||
*** Stdio-based transport for process-based communication
|
||||
*** Java HttpClient-based SSE transport for HTTP streaming
|
||||
** Spring-based transports:
|
||||
*** WebFlux SSE transport for reactive HTTP streaming
|
||||
*** WebMVC SSE transport for servlet-based HTTP streaming
|
||||
|
||||
=== Dependencies
|
||||
|
||||
Add the following dependency to your Maven project:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
The core MCP functionality:
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
|
|
@ -28,6 +38,51 @@ Add the following dependency to your Maven project:
|
|||
<version>0.5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
----
|
||||
+
|
||||
For HTTP SSE transport implementations, add one of the following dependencies
|
||||
(Note that you already have HTTP Client SSE client transport in the core module):
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<!-- Spring WebFlux-based SSE client and server transport -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webflux-sse-transport</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring WebMVC-based SSE server transport -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webmvc-sse-transport</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
Gradle::
|
||||
+
|
||||
The core MCP functionality:
|
||||
+
|
||||
[source,groovy]
|
||||
----
|
||||
dependencies {
|
||||
implementation 'org.springframework.experimental:mcp'
|
||||
}
|
||||
----
|
||||
+
|
||||
For HTTP SSE transport implementations, add one of the following dependencies
|
||||
(note that you already have HTTP Client SSE client transport in the core module):
|
||||
+
|
||||
[source,groovy]
|
||||
----
|
||||
// Spring WebFlux-based SSE client and server transport
|
||||
implementation 'org.springframework.experimental:mcp-webflux-sse-transport'
|
||||
|
||||
// Spring WebMVC-based SSE server transport
|
||||
implementation 'org.springframework.experimental:mcp-webmvc-sse-transport'
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
== Architecture
|
||||
|
||||
|
|
@ -37,7 +92,9 @@ The SDK follows a layered architecture with clear separation of concerns:
|
|||
|
||||
* *Client/Server Layer*: Both use McpSession for sync/async operations, with McpClient handling client-side protocol operations and McpServer managing server-side protocol operations.
|
||||
* *Session Layer (McpSession)*: Manages communication patterns and state using DefaultMcpSession implementation.
|
||||
* *Transport Layer (McpTransport)*: Handles JSON-RPC message serialization/deserialization via StdioTransport (stdin/stdout) and SseTransport (HTTP streaming).
|
||||
* *Transport Layer (McpTransport)*: Handles JSON-RPC message serialization/deserialization via:
|
||||
** StdioTransport (stdin/stdout) in the core module
|
||||
** HTTP SSE transports in dedicated transport modules (Java HttpClient, Spring WebFlux, Spring WebMVC)
|
||||
|
||||
Following class diagram illustrates the layered architecture of the MCP SDK, showing the relationships between core interfaces (McpTransport, McpSession), their implementations, and the client/server components. It highlights how the transport layer connects to sessions, which in turn support both synchronous and asynchronous client/server implementations.
|
||||
|
||||
|
|
@ -187,23 +244,36 @@ The transport layer handles the communication between MCP clients and servers, p
|
|||
======
|
||||
STDIO::
|
||||
+
|
||||
Creates transport for in-process based communication
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
// Create transport for process-based communication
|
||||
ServerParameters params = ServerParameters.builder("npx")
|
||||
.args("-y", "@modelcontextprotocol/server-everything", "dir")
|
||||
.build();
|
||||
McpTransport transport = new StdioClientTransport(params);
|
||||
----
|
||||
|
||||
SSE::
|
||||
+
|
||||
SSE (HttpClient)::
|
||||
+
|
||||
Creates a framework agnostic (pure Java API) SSE client transport.
|
||||
Included in the core `mcp` module.
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
McpTransport transport = new HttpClientSseClientTransport("http://your-mcp-server");
|
||||
----
|
||||
+
|
||||
SSE (WebFlux)::
|
||||
+
|
||||
Creates WebFlux-based SSE client transport.
|
||||
Requires the `mcp-webflux-sse-transport` dependency.
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
// Create transport for HTTP streaming
|
||||
WebClient.Builder webClientBuilder = WebClient.builder()
|
||||
.baseUrl("http://your-mcp-server");
|
||||
McpTransport transport = new SseClientTransport(webClientBuilder);
|
||||
McpTransport transport = new WebFluxSseClientTransport(webClientBuilder);
|
||||
----
|
||||
======
|
||||
|
||||
|
|
@ -381,29 +451,72 @@ The server transport layer implements the server-side communication protocols, e
|
|||
======
|
||||
STDIO::
|
||||
+
|
||||
Create in-process based transport
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
// Create transport with custom ObjectMapper
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
StdioServerTransport transport = new StdioServerTransport(mapper);
|
||||
StdioServerTransport transport = new StdioServerTransport(new ObjectMapper());
|
||||
----
|
||||
+
|
||||
Provides bidirectional JSON-RPC message handling over standard input/output streams with non-blocking message processing, serialization/deserialization, and graceful shutdown support.
|
||||
|
||||
SSE::
|
||||
SSE (WebFlux)::
|
||||
+
|
||||
Creates WebFlux-based SSE server transport.
|
||||
Requires the `mcp-webflux-sse-transport` dependency.
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
// Create SSE transport
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String messageEndpoint = "/mcp/message";
|
||||
SseServerTransport transport = new SseServerTransport(mapper, messageEndpoint);
|
||||
@Configuration
|
||||
class McpConfig {
|
||||
@Bean
|
||||
WebFluxSseServerTransport webFluxSseServerTransport(ObjectMapper mapper) {
|
||||
return new WebFluxSseServerTransport(mapper, "/mcp/message");
|
||||
}
|
||||
|
||||
// Get router function for web server configuration
|
||||
RouterFunction<?> router = transport.getRouterFunction();
|
||||
@Bean
|
||||
RouterFunction<?> mcpRouterFunction(WebFluxSseServerTransport transport) {
|
||||
return transport.getRouterFunction();
|
||||
}
|
||||
}
|
||||
----
|
||||
+
|
||||
Implements the MCP HTTP with SSE transport specification, providing concurrent client connections through SSE endpoints with message routing, session management, and graceful shutdown capabilities.
|
||||
Implements the MCP HTTP with SSE transport specification, providing:
|
||||
+
|
||||
* Reactive HTTP streaming with WebFlux
|
||||
* Concurrent client connections through SSE endpoints
|
||||
* Message routing and session management
|
||||
* Graceful shutdown capabilities
|
||||
|
||||
SSE (WebMvc)::
|
||||
+
|
||||
Creates WebMvc-based SSE server transport.
|
||||
Requires the `mcp-webmvc-sse-transport` dependency.
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
class McpConfig {
|
||||
@Bean
|
||||
WebMvcSseServerTransport webMvcSseServerTransport(ObjectMapper mapper) {
|
||||
return new WebMvcSseServerTransport(mapper, "/mcp/message");
|
||||
}
|
||||
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> mcpRouterFunction(WebMvcSseServerTransport transport) {
|
||||
return transport.getRouterFunction();
|
||||
}
|
||||
}
|
||||
----
|
||||
+
|
||||
Implements the MCP HTTP with SSE transport specification, providing:
|
||||
+
|
||||
* Servlet-based HTTP streaming with Spring MVC
|
||||
* Concurrent client connections through SSE endpoints
|
||||
* Message routing and session management
|
||||
* Graceful shutdown capabilities
|
||||
|
||||
======
|
||||
|
||||
=== Server Capabilities
|
||||
|
|
|
|||
|
|
@ -13,19 +13,38 @@ There are two main projects in this repository:
|
|||
|
||||
Java implementation of the Model Context Protocol specification. It includes:
|
||||
|
||||
* Synchronous and asynchronous link:https://github.com/spring-projects-experimental/spring-ai-mcp/blob/main/mcp/README.md#client-usage-examples[MCP Client] and link:https://github.com/spring-projects-experimental/spring-ai-mcp/blob/main/mcp/README.md#server-usage-examples[MCP Server] implementations
|
||||
* Standard MCP operations support (tool discovery, resource management, prompt handling, structured logging). Support for request and notification handling.
|
||||
* link:https://spec.modelcontextprotocol.io/specification/basic/transports/#stdio[Stdio] and link:https://spec.modelcontextprotocol.io/specification/basic/transports/#http-with-sse[SSE] transport implementations.
|
||||
* Synchronous and asynchronous xref:mcp.adoc#mcp-client[MCP Client] and xref:mcp.adoc#mcp-server[MCP Server] implementations
|
||||
* Standard MCP operations support:
|
||||
** Tool discovery and execution
|
||||
** Resource management with URI templates
|
||||
** Prompt handling and management
|
||||
** Structured logging
|
||||
** Request and notification handling
|
||||
* Multiple transport implementations:
|
||||
** Core transports:
|
||||
*** Stdio-based transport for process-based communication
|
||||
*** Java HttpClient-based SSE transport for HTTP streaming
|
||||
** Spring-based transports:
|
||||
*** WebFlux SSE transport for reactive HTTP streaming
|
||||
*** WebMVC SSE transport for servlet-based HTTP streaming
|
||||
|
||||
==== WebFlux SSE Transport
|
||||
The link:https://github.com/spring-projects-experimental/spring-ai-mcp/tree/main/mcp-transport/mcp-webflux-sse-transport[mcp-webflux-sse-transport] project provides WebFlux-based implementation for the SSE client and server transports, enabling reactive HTTP streaming with Spring WebFlux.
|
||||
|
||||
==== WebMvc SSE Transport
|
||||
The link:https://github.com/spring-projects-experimental/spring-ai-mcp/tree/main/mcp-transport/mcp-webmvc-sse-transport[mcp-webmvc-sse-transport] project provides WebMvc-based implementation for the SSE server transport. The core module's HttpClientSseClientTransport can be used for client-side communication.
|
||||
|
||||
=== xref:spring-mcp.adoc[Spring AI MCP]
|
||||
|
||||
The Spring integration module provides Spring-specific functionality:
|
||||
|
||||
* Integration with Spring AI's function calling system
|
||||
* Bidirectional conversion between Spring AI function callbacks and MCP tools
|
||||
* JSON schema generation for tool input validation
|
||||
* Automatic type conversion and error handling
|
||||
* Spring-friendly abstractions for MCP clients
|
||||
* Auto-configurations (WIP)
|
||||
|
||||
|
||||
== Installation
|
||||
|
||||
Add the following dependencies to your Maven project:
|
||||
|
|
@ -34,9 +53,10 @@ Add the following dependencies to your Maven project:
|
|||
======
|
||||
Maven::
|
||||
+
|
||||
The core MCP functionality:
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<!-- For core MCP functionality -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp</artifactId>
|
||||
|
|
@ -44,9 +64,29 @@ Maven::
|
|||
</dependency>
|
||||
----
|
||||
+
|
||||
For HTTP SSE transport implementations, add one or more of the following dependencies:
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<!-- Spring WebFlux-based SSE client and server transport -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webflux-sse-transport</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring WebMVC-based SSE server transport -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-webmvc-sse-transport</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
----
|
||||
+
|
||||
For Spring AI integration:
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<!-- For Spring AI integration -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>spring-ai-mcp</artifactId>
|
||||
|
|
@ -55,31 +95,45 @@ Maven::
|
|||
----
|
||||
+
|
||||
This is a milestone release, not available on Maven Central.
|
||||
Add this repository to your POM:
|
||||
Add the Spring milestone repository to your POM:
|
||||
+
|
||||
[source,xml]
|
||||
----
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/libs-milestone-local</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
----
|
||||
|
||||
Gradle::
|
||||
+
|
||||
The core MCP functionality:
|
||||
+
|
||||
[source,groovy]
|
||||
----
|
||||
dependencies {
|
||||
implementation 'org.springframework.experimental:mcp'
|
||||
}
|
||||
----
|
||||
Or for Spring AI integration:
|
||||
+
|
||||
For HTTP SSE transport implementations, add one or more of the following dependencies:
|
||||
+
|
||||
[source,groovy]
|
||||
----
|
||||
// Spring WebFlux-based SSE client and server transport
|
||||
implementation 'org.springframework.experimental:mcp-webflux-sse-transport'
|
||||
|
||||
// Spring WebMVC-based SSE server transport
|
||||
implementation 'org.springframework.experimental:mcp-webmvc-sse-transport'
|
||||
----
|
||||
+
|
||||
For Spring AI integration:
|
||||
+
|
||||
[source,groovy]
|
||||
----
|
||||
|
|
@ -87,6 +141,16 @@ dependencies {
|
|||
implementation 'org.springframework.experimental:spring-ai-mcp'
|
||||
}
|
||||
----
|
||||
+
|
||||
This is a milestone release, not available on Maven Central.
|
||||
Add the Spring milestone repository to your build:
|
||||
+
|
||||
[source,groovy]
|
||||
----
|
||||
repositories {
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
== Examples
|
||||
|
|
@ -96,14 +160,14 @@ Explore these MCP examples in the link:https://github.com/spring-projects/spring
|
|||
* link:https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/sqlite/simple[SQLite Simple] - Demonstrates LLM integration with a database
|
||||
* link:https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/sqlite/chatbot[SQLite Chatbot] - Interactive chatbot with SQLite database interaction
|
||||
* link:https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/filesystem[Filesystem] - Enables LLM interaction with local filesystem folders and files
|
||||
* link:https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/brave[Brave] - Enables natural language interactions with Brave Search, allowing you to perform internet searches.
|
||||
* link:./spring-ai-mcp-sample/[Spring-ai-mcp-sample] - Showcases how to create and use MCP servers and clients with different transport modes and capabilities.
|
||||
* link:https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/brave[Brave] - Enables natural language interactions with Brave Search
|
||||
* link:https://github.com/spring-projects-experimental/spring-ai-mcp/tree/main/samples/spring-ai-mcp-sample[Spring-ai-mcp-sample] - Showcases MCP servers and clients with different transport modes and capabilities
|
||||
* link:https://github.com/habuma/spring-ai-examples/tree/main/spring-ai-mcp[Theme Park API Example] - Shows how to create an MCP server and client with Spring AI, exposing Theme Park API tools
|
||||
|
||||
== Documentation
|
||||
|
||||
* link:mcp/README.md[Java MCP SDK documentation]
|
||||
** link:docs/ref-index.md[Reference documentation]
|
||||
* link:spring-ai-mcp/README.md[Spring Integration documentation]
|
||||
* xref:mcp.adoc[Java MCP SDK Documentation]
|
||||
* xref:spring-mcp.adoc[Spring Integration Documentation]
|
||||
|
||||
== Development
|
||||
|
||||
|
|
|
|||
|
|
@ -14,18 +14,100 @@ The `spring-ai-mcp` module is part of the https://github.com/spring-projects-exp
|
|||
== Features
|
||||
|
||||
* Spring integration for MCP clients
|
||||
* Function callback implementation for tool invocations
|
||||
* Seamless integration with Spring AI's function calling capabilities
|
||||
* Bidirectional conversion between Spring AI function callbacks and MCP tools
|
||||
* JSON schema generation for tool input validation
|
||||
* Automatic type conversion and error handling
|
||||
* Integration with Spring AI's function calling capabilities
|
||||
|
||||
== Main Components
|
||||
|
||||
=== McpFunctionCallback
|
||||
|
||||
The `McpFunctionCallback` class is the primary component of this module. It implements Spring AI's `FunctionCallback` interface and provides:
|
||||
The `McpFunctionCallback` class implements Spring AI's `FunctionCallback` interface and provides integration between Spring AI's function calling system and MCP tools. Key features include:
|
||||
|
||||
* Integration between Spring AI's function calling system and MCP tools
|
||||
* Automatic conversion between JSON and Java objects for tool arguments
|
||||
* Synchronous tool execution support
|
||||
* Error handling and result formatting
|
||||
* Integration with Spring AI's function calling system
|
||||
|
||||
Example usage:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// Create an MCP client
|
||||
McpSyncClient mcpClient = McpClient.using(transport)
|
||||
.sync();
|
||||
|
||||
// Create a function callback for an MCP tool
|
||||
Tool calculatorTool = new Tool("calculator", "Basic calculator",
|
||||
Map.of("operation", "string", "a", "number", "b", "number"));
|
||||
FunctionCallback callback = new McpFunctionCallback(mcpClient, calculatorTool);
|
||||
|
||||
// Use the callback with Spring AI
|
||||
String result = callback.call("{\"operation\": \"add\", \"a\": 2, \"b\": 3}");
|
||||
----
|
||||
|
||||
=== ToolHelper
|
||||
|
||||
The `ToolHelper` utility class facilitates the integration between Spring AI's function callbacks and MCP's tool system. It provides methods for:
|
||||
|
||||
* Converting Spring AI's `FunctionCallback` instances to MCP tool registrations
|
||||
* Generating JSON schemas for tool input validation
|
||||
* Handling error cases and result formatting
|
||||
|
||||
Example usage:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// Convert Spring AI function callbacks to MCP tool registrations
|
||||
List<FunctionCallback> callbacks = List.of(
|
||||
new CalculatorFunction(),
|
||||
new WeatherFunction()
|
||||
);
|
||||
List<ToolRegistration> tools = ToolHelper.toToolRegistration(callbacks);
|
||||
|
||||
// Generate JSON schema for tool inputs
|
||||
Map<String, Class<?>> inputTypes = Map.of(
|
||||
"calculator", CalculatorInput.class,
|
||||
"weather", WeatherInput.class
|
||||
);
|
||||
String schema = ToolHelper.generateJsonSchema(inputTypes);
|
||||
----
|
||||
|
||||
==== Converting Function Callbacks to Tools
|
||||
|
||||
The `ToolHelper` provides several methods to convert Spring AI function callbacks to MCP tools:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// Convert a single function callback
|
||||
ToolRegistration tool = ToolHelper.toToolRegistration(myCallback);
|
||||
|
||||
// Convert multiple callbacks
|
||||
List<ToolRegistration> tools = ToolHelper.toToolRegistration(callback1, callback2);
|
||||
|
||||
// Convert a list of callbacks
|
||||
List<ToolRegistration> tools = ToolHelper.toToolRegistration(callbackList);
|
||||
----
|
||||
|
||||
==== JSON Schema Generation
|
||||
|
||||
The `ToolHelper` can generate JSON schemas for tool input validation:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// Using default ObjectMapper
|
||||
String schema = ToolHelper.generateJsonSchema(inputTypes);
|
||||
|
||||
// Using custom ObjectMapper
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String schema = ToolHelper.generateJsonSchema(inputTypes, mapper);
|
||||
----
|
||||
|
||||
The generated schema follows the JSON Schema Draft 2020-12 specification and:
|
||||
* Validates the structure of tool inputs
|
||||
* Excludes ToolContext class from schema generation
|
||||
* Uses Jackson's JsonSchemaGenerator for accurate type representation
|
||||
|
||||
== Usage
|
||||
|
||||
|
|
@ -39,3 +121,41 @@ To use this module, add the following dependency to your Maven project:
|
|||
<version>0.5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
=== Example: Creating an MCP Tool Server with Spring AI Functions
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
class McpConfig {
|
||||
|
||||
@Bean
|
||||
McpServer mcpServer(List<FunctionCallback> callbacks) {
|
||||
// Convert Spring AI callbacks to MCP tools
|
||||
List<ToolRegistration> tools = ToolHelper.toToolRegistration(callbacks);
|
||||
|
||||
return McpServer.using(transport)
|
||||
.info("spring-ai-server", "1.0.0")
|
||||
.tools(tools)
|
||||
.sync();
|
||||
}
|
||||
|
||||
@Bean
|
||||
FunctionCallback calculatorFunction() {
|
||||
return FunctionCallback.builder()
|
||||
.name("calculator")
|
||||
.description("Basic calculator")
|
||||
.function(input -> {
|
||||
// Function implementation
|
||||
return result;
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
This configuration:
|
||||
1. Creates Spring AI function callbacks
|
||||
2. Converts them to MCP tools using ToolHelper
|
||||
3. Registers the tools with an MCP server
|
||||
4. Makes the tools available for discovery and execution by MCP clients
|
||||
|
|
|
|||
101
mcp-test/pom.xml
Normal file
101
mcp-test/pom.xml
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp-parent</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>mcp-test</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Tests for the Java MCP SDK</name>
|
||||
<description>Provides some shared test fasilities for the MCP Java SDK</description>
|
||||
<url>https://github.com/spring-projects-experimental/spring-ai-mcp</url>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/spring-projects-experimental/spring-ai-mcp</url>
|
||||
<connection>git://github.com/spring-projects-experimental/spring-ai-mcp.git</connection>
|
||||
<developerConnection>git@github.com:spring-projects-experimental/spring-ai-mcp.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.experimental</groupId>
|
||||
<artifactId>mcp</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j-api.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>${assert4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<version>4.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>5.0.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
<version>10.1.18</version>
|
||||
<scope>test</scope>
|
||||
</dependency> -->
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.mcp;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
||||
import org.springframework.ai.mcp.spec.McpSchema;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCNotification;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCRequest;
|
||||
import org.springframework.ai.mcp.spec.ServerMcpTransport;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class MockMcpTransport implements ClientMcpTransport, ServerMcpTransport {
|
||||
|
||||
private final AtomicInteger inboundMessageCount = new AtomicInteger(0);
|
||||
|
||||
private final Sinks.Many<McpSchema.JSONRPCMessage> outgoing = Sinks.many().multicast().onBackpressureBuffer();
|
||||
|
||||
private final Sinks.Many<McpSchema.JSONRPCMessage> inbound = Sinks.many().unicast().onBackpressureBuffer();
|
||||
|
||||
private final Flux<McpSchema.JSONRPCMessage> outboundView = outgoing.asFlux().cache(1);
|
||||
|
||||
public void simulateIncomingMessage(McpSchema.JSONRPCMessage message) {
|
||||
if (inbound.tryEmitNext(message).isFailure()) {
|
||||
throw new RuntimeException("Failed to emit message " + message);
|
||||
}
|
||||
inboundMessageCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||
if (outgoing.tryEmitNext(message).isFailure()) {
|
||||
return Mono.error(new RuntimeException("Can't emit outgoing message " + message));
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
public McpSchema.JSONRPCRequest getLastSentMessageAsRequest() {
|
||||
return (JSONRPCRequest) outboundView.blockFirst();
|
||||
}
|
||||
|
||||
public McpSchema.JSONRPCNotification getLastSentMessageAsNotifiation() {
|
||||
return (JSONRPCNotification) outboundView.blockFirst();
|
||||
}
|
||||
|
||||
public McpSchema.JSONRPCMessage getLastSentMessage() {
|
||||
return outboundView.blockFirst();
|
||||
}
|
||||
|
||||
private volatile boolean connected = false;
|
||||
|
||||
@Override
|
||||
public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
|
||||
if (connected) {
|
||||
return Mono.error(new IllegalStateException("Already connected"));
|
||||
}
|
||||
connected = true;
|
||||
return inbound.asFlux()
|
||||
.publishOn(Schedulers.boundedElastic())
|
||||
.flatMap(message -> Mono.just(message).transform(handler))
|
||||
.doFinally(signal -> connected = false)
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Mono.defer(() -> {
|
||||
connected = false;
|
||||
outgoing.tryEmitComplete();
|
||||
inbound.tryEmitComplete();
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
||||
return new ObjectMapper().convertValue(data, typeRef);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.mcp.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
||||
import org.springframework.ai.mcp.spec.McpSchema;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.CreateMessageRequest;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.CreateMessageResult;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Prompt;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Resource;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Root;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.SubscribeRequest;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Tool;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.UnsubscribeRequest;
|
||||
import org.springframework.ai.mcp.spec.McpTransport;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Test suite for the {@link McpAsyncClient} that can be used with different
|
||||
* {@link McpTransport} implementations.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Dariusz Jędrzejczyk
|
||||
*/
|
||||
public abstract class BlaAbstractMcpAsyncClientTests {
|
||||
|
||||
private McpAsyncClient mcpAsyncClient;
|
||||
|
||||
protected ClientMcpTransport mcpTransport;
|
||||
|
||||
private static final Duration TIMEOUT = Duration.ofSeconds(20);
|
||||
|
||||
private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!";
|
||||
|
||||
abstract protected ClientMcpTransport createMcpTransport();
|
||||
|
||||
protected void onStart() {
|
||||
}
|
||||
|
||||
protected void onClose() {
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
onStart();
|
||||
this.mcpTransport = createMcpTransport();
|
||||
|
||||
assertThatCode(() -> {
|
||||
mcpAsyncClient = McpClient.using(mcpTransport)
|
||||
.requestTimeout(TIMEOUT)
|
||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||
.async();
|
||||
mcpAsyncClient.initialize().block(Duration.ofSeconds(10));
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (mcpAsyncClient != null) {
|
||||
assertThatCode(() -> mcpAsyncClient.closeGracefully().block(Duration.ofSeconds(10)))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithInvalidArguments() {
|
||||
assertThatThrownBy(() -> McpClient.using(null).sync()).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Transport must not be null");
|
||||
|
||||
assertThatThrownBy(() -> McpClient.using(mcpTransport).requestTimeout(null).sync())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Request timeout must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListTools() {
|
||||
StepVerifier.create(mcpAsyncClient.listTools(null)).consumeNextWith(result -> {
|
||||
assertThat(result.tools()).isNotNull().isNotEmpty();
|
||||
|
||||
Tool firstTool = result.tools().get(0);
|
||||
assertThat(firstTool.name()).isNotNull();
|
||||
assertThat(firstTool.description()).isNotNull();
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPing() {
|
||||
assertThatCode(() -> mcpAsyncClient.ping().block()).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallTool() {
|
||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
||||
|
||||
StepVerifier.create(mcpAsyncClient.callTool(callToolRequest)).consumeNextWith(callToolResult -> {
|
||||
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
||||
assertThat(result.content()).isNotNull();
|
||||
assertThat(result.isError()).isNull();
|
||||
});
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallToolWithInvalidTool() {
|
||||
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", ECHO_TEST_MESSAGE));
|
||||
|
||||
assertThatThrownBy(() -> mcpAsyncClient.callTool(invalidRequest).block()).isInstanceOf(Exception.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListResources() {
|
||||
StepVerifier.create(mcpAsyncClient.listResources(null)).consumeNextWith(resources -> {
|
||||
assertThat(resources).isNotNull().satisfies(result -> {
|
||||
assertThat(result.resources()).isNotNull();
|
||||
|
||||
if (!result.resources().isEmpty()) {
|
||||
Resource firstResource = result.resources().get(0);
|
||||
assertThat(firstResource.uri()).isNotNull();
|
||||
assertThat(firstResource.name()).isNotNull();
|
||||
}
|
||||
});
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMcpAsyncClientState() {
|
||||
assertThat(mcpAsyncClient).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListPrompts() {
|
||||
StepVerifier.create(mcpAsyncClient.listPrompts(null)).consumeNextWith(prompts -> {
|
||||
assertThat(prompts).isNotNull().satisfies(result -> {
|
||||
assertThat(result.prompts()).isNotNull();
|
||||
|
||||
if (!result.prompts().isEmpty()) {
|
||||
Prompt firstPrompt = result.prompts().get(0);
|
||||
assertThat(firstPrompt.name()).isNotNull();
|
||||
assertThat(firstPrompt.description()).isNotNull();
|
||||
}
|
||||
});
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetPrompt() {
|
||||
StepVerifier.create(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of())))
|
||||
.consumeNextWith(prompt -> {
|
||||
assertThat(prompt).isNotNull().satisfies(result -> {
|
||||
assertThat(result.messages()).isNotEmpty();
|
||||
assertThat(result.messages()).hasSize(1);
|
||||
});
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRootsListChanged() {
|
||||
assertThatCode(() -> mcpAsyncClient.rootsListChangedNotification().block()).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitializeWithRootsListProviders() {
|
||||
var transport = createMcpTransport();
|
||||
|
||||
var client = McpClient.using(transport)
|
||||
.requestTimeout(TIMEOUT)
|
||||
.roots(new Root("file:///test/path", "test-root"))
|
||||
.async();
|
||||
|
||||
assertThatCode(() -> client.initialize().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||
|
||||
assertThatCode(() -> client.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddRoot() {
|
||||
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
||||
assertThatCode(() -> mcpAsyncClient.addRoot(newRoot).block()).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddRootWithNullValue() {
|
||||
assertThatThrownBy(() -> mcpAsyncClient.addRoot(null).block()).hasMessageContaining("Root must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveRoot() {
|
||||
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
||||
assertThatCode(() -> {
|
||||
mcpAsyncClient.addRoot(root).block();
|
||||
mcpAsyncClient.removeRoot(root.uri()).block();
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveNonExistentRoot() {
|
||||
assertThatThrownBy(() -> mcpAsyncClient.removeRoot("nonexistent-uri").block())
|
||||
.hasMessageContaining("Root with uri 'nonexistent-uri' not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testReadResource() {
|
||||
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
||||
if (!resources.resources().isEmpty()) {
|
||||
Resource firstResource = resources.resources().get(0);
|
||||
StepVerifier.create(mcpAsyncClient.readResource(firstResource)).consumeNextWith(result -> {
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.contents()).isNotNull();
|
||||
}).verifyComplete();
|
||||
}
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListResourceTemplates() {
|
||||
StepVerifier.create(mcpAsyncClient.listResourceTemplates()).consumeNextWith(result -> {
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.resourceTemplates()).isNotNull();
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
// @Test
|
||||
void testResourceSubscription() {
|
||||
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
||||
if (!resources.resources().isEmpty()) {
|
||||
Resource firstResource = resources.resources().get(0);
|
||||
|
||||
// Test subscribe
|
||||
StepVerifier.create(mcpAsyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
||||
.verifyComplete();
|
||||
|
||||
// Test unsubscribe
|
||||
StepVerifier.create(mcpAsyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
||||
.verifyComplete();
|
||||
}
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNotificationHandlers() {
|
||||
AtomicBoolean toolsNotificationReceived = new AtomicBoolean(false);
|
||||
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||
|
||||
var transport = createMcpTransport();
|
||||
var client = McpClient.using(transport)
|
||||
.requestTimeout(TIMEOUT)
|
||||
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
||||
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
||||
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
||||
.async();
|
||||
|
||||
assertThatCode(() -> {
|
||||
client.initialize().block();
|
||||
// Trigger notifications
|
||||
client.sendResourcesListChanged().block();
|
||||
client.promptListChangedNotification().block();
|
||||
client.closeGracefully().block();
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitializeWithSamplingCapability() {
|
||||
var transport = createMcpTransport();
|
||||
|
||||
var capabilities = ClientCapabilities.builder().sampling().build();
|
||||
|
||||
var client = McpClient.using(transport).requestTimeout(TIMEOUT).capabilities(capabilities).sampling(request -> {
|
||||
return CreateMessageResult.builder().message("test").model("test-model").build();
|
||||
}).async();
|
||||
|
||||
assertThatCode(() -> {
|
||||
client.initialize().block(Duration.ofSeconds(10));
|
||||
client.closeGracefully().block(Duration.ofSeconds(10));
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitializeWithAllCapabilities() {
|
||||
var transport = createMcpTransport();
|
||||
|
||||
var capabilities = ClientCapabilities.builder()
|
||||
.experimental(Map.of("feature", "test"))
|
||||
.roots(true)
|
||||
.sampling()
|
||||
.build();
|
||||
|
||||
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
||||
return CreateMessageResult.builder().message("test").model("test-model").build();
|
||||
};
|
||||
var client = McpClient.using(transport)
|
||||
.requestTimeout(TIMEOUT)
|
||||
.capabilities(capabilities)
|
||||
.sampling(samplingHandler)
|
||||
.async();
|
||||
|
||||
assertThatCode(() -> {
|
||||
var result = client.initialize().block(Duration.ofSeconds(10));
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.capabilities()).isNotNull();
|
||||
client.closeGracefully().block(Duration.ofSeconds(10));
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Logging Tests
|
||||
// ---------------------------------------
|
||||
|
||||
@Test
|
||||
void testLoggingLevels() {
|
||||
// Test all logging levels
|
||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
||||
StepVerifier.create(mcpAsyncClient.setLoggingLevel(level)).verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingConsumer() {
|
||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||
var transport = createMcpTransport();
|
||||
|
||||
var client = McpClient.using(transport)
|
||||
.requestTimeout(TIMEOUT)
|
||||
.loggingConsumer(notification -> logReceived.set(true))
|
||||
.async();
|
||||
|
||||
assertThatCode(() -> {
|
||||
client.initialize().block(Duration.ofSeconds(10));
|
||||
client.closeGracefully().block(Duration.ofSeconds(10));
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingWithNullNotification() {
|
||||
assertThatThrownBy(() -> mcpAsyncClient.setLoggingLevel(null).block())
|
||||
.hasMessageContaining("Logging level must not be null");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.mcp.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.mcp.spec.ClientMcpTransport;
|
||||
import org.springframework.ai.mcp.spec.McpSchema;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.ListResourceTemplatesResult;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.ListResourcesResult;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.ListToolsResult;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.ReadResourceResult;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Resource;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Root;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.SubscribeRequest;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.TextContent;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Tool;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.UnsubscribeRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for MCP Client Session functionality.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Dariusz Jędrzejczyk
|
||||
*/
|
||||
public abstract class BlaAbstractMcpSyncClientTests {
|
||||
|
||||
private McpSyncClient mcpSyncClient;
|
||||
|
||||
private static final Duration TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
private static final String TEST_MESSAGE = "Hello MCP Spring AI!";
|
||||
|
||||
protected ClientMcpTransport mcpTransport;
|
||||
|
||||
abstract protected ClientMcpTransport createMcpTransport();
|
||||
|
||||
abstract protected void onStart();
|
||||
|
||||
abstract protected void onClose();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
onStart();
|
||||
this.mcpTransport = createMcpTransport();
|
||||
|
||||
assertThatCode(() -> {
|
||||
mcpSyncClient = McpClient.using(mcpTransport)
|
||||
.requestTimeout(TIMEOUT)
|
||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||
.sync();
|
||||
mcpSyncClient.initialize();
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (mcpSyncClient != null) {
|
||||
assertThatCode(() -> mcpSyncClient.close()).doesNotThrowAnyException();
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithInvalidArguments() {
|
||||
assertThatThrownBy(() -> McpClient.using(null).sync()).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Transport must not be null");
|
||||
|
||||
assertThatThrownBy(() -> McpClient.using(mcpTransport).requestTimeout(null).sync())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Request timeout must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListTools() {
|
||||
ListToolsResult tools = mcpSyncClient.listTools(null);
|
||||
|
||||
assertThat(tools).isNotNull().satisfies(result -> {
|
||||
assertThat(result.tools()).isNotNull().isNotEmpty();
|
||||
|
||||
Tool firstTool = result.tools().get(0);
|
||||
assertThat(firstTool.name()).isNotNull();
|
||||
assertThat(firstTool.description()).isNotNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallTools() {
|
||||
CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4)));
|
||||
|
||||
assertThat(toolResult).isNotNull().satisfies(result -> {
|
||||
|
||||
assertThat(result.content()).hasSize(1);
|
||||
|
||||
TextContent content = (TextContent) result.content().get(0);
|
||||
|
||||
assertThat(content).isNotNull();
|
||||
assertThat(content.text()).isNotNull();
|
||||
assertThat(content.text()).contains("7");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPing() {
|
||||
assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallTool() {
|
||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
||||
|
||||
CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest);
|
||||
|
||||
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
||||
assertThat(result.content()).isNotNull();
|
||||
assertThat(result.isError()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallToolWithInvalidTool() {
|
||||
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE));
|
||||
|
||||
assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRootsListChanged() {
|
||||
assertThatCode(() -> mcpSyncClient.rootsListChangedNotification()).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListResources() {
|
||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||
|
||||
assertThat(resources).isNotNull().satisfies(result -> {
|
||||
assertThat(result.resources()).isNotNull();
|
||||
|
||||
if (!result.resources().isEmpty()) {
|
||||
Resource firstResource = result.resources().get(0);
|
||||
assertThat(firstResource.uri()).isNotNull();
|
||||
assertThat(firstResource.name()).isNotNull();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClientSessionState() {
|
||||
assertThat(mcpSyncClient).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitializeWithRootsListProviders() {
|
||||
var transport = createMcpTransport();
|
||||
|
||||
var client = McpClient.using(transport)
|
||||
.requestTimeout(TIMEOUT)
|
||||
.roots(new Root("file:///test/path", "test-root"))
|
||||
.sync();
|
||||
|
||||
assertThatCode(() -> {
|
||||
client.initialize();
|
||||
client.close();
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddRoot() {
|
||||
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
||||
assertThatCode(() -> mcpSyncClient.addRoot(newRoot)).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddRootWithNullValue() {
|
||||
assertThatThrownBy(() -> mcpSyncClient.addRoot(null)).hasMessageContaining("Root must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveRoot() {
|
||||
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
||||
assertThatCode(() -> {
|
||||
mcpSyncClient.addRoot(root);
|
||||
mcpSyncClient.removeRoot(root.uri());
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveNonExistentRoot() {
|
||||
assertThatThrownBy(() -> mcpSyncClient.removeRoot("nonexistent-uri"))
|
||||
.hasMessageContaining("Root with uri 'nonexistent-uri' not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReadResource() {
|
||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||
|
||||
if (!resources.resources().isEmpty()) {
|
||||
Resource firstResource = resources.resources().get(0);
|
||||
ReadResourceResult result = mcpSyncClient.readResource(firstResource);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.contents()).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListResourceTemplates() {
|
||||
ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(null);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.resourceTemplates()).isNotNull();
|
||||
}
|
||||
|
||||
// @Test
|
||||
void testResourceSubscription() {
|
||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||
|
||||
if (!resources.resources().isEmpty()) {
|
||||
Resource firstResource = resources.resources().get(0);
|
||||
|
||||
// Test subscribe
|
||||
assertThatCode(() -> mcpSyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
||||
.doesNotThrowAnyException();
|
||||
|
||||
// Test unsubscribe
|
||||
assertThatCode(() -> mcpSyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNotificationHandlers() {
|
||||
AtomicBoolean toolsNotificationReceived = new AtomicBoolean(false);
|
||||
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||
|
||||
var transport = createMcpTransport();
|
||||
var client = McpClient.using(transport)
|
||||
.requestTimeout(TIMEOUT)
|
||||
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
||||
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
||||
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
||||
.sync();
|
||||
|
||||
assertThatCode(() -> {
|
||||
client.initialize();
|
||||
// Trigger notifications
|
||||
client.sendResourcesListChanged();
|
||||
client.promptListChangedNotification();
|
||||
client.close();
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Logging Tests
|
||||
// ---------------------------------------
|
||||
|
||||
@Test
|
||||
void testLoggingLevels() {
|
||||
// Test all logging levels
|
||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
||||
assertThatCode(() -> mcpSyncClient.setLoggingLevel(level)).doesNotThrowAnyException();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingConsumer() {
|
||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||
var transport = createMcpTransport();
|
||||
|
||||
var client = McpClient.using(transport)
|
||||
.requestTimeout(TIMEOUT)
|
||||
.loggingConsumer(notification -> logReceived.set(true))
|
||||
.sync();
|
||||
|
||||
assertThatCode(() -> {
|
||||
client.initialize();
|
||||
client.close();
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingWithNullNotification() {
|
||||
assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(null))
|
||||
.hasMessageContaining("Logging level must not be null");
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue