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:
Christian Tzolov 2025-01-08 16:21:21 +01:00
commit 611f3258d6
63 changed files with 5002 additions and 1588 deletions

105
0.5.0-BREAKING-CHANGES.md Normal file
View 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

View file

@ -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)

View file

@ -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`.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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");
});
}

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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

Before After
Before After

View file

@ -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

View file

@ -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

View file

@ -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
View 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>

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -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