feat: Add MCP server with Server stdio/sse transport improve MCP client

- Implement MCP server with async/sync support and builder pattern configuration
 - Add Server stdio/SSE transport layers with robust error handling
 - Convert McpClient and McpServer to interfaces for better extensibility
 - Add sampling capabilities for LLM interactions (text/image)
 - Implement dynamic root management and runtime tool management
 - Add comprehensive prompt and resource capabilities
 - Add support for dynamic root management (add/remove roots at runtime)

 Architecture Changes

 - Introduce registration records for tools, resources, and prompts
 - Improve thread safety with concurrent collections
 - Enhance resource template handling
 - Implement builder patterns for Server/Client capabilities
 - Add support for server capabilities and implementation info

Transport Layer:
 - Implement SseServerTransport with SSE endpoints
 - Add StdioServerTransport for process-based communication
 - Reorganize transport packages for better organization
 - Improve transport error handling and shutdown processes

Documentation:

 - Add detailed MCP reference documentation
 - Include architecture diagrams and API documentation
 - Add comprehensive test coverage for all components
 - Include sample projects demonstrating stdio/SSE transports

BREAKING CHANGES:

 Transport Package Reorganization

 - Move StdioClientTransport to o.s.ai.mcp.client.transport
 - Move SseClientTransport to o.s.ai.mcp.client.transport
 - Move ServerParameters to o.s.ai.mcp.client.transport
 - StdioClientTransport.errorHandler changed to private with
   setter method Migration: Use setErrorHandler() method
   instead of direct field access

 API Changes

 - Replace static factory methods with builder pattern
 - Update ServerCapabilities to use builder pattern
 - Make StdioClientTransport.errorHandler private with setter
 - Replace rootsListProvider with direct root management
 - Remove deprecated static factory methods

Resolves #27,#25,#24,#11
Related to #12
This commit is contained in:
Christian Tzolov 2024-12-19 19:38:38 +01:00
commit abd0facf4e

View file

@ -8,23 +8,19 @@ This SDK implements the Model Context Protocol, enabling seamless integration wi
## Features
- Synchronous and Asynchronous MCP Client implementations
- Synchronous and Asynchronous MCP Server implementations (WIP: comming with 0.3.0)
- Synchronous and Asynchronous MCP Client and MCP Server implementations
- Standard MCP operations support:
- Protocol version compatibility negotiation
- Client-server capability exchange
- Tool discovery and execution with change notifications
- Tool list change notifications with non-blocking consumer support
- Resource management with URI templates
- Resource subscription system
- Roots list management and notifications
- Prompt handling and management
- Server initialization and ping
- Multiple transport implementations:
- Stdio-based transport for process-based communication
- SSE-based transport for HTTP streaming
- Configurable request timeouts
- Customizable JSON serialization/deserialization
## Installation
@ -34,17 +30,116 @@ Add the following dependency to your Maven project:
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>mcp</artifactId>
<version>0.2.0-SNAPSHOT</version>
<version>0.3.0-SNAPSHOT</version>
</dependency>
```
## Usage
## Architecture
### Transport Layer Options
The SDK follows a layered architecture with clear separation of concerns:
The SDK provides two transport implementations:
### Core Components Hierarchy
#### StdioClientTransport
<img src="../docs/MCP-layers.svg" width="300" align="right"/>
- **Transport Layer (McpTransport)**: Handles JSON-RPC message serialization/deserialization via StdioTransport (stdin/stdout) and SseTransport (HTTP streaming).
- **Session Layer (McpSession)**: Manages communication patterns and state using DefaultMcpSession implementation.
- **Client/Server Layer**: Both use McpSession for sync/async operations, with McpClient handling client-side protocol operations and McpServer managing server-side protocol operations.
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.
<img src="../docs/spring-ai-mcp-uml-classdiagram.svg" width="600"/>
### Key Interactions
- **Client/Server Initialization**: Transport setup, protocol compatibility check, capability negotiation, and implementation details exchange.
- **Message Flow**: JSON-RPC message handling with validation, type-safe response processing, and error handling.
- **Resource Management**: Resource discovery, URI template-based access, subscription system, and content retrieval.
- **Prompt System**: Discovery, parameter-based retrieval, change notifications, and content management.
- **Tool Execution**: Discovery, parameter validation, timeout-aware execution, and result processing.
### [Reference Documentation](../docs/ref-index.md)
Detailed reference documentation covering the Spring AI implementation of the Model Context Protocol (MCP) specification.
## Quick Start
### McpClient
Factory for creating `sync` and `async` variants MCP clients. Uses McpSession for communication and handles client-side protocol operations.
#### Sync MCP Client
```java
// Create and initialize sync client
McpTransport mcpTransport = ...
try (McpSyncClient client = McpClient.using(mcpTransport).sync()) {
// Initialize connection with protocol version and capabilities
InitializeResult initResult = client.initialize();
// List available tools
ListToolsResult tools = client.listTools();
// Execute a tool
CallToolResult result = client.callTool(
new CallToolRequest("echo", Map.of("message", "Hello!"))
);
// Resource management
ListResourcesResult resources = client.listResources();
ReadResourceResult resource = client.readResource(
new ReadResourceRequest("resource-uri")
);
// List and retrieve prompts
ListPromptsResult prompts = client.listPrompts();
GetPromptResult prompt = client.getPrompt(
new GetPromptRequest("prompt-id", Map.of())
);
}
```
#### Async MCP Client
```java
McpTransport mcpTransport = ...
// Initialize async client with custom timeout
McpAsyncClient client = McpClient.using(mcpTransport)
.requestTimeout(Duration.ofSeconds(30))
.async();
// Initialize connection and chain operations
var result = client.initialize()
// Connection initialized with protocol version compatibility
.flatMap(initResult -> client.listTools())
// Process available tools
.flatMap(tools -> client.callTool(
new CallToolRequest("echo", Map.of("message", "Hello MCP!"))))
// Handle tool execution result
.flatMap(toolResult -> client.listPrompts())
// Process available prompts
.flatMap(prompts -> client.getPrompt(new GetPromptRequest("prompt-id", Map.of()));
// Subscribe to resource changes
var subscription = client.listResources()
.flatMap(resources -> client.subscribeResource(new SubscribeRequest("resource-uri")));
// Handle results reactively or block if needed
GetPromptResult promptResult = result.block();
subscription.block();
// Cleanup
client.closeGracefully().block();
```
#### Client Transports
Clients can use the following transport options:
##### StdioClientTransport
Standard I/O based transport for process-based communication with MCP servers:
```java
@ -54,7 +149,7 @@ ServerParameters params = ServerParameters.builder("npx")
McpTransport transport = new StdioClientTransport(params);
```
#### SseClientTransport
##### SseClientTransport
Server-Sent Events (SSE) based transport following the MCP HTTP with SSE transport specification:
```java
@ -63,279 +158,102 @@ WebClient.Builder webClientBuilder = WebClient.builder()
McpTransport transport = new SseClientTransport(webClientBuilder);
```
The SSE transport provides:
- Bidirectional communication over HTTP
- Automatic reconnection for transient failures
- Inbound message streaming via SSE
- Outbound message delivery via HTTP POST
- Graceful shutdown handling
- Configurable JSON serialization
### MCP Server
### Sync MCP Client Example
Factory for creating `sync` and `async` variants MCP servers. Uses McpSession for communication and handles server-side protocol operations.
#### MCP Sync Server
```java
// Create and initialize sync client
McpTransport mcpTransport = ...
// Create a sync server with custom configuration
McpSyncServer syncServer = McpServer.using(transport)
.serverInfo("my-server", "1.0.0")
.serverCapabilities(new ServerCapabilities(...))
.addTool(new CalculatorTool())
.resourcesProvider(cursor -> resources)
.promptsProvider(cursor -> prompts)
.sync();
try (McpSyncClient client = McpClient.using(mcpTransport).sync()) {
// Initialize connection with protocol version and capabilities
McpSchema.InitializeResult initResult = client.initialize();
// Add a tool handler at runtime
syncServer.addTool(new CalculatorTool());
logger.info("Tool added");
// List available tools
McpSchema.ListToolsResult tools = client.listTools();
// Remove a tool handler
syncServer.removeTool("calculator");
logger.info("Tool removed");
// Execute a tool
McpSchema.CallToolResult result = client.callTool(
new McpSchema.CallToolRequest("echo", Map.of("message", "Hello!"))
);
// Notify clients of changes
syncServer.notifyToolsListChanged();
syncServer.notifyResourcesListChanged();
syncServer.notifyPromptsListChanged();
// Resource management
McpSchema.ListResourcesResult resources = client.listResources();
McpSchema.ReadResourceResult resource = client.readResource(
new McpSchema.ReadResourceRequest("resource-uri")
);
// List and retrieve prompts
ListPromptsResult prompts = client.listPrompts();
GetPromptResult prompt = client.getPrompt(
new McpSchema.GetPromptRequest("prompt-id", Map.of())
);
}
// Graceful shutdown
syncServer.closeGracefully();
```
### Async MCP Client Example with Custom Configuration
#### MCP Async Server
```java
McpTransport mcpTransport = ...
// Initialize async client with custom timeout and object mapper
McpAsyncClient client = McpClient.using(mcpTransport)
.requestTimeout(Duration.ofSeconds(30))
// Create an async server with custom configuration
McpAsyncServer asyncServer = McpServer.using(transport)
.serverInfo("my-server", "1.0.0")
.serverCapabilities(new ServerCapabilities(...))
.addTool(new CalculatorTool())
.resourcesProvider(cursor -> resources)
.promptsProvider(cursor -> prompts)
.async();
// Initialize connection and chain operations
var result = client.initialize()
.flatMap(initResult -> {
// Connection initialized with protocol version compatibility
return client.listTools();
})
.flatMap(tools -> {
// Process available tools
return client.callTool(new McpSchema.CallToolRequest("echo",
Map.of("message", "Hello MCP!")));
})
.flatMap(toolResult -> {
// Handle tool execution result
return client.listPrompts();
})
.flatMap(prompts -> {
// Process available prompts
return client.getPrompt(new McpSchema.GetPromptRequest("prompt-id", Map.of()));
});
// Subscribe to resource changes
var subscription = client.listResources()
.flatMap(resources -> {
return client.subscribeResource(new McpSchema.SubscribeRequest("resource-uri"));
});
// Set up tools change notification handling
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers = List.of(
tools -> {
// Handle tools list changes reactively
tools.forEach(tool -> {
System.out.println("Tool updated: " + tool.name());
});
}
);
McpAsyncClient clientWithToolsNotifications = McpClient.using(transport)
.toolsChangeConsumer(toolsChangeConsumers)
.async();
// The client will now automatically handle tools/list_changed notifications
// and invoke the consumers on the boundedElastic scheduler to avoid blocking
// Handle results reactively or block if needed
McpSchema.GetPromptResult promptResult = result.block();
subscription.block();
// Cleanup
client.closeGracefully().block();
```
## Architecture
The SDK follows a layered architecture with clear separation of concerns:
### Core Components
- **McpClient**: Factory class for creating sync and async clients with optional custom configuration
- **McpAsyncClient**: Primary async implementation using Project Reactor for non-blocking operations
- **McpSyncClient**: Synchronous wrapper around the async client for blocking operations
- **McpSession**: Core session interface defining communication patterns
- **McpTransport**: Transport layer interface for client/server communication
- **McpSchema**: Comprehensive protocol schema definitions
- **DefaultMcpSession**: Default implementation of the session management
- **StdioClientTransport**: Standard I/O based client to server communication
- **SseClientTransport**: HTTP-based transport using Server-Sent Events for bidirectional client-sever communication
<img src="../docs/spring-ai-mcp-uml-classdiagram.svg" width="600"/>
### Key Interactions
1. Client Initialization
- Transport setup and connection establishment
- Protocol version compatibility check
- Capability negotiation
- Implementation details exchange
2. Message Flow
- JSON-RPC message creation and validation
- Transport layer handling
- Response processing with type safety
- Error handling with specific error codes
3. Resource Management
- Resource discovery and listing
- URI template-based resource access
- Subscription system for change notifications
- Resource content retrieval
4. Prompt System
- Prompt discovery and listing
- Parameter-based prompt retrieval
- Change notifications support
- Prompt content management
5. Tool Execution
- Tool discovery and capability checking
- Parameter validation and processing
- Execution handling with timeout support
- Result processing with error handling
### Roots List Support
The SDK supports the MCP roots list capability, which allows servers to understand which directories and files they have access to. Clients can provide a list of root directories/files and notify servers when this list changes.
#### Features
- Define root providers that supply filesystem access boundaries
- Support for roots list changed notifications
- Automatic roots list request handling
#### Example with Roots List Configuration
```java
// Create root providers
List<Supplier<List<Root>>> rootProviders = List.of(
() -> List.of(new Root("file:///workspace/project", "Project Root")),
() -> List.of(new Root("file:///workspace/docs", "Documentation"))
);
// Create async client with roots list support
McpAsyncClient client = new McpAsyncClient(
new StdioClientTransport(params),
Duration.ofSeconds(30),
rootProviders, // Configure root providers
true // Enable roots list changed notifications
);
// Initialize connection
client.initialize()
.doOnSuccess(result -> {
// Connection initialized with roots list capability
// Notify server when roots list changes
return client.sendRootsListChanged();
})
// Add a tool handler at runtime
asyncServer.addTool(new CalculatorTool())
.doOnSuccess(v -> logger.info("Tool added"))
.subscribe();
```
### Change Notifications Support
The SDK supports automatic handling of changes through a non-blocking notification system for tools, resources, and prompts:
#### Features
- Register multiple consumers for tools, resources, and prompts changes
- Non-blocking execution using Project Reactor's boundedElastic scheduler
- Automatic list request handling when notifications are received
- Error resilient with proper error handling and logging
#### Example with Change Notifications
```java
// Create tool change consumers
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers = List.of(
tools -> {
// First consumer - e.g., update UI
tools.forEach(tool -> updateToolsUI(tool));
},
tools -> {
// Second consumer - e.g., update cache
toolsCache.updateTools(tools);
}
);
// Create resource change consumers
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers = List.of(
resources -> {
// Handle resource changes
resources.forEach(resource -> {
System.out.println("Resource updated: " + resource.uri());
updateResourcesUI(resource);
});
}
);
// Create prompt change consumers
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers = List.of(
prompts -> {
// Handle prompt changes
prompts.forEach(prompt -> {
System.out.println("Prompt updated: " + prompt.name());
updatePromptsCache(prompt);
});
}
);
// Create client with change notification support
McpAsyncClient client = McpClient.using(transport)
.toolsChangeConsumer(toolsChangeConsumer)
.resourcesChangeConsumer(resourcesChangeConsumer)
.promptsChangeConsumer(promptsChangeConsumer)
.async();
// Initialize client
client.initialize()
.doOnSuccess(result -> {
// Client will automatically handle all change notifications
// and invoke consumers non-blockingly on boundedElastic scheduler
})
// Remove a tool handler
asyncServer.removeTool("calculator")
.doOnSuccess(v -> logger.info("Tool removed"))
.subscribe();
// Notify clients of changes
asyncServer.notifyToolsListChanged().subscribe();
asyncServer.notifyResourcesListChanged().subscribe();
asyncServer.notifyPromptsListChanged().subscribe();
// Graceful shutdown
asyncServer.closeGracefully().subscribe();
```
The change notification system ensures that all consumers are executed non-blockingly, preventing any potential performance impact from blocking implementations. All consumers are executed on Project Reactor's boundedElastic scheduler, making it safe to perform potentially blocking operations within the consumers.
#### Server Transports
Each type of notification handler operates independently:
- Tools: Handles notifications when available tools change
- Resources: Handles notifications when available resources change
- Prompts: Handles notifications when available prompts change
Servers can use the following transport options:
##### StdioServerTransport
Standard I/O based transport for process-based communication:
```java
// Create transport with custom ObjectMapper
ObjectMapper mapper = new ObjectMapper();
StdioServerTransport transport = new StdioServerTransport(mapper);
```
Provides bidirectional JSON-RPC message handling over standard input/output streams with non-blocking message processing, serialization/deserialization, and graceful shutdown support.
##### SseServerTransport
Server-Sent Events based transport for HTTP streaming:
```java
// Create SSE transport
ObjectMapper mapper = new ObjectMapper();
String messageEndpoint = "/mcp/message";
SseServerTransport transport = new SseServerTransport(mapper, messageEndpoint);
// Get router function for web server configuration
RouterFunction<?> router = 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.
## Error Handling
The SDK provides comprehensive error handling through the McpError class:
- Protocol version incompatibility
- Transport-level communication errors
- JSON-RPC protocol violations
- Tool execution failures
- Resource access and subscription errors
- Prompt management errors
- Request timeout handling
- Server capability mismatches
- SSE connection failures and retry handling
- HTTP request/response errors
The SDK provides comprehensive error handling through the McpError class, covering protocol compatibility, transport communication, JSON-RPC messaging, tool execution, resource management, prompt handling, timeouts, and connection issues. This unified error handling approach ensures consistent and reliable error management across both synchronous and asynchronous operations.
## Contributing