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:
parent
c83a45f880
commit
abd0facf4e
57 changed files with 5727 additions and 471 deletions
|
|
@ -42,14 +42,14 @@ Add the following dependencies to your Maven project:
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.experimental</groupId>
|
<groupId>org.springframework.experimental</groupId>
|
||||||
<artifactId>mcp</artifactId>
|
<artifactId>mcp</artifactId>
|
||||||
<version>0.2.0-SNAPSHOT</version>
|
<version>0.3.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- For Spring AI integration -->
|
<!-- For Spring AI integration -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.experimental</groupId>
|
<groupId>org.springframework.experimental</groupId>
|
||||||
<artifactId>spring-ai-mcp</artifactId>
|
<artifactId>spring-ai-mcp</artifactId>
|
||||||
<version>0.2.0-SNAPSHOT</version>
|
<version>0.3.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ mvn test
|
||||||
## Project Information
|
## Project Information
|
||||||
|
|
||||||
- **Group ID**: org.springframework.experimental
|
- **Group ID**: org.springframework.experimental
|
||||||
- **Version**: 0.2.0-SNAPSHOT
|
- **Version**: 0.3.0-SNAPSHOT
|
||||||
- **Java Version**: 17
|
- **Java Version**: 17
|
||||||
- **Spring AI Version**: 1.0.0-M4
|
- **Spring AI Version**: 1.0.0-M4
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
# MCP Session Components
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
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 robust foundation for request-response patterns and notification handling.
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
### McpSession Interface
|
|
||||||
|
|
||||||
The `McpSession` interface defines the contract for MCP communication with three primary operations:
|
|
||||||
|
|
||||||
1. Request-Response Communication
|
|
||||||
```java
|
|
||||||
<T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef);
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Notification Handling
|
|
||||||
```java
|
|
||||||
Mono<Void> sendNotification(String method, Map<String, Object> params);
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Session Lifecycle Management
|
|
||||||
```java
|
|
||||||
Mono<Void> closeGracefully();
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Transport Integration
|
|
||||||
- Abstracts transport details through `McpTransport` interface
|
|
||||||
- Supports different transport implementations (SSE, STDIO)
|
|
||||||
- Manages message serialization using Jackson
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
- The session processes three types of messages: McpSchema.JSONRPCResponse, McpSchema.JSONRPCRequest, McpSchema.JSONRPCNotificaiton.
|
|
||||||
- The implementation ensures thread safety through
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
WIP
|
|
||||||
|
|
||||||
## Integration Guidelines
|
|
||||||
|
|
||||||
1. **Transport Selection**
|
|
||||||
- Choose appropriate transport (SSE, STDIO) based on use case
|
|
||||||
- Configure transport-specific parameters
|
|
||||||
- Handle transport lifecycle properly
|
|
||||||
|
|
||||||
2. **Message Handling**
|
|
||||||
- Register handlers before starting session
|
|
||||||
- Use appropriate error codes from MCP specification
|
|
||||||
- Implement proper request/response correlation
|
|
||||||
|
|
||||||
3. **Type Safety**
|
|
||||||
- Use appropriate TypeReference for response deserialization
|
|
||||||
- Validate request parameters
|
|
||||||
- Handle type conversion errors
|
|
||||||
181
docs/MCP-layers.svg
Normal file
181
docs/MCP-layers.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 81 KiB |
|
|
@ -1,50 +1,82 @@
|
||||||
@startuml Core Components
|
@startuml Core Components
|
||||||
|
|
||||||
|
' Core Interfaces
|
||||||
interface McpTransport {
|
interface McpTransport {
|
||||||
+Mono<Void> connect(Function<Mono<JSONRPCMessage>,
|
+Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> handler)
|
||||||
Mono<JSONRPCMessage>> handler)
|
+Mono<Void> sendMessage(JSONRPCMessage message)
|
||||||
+Mono<Void> sendMessage()
|
|
||||||
+void close()
|
+void close()
|
||||||
+Mono<Void> closeGracefully()
|
+Mono<Void> closeGracefully()
|
||||||
|
+<T> T unmarshalFrom(Object data, TypeReference<T> typeRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface McpSession {
|
interface McpSession {
|
||||||
+<T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef)
|
+<T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef)
|
||||||
+Mono<Void> sendNotification(String method, Map<String, Object> params)
|
+Mono<Void> sendNotification(String method, Map<String, Object> params)
|
||||||
+Mono<Void> closeGracefully()
|
+Mono<Void> closeGracefully()
|
||||||
+close()
|
+void close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
' Core Implementation Classes
|
||||||
class DefaultMcpSession {
|
class DefaultMcpSession {
|
||||||
-McpTransport transport
|
-McpTransport transport
|
||||||
-ObjectMapper objectMapper
|
-ObjectMapper objectMapper
|
||||||
|
+interface RequestHandler
|
||||||
|
+interface NotificationHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
' Client Classes
|
||||||
class McpClient {
|
class McpClient {
|
||||||
+{static} async()
|
+{static} Builder using(McpTransport transport)
|
||||||
+{static} sync()
|
+{static} McpAsyncClient async(McpTransport transport)
|
||||||
|
+{static} McpSyncClient sync(McpTransport transport)
|
||||||
}
|
}
|
||||||
|
|
||||||
class McpAsyncClient {
|
class McpAsyncClient {
|
||||||
+Mono<InitializeResult> initialize()
|
+Mono<InitializeResult> initialize()
|
||||||
+Mono<CallToolResult> callTool()
|
+Mono<CallToolResult> callTool(CallToolRequest request)
|
||||||
+Mono<ListToolsResult> listTools()
|
+Mono<ListToolsResult> listTools()
|
||||||
+Mono<ListResourcesResult> listResources()
|
+Mono<ListResourcesResult> listResources()
|
||||||
+Mono<ReadResourceResult> readResource()
|
+Mono<ReadResourceResult> readResource(ReadResourceRequest request)
|
||||||
|
+Mono<ListResourceTemplatesResult> listResourceTemplates()
|
||||||
+Mono<ListPromptsResult> listPrompts()
|
+Mono<ListPromptsResult> listPrompts()
|
||||||
+Mono<GetPromptResult> getPrompt()
|
+Mono<GetPromptResult> getPrompt(GetPromptRequest request)
|
||||||
}
|
}
|
||||||
|
|
||||||
class McpSyncClient {
|
class McpSyncClient {
|
||||||
+InitializeResult initialize()
|
+InitializeResult initialize()
|
||||||
+CallToolResult callTool()
|
+CallToolResult callTool(CallToolRequest request)
|
||||||
+ListToolsResult listTools()
|
+ListToolsResult listTools()
|
||||||
+ListResourcesResult listResources()
|
+ListResourcesResult listResources()
|
||||||
+ReadResourceResult readResource()
|
+ReadResourceResult readResource(ReadResourceRequest request)
|
||||||
|
+ListResourceTemplatesResult listResourceTemplates()
|
||||||
+ListPromptsResult listPrompts()
|
+ListPromptsResult listPrompts()
|
||||||
+GetPromptResult getPrompt()
|
+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 {
|
class StdioClientTransport {
|
||||||
-Process serverProcess
|
-Process serverProcess
|
||||||
-ServerParameters parameters
|
-ServerParameters parameters
|
||||||
|
|
@ -54,12 +86,21 @@ class SseClientTransport {
|
||||||
-WebClient webClient
|
-WebClient webClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StdioServerTransport {
|
||||||
|
}
|
||||||
|
|
||||||
|
class SseServerTransport {
|
||||||
|
}
|
||||||
|
|
||||||
|
' Schema and Error Classes
|
||||||
class McpSchema {
|
class McpSchema {
|
||||||
+class ErrorCodes
|
+class ErrorCodes
|
||||||
+interface Request
|
+interface Request
|
||||||
+interface JSONRPCMessage
|
+interface JSONRPCMessage
|
||||||
+interface ResourceContents
|
+interface ResourceContents
|
||||||
+interface Content
|
+interface Content
|
||||||
|
+interface ServerCapabilities
|
||||||
|
+{static} JSONRPCMessage deserializeJsonRpcMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
class McpError {
|
class McpError {
|
||||||
|
|
@ -67,13 +108,24 @@ class McpError {
|
||||||
-String message
|
-String message
|
||||||
}
|
}
|
||||||
|
|
||||||
McpTransport <|-- StdioClientTransport
|
' Relationships
|
||||||
McpTransport <|-- SseClientTransport
|
McpTransport <|.. StdioClientTransport
|
||||||
McpSession <|-- DefaultMcpSession
|
McpTransport <|.. SseClientTransport
|
||||||
|
McpTransport <|.. StdioServerTransport
|
||||||
|
McpTransport <|.. SseServerTransport
|
||||||
|
|
||||||
|
McpSession <|.. DefaultMcpSession
|
||||||
DefaultMcpSession --o McpAsyncClient
|
DefaultMcpSession --o McpAsyncClient
|
||||||
|
DefaultMcpSession --o McpAsyncServer
|
||||||
|
|
||||||
McpClient ..> McpAsyncClient : creates
|
McpClient ..> McpAsyncClient : creates
|
||||||
McpClient ..> McpSyncClient : creates
|
McpClient ..> McpSyncClient : creates
|
||||||
McpSyncClient o-- McpAsyncClient
|
McpSyncClient o-- McpAsyncClient
|
||||||
|
|
||||||
|
McpServer ..> McpAsyncServer : creates
|
||||||
|
McpServer ..> McpSyncServer : creates
|
||||||
|
McpSyncServer o-- McpAsyncServer
|
||||||
|
|
||||||
DefaultMcpSession o-- McpTransport
|
DefaultMcpSession o-- McpTransport
|
||||||
McpSchema <.. McpSession : uses
|
McpSchema <.. McpSession : uses
|
||||||
McpError ..> McpSession : throws
|
McpError ..> McpSession : throws
|
||||||
|
|
@ -92,8 +144,11 @@ package "MCP Schema" {
|
||||||
|
|
||||||
class InitializeRequest
|
class InitializeRequest
|
||||||
class CallToolRequest
|
class CallToolRequest
|
||||||
class CreateMessageRequest
|
class ListToolsRequest
|
||||||
class CompleteRequest
|
class ListResourcesRequest
|
||||||
|
class ReadResourceRequest
|
||||||
|
class ListResourceTemplatesRequest
|
||||||
|
class ListPromptsRequest
|
||||||
class GetPromptRequest
|
class GetPromptRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,13 +168,27 @@ package "Resource Types" {
|
||||||
class TextContent
|
class TextContent
|
||||||
class ImageContent
|
class ImageContent
|
||||||
class EmbeddedResource
|
class EmbeddedResource
|
||||||
|
|
||||||
|
interface Annotated {
|
||||||
|
+Annotations annotations()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromptOrResourceReference {
|
||||||
|
+String type()
|
||||||
|
}
|
||||||
|
|
||||||
|
class PromptReference
|
||||||
|
class ResourceReference
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONRPCMessage <|.. Request
|
JSONRPCMessage <|.. Request
|
||||||
Request <|.. InitializeRequest
|
Request <|.. InitializeRequest
|
||||||
Request <|.. CallToolRequest
|
Request <|.. CallToolRequest
|
||||||
Request <|.. CreateMessageRequest
|
Request <|.. ListToolsRequest
|
||||||
Request <|.. CompleteRequest
|
Request <|.. ListResourcesRequest
|
||||||
|
Request <|.. ReadResourceRequest
|
||||||
|
Request <|.. ListResourceTemplatesRequest
|
||||||
|
Request <|.. ListPromptsRequest
|
||||||
Request <|.. GetPromptRequest
|
Request <|.. GetPromptRequest
|
||||||
|
|
||||||
ResourceContents <|.. TextResourceContents
|
ResourceContents <|.. TextResourceContents
|
||||||
|
|
@ -129,4 +198,7 @@ Content <|.. TextContent
|
||||||
Content <|.. ImageContent
|
Content <|.. ImageContent
|
||||||
Content <|.. EmbeddedResource
|
Content <|.. EmbeddedResource
|
||||||
|
|
||||||
|
PromptOrResourceReference <|.. PromptReference
|
||||||
|
PromptOrResourceReference <|.. ResourceReference
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
|
|
|
||||||
299
docs/ref-client.md
Normal file
299
docs/ref-client.md
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
# 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
|
||||||
154
docs/ref-errors.md
Normal file
154
docs/ref-errors.md
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
# 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
91
docs/ref-index.md
Normal file
91
docs/ref-index.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# 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
|
||||||
162
docs/ref-server.md
Normal file
162
docs/ref-server.md
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
# 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();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Notes
|
||||||
|
|
||||||
|
- The server factory uses a builder pattern for configuration
|
||||||
|
- Default server info is "mcp-server" version "1.0.0" if not specified
|
||||||
|
- 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
|
||||||
116
docs/ref-session.md
Normal file
116
docs/ref-session.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# 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.
|
||||||
124
docs/ref-transport.md
Normal file
124
docs/ref-transport.md
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
# 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.
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 44 KiB |
440
mcp/README.md
440
mcp/README.md
|
|
@ -8,23 +8,19 @@ This SDK implements the Model Context Protocol, enabling seamless integration wi
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Synchronous and Asynchronous MCP Client implementations
|
- Synchronous and Asynchronous MCP Client and MCP Server implementations
|
||||||
- Synchronous and Asynchronous MCP Server implementations (WIP: comming with 0.3.0)
|
|
||||||
- Standard MCP operations support:
|
- Standard MCP operations support:
|
||||||
- Protocol version compatibility negotiation
|
- Protocol version compatibility negotiation
|
||||||
- Client-server capability exchange
|
|
||||||
- Tool discovery and execution with change notifications
|
- Tool discovery and execution with change notifications
|
||||||
- Tool list change notifications with non-blocking consumer support
|
- Tool list change notifications with non-blocking consumer support
|
||||||
- Resource management with URI templates
|
- Resource management with URI templates
|
||||||
- Resource subscription system
|
- Resource subscription system
|
||||||
- Roots list management and notifications
|
- Roots list management and notifications
|
||||||
- Prompt handling and management
|
- Prompt handling and management
|
||||||
- Server initialization and ping
|
|
||||||
- Multiple transport implementations:
|
- Multiple transport implementations:
|
||||||
- Stdio-based transport for process-based communication
|
- Stdio-based transport for process-based communication
|
||||||
- SSE-based transport for HTTP streaming
|
- SSE-based transport for HTTP streaming
|
||||||
- Configurable request timeouts
|
- Configurable request timeouts
|
||||||
- Customizable JSON serialization/deserialization
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -34,17 +30,116 @@ Add the following dependency to your Maven project:
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.experimental</groupId>
|
<groupId>org.springframework.experimental</groupId>
|
||||||
<artifactId>mcp</artifactId>
|
<artifactId>mcp</artifactId>
|
||||||
<version>0.2.0-SNAPSHOT</version>
|
<version>0.3.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</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:
|
Standard I/O based transport for process-based communication with MCP servers:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|
@ -54,7 +149,7 @@ ServerParameters params = ServerParameters.builder("npx")
|
||||||
McpTransport transport = new StdioClientTransport(params);
|
McpTransport transport = new StdioClientTransport(params);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### SseClientTransport
|
##### SseClientTransport
|
||||||
Server-Sent Events (SSE) based transport following the MCP HTTP with SSE transport specification:
|
Server-Sent Events (SSE) based transport following the MCP HTTP with SSE transport specification:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|
@ -63,279 +158,102 @@ WebClient.Builder webClientBuilder = WebClient.builder()
|
||||||
McpTransport transport = new SseClientTransport(webClientBuilder);
|
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
|
```java
|
||||||
// Create and initialize sync client
|
// Create a sync server with custom configuration
|
||||||
McpTransport mcpTransport = ...
|
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()) {
|
// Add a tool handler at runtime
|
||||||
// Initialize connection with protocol version and capabilities
|
syncServer.addTool(new CalculatorTool());
|
||||||
McpSchema.InitializeResult initResult = client.initialize();
|
logger.info("Tool added");
|
||||||
|
|
||||||
// List available tools
|
// Remove a tool handler
|
||||||
McpSchema.ListToolsResult tools = client.listTools();
|
syncServer.removeTool("calculator");
|
||||||
|
logger.info("Tool removed");
|
||||||
|
|
||||||
// Execute a tool
|
// Notify clients of changes
|
||||||
McpSchema.CallToolResult result = client.callTool(
|
syncServer.notifyToolsListChanged();
|
||||||
new McpSchema.CallToolRequest("echo", Map.of("message", "Hello!"))
|
syncServer.notifyResourcesListChanged();
|
||||||
);
|
syncServer.notifyPromptsListChanged();
|
||||||
|
|
||||||
// Resource management
|
// Graceful shutdown
|
||||||
McpSchema.ListResourcesResult resources = client.listResources();
|
syncServer.closeGracefully();
|
||||||
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())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Async MCP Client Example with Custom Configuration
|
#### MCP Async Server
|
||||||
|
|
||||||
```java
|
```java
|
||||||
McpTransport mcpTransport = ...
|
// Create an async server with custom configuration
|
||||||
|
McpAsyncServer asyncServer = McpServer.using(transport)
|
||||||
// Initialize async client with custom timeout and object mapper
|
.serverInfo("my-server", "1.0.0")
|
||||||
McpAsyncClient client = McpClient.using(mcpTransport)
|
.serverCapabilities(new ServerCapabilities(...))
|
||||||
.requestTimeout(Duration.ofSeconds(30))
|
.addTool(new CalculatorTool())
|
||||||
|
.resourcesProvider(cursor -> resources)
|
||||||
|
.promptsProvider(cursor -> prompts)
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
// Initialize connection and chain operations
|
// Add a tool handler at runtime
|
||||||
var result = client.initialize()
|
asyncServer.addTool(new CalculatorTool())
|
||||||
.flatMap(initResult -> {
|
.doOnSuccess(v -> logger.info("Tool added"))
|
||||||
// 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();
|
|
||||||
})
|
|
||||||
.subscribe();
|
.subscribe();
|
||||||
```
|
|
||||||
|
|
||||||
### Change Notifications Support
|
// Remove a tool handler
|
||||||
|
asyncServer.removeTool("calculator")
|
||||||
The SDK supports automatic handling of changes through a non-blocking notification system for tools, resources, and prompts:
|
.doOnSuccess(v -> logger.info("Tool removed"))
|
||||||
|
|
||||||
#### 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
|
|
||||||
})
|
|
||||||
.subscribe();
|
.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:
|
Servers can use the following transport options:
|
||||||
- Tools: Handles notifications when available tools change
|
|
||||||
- Resources: Handles notifications when available resources change
|
##### StdioServerTransport
|
||||||
- Prompts: Handles notifications when available prompts change
|
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
|
## Error Handling
|
||||||
|
|
||||||
The SDK provides comprehensive error handling through the McpError class:
|
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.
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,13 @@
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-test</artifactId>
|
||||||
|
<version>${springframework.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.assertj</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
<artifactId>assertj-core</artifactId>
|
<artifactId>assertj-core</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,9 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
@ -34,8 +35,14 @@ import org.springframework.ai.mcp.spec.DefaultMcpSession.NotificationHandler;
|
||||||
import org.springframework.ai.mcp.spec.DefaultMcpSession.RequestHandler;
|
import org.springframework.ai.mcp.spec.DefaultMcpSession.RequestHandler;
|
||||||
import org.springframework.ai.mcp.spec.McpError;
|
import org.springframework.ai.mcp.spec.McpError;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema;
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities.RootCapabilities;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities.Sampling;
|
||||||
|
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.GetPromptRequest;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult;
|
import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.Implementation;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult;
|
import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.PaginatedRequest;
|
import org.springframework.ai.mcp.spec.McpSchema.PaginatedRequest;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.Root;
|
import org.springframework.ai.mcp.spec.McpSchema.Root;
|
||||||
|
|
@ -62,13 +69,38 @@ public class McpAsyncClient {
|
||||||
*/
|
*/
|
||||||
private final DefaultMcpSession mcpSession;
|
private final DefaultMcpSession mcpSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client capabilities.
|
||||||
|
*/
|
||||||
|
private final McpSchema.ClientCapabilities clientCapabilities;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Roots define the boundaries of where servers can operate within the filesystem,
|
* Roots define the boundaries of where servers can operate within the filesystem,
|
||||||
* allowing them to understand which directories and files they have access to.
|
* allowing them to understand which directories and files they have access to.
|
||||||
* Servers can request the list of roots from supporting clients and receive
|
* Servers can request the list of roots from supporting clients and receive
|
||||||
* notifications when that list changes.
|
* notifications when that list changes.
|
||||||
*/
|
*/
|
||||||
private McpSchema.ClientCapabilities.RootCapabilities rootCapabilities;
|
private final ConcurrentHashMap<String, Root> roots;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP provides a standardized way for servers to request LLM sampling (“completions”
|
||||||
|
* or “generations”) from language models via clients. This flow allows clients to
|
||||||
|
* maintain control over model access, selection, and permissions while enabling
|
||||||
|
* servers to leverage AI capabilities—with no server API keys necessary. Servers can
|
||||||
|
* request text or image-based interactions and optionally include context from MCP
|
||||||
|
* servers in their prompts.
|
||||||
|
*/
|
||||||
|
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client transport implementation.
|
||||||
|
*/
|
||||||
|
private final McpTransport transport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client implementation information.
|
||||||
|
*/
|
||||||
|
private Implementation clientInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new McpAsyncClient with the given transport and session request-response
|
* Create a new McpAsyncClient with the given transport and session request-response
|
||||||
|
|
@ -80,19 +112,35 @@ public class McpAsyncClient {
|
||||||
* @param rootsListChangedNotification whether the client supports roots/list_changed
|
* @param rootsListChangedNotification whether the client supports roots/list_changed
|
||||||
* notification.
|
* notification.
|
||||||
*/
|
*/
|
||||||
public McpAsyncClient(McpTransport transport, Duration requestTimeout,
|
public McpAsyncClient(McpTransport transport, Duration requestTimeout, Implementation clientInfo,
|
||||||
List<Supplier<List<Root>>> rootsListProviders, boolean rootsListChangedNotification,
|
ClientCapabilities clientCapabilities, Map<String, Root> roots,
|
||||||
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||||
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
||||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers) {
|
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||||
|
Function<CreateMessageRequest, CreateMessageResult> samplingHandler) {
|
||||||
|
|
||||||
|
this.clientInfo = clientInfo;
|
||||||
|
|
||||||
|
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
||||||
|
: new McpSchema.ClientCapabilities(null, !Utils.isEmpty(roots) ? new RootCapabilities(false) : null,
|
||||||
|
samplingHandler != null ? new Sampling() : null);
|
||||||
|
|
||||||
|
this.transport = transport;
|
||||||
|
|
||||||
|
this.roots = !Utils.isEmpty(roots) ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// Request Handlers
|
// Request Handlers
|
||||||
Map<String, RequestHandler> requestHanlers = new HashMap<>();
|
Map<String, RequestHandler> requestHanlers = new HashMap<>();
|
||||||
|
|
||||||
// Roots List Request Handler
|
// Roots List Request Handler
|
||||||
if (rootsListProviders != null && !rootsListProviders.isEmpty()) {
|
if (!Utils.isEmpty(this.roots) && this.clientCapabilities.roots() != null) {
|
||||||
requestHanlers.put("roots/list", rootsListRequestHandler(rootsListProviders));
|
requestHanlers.put("roots/list", rootsListRequestHandler());
|
||||||
this.rootCapabilities = new McpSchema.ClientCapabilities.RootCapabilities(rootsListChangedNotification);
|
}
|
||||||
|
|
||||||
|
// Sampling Handler
|
||||||
|
if (samplingHandler != null && this.clientCapabilities.sampling() != null) {
|
||||||
|
this.samplingHandler = samplingHandler;
|
||||||
|
requestHanlers.put("sampling/createMessage", samplingCreateMessageHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification Handlers
|
// Notification Handlers
|
||||||
|
|
@ -129,30 +177,75 @@ public class McpAsyncClient {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequestHandler rootsListRequestHandler(List<Supplier<List<Root>>> rootsListProviders) {
|
public Mono<Void> addRoot(Root root) {
|
||||||
return new RequestHandler() {
|
|
||||||
@Override
|
|
||||||
public Mono<Object> handle(Object params) {
|
|
||||||
if (rootsListProviders == null || rootsListProviders.isEmpty()) {
|
|
||||||
return Mono.just(new ArrayList<Root>());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Mono<List<Root>>> monos = rootsListProviders.stream()
|
if (root == null) {
|
||||||
.map(supplier -> Mono.fromSupplier(supplier).subscribeOn(Schedulers.boundedElastic()))
|
return Mono.error(new McpError("Root must not be null"));
|
||||||
.toList();
|
}
|
||||||
|
|
||||||
return Mono.zip(monos, arrays -> {
|
if (this.clientCapabilities.roots() == null) {
|
||||||
List<Root> combinedList = new ArrayList<>();
|
return Mono.error(new McpError("Client must be configured with roots capabilities"));
|
||||||
for (Object array : arrays) {
|
}
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Root> roots = (List<Root>) array;
|
if (this.roots.containsKey(root.uri())) {
|
||||||
combinedList.addAll(roots);
|
return Mono.error(new McpError("Root with uri '" + root.uri() + "' already exists"));
|
||||||
}
|
}
|
||||||
return combinedList;
|
|
||||||
});
|
this.roots.put(root.uri(), root);
|
||||||
|
|
||||||
|
logger.info("Added root: {}", root);
|
||||||
|
|
||||||
|
if (this.clientCapabilities.roots().listChanged()) {
|
||||||
|
return this.rootsListChangedNotification();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> removeRoot(String rootUri) {
|
||||||
|
|
||||||
|
if (rootUri == null) {
|
||||||
|
return Mono.error(new McpError("Root uri must not be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.clientCapabilities.roots() == null) {
|
||||||
|
return Mono.error(new McpError("Client must be configured with roots capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Root removed = this.roots.remove(rootUri);
|
||||||
|
|
||||||
|
if (removed != null) {
|
||||||
|
logger.info("Removed Root: {}", rootUri);
|
||||||
|
if (this.clientCapabilities.roots().listChanged()) {
|
||||||
|
return this.rootsListChangedNotification();
|
||||||
}
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
return Mono.error(new McpError("Root with uri '" + rootUri + "' not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestHandler samplingCreateMessageHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.CreateMessageRequest request = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.CreateMessageRequest>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
CreateMessageResult response = this.samplingHandler.apply(request);
|
||||||
|
|
||||||
|
return Mono.just(response);
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
|
private RequestHandler rootsListRequestHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.PaginatedRequest>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Root> roots = this.roots.values().stream().toList();
|
||||||
|
|
||||||
|
return Mono.just(roots);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private NotificationHandler toolsChangeNotificationHandler(
|
private NotificationHandler toolsChangeNotificationHandler(
|
||||||
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers) {
|
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers) {
|
||||||
|
|
@ -242,8 +335,8 @@ public class McpAsyncClient {
|
||||||
public Mono<McpSchema.InitializeResult> initialize() {
|
public Mono<McpSchema.InitializeResult> initialize() {
|
||||||
McpSchema.InitializeRequest initializeRequest = new McpSchema.InitializeRequest(// @formatter:off
|
McpSchema.InitializeRequest initializeRequest = new McpSchema.InitializeRequest(// @formatter:off
|
||||||
McpSchema.LATEST_PROTOCOL_VERSION,
|
McpSchema.LATEST_PROTOCOL_VERSION,
|
||||||
new McpSchema.ClientCapabilities(null, rootCapabilities, null),
|
this.clientCapabilities,
|
||||||
new McpSchema.Implementation("mcp-java-client", "0.2.0")); // @formatter:on
|
this.clientInfo); // @formatter:on
|
||||||
|
|
||||||
Mono<McpSchema.InitializeResult> result = this.mcpSession.sendRequest("initialize", initializeRequest,
|
Mono<McpSchema.InitializeResult> result = this.mcpSession.sendRequest("initialize", initializeRequest,
|
||||||
new TypeReference<McpSchema.InitializeResult>() {
|
new TypeReference<McpSchema.InitializeResult>() {
|
||||||
|
|
@ -268,15 +361,16 @@ public class McpAsyncClient {
|
||||||
/**
|
/**
|
||||||
* Send a roots/list_changed notification.
|
* Send a roots/list_changed notification.
|
||||||
*/
|
*/
|
||||||
public Mono<Void> sendRootsListChanged() {
|
public Mono<Void> rootsListChangedNotification() {
|
||||||
return this.mcpSession.sendNotification("notifications/roots/list_changed");
|
return this.mcpSession.sendNotification("notifications/roots/list_changed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a synchronous ping request.
|
* Send a synchronous ping request.
|
||||||
*/
|
*/
|
||||||
public Mono<Void> ping() {
|
public Mono<Object> ping() {
|
||||||
return this.mcpSession.sendRequest("ping", null, VOID_TYPE_REFERENCE);
|
return this.mcpSession.sendRequest("ping", null, new TypeReference<Object>() {
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,17 @@ package org.springframework.ai.mcp.client;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.springframework.ai.mcp.spec.McpSchema;
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.CreateMessageRequest;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.CreateMessageResult;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.Implementation;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.Root;
|
import org.springframework.ai.mcp.spec.McpSchema.Root;
|
||||||
import org.springframework.ai.mcp.spec.McpTransport;
|
import org.springframework.ai.mcp.spec.McpTransport;
|
||||||
import org.springframework.ai.mcp.util.Assert;
|
import org.springframework.ai.mcp.util.Assert;
|
||||||
|
|
@ -51,14 +57,7 @@ import org.springframework.ai.mcp.util.Assert;
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
* @author Dariusz Jędrzejczyk
|
* @author Dariusz Jędrzejczyk
|
||||||
*/
|
*/
|
||||||
public class McpClient {
|
public interface McpClient {
|
||||||
|
|
||||||
/**
|
|
||||||
* Private constructor to prevent instantiation as this is a utility class containing
|
|
||||||
* only static factory methods.
|
|
||||||
*/
|
|
||||||
private McpClient() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start building an MCP client with the specified transport.
|
* Start building an MCP client with the specified transport.
|
||||||
|
|
@ -78,9 +77,11 @@ public class McpClient {
|
||||||
|
|
||||||
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
|
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
|
||||||
|
|
||||||
private boolean rootsListChangedNotification = false;
|
private ClientCapabilities capabilities;
|
||||||
|
|
||||||
private List<Supplier<List<Root>>> rootsListProviders = new ArrayList<>();
|
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.0");
|
||||||
|
|
||||||
|
private Map<String, Root> roots = new HashMap<>();
|
||||||
|
|
||||||
private List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers = new ArrayList<>();
|
private List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
|
@ -88,6 +89,8 @@ public class McpClient {
|
||||||
|
|
||||||
private List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers = new ArrayList<>();
|
private List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
||||||
|
|
||||||
private Builder(McpTransport transport) {
|
private Builder(McpTransport transport) {
|
||||||
Assert.notNull(transport, "Transport must not be null");
|
Assert.notNull(transport, "Transport must not be null");
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
|
@ -104,13 +107,32 @@ public class McpClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder rootsListChangedNotification(boolean rootsListChangedNotification) {
|
public Builder capabilities(ClientCapabilities capabilities) {
|
||||||
this.rootsListChangedNotification = rootsListChangedNotification;
|
this.capabilities = capabilities;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder rootsListProvider(Supplier<List<Root>> rootsListProvider) {
|
public Builder clientInfo(Implementation clientInfo) {
|
||||||
this.rootsListProviders.add(rootsListProvider);
|
this.clientInfo = clientInfo;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder roots(List<Root> roots) {
|
||||||
|
for (Root root : roots) {
|
||||||
|
this.roots.put(root.uri(), root);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder roots(Root... roots) {
|
||||||
|
for (Root root : roots) {
|
||||||
|
this.roots.put(root.uri(), root);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder sampling(Function<CreateMessageRequest, CreateMessageResult> samplingHandler) {
|
||||||
|
this.samplingHandler = samplingHandler;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,42 +164,10 @@ public class McpClient {
|
||||||
* @return A new instance of {@link McpAsyncClient}
|
* @return A new instance of {@link McpAsyncClient}
|
||||||
*/
|
*/
|
||||||
public McpAsyncClient async() {
|
public McpAsyncClient async() {
|
||||||
return new McpAsyncClient(transport, requestTimeout, rootsListProviders, rootsListChangedNotification,
|
return new McpAsyncClient(transport, requestTimeout, clientInfo, capabilities, roots, toolsChangeConsumers,
|
||||||
toolsChangeConsumers, resourcesChangeConsumers, promptsChangeConsumers);
|
resourcesChangeConsumers, promptsChangeConsumers, samplingHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use {@link #using(McpTransport)} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static McpAsyncClient async(McpTransport transport) {
|
|
||||||
return using(transport).async();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use {@link #using(McpTransport)} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static McpAsyncClient async(McpTransport transport, Duration requestTimeout) {
|
|
||||||
return using(transport).requestTimeout(requestTimeout).async();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use {@link #using(McpTransport)} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static McpSyncClient sync(McpTransport transport) {
|
|
||||||
return using(transport).sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use {@link #using(McpTransport)} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static McpSyncClient sync(McpTransport transport, Duration requestTimeout) {
|
|
||||||
return using(transport).requestTimeout(requestTimeout).sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package org.springframework.ai.mcp.client;
|
package org.springframework.ai.mcp.client;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
@ -101,15 +100,30 @@ public class McpSyncClient implements AutoCloseable {
|
||||||
/**
|
/**
|
||||||
* Send a roots/list_changed notification.
|
* Send a roots/list_changed notification.
|
||||||
*/
|
*/
|
||||||
public void sendRootsListChanged() {
|
public void rootsListChangedNotification() {
|
||||||
this.delegate.sendRootsListChanged().block();
|
this.delegate.rootsListChangedNotification().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a roots dynamically.
|
||||||
|
*/
|
||||||
|
public void addRoot(McpSchema.Root root) {
|
||||||
|
this.delegate.addRoot(root).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a root dynamically.
|
||||||
|
*/
|
||||||
|
public void removeRoot(String rootUri) {
|
||||||
|
this.delegate.removeRoot(rootUri).block();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a synchronous ping request.
|
* Send a synchronous ping request.
|
||||||
|
* @return
|
||||||
*/
|
*/
|
||||||
public void ping() {
|
public Object ping() {
|
||||||
this.delegate.ping().block();
|
return this.delegate.ping().block();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* 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.transport;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server parameters for stdio client.
|
||||||
|
*
|
||||||
|
* @author Christian Tzolov
|
||||||
|
* @author Dariusz Jędrzejczyk
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
public class ServerParameters {
|
||||||
|
|
||||||
|
// Environment variables to inherit by default
|
||||||
|
private static final List<String> DEFAULT_INHERITED_ENV_VARS = System.getProperty("os.name")
|
||||||
|
.toLowerCase()
|
||||||
|
.contains("win")
|
||||||
|
? Arrays.asList("APPDATA", "HOMEDRIVE", "HOMEPATH", "LOCALAPPDATA", "PATH", "PROCESSOR_ARCHITECTURE",
|
||||||
|
"SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE")
|
||||||
|
: Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER");
|
||||||
|
|
||||||
|
@JsonProperty("command")
|
||||||
|
private String command;
|
||||||
|
|
||||||
|
@JsonProperty("args")
|
||||||
|
private List<String> args = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("env")
|
||||||
|
private Map<String, String> env;
|
||||||
|
|
||||||
|
private ServerParameters(String command, List<String> args, Map<String, String> env) {
|
||||||
|
Assert.notNull(command, "The command can not be null");
|
||||||
|
Assert.notNull(args, "The args can not be null");
|
||||||
|
|
||||||
|
this.command = command;
|
||||||
|
this.args = args;
|
||||||
|
this.env = new HashMap<>(getDefaultEnvironment());
|
||||||
|
if (env != null && !env.isEmpty()) {
|
||||||
|
this.env.putAll(env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCommand() {
|
||||||
|
return this.command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getArgs() {
|
||||||
|
return this.args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getEnv() {
|
||||||
|
return this.env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder(String command) {
|
||||||
|
return new Builder(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private String command;
|
||||||
|
|
||||||
|
private List<String> args = new ArrayList<>();
|
||||||
|
|
||||||
|
private Map<String, String> env = new HashMap<>();
|
||||||
|
|
||||||
|
public Builder(String command) {
|
||||||
|
Assert.notNull(command, "The command can not be null");
|
||||||
|
this.command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder args(String... args) {
|
||||||
|
Assert.notNull(args, "The args can not be null");
|
||||||
|
this.args = Arrays.asList(args);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder args(List<String> args) {
|
||||||
|
Assert.notNull(args, "The args can not be null");
|
||||||
|
this.args = new ArrayList<>(args);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder arg(String arg) {
|
||||||
|
Assert.notNull(arg, "The arg can not be null");
|
||||||
|
this.args.add(arg);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder env(Map<String, String> env) {
|
||||||
|
if (env != null && !env.isEmpty()) {
|
||||||
|
this.env.putAll(env);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder addEnvVar(String key, String value) {
|
||||||
|
Assert.notNull(key, "The key can not be null");
|
||||||
|
Assert.notNull(value, "The value can not be null");
|
||||||
|
this.env.put(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerParameters build() {
|
||||||
|
return new ServerParameters(command, args, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a default environment object including only environment variables deemed
|
||||||
|
* safe to inherit.
|
||||||
|
*/
|
||||||
|
private static Map<String, String> getDefaultEnvironment() {
|
||||||
|
return System.getenv()
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.filter(entry -> DEFAULT_INHERITED_ENV_VARS.contains(entry.getKey()))
|
||||||
|
.filter(entry -> entry.getValue() != null)
|
||||||
|
.filter(entry -> !entry.getValue().startsWith("()"))
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
/*
|
||||||
|
* 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.transport;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import reactor.core.Disposable;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.publisher.Sinks;
|
||||||
|
import reactor.core.publisher.SynchronousSink;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import reactor.util.retry.Retry;
|
||||||
|
import reactor.util.retry.Retry.RetrySignal;
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.spec.McpError;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
|
||||||
|
import org.springframework.ai.mcp.spec.McpTransport;
|
||||||
|
import org.springframework.ai.mcp.util.Assert;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-Sent Events (SSE) implementation of the
|
||||||
|
* {@link org.springframework.ai.mcp.spec.McpTransport} that follows the MCP HTTP with SSE
|
||||||
|
* transport specification.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This transport establishes a bidirectional communication channel where:
|
||||||
|
* <ul>
|
||||||
|
* <li>Inbound messages are received through an SSE connection from the server</li>
|
||||||
|
* <li>Outbound messages are sent via HTTP POST requests to a server-provided
|
||||||
|
* endpoint</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The message flow follows these steps:
|
||||||
|
* <ol>
|
||||||
|
* <li>The client establishes an SSE connection to the server's /sse endpoint</li>
|
||||||
|
* <li>The server sends an 'endpoint' event containing the URI for sending messages</li>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation handles automatic reconnection for transient failures and provides
|
||||||
|
* graceful shutdown capabilities. It uses {@link WebClient} for HTTP communications and
|
||||||
|
* supports JSON serialization/deserialization of messages.
|
||||||
|
*
|
||||||
|
* @author Christian Tzolov
|
||||||
|
* @see <a href=
|
||||||
|
* "https://spec.modelcontextprotocol.io/specification/basic/transports/#http-with-sse">MCP
|
||||||
|
* HTTP with SSE Transport Specification</a>
|
||||||
|
*/
|
||||||
|
public class SseClientTransport implements McpTransport {
|
||||||
|
|
||||||
|
private final static Logger logger = LoggerFactory.getLogger(SseClientTransport.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event type for JSON-RPC messages received through the SSE connection. The server
|
||||||
|
* sends messages with this event type to transmit JSON-RPC protocol data.
|
||||||
|
*/
|
||||||
|
private final static String MESSAGE_EVENT_TYPE = "message";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event type for receiving the message endpoint URI from the server. The server MUST
|
||||||
|
* send this event when a client connects, providing the URI where the client should
|
||||||
|
* send its messages via HTTP POST.
|
||||||
|
*/
|
||||||
|
private final static String ENDPOINT_EVENT_TYPE = "endpoint";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default SSE endpoint path as specified by the MCP transport specification. This
|
||||||
|
* endpoint is used to establish the SSE connection with the server.
|
||||||
|
*/
|
||||||
|
private final static String SSE_ENDPOINT = "/sse";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type reference for parsing SSE events containing string data.
|
||||||
|
*/
|
||||||
|
private final static ParameterizedTypeReference<ServerSentEvent<String>> SSE_TYPE = new ParameterizedTypeReference<>() {
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebClient instance for handling both SSE connections and HTTP POST requests. Used
|
||||||
|
* for establishing the SSE connection and sending outbound messages.
|
||||||
|
*/
|
||||||
|
private final WebClient webClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ObjectMapper for serializing outbound messages and deserializing inbound messages.
|
||||||
|
* Handles conversion between JSON-RPC messages and their string representation.
|
||||||
|
*/
|
||||||
|
protected ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription for the SSE connection handling inbound messages. Used for cleanup
|
||||||
|
* during transport shutdown.
|
||||||
|
*/
|
||||||
|
private Disposable inboundSubscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag indicating if the transport is in the process of shutting down. Used to
|
||||||
|
* prevent new operations during shutdown and handle cleanup gracefully.
|
||||||
|
*/
|
||||||
|
private volatile boolean isClosing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sink for managing the message endpoint URI provided by the server. Stores the most
|
||||||
|
* recent endpoint URI and makes it available for outbound message processing.
|
||||||
|
*/
|
||||||
|
protected final Sinks.One<String> messageEndpointSink = Sinks.one();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new SseClientTransport with the specified WebClient builder. Uses a
|
||||||
|
* default ObjectMapper instance for JSON processing.
|
||||||
|
* @param webClientBuilder the WebClient.Builder to use for creating the WebClient
|
||||||
|
* instance
|
||||||
|
* @throws IllegalArgumentException if webClientBuilder is null
|
||||||
|
*/
|
||||||
|
public SseClientTransport(WebClient.Builder webClientBuilder) {
|
||||||
|
this(webClientBuilder, new ObjectMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new SseClientTransport with the specified WebClient builder and
|
||||||
|
* ObjectMapper. Initializes both inbound and outbound message processing pipelines.
|
||||||
|
* @param webClientBuilder the WebClient.Builder to use for creating the WebClient
|
||||||
|
* instance
|
||||||
|
* @param objectMapper the ObjectMapper to use for JSON processing
|
||||||
|
* @throws IllegalArgumentException if either parameter is null
|
||||||
|
*/
|
||||||
|
public SseClientTransport(WebClient.Builder webClientBuilder, ObjectMapper objectMapper) {
|
||||||
|
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
||||||
|
Assert.notNull(webClientBuilder, "WebClient.Builder must not be null");
|
||||||
|
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.webClient = webClientBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> handler) {
|
||||||
|
Flux<ServerSentEvent<String>> events = eventStream();
|
||||||
|
this.inboundSubscription = events.concatMap(event -> Mono.just(event).<JSONRPCMessage>handle((e, s) -> {
|
||||||
|
if (ENDPOINT_EVENT_TYPE.equals(event.event())) {
|
||||||
|
String messageEndpointUri = event.data();
|
||||||
|
if (messageEndpointSink.tryEmitValue(messageEndpointUri).isSuccess()) {
|
||||||
|
s.complete();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO: clarify with the spec if multiple events can be
|
||||||
|
// received
|
||||||
|
s.error(new McpError("Failed to handle SSE endpoint event"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (MESSAGE_EVENT_TYPE.equals(event.event())) {
|
||||||
|
try {
|
||||||
|
JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, event.data());
|
||||||
|
s.next(message);
|
||||||
|
}
|
||||||
|
catch (IOException ioException) {
|
||||||
|
s.error(ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s.error(new McpError("Received unrecognized SSE event type: " + event.event()));
|
||||||
|
}
|
||||||
|
}).transform(handler)).subscribe();
|
||||||
|
|
||||||
|
// The connection is established once the server sends the endpoint event
|
||||||
|
return messageEndpointSink.asMono().then();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> sendMessage(JSONRPCMessage message) {
|
||||||
|
// The messageEndpoint is the endpoint URI to send the messages
|
||||||
|
// It is provided by the server as part of the endpoint event
|
||||||
|
return messageEndpointSink.asMono().flatMap(messageEndpointUri -> {
|
||||||
|
if (isClosing) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String jsonText = this.objectMapper.writeValueAsString(message);
|
||||||
|
return webClient.post()
|
||||||
|
.uri(messageEndpointUri)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(jsonText)
|
||||||
|
.retrieve()
|
||||||
|
.toBodilessEntity()
|
||||||
|
.doOnSuccess(response -> {
|
||||||
|
logger.debug("Message sent successfully");
|
||||||
|
})
|
||||||
|
.doOnError(error -> {
|
||||||
|
if (!isClosing) {
|
||||||
|
logger.error("Error sending message: {}", error.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
if (!isClosing) {
|
||||||
|
return Mono.error(new RuntimeException("Failed to serialize message", e));
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
}).then(); // TODO: Consider non-200-ok response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes and starts the inbound SSE event processing. Establishes the SSE
|
||||||
|
* connection and sets up event handling for both message and endpoint events.
|
||||||
|
* Includes automatic retry logic for handling transient connection failures.
|
||||||
|
*/
|
||||||
|
// visible for tests
|
||||||
|
protected Flux<ServerSentEvent<String>> eventStream() {// @formatter:off
|
||||||
|
return this.webClient
|
||||||
|
.get()
|
||||||
|
.uri(SSE_ENDPOINT)
|
||||||
|
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToFlux(SSE_TYPE)
|
||||||
|
.retryWhen(Retry.from(retrySignal -> retrySignal.handle(inboundRetryHandler)));
|
||||||
|
} // @formatter:on
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry handler for the inbound SSE stream. Implements the retry logic for handling
|
||||||
|
* connection failures and other errors.
|
||||||
|
*/
|
||||||
|
private BiConsumer<RetrySignal, SynchronousSink<Object>> inboundRetryHandler = (retrySpec, sink) -> {
|
||||||
|
if (isClosing) {
|
||||||
|
logger.debug("SSE connection closed during shutdown");
|
||||||
|
sink.error(retrySpec.failure());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (retrySpec.failure() instanceof IOException) {
|
||||||
|
logger.debug("Retrying SSE connection after IO error");
|
||||||
|
sink.next(retrySpec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.error("Fatal SSE error, not retrying: {}", retrySpec.failure().getMessage());
|
||||||
|
sink.error(retrySpec.failure());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements graceful shutdown of the transport. Cleans up all resources including
|
||||||
|
* subscriptions and schedulers. Ensures orderly shutdown of both inbound and outbound
|
||||||
|
* message processing.
|
||||||
|
* @return a Mono that completes when shutdown is finished
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Mono<Void> closeGracefully() { // @formatter:off
|
||||||
|
return Mono.fromRunnable(() -> {
|
||||||
|
isClosing = true;
|
||||||
|
|
||||||
|
// Dispose of subscriptions
|
||||||
|
|
||||||
|
if (inboundSubscription != null) {
|
||||||
|
inboundSubscription.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
.then()
|
||||||
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
} // @formatter:on
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
||||||
|
return this.objectMapper.convertValue(data, typeRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,351 @@
|
||||||
|
/*
|
||||||
|
* 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.transport;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.publisher.Sinks;
|
||||||
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
|
||||||
|
import org.springframework.ai.mcp.util.Assert;
|
||||||
|
import org.springframework.ai.mcp.spec.McpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the MCP Stdio transport that communicates with a server process using
|
||||||
|
* standard input/output streams. Messages are exchanged as newline-delimited JSON-RPC
|
||||||
|
* messages over stdin/stdout, with errors and debug information sent to stderr.
|
||||||
|
*
|
||||||
|
* @author Christian Tzolov
|
||||||
|
* @author Dariusz Jędrzejczyk
|
||||||
|
*/
|
||||||
|
public class StdioClientTransport implements McpTransport {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(StdioClientTransport.class);
|
||||||
|
|
||||||
|
private final Sinks.Many<JSONRPCMessage> inboundSink;
|
||||||
|
|
||||||
|
private final Sinks.Many<JSONRPCMessage> outboundSink;
|
||||||
|
|
||||||
|
/** The server process being communicated with */
|
||||||
|
private Process process;
|
||||||
|
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/** Scheduler for handling inbound messages from the server process */
|
||||||
|
private Scheduler inboundScheduler;
|
||||||
|
|
||||||
|
/** Scheduler for handling outbound messages to the server process */
|
||||||
|
private Scheduler outboundScheduler;
|
||||||
|
|
||||||
|
/** Scheduler for handling error messages from the server process */
|
||||||
|
private Scheduler errorScheduler;
|
||||||
|
|
||||||
|
/** Parameters for configuring and starting the server process */
|
||||||
|
private final ServerParameters params;
|
||||||
|
|
||||||
|
private final Sinks.Many<String> errorSink;
|
||||||
|
|
||||||
|
// visible for tests
|
||||||
|
private Consumer<String> errorHandler = error -> logger.error("Error received: {}", error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new StdioClientTransport with the specified parameters and default
|
||||||
|
* ObjectMapper.
|
||||||
|
* @param params The parameters for configuring the server process
|
||||||
|
*/
|
||||||
|
public StdioClientTransport(ServerParameters params) {
|
||||||
|
this(params, new ObjectMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new StdioClientTransport with the specified parameters and ObjectMapper.
|
||||||
|
* @param params The parameters for configuring the server process
|
||||||
|
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
||||||
|
*/
|
||||||
|
public StdioClientTransport(ServerParameters params, ObjectMapper objectMapper) {
|
||||||
|
Assert.notNull(params, "The params can not be null");
|
||||||
|
Assert.notNull(objectMapper, "The ObjectMapper can not be null");
|
||||||
|
|
||||||
|
this.inboundSink = Sinks.many().unicast().onBackpressureBuffer();
|
||||||
|
this.outboundSink = Sinks.many().unicast().onBackpressureBuffer();
|
||||||
|
|
||||||
|
this.params = params;
|
||||||
|
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
|
||||||
|
this.errorSink = Sinks.many().unicast().onBackpressureBuffer();
|
||||||
|
|
||||||
|
// Start threads
|
||||||
|
this.inboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "inbound");
|
||||||
|
this.outboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "outbound");
|
||||||
|
this.errorScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server process and initializes the message processing streams. This
|
||||||
|
* method sets up the process with the configured command, arguments, and environment,
|
||||||
|
* then starts the inbound, outbound, and error processing threads.
|
||||||
|
* @throws RuntimeException if the process fails to start or if the process streams
|
||||||
|
* are null
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> handler) {
|
||||||
|
return Mono.<Void>fromRunnable(() -> {
|
||||||
|
handleIncomingMessages(handler);
|
||||||
|
handleIncomingErrors();
|
||||||
|
|
||||||
|
// Prepare command and environment
|
||||||
|
List<String> fullCommand = new ArrayList<>();
|
||||||
|
fullCommand.add(params.getCommand());
|
||||||
|
fullCommand.addAll(params.getArgs());
|
||||||
|
|
||||||
|
ProcessBuilder processBuilder = this.getProcessBuilder();
|
||||||
|
processBuilder.command(fullCommand);
|
||||||
|
processBuilder.environment().putAll(params.getEnv());
|
||||||
|
|
||||||
|
// Start the process
|
||||||
|
try {
|
||||||
|
this.process = processBuilder.start();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to start process with command: " + fullCommand, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate process streams
|
||||||
|
if (this.process.getInputStream() == null || process.getOutputStream() == null) {
|
||||||
|
this.process.destroy();
|
||||||
|
throw new RuntimeException("Process input or output stream is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start threads
|
||||||
|
startInboundProcessing();
|
||||||
|
startOutboundProcessing();
|
||||||
|
startErrorProcessing();
|
||||||
|
}).subscribeOn(Schedulers.boundedElastic());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and returns a new ProcessBuilder instance. Protected to allow overriding in
|
||||||
|
* tests.
|
||||||
|
* @return A new ProcessBuilder instance
|
||||||
|
*/
|
||||||
|
protected ProcessBuilder getProcessBuilder() {
|
||||||
|
return new ProcessBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the handler for processing transport-level errors.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The provided handler will be called when errors occur during transport operations,
|
||||||
|
* such as connection failures or protocol violations.
|
||||||
|
* </p>
|
||||||
|
* @param errorHandler a consumer that processes error messages
|
||||||
|
*/
|
||||||
|
public void setErrorHandler(Consumer<String> errorHandler) {
|
||||||
|
this.errorHandler = errorHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for the server process to exit.
|
||||||
|
* @throws RuntimeException if the process is interrupted while waiting
|
||||||
|
*/
|
||||||
|
public void awaitForExit() {
|
||||||
|
try {
|
||||||
|
this.process.waitFor();
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException("Process interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the error processing thread that reads from the process's error stream.
|
||||||
|
* Error messages are logged and emitted to the error sink.
|
||||||
|
*/
|
||||||
|
private void startErrorProcessing() {
|
||||||
|
this.errorScheduler.schedule(() -> {
|
||||||
|
try (BufferedReader processErrorReader = new BufferedReader(
|
||||||
|
new InputStreamReader(process.getErrorStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = processErrorReader.readLine()) != null) {
|
||||||
|
try {
|
||||||
|
logger.error("Received error line: {}", line);
|
||||||
|
// TODO: handle errors, etc.
|
||||||
|
this.errorSink.tryEmitNext(line);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleIncomingMessages(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> inboundMessageHandler) {
|
||||||
|
this.inboundSink.asFlux()
|
||||||
|
.flatMap(message -> Mono.just(message)
|
||||||
|
.transform(inboundMessageHandler)
|
||||||
|
.contextWrite(ctx -> ctx.put("observation", "myObservation")))
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleIncomingErrors() {
|
||||||
|
this.errorSink.asFlux().subscribe(e -> {
|
||||||
|
this.errorHandler.accept(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> sendMessage(JSONRPCMessage message) {
|
||||||
|
if (this.outboundSink.tryEmitNext(message).isSuccess()) {
|
||||||
|
// TODO: essentially we could reschedule ourselves in some time and make
|
||||||
|
// another attempt with the already read data but pause reading until
|
||||||
|
// success
|
||||||
|
// In this approach we delegate the retry and the backpressure onto the
|
||||||
|
// caller. This might be enough for most cases.
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Mono.error(new RuntimeException("Failed to enqueue message"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the inbound processing thread that reads JSON-RPC messages from the
|
||||||
|
* process's input stream. Messages are deserialized and emitted to the inbound sink.
|
||||||
|
*/
|
||||||
|
private void startInboundProcessing() {
|
||||||
|
this.inboundScheduler.schedule(() -> {
|
||||||
|
try (BufferedReader processReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = processReader.readLine()) != null) {
|
||||||
|
try {
|
||||||
|
JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, line);
|
||||||
|
if (!this.inboundSink.tryEmitNext(message).isSuccess()) {
|
||||||
|
// TODO: Back off, reschedule, give up?
|
||||||
|
throw new RuntimeException("Failed to enqueue message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the outbound processing thread that writes JSON-RPC messages to the
|
||||||
|
* process's output stream. Messages are serialized to JSON and written with a newline
|
||||||
|
* delimiter.
|
||||||
|
*/
|
||||||
|
private void startOutboundProcessing() {
|
||||||
|
this.handleOutbound(messages -> messages
|
||||||
|
// this bit is important since writes come from user threads and we
|
||||||
|
// want to ensure that the actual writing happens on a dedicated thread
|
||||||
|
.publishOn(outboundScheduler)
|
||||||
|
.handle((message, s) -> {
|
||||||
|
if (message != null) {
|
||||||
|
try {
|
||||||
|
String jsonMessage = objectMapper.writeValueAsString(message);
|
||||||
|
// Escape any embedded newlines in the JSON message as per spec:
|
||||||
|
// https://spec.modelcontextprotocol.io/specification/basic/transports/#stdio
|
||||||
|
// - Messages are delimited by newlines, and MUST NOT contain
|
||||||
|
// embedded newlines.
|
||||||
|
jsonMessage = jsonMessage.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "\\n");
|
||||||
|
|
||||||
|
this.process.getOutputStream().write(jsonMessage.getBytes(StandardCharsets.UTF_8));
|
||||||
|
this.process.getOutputStream().write("\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
this.process.getOutputStream().flush();
|
||||||
|
s.next(message);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
s.error(new RuntimeException(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handleOutbound(Function<Flux<JSONRPCMessage>, Flux<JSONRPCMessage>> outboundConsumer) {
|
||||||
|
outboundConsumer.apply(outboundSink.asFlux()).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully closes the transport by destroying the process and disposing of the
|
||||||
|
* schedulers. This method sends a TERM signal to the process and waits for it to exit
|
||||||
|
* before cleaning up resources.
|
||||||
|
* @return A Mono that completes when the transport is closed
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Mono<Void> closeGracefully() {
|
||||||
|
return Mono.fromFuture(() -> {
|
||||||
|
logger.info("Sending TERM to process");
|
||||||
|
if (this.process != null) {
|
||||||
|
this.process.destroy();
|
||||||
|
return process.onExit();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return CompletableFuture.failedFuture(new RuntimeException("Process not started"));
|
||||||
|
}
|
||||||
|
}).doOnNext(process -> {
|
||||||
|
if (process.exitValue() != 0) {
|
||||||
|
logger.warn("Process terminated with code " + process.exitValue());
|
||||||
|
}
|
||||||
|
}).then(Mono.fromRunnable(() -> {
|
||||||
|
// The Threads are blocked on readLine so disposeGracefully would not
|
||||||
|
// interrupt them, therefore we issue an async hard dispose.
|
||||||
|
inboundScheduler.dispose();
|
||||||
|
errorScheduler.dispose();
|
||||||
|
outboundScheduler.dispose();
|
||||||
|
})).then().subscribeOn(Schedulers.boundedElastic());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Sinks.Many<String> getErrorSink() {
|
||||||
|
return this.errorSink;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
||||||
|
return this.objectMapper.convertValue(data, typeRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,444 @@
|
||||||
|
package org.springframework.ai.mcp.server;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.server.McpServer.PromptRegistration;
|
||||||
|
import org.springframework.ai.mcp.server.McpServer.ResourceRegistration;
|
||||||
|
import org.springframework.ai.mcp.server.McpServer.ToolRegistration;
|
||||||
|
import org.springframework.ai.mcp.spec.DefaultMcpSession;
|
||||||
|
import org.springframework.ai.mcp.spec.DefaultMcpSession.NotificationHandler;
|
||||||
|
import org.springframework.ai.mcp.spec.McpError;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.Tool;
|
||||||
|
import org.springframework.ai.mcp.spec.McpTransport;
|
||||||
|
import org.springframework.ai.mcp.util.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Model Context Protocol (MCP) server implementation that provides asynchronous
|
||||||
|
* communication.
|
||||||
|
*
|
||||||
|
* @author Christian Tzolov
|
||||||
|
*/
|
||||||
|
public class McpAsyncServer {
|
||||||
|
|
||||||
|
private final static Logger logger = LoggerFactory.getLogger(McpAsyncServer.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The MCP session implementation that manages bidirectional JSON-RPC communication
|
||||||
|
* between clients and servers.
|
||||||
|
*/
|
||||||
|
private final DefaultMcpSession mcpSession;
|
||||||
|
|
||||||
|
private final McpTransport transport;
|
||||||
|
|
||||||
|
private final McpSchema.ServerCapabilities serverCapabilities;
|
||||||
|
|
||||||
|
private final McpSchema.Implementation serverInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread-safe list of tool handlers that can be modified at runtime.
|
||||||
|
*/
|
||||||
|
private final CopyOnWriteArrayList<ToolRegistration> tools;
|
||||||
|
|
||||||
|
private final CopyOnWriteArrayList<McpSchema.ResourceTemplate> resourceTemplates;
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<String, ResourceRegistration> resources;
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<String, PromptRegistration> prompts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new McpAsyncServer with the given transport and capabilities.
|
||||||
|
* @param transport The transport to use for communication
|
||||||
|
* @param tools List of tool registrations
|
||||||
|
* @param resources Map of resource registrations
|
||||||
|
* @param prompts Map of prompt registrations
|
||||||
|
*/
|
||||||
|
public McpAsyncServer(McpTransport mcpTransport, McpSchema.Implementation serverInfo,
|
||||||
|
McpSchema.ServerCapabilities serverCapabilities, List<ToolRegistration> tools,
|
||||||
|
Map<String, ResourceRegistration> resources, List<McpSchema.ResourceTemplate> resourceTemplates,
|
||||||
|
Map<String, PromptRegistration> prompts) {
|
||||||
|
|
||||||
|
this.serverInfo = serverInfo;
|
||||||
|
this.tools = new CopyOnWriteArrayList<>(tools != null ? tools : List.of());
|
||||||
|
this.resources = !Utils.isEmpty(resources) ? new ConcurrentHashMap<>(resources) : new ConcurrentHashMap<>();
|
||||||
|
this.resourceTemplates = !Utils.isEmpty(resourceTemplates) ? new CopyOnWriteArrayList<>(resourceTemplates)
|
||||||
|
: new CopyOnWriteArrayList<>();
|
||||||
|
this.prompts = !Utils.isEmpty(prompts) ? new ConcurrentHashMap<>(prompts) : new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities : new McpSchema.ServerCapabilities(
|
||||||
|
null, // experimental
|
||||||
|
null, // logging
|
||||||
|
!Utils.isEmpty(this.prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
|
||||||
|
!Utils.isEmpty(this.resources) ? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false)
|
||||||
|
: null,
|
||||||
|
!Utils.isEmpty(this.tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
|
||||||
|
|
||||||
|
Map<String, DefaultMcpSession.RequestHandler> requestHandlers = new HashMap<>();
|
||||||
|
|
||||||
|
// Initialize request handlers for standard MCP methods
|
||||||
|
requestHandlers.put("initialize", initializeRequestHandler());
|
||||||
|
|
||||||
|
// Ping MUST respond with an empty data, but not NULL response.
|
||||||
|
requestHandlers.put("ping", (params) -> Mono.<Object>just(""));
|
||||||
|
|
||||||
|
// Add tools API handlers if the tool capability is enabled
|
||||||
|
if (this.serverCapabilities.tools() != null) {
|
||||||
|
requestHandlers.put("tools/list", toolsListRequestHandler());
|
||||||
|
requestHandlers.put("tools/call", toolsCallRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resources API handlers if provided
|
||||||
|
if (!Utils.isEmpty(this.resources)) {
|
||||||
|
requestHandlers.put("resources/list", resourcesListRequestHandler());
|
||||||
|
requestHandlers.put("resources/read", resourcesReadRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resource templates API handlers if provided.
|
||||||
|
if (!Utils.isEmpty(this.resourceTemplates)) {
|
||||||
|
requestHandlers.put("resources/templates/list", resourceTemplateListRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add prompts API handlers if provider exists
|
||||||
|
if (!Utils.isEmpty(this.prompts)) {
|
||||||
|
requestHandlers.put("prompts/list", promptsListRequestHandler());
|
||||||
|
requestHandlers.put("prompts/get", promptsGetRequestHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
|
||||||
|
|
||||||
|
notificationHandlers.put("notifications/initialized", (params) -> Mono.empty());
|
||||||
|
|
||||||
|
this.transport = mcpTransport;
|
||||||
|
this.mcpSession = new DefaultMcpSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
|
||||||
|
notificationHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new tool registration at runtime.
|
||||||
|
* @param toolRegistration The tool registration to add
|
||||||
|
* @return Mono that completes when clients have been notified of the change
|
||||||
|
*/
|
||||||
|
public Mono<Void> addTool(ToolRegistration toolRegistration) {
|
||||||
|
if (toolRegistration == null) {
|
||||||
|
return Mono.error(new McpError("Tool registration must not be null"));
|
||||||
|
}
|
||||||
|
if (toolRegistration.tool() == null) {
|
||||||
|
return Mono.error(new McpError("Tool must not be null"));
|
||||||
|
}
|
||||||
|
if (toolRegistration.call() == null) {
|
||||||
|
return Mono.error(new McpError("Tool call handler must not be null"));
|
||||||
|
}
|
||||||
|
if (this.serverCapabilities.tools() == null) {
|
||||||
|
return Mono.error(new McpError("Server must be configured with tool capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate tool names
|
||||||
|
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolRegistration.tool().name()))) {
|
||||||
|
return Mono.error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tools.add(toolRegistration);
|
||||||
|
logger.info("Added tool handler: {}", toolRegistration.tool().name());
|
||||||
|
if (this.serverCapabilities.tools().listChanged()) {
|
||||||
|
return notifyToolsListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a tool handler at runtime.
|
||||||
|
* @param toolName The name of the tool handler to remove
|
||||||
|
* @return Mono that completes when clients have been notified of the change
|
||||||
|
*/
|
||||||
|
public Mono<Void> removeTool(String toolName) {
|
||||||
|
if (toolName == null) {
|
||||||
|
return Mono.error(new McpError("Tool name must not be null"));
|
||||||
|
}
|
||||||
|
if (this.serverCapabilities.tools() == null) {
|
||||||
|
return Mono.error(new McpError("Server must be configured with tool capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean removed = this.tools.removeIf(toolRegistration -> toolRegistration.tool().name().equals(toolName));
|
||||||
|
if (removed) {
|
||||||
|
logger.info("Removed tool handler: {}", toolName);
|
||||||
|
if (this.serverCapabilities.tools().listChanged()) {
|
||||||
|
return notifyToolsListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new resource handler at runtime.
|
||||||
|
* @param resourceHandler The resource handler to add
|
||||||
|
* @return Mono that completes when clients have been notified of the change
|
||||||
|
*/
|
||||||
|
public Mono<Void> addResource(ResourceRegistration resourceHandler) {
|
||||||
|
if (resourceHandler == null || resourceHandler.resource() == null) {
|
||||||
|
return Mono.error(new McpError("Resource must not be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.serverCapabilities.resources() == null) {
|
||||||
|
return Mono.error(new McpError("Server must be configured with resource capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.resources.containsKey(resourceHandler.resource().uri())) {
|
||||||
|
return Mono
|
||||||
|
.error(new McpError("Resource with URI '" + resourceHandler.resource().uri() + "' already exists"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resources.put(resourceHandler.resource().uri(), resourceHandler);
|
||||||
|
logger.info("Added resource handler: {}", resourceHandler.resource().uri());
|
||||||
|
if (this.serverCapabilities.resources().listChanged()) {
|
||||||
|
return notifyResourcesListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a resource handler at runtime.
|
||||||
|
* @param resourceUri The URI of the resource handler to remove
|
||||||
|
* @return Mono that completes when clients have been notified of the change
|
||||||
|
*/
|
||||||
|
public Mono<Void> removeResource(String resourceUri) {
|
||||||
|
if (resourceUri == null) {
|
||||||
|
return Mono.error(new McpError("Resource URI must not be null"));
|
||||||
|
}
|
||||||
|
if (this.serverCapabilities.resources() == null) {
|
||||||
|
return Mono.error(new McpError("Server must be configured with resource capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceRegistration removed = this.resources.remove(resourceUri);
|
||||||
|
if (removed != null) {
|
||||||
|
logger.info("Removed resource handler: {}", resourceUri);
|
||||||
|
if (this.serverCapabilities.resources().listChanged()) {
|
||||||
|
return notifyResourcesListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new prompt handler at runtime.
|
||||||
|
* @param promptRegistration The prompt handler to add
|
||||||
|
* @return Mono that completes when clients have been notified of the change
|
||||||
|
*/
|
||||||
|
public Mono<Void> addPrompt(PromptRegistration promptRegistration) {
|
||||||
|
if (promptRegistration == null) {
|
||||||
|
return Mono.error(new McpError("Prompt registration must not be null"));
|
||||||
|
}
|
||||||
|
if (this.serverCapabilities.prompts() == null) {
|
||||||
|
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.prompts.containsKey(promptRegistration.propmpt().name())) {
|
||||||
|
return Mono
|
||||||
|
.error(new McpError("Prompt with name '" + promptRegistration.propmpt().name() + "' already exists"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prompts.put(promptRegistration.propmpt().name(), promptRegistration);
|
||||||
|
logger.info("Added prompt handler: {}", promptRegistration.propmpt().name());
|
||||||
|
if (this.serverCapabilities.prompts().listChanged()) {
|
||||||
|
return notifyPromptsListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a prompt handler at runtime.
|
||||||
|
* @param promptName The name of the prompt handler to remove
|
||||||
|
* @return Mono that completes when clients have been notified of the change
|
||||||
|
*/
|
||||||
|
public Mono<Void> removePrompt(String promptName) {
|
||||||
|
if (promptName == null) {
|
||||||
|
return Mono.error(new McpError("Prompt name must not be null"));
|
||||||
|
}
|
||||||
|
if (this.serverCapabilities.prompts() == null) {
|
||||||
|
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptRegistration removed = this.prompts.remove(promptName);
|
||||||
|
if (removed != null) {
|
||||||
|
logger.info("Removed prompt handler: {}", promptName);
|
||||||
|
if (this.serverCapabilities.prompts().listChanged()) {
|
||||||
|
return this.notifyPromptsListChanged();
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// Request Handlers
|
||||||
|
// ---------------------------------------
|
||||||
|
private DefaultMcpSession.RequestHandler initializeRequestHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.InitializeRequest request = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.InitializeRequest>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}",
|
||||||
|
request.protocolVersion(), request.capabilities(), request.clientInfo());
|
||||||
|
|
||||||
|
if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(request.protocolVersion())) {
|
||||||
|
return Mono
|
||||||
|
.<Object>error(
|
||||||
|
new McpError("Unsupported protocol version from client: " + request.protocolVersion()))
|
||||||
|
.publishOn(Schedulers.boundedElastic());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono
|
||||||
|
.<Object>just(new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, this.serverCapabilities,
|
||||||
|
this.serverInfo, null))
|
||||||
|
.publishOn(Schedulers.boundedElastic());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMcpSession.RequestHandler toolsListRequestHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.PaginatedRequest>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Tool> tools = this.tools.stream().map(toolRegistration -> {
|
||||||
|
return toolRegistration.tool();
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
logger.info("Client tools list request - Cursor: {}", request.cursor());
|
||||||
|
return Mono.just(new McpSchema.ListToolsResult(tools, null));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMcpSession.RequestHandler toolsCallRequestHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.CallToolRequest callToolRequest = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.CallToolRequest>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
Optional<ToolRegistration> toolRegistration = this.tools.stream()
|
||||||
|
.filter(tr -> callToolRequest.name().equals(tr.tool().name()))
|
||||||
|
.findAny();
|
||||||
|
|
||||||
|
if (toolRegistration.isEmpty()) {
|
||||||
|
return Mono.<Object>error(new McpError("Tool not found: " + callToolRequest.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
CallToolResult callResponse = toolRegistration.get().call().apply(callToolRequest.arguments());
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMcpSession.RequestHandler resourcesListRequestHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.PaginatedRequest>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
var resourceList = this.resources.values().stream().map(ResourceRegistration::resource).toList();
|
||||||
|
|
||||||
|
return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMcpSession.RequestHandler resourceTemplateListRequestHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.PaginatedRequest>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
return Mono.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMcpSession.RequestHandler resourcesReadRequestHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.ReadResourceRequest request = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.ReadResourceRequest>() {
|
||||||
|
});
|
||||||
|
var resourceUri = request.uri();
|
||||||
|
if (this.resources.containsKey(resourceUri)) {
|
||||||
|
return Mono.just(this.resources.get(resourceUri).readHandler().apply(request));
|
||||||
|
}
|
||||||
|
return Mono.error(new McpError("Resource not found: " + resourceUri));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMcpSession.RequestHandler promptsListRequestHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.PaginatedRequest>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
var promptList = this.prompts.values().stream().map(PromptRegistration::propmpt).toList();
|
||||||
|
|
||||||
|
return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMcpSession.RequestHandler promptsGetRequestHandler() {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.GetPromptRequest request = transport.unmarshalFrom(params,
|
||||||
|
new TypeReference<McpSchema.GetPromptRequest>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
// Implement prompt retrieval logic here
|
||||||
|
if (this.prompts.containsKey(request.name())) {
|
||||||
|
return Mono.just(this.prompts.get(request.name()).promptHandler().apply(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.error(new McpError("Prompt not found: " + request.name()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify clients that the list of available tools has changed.
|
||||||
|
*/
|
||||||
|
public Mono<Void> notifyToolsListChanged() {
|
||||||
|
return this.mcpSession.sendNotification("notifications/tools/list_changed", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify clients that the list of available resources has changed.
|
||||||
|
*/
|
||||||
|
public Mono<Void> notifyResourcesListChanged() {
|
||||||
|
return this.mcpSession.sendNotification("notifications/resources/list_changed", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify clients that the list of available prompts has changed.
|
||||||
|
*/
|
||||||
|
public Mono<Void> notifyPromptsListChanged() {
|
||||||
|
return this.mcpSession.sendNotification("notifications/prompts/list_changed", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the server gracefully.
|
||||||
|
*/
|
||||||
|
public Mono<Void> closeGracefully() {
|
||||||
|
return this.mcpSession.closeGracefully();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the server immediately.
|
||||||
|
*/
|
||||||
|
public void close() {
|
||||||
|
this.mcpSession.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue