diff --git a/README.md b/README.md
index 2e27d66..597e2c7 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Spring AI MCP is an experimental project that provides Java and Spring Framework
The project consists of two main modules:
-### spring-ai-mcp-core
+### [spring-ai-mcp-core](./spring-ai-mcp-core/README.md)
The core module provides a Java implementation of the Model Context Protocol specification. It includes:
- Synchronous and asynchronous client implementations
@@ -20,7 +20,9 @@ The core module provides a Java implementation of the Model Context Protocol spe
- Stdio-based server transport
- Reactive programming support using Project Reactor
-### spring-ai-mcp-spring
+[find more](./spring-ai-mcp-core/README.md)
+
+### [spring-ai-mcp-spring](./spring-ai-mcp-spring/README.md)
The Spring integration module provides Spring-specific functionality:
- Integration with Spring AI's function calling system
diff --git a/pom.xml b/pom.xml
index 7de059e..0da76c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,7 +70,12 @@
3.26.35.11.35.11.0
+ 1.20.4
+ 2.0.16
+ 1.5.12
+ 2.18.2
+ 6.1.133.11.0
@@ -347,4 +352,4 @@
-
\ No newline at end of file
+
diff --git a/spring-ai-mcp-architecture.jpg b/spring-ai-mcp-architecture.jpg
index 7447e47..7463b84 100644
Binary files a/spring-ai-mcp-architecture.jpg and b/spring-ai-mcp-architecture.jpg differ
diff --git a/spring-ai-mcp-core/README.md b/spring-ai-mcp-core/README.md
index cc3a72e..57fecec 100644
--- a/spring-ai-mcp-core/README.md
+++ b/spring-ai-mcp-core/README.md
@@ -10,13 +10,19 @@ This SDK implements the Model Context Protocol, enabling seamless integration wi
- Synchronous and Asynchronous client implementations
- Standard MCP operations support:
+ - Protocol version compatibility negotiation
+ - Client-server capability exchange
- Tool discovery and execution
- - Resource management and templates
- - Prompt handling and management
+ - Resource management with URI templates
- Resource subscription system
+ - Prompt handling and management
- Server initialization and ping
-- Stdio-based server transport
+- Multiple transport implementations:
+ - Stdio-based transport for process-based communication
+ - SSE-based transport for HTTP streaming
- Reactive programming support using Project Reactor
+- Configurable request timeouts
+- Customizable JSON serialization/deserialization
## Installation
@@ -38,6 +44,41 @@ Detailed UML class diagrams showing the relationships between components can be
## Usage
+### Transport Layer Options
+
+The SDK provides two transport implementations:
+
+#### StdioServerTransport
+Standard I/O based transport for process-based communication with MCP servers:
+
+```java
+ServerParameters params = ServerParameters.builder("npx")
+ .args("-y", "@modelcontextprotocol/server-everything", "dir")
+ .build();
+McpTransport transport = new StdioServerTransport(params);
+```
+
+#### SseServerTransport
+Server-Sent Events (SSE) based transport following the MCP HTTP with SSE transport specification:
+
+```java
+WebClient.Builder webClientBuilder = WebClient.builder()
+ .baseUrl("http://your-mcp-server");
+McpTransport transport = new SseServerTransport(webClientBuilder);
+
+// Or with custom ObjectMapper
+ObjectMapper mapper = new ObjectMapper();
+McpTransport transport = new SseServerTransport(webClientBuilder, mapper);
+```
+
+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
+
### Sync Client Example
```java
@@ -47,32 +88,32 @@ ServerParameters params = ServerParameters.builder("npx")
.build();
try (McpSyncClient client = McpClient.sync(new StdioServerTransport(params))) {
- // Initialize connection
+ // Initialize connection with protocol version and capabilities
McpSchema.InitializeResult initResult = client.initialize();
- // List tools synchronously
- McpSchema.ListToolsResult tools = client.listTools(null);
+ // List available tools
+ McpSchema.ListToolsResult tools = client.listTools();
- // Call tool synchronously
+ // Execute a tool
McpSchema.CallToolResult result = client.callTool(
new McpSchema.CallToolRequest("echo", Map.of("message", "Hello!"))
);
// Resource management
- McpSchema.ListResourcesResult resources = client.listResources(null);
+ McpSchema.ListResourcesResult resources = client.listResources();
McpSchema.ReadResourceResult resource = client.readResource(
new McpSchema.ReadResourceRequest("resource-uri")
);
- // Prompt management
- ListPromptsResult prompts = client.listPrompts(null);
+ // List and retrieve prompts
+ ListPromptsResult prompts = client.listPrompts();
GetPromptResult prompt = client.getPrompt(
new McpSchema.GetPromptRequest("prompt-id", Map.of())
);
}
```
-### Async Client Example
+### Async Client Example with Custom Configuration
```java
// Create server parameters
@@ -80,43 +121,42 @@ ServerParameters params = ServerParameters.builder("npx")
.args("-y", "@modelcontextprotocol/server-everything", "dir")
.build();
-// Initialize the async client
+// Initialize async client with custom timeout and object mapper
McpAsyncClient client = McpClient.async(
- new StdioServerTransport(params)
+ new StdioServerTransport(params),
+ Duration.ofSeconds(30),
+ new ObjectMapper()
);
-// Initialize the connection
-var promptResult = client.initialize()
- flatMap(result -> {
- // Connection initialized
- return client.listTools(null);
+// Initialize connection and chain operations
+var result = client.initialize()
+ .flatMap(initResult -> {
+ // Connection initialized with protocol version compatibility
+ return client.listTools();
})
.flatMap(tools -> {
- // Process tools
+ // Process available tools
return client.callTool(new McpSchema.CallToolRequest("echo",
Map.of("message", "Hello MCP!")));
})
- .flatMap(result -> {
- // Handle tool result
- return client.listPrompts(null);
+ .flatMap(toolResult -> {
+ // Handle tool execution result
+ return client.listPrompts();
})
.flatMap(prompts -> {
// Process available prompts
return client.getPrompt(new McpSchema.GetPromptRequest("prompt-id", Map.of()));
});
-// Handle prompt result, e.g. by blocking on it
-McpSchema.GetPromptResult result = promptResult.block();
-
-// Resource management example
-var resourcesResult = client.listResources(null)
+// Subscribe to resource changes
+var subscription = client.listResources()
.flatMap(resources -> {
- // Subscribe to resource changes
return client.subscribeResource(new McpSchema.SubscribeRequest("resource-uri"));
});
-// Handle resources result
-resourcesResult.block();
+// Handle results reactively or block if needed
+McpSchema.GetPromptResult promptResult = result.block();
+subscription.block();
// Cleanup
client.closeGracefully().block();
@@ -124,62 +164,68 @@ client.closeGracefully().block();
## Architecture
-The SDK follows a layered architecture:
+The SDK follows a layered architecture with clear separation of concerns:
### Core Components
-- **McpClient**: Factory class for creating sync and async clients
-- **McpAsyncClient**: Primary async implementation using Project Reactor
-- **McpSyncClient**: Synchronous wrapper around the async client
+- **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
+- **McpTransport**: Transport layer interface for server communication
- **McpSchema**: Comprehensive protocol schema definitions
-- **AbstractMcpTransport**: Base transport implementation
-- **StdioServerTransport**: Stdio-based server communication
+- **DefaultMcpSession**: Base implementation of the session management
+- **StdioServerTransport**: Standard I/O based server communication
+- **SseServerTransport**: HTTP-based transport using Server-Sent Events for bidirectional communication
### Key Interactions
-1. Client initialization
- - Transport setup
- - Server connection establishment
- - Protocol handshake
+1. Client Initialization
+ - Transport setup and connection establishment
+ - Protocol version compatibility check
+ - Capability negotiation
+ - Implementation details exchange
2. Message Flow
- - JSON-RPC message creation
+ - JSON-RPC message creation and validation
- Transport layer handling
- - Response processing
- - Error handling
+ - Response processing with type safety
+ - Error handling with specific error codes
3. Resource Management
- - Resource listing and reading
- - Template management
- - Subscription handling
- - Change notifications
+ - Resource discovery and listing
+ - URI template-based resource access
+ - Subscription system for change notifications
+ - Resource content retrieval
4. Prompt System
- - Prompt discovery
- - Prompt retrieval
- - Change notifications
+ - Prompt discovery and listing
+ - Parameter-based prompt retrieval
+ - Change notifications support
+ - Prompt content management
5. Tool Execution
- - Tool discovery
- - Parameter validation
- - Execution handling
- - Result processing
+ - Tool discovery and capability checking
+ - Parameter validation and processing
+ - Execution handling with timeout support
+ - Result processing with error handling
## Error Handling
The SDK provides comprehensive error handling through the McpError class:
-- Transport-level errors
-- Protocol violations
+- Protocol version incompatibility
+- Transport-level communication errors
+- JSON-RPC protocol violations
- Tool execution failures
-- Resource access errors
-- Subscription handling errors
+- Resource access and subscription errors
- Prompt management errors
-- Timeout handling
+- Request timeout handling
+- Server capability mismatches
+- SSE connection failures and retry handling
+- HTTP request/response errors
## Contributing
diff --git a/spring-ai-mcp-core/docs/CLIENT.SSE.TRANSPORT.md b/spring-ai-mcp-core/docs/CLIENT.SSE.TRANSPORT.md
new file mode 100644
index 0000000..62bce86
--- /dev/null
+++ b/spring-ai-mcp-core/docs/CLIENT.SSE.TRANSPORT.md
@@ -0,0 +1,144 @@
+# SSE Server Transport Implementation for MCP
+
+## Overview
+
+The `SseServerTransport` class implements the Model Context Protocol (MCP) HTTP with SSE transport specification, providing a bidirectional communication channel between clients and servers. This implementation is part of the Spring AI MCP Core library and follows the [MCP HTTP with SSE Transport Specification](https://spec.modelcontextprotocol.io/specification/basic/transports/#http-with-sse).
+
+## Key Features
+
+- **Bidirectional Communication**:
+ - Inbound: Receives messages through SSE connection
+ - Outbound: Sends messages via HTTP POST requests
+
+- **Error Handling**:
+ - Automatic reconnection for transient failures
+ - Graceful shutdown capabilities
+ - Comprehensive error reporting
+
+- **Message Flow**:
+ 1. Client establishes SSE connection to `/sse` endpoint
+ 2. Server sends 'endpoint' event with message URI
+ 3. Client uses provided URI for outbound messages
+
+## Technical Details
+
+### Transport Architecture
+
+The transport consists of two main processing pipelines:
+
+1. **Inbound Processing**:
+ - Establishes SSE connection to `/sse` endpoint
+ - Handles two types of events:
+ - `message`: JSON-RPC protocol messages
+ - `endpoint`: Server-provided URI for outbound messages
+ - Implements automatic retry logic for connection failures
+
+2. **Outbound Processing**:
+ - Sends messages via HTTP POST to server-provided endpoint
+ - Uses dedicated scheduler for sequential message processing
+ - Handles JSON serialization and HTTP communication
+
+### Key Components
+
+- **WebClient**: Handles HTTP communications for both SSE and POST requests
+- **ObjectMapper**: Manages JSON serialization/deserialization
+- **Scheduler**: Ensures sequential outbound message processing
+- **Message Endpoint Sink**: Manages server-provided endpoint URI
+
+## Usage Example
+
+```java
+// Create WebClient builder with base URL
+WebClient.Builder webClientBuilder = WebClient.builder()
+ .baseUrl("http://localhost:3001");
+
+// Initialize transport
+SseServerTransport transport = new SseServerTransport(webClientBuilder);
+
+// Create a JSON-RPC request
+JSONRPCRequest request = new JSONRPCRequest(
+ McpSchema.JSONRPC_VERSION,
+ "method-name",
+ "request-id",
+ Map.of("key", "value")
+);
+
+// Send message
+transport.sendMessage(request)
+ .subscribe(
+ success -> System.out.println("Message sent successfully"),
+ error -> System.err.println("Error sending message: " + error)
+ );
+
+// Graceful shutdown when done
+transport.closeGracefully()
+ .subscribe();
+```
+
+## Implementation Details
+
+### Message Processing
+
+1. **Inbound Messages**:
+ - Received through SSE connection
+ - Automatically deserialized from JSON
+ - Routed to appropriate handlers based on event type
+
+2. **Outbound Messages**:
+ - Serialized to JSON
+ - Sent via HTTP POST
+ - Processed sequentially on dedicated thread
+
+### Error Handling
+
+- **Connection Failures**:
+ - Automatic retry for transient failures
+ - Configurable retry policies
+ - Graceful handling of permanent failures
+
+- **Message Processing Errors**:
+ - Comprehensive error reporting
+ - Clean failure handling
+ - Maintains system stability
+
+### Shutdown Process
+
+The transport implements a graceful shutdown mechanism that:
+1. Marks transport as closing
+2. Disposes of all subscriptions
+3. Cleans up schedulers
+4. Completes message endpoint sink
+
+## Technical Specifications
+
+- **Protocol**: HTTP with Server-Sent Events (SSE)
+- **Message Format**: JSON-RPC 2.0
+- **Content Type**: application/json for outbound, text/event-stream for inbound
+- **Endpoints**:
+ - SSE Connection: `/sse`
+ - Message Endpoint: Dynamically provided by server
+
+## Dependencies
+
+- Spring WebFlux for reactive HTTP client
+- Jackson for JSON processing
+- Project Reactor for reactive streams
+- SLF4J for logging
+
+## Thread Safety
+
+The implementation is thread-safe and designed for concurrent use:
+- Inbound processing uses SSE event loop
+- Outbound processing uses dedicated single-threaded scheduler
+- State management uses thread-safe constructs
+
+## Testing
+
+The implementation includes comprehensive tests covering:
+- Message processing
+- Error handling
+- Connection management
+- Shutdown procedures
+- Edge cases and failure scenarios
+
+For detailed test examples, see `SseServerTransportTests.java`.
diff --git a/spring-ai-mcp-core/docs/CLIENT.STDIO.TRANSPORT.md b/spring-ai-mcp-core/docs/CLIENT.STDIO.TRANSPORT.md
new file mode 100644
index 0000000..b78ab55
--- /dev/null
+++ b/spring-ai-mcp-core/docs/CLIENT.STDIO.TRANSPORT.md
@@ -0,0 +1,123 @@
+# StdioServerTransport
+
+The `StdioServerTransport` class implements the [MCP Stdio Transport](https://spec.modelcontextprotocol.io/specification/basic/transports/#stdio) specification, providing communication with MCP servers over standard input/output streams.
+
+## Overview
+
+The Stdio transport uses standard input/output streams for bidirectional communication between the client and server:
+- Messages are sent to the server via stdout
+- Messages are received from the server via stdin
+- Error and debug information is sent via stderr
+
+## Message Format
+
+Messages are exchanged as newline-delimited JSON-RPC messages:
+- Each message is a single line of JSON text
+- Messages are terminated with a newline character (`\n`)
+- Messages follow the JSON-RPC 2.0 specification
+- UTF-8 encoding is used for all messages
+
+### Message Types
+
+The transport handles three types of JSON-RPC messages:
+
+1. **Requests** - Messages with both `method` and `id` fields:
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "example.method",
+ "params": {}
+}
+```
+
+2. **Notifications** - Messages with `method` but no `id` field:
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "example.notification",
+ "params": {}
+}
+```
+
+3. **Responses** - Messages with either `result` or `error` fields:
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {}
+}
+```
+
+## Implementation Details
+
+### Process Management
+
+The transport manages a server process with:
+- Command and arguments specified via `ServerParameters`
+- Environment variables for configuration
+- Proper process cleanup on shutdown
+
+### Threading Model
+
+The implementation uses three dedicated threads:
+1. **Inbound** - Reads and processes messages from stdin
+2. **Outbound** - Writes messages to stdout
+3. **Error** - Handles stderr messages
+
+### Error Handling
+
+Errors are handled at multiple levels:
+- Process startup failures
+- Stream I/O errors
+- Message parsing errors
+- Process termination handling
+
+## Usage Example
+
+```java
+// Create server parameters
+ServerParameters params = ServerParameters.builder()
+ .command("path/to/server")
+ .args(List.of("--arg1", "value1"))
+ .env(Map.of("ENV_VAR", "value"))
+ .build();
+
+// Create transport
+StdioServerTransport transport = new StdioServerTransport(params);
+
+// Start transport
+transport.start();
+
+// Send message
+JSONRPCRequest request = new JSONRPCRequest();
+request.setId(1);
+request.setMethod("example.method");
+request.setParams(new HashMap<>());
+transport.send(request);
+
+// Close transport
+transport.closeGracefully().block();
+```
+
+## Error Messages
+
+Error messages from the server process are:
+- Written to stderr
+- Logged at ERROR level
+- Emitted through the error sink for handling
+- UTF-8 encoded
+
+## Debugging
+
+For debugging purposes:
+1. Enable DEBUG level logging to see detailed message flow
+2. Monitor stderr for server process errors
+3. Check process exit codes for abnormal termination
+
+## Implementation Notes
+
+- The transport ensures thread safety through dedicated schedulers
+- Messages are processed asynchronously using Project Reactor
+- Resources are properly cleaned up on shutdown
+- The implementation is resilient to various failure modes
diff --git a/spring-ai-mcp-core/docs/class-diagrams.puml b/spring-ai-mcp-core/docs/class-diagrams.puml
index f98cd2b..14a7f3a 100644
--- a/spring-ai-mcp-core/docs/class-diagrams.puml
+++ b/spring-ai-mcp-core/docs/class-diagrams.puml
@@ -57,6 +57,10 @@ class StdioServerTransport {
-ServerParameters parameters
}
+class SseServerTransport {
+ -WebClient webClient
+}
+
class McpSchema {
+class ErrorCodes
+interface Request
@@ -72,6 +76,7 @@ class McpError {
McpTransport <|.. AbstractMcpTransport
AbstractMcpTransport <|-- StdioServerTransport
+AbstractMcpTransport <|-- SseServerTransport
McpSession <|-- DefaultMcpSession
DefaultMcpSession <|-- McpAsyncClient
McpClient ..> McpAsyncClient : creates
diff --git a/spring-ai-mcp-core/docs/spring-ai-mcp-uml-classdiagram.svg b/spring-ai-mcp-core/docs/spring-ai-mcp-uml-classdiagram.svg
index a46137e..fc6b72b 100644
--- a/spring-ai-mcp-core/docs/spring-ai-mcp-uml-classdiagram.svg
+++ b/spring-ai-mcp-core/docs/spring-ai-mcp-uml-classdiagram.svg
@@ -1,3 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/spring-ai-mcp-core/pom.xml b/spring-ai-mcp-core/pom.xml
index e3ba148..87438b9 100644
--- a/spring-ai-mcp-core/pom.xml
+++ b/spring-ai-mcp-core/pom.xml
@@ -21,16 +21,35 @@
+
+ org.slf4j
+ slf4j-api
+ ${slf4j-api.version}
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+
+
com.fasterxml.jackson.corejackson-databind
- 2.18.2
+ ${jackson.version}
+
io.projectreactorreactor-core
+
+ org.springframework
+ spring-webflux
+ ${springframework.version}
+
+
org.assertjassertj-core
@@ -54,8 +73,14 @@
reactor-testtest
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+ test
+
-
\ No newline at end of file
+
diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java
index 7c5af87..819409c 100644
--- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java
+++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java
@@ -36,7 +36,7 @@ import org.springframework.ai.mcp.spec.McpTransport;
*/
public class McpAsyncClient extends DefaultMcpSession {
- private static TypeReference VOID_TYPE_REFERENCE = new TypeReference() {
+ private static TypeReference VOID_TYPE_REFERENCE = new TypeReference<>() {
};
public McpAsyncClient(McpTransport transport) {
@@ -113,10 +113,10 @@ public class McpAsyncClient extends DefaultMcpSession {
// --------------------------
// Tools
// --------------------------
- private static TypeReference CALL_TOOL_RESULT_TYPE_REF = new TypeReference() {
+ private static TypeReference CALL_TOOL_RESULT_TYPE_REF = new TypeReference<>() {
};
- private static TypeReference LIST_TOOLS_RESULT_TYPE_REF = new TypeReference() {
+ private static TypeReference LIST_TOOLS_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
@@ -149,13 +149,13 @@ public class McpAsyncClient extends DefaultMcpSession {
// Resources
// --------------------------
- private static TypeReference LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference() {
+ private static TypeReference LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference<>() {
};
- private static TypeReference READ_RESOURCE_RESULT_TYPE_REF = new TypeReference() {
+ private static TypeReference READ_RESOURCE_RESULT_TYPE_REF = new TypeReference<>() {
};
- private static TypeReference LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference() {
+ private static TypeReference LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
@@ -251,10 +251,10 @@ public class McpAsyncClient extends DefaultMcpSession {
// --------------------------
// Prompts
// --------------------------
- private static TypeReference LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference() {
+ private static TypeReference LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference<>() {
};
- private static TypeReference GET_PROMPT_RESULT_TYPE_REF = new TypeReference() {
+ private static TypeReference GET_PROMPT_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/sse/SseServerTransport.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/sse/SseServerTransport.java
new file mode 100644
index 0000000..504f504
--- /dev/null
+++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/sse/SseServerTransport.java
@@ -0,0 +1,353 @@
+/*
+ * 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.sse;
+
+import java.io.IOException;
+import java.util.concurrent.Executors;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.Disposable;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.Sinks;
+import reactor.core.publisher.SynchronousSink;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
+import reactor.util.retry.Retry;
+import reactor.util.retry.Retry.RetrySignal;
+
+import org.springframework.ai.mcp.client.util.Assert;
+import org.springframework.ai.mcp.spec.AbstractMcpTransport;
+import org.springframework.ai.mcp.spec.McpSchema;
+import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
+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.
+ *
+ *
+ * This transport establishes a bidirectional communication channel where:
+ *
+ *
Inbound messages are received through an SSE connection from the server
+ *
Outbound messages are sent via HTTP POST requests to a server-provided
+ * endpoint
+ *
+ *
+ *
+ * The message flow follows these steps:
+ *
+ *
The client establishes an SSE connection to the server's /sse endpoint
+ *
The server sends an 'endpoint' event containing the URI for sending messages
+ *
+ *
+ * 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 MCP
+ * HTTP with SSE Transport Specification
+ */
+public class SseServerTransport extends AbstractMcpTransport {
+
+ private final static Logger logger = LoggerFactory.getLogger(SseServerTransport.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> 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.
+ */
+ private ObjectMapper objectMapper;
+
+ /**
+ * Dedicated scheduler for processing outbound messages. Ensures sequential processing
+ * of outbound messages on a single thread.
+ */
+ private Scheduler outboundScheduler;
+
+ /**
+ * Subscription for the SSE connection handling inbound messages. Used for cleanup
+ * during transport shutdown.
+ */
+ private Disposable inboundSubscription;
+
+ /**
+ * Subscription for the outbound message processing pipeline. Used for cleanup during
+ * transport shutdown.
+ */
+ private Disposable outboundSubscription;
+
+ /**
+ * 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.Many messageEndpointSink = Sinks.many().replay().latest();
+
+ /**
+ * Constructs a new SseServerTransport 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 SseServerTransport(WebClient.Builder webClientBuilder) {
+ this(webClientBuilder, new ObjectMapper());
+ }
+
+ /**
+ * Constructs a new SseServerTransport 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 SseServerTransport(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();
+
+ this.outboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "outbound");
+
+ startInboundProcessing();
+ startOutboundProcessing();
+ }
+
+ /**
+ * 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.
+ */
+ private void startInboundProcessing() {// @formatter:off
+ this.inboundSubscription = this.webClient
+ .get()
+ .uri(SSE_ENDPOINT)
+ .accept(MediaType.TEXT_EVENT_STREAM)
+ .retrieve()
+ .bodyToFlux(SSE_TYPE)
+ .retryWhen(Retry.from(retrySignal -> retrySignal.handle(inboundRetryHandler)))
+ .subscribe(
+ onInboudEvent,
+ onInboundError,
+ onInboundComplete);
+ } // @formatter:on
+
+ /**
+ * Retry handler for the inbound SSE stream. Implements the retry logic for handling
+ * connection failures and other errors.
+ */
+ private BiConsumer> 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());
+ };
+
+ /**
+ * Processes incoming SSE events, handling both message and endpoint events. For
+ * message events, deserializes and emits JSON-RPC messages to the inbound sink. For
+ * endpoint events, updates the message endpoint URI for outbound messages.
+ */
+ private Consumer> onInboudEvent = event -> {
+ if (!isClosing) {
+ try {
+ logger.debug("Received SSE event: {}", event);
+
+ // When a client connects, the server MUST send an endpoint event
+ // containing a URI for the client to use for sending messages.
+ // All subsequent client messages MUST be sent as HTTP POST requests
+ // to this endpoint.
+ if (ENDPOINT_EVENT_TYPE.equals(event.event())) {
+
+ String messageEndpointUri = event.data();
+ messageEndpointSink.tryEmitNext(messageEndpointUri);
+
+ }
+ else if (MESSAGE_EVENT_TYPE.equals(event.event())) {
+ JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, event.data());
+
+ if (!this.getInboundSink().tryEmitNext(message).isSuccess()) {
+ throw new RuntimeException("Failed to enqueue message");
+ }
+ }
+ }
+ catch (Exception e) {
+ if (!isClosing) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ };
+
+ /**
+ * Error handler for the inbound SSE stream.
+ */
+ private Consumer onInboundError = error -> {
+ if (!isClosing) {
+ logger.error("Error receiving SSE: {}", error);
+ }
+ };
+
+ /**
+ * Completion handler for the inbound SSE stream.
+ */
+ private Runnable onInboundComplete = () -> {
+ if (!isClosing) {
+ logger.info("SSE stream completed!");
+ }
+ };
+
+ /**
+ * Initializes and starts the outbound message processing. Sets up the pipeline for
+ * sending messages to the server-provided endpoint. Messages are sent as HTTP POST
+ * requests with JSON content.
+ *
+ *
+ *
+ * This pipeline depends on the inbound pipeline to provide the message endpoint URI
+ * for the client to use for sending messages. The messageEndpointSink is used to
+ * store the most recent message endpoint URI provided by the server.
+ */
+ private void startOutboundProcessing() {// @formatter:off
+ this.outboundSubscription = messageEndpointSink.asFlux()
+ // The messageEndpoint is the endpoint URI to send the messages
+ // It is provided by the server as part of the endpoint event
+ .flatMap(messageEndpointUri -> this.getOutboundSink()
+ .asFlux()
+ .publishOn(outboundScheduler)
+ .flatMap(jsonRpcMessage -> {
+ if (isClosing) {
+ return Mono.empty();
+ }
+
+ try {
+ String jsonText = this.objectMapper.writeValueAsString(jsonRpcMessage);
+
+ 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());
+ }
+ })
+ .thenReturn(jsonRpcMessage);
+ }
+ catch (IOException e) {
+ if (!isClosing) {
+ return Mono.error(new RuntimeException("Failed to serialize message", e));
+ }
+ return Mono.empty();
+ }
+ }))
+ .subscribe();
+ } // @formatter:on
+
+ /**
+ * 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 closeGracefully() { // @formatter:off
+ return Mono.fromRunnable(() -> {
+ isClosing = true;
+
+ // Dispose of subscriptions
+
+ if (inboundSubscription != null) {
+ inboundSubscription.dispose();
+ }
+
+ if (outboundSubscription != null) {
+ outboundSubscription.dispose();
+ }
+
+ // Dispose of scheduler
+ if (outboundScheduler != null) {
+ outboundScheduler.dispose();
+ }
+
+ // Complete the endpoint sink
+ // messageEndpointSink.tryEmitComplete();
+ })
+ .then()
+ .subscribeOn(Schedulers.boundedElastic());
+ } // @formatter:on
+
+}
diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/StdioServerTransport.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/StdioServerTransport.java
index 3b28f00..394bdec 100644
--- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/StdioServerTransport.java
+++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/StdioServerTransport.java
@@ -21,51 +21,64 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
-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.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import org.springframework.ai.mcp.client.util.Assert;
import org.springframework.ai.mcp.spec.AbstractMcpTransport;
+import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
-import org.springframework.ai.mcp.spec.McpSchema.JSONRPCNotification;
-import org.springframework.ai.mcp.spec.McpSchema.JSONRPCRequest;
-import org.springframework.ai.mcp.spec.McpSchema.JSONRPCResponse;
/**
- * Stdio client for communicating with a server process.
+ * 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 StdioServerTransport extends AbstractMcpTransport {
- private final static TypeReference> MAP_TYPE_REF = new TypeReference>() {
- };
+ private static final Logger logger = LoggerFactory.getLogger(StdioServerTransport.class);
+ /** The server process being communicated with */
private Process process;
+ /** 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;
+ /**
+ * Creates a new StdioServerTransport with the specified parameters and default
+ * ObjectMapper.
+ * @param params The parameters for configuring the server process
+ */
public StdioServerTransport(ServerParameters params) {
this(params, new ObjectMapper());
}
+ /**
+ * Creates a new StdioServerTransport 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 StdioServerTransport(ServerParameters params, ObjectMapper objectMapper) {
-
super(objectMapper);
Assert.notNull(params, "The params can not be null");
@@ -79,6 +92,13 @@ public class StdioServerTransport extends AbstractMcpTransport {
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 void start() {
// Let's kick off the abstraction layer that will consume the logical messages
@@ -115,10 +135,19 @@ public class StdioServerTransport extends AbstractMcpTransport {
startErrorProcessing();
}
+ /**
+ * Creates and returns a new ProcessBuilder instance. Protected to allow overriding in
+ * tests.
+ * @return A new ProcessBuilder instance
+ */
protected ProcessBuilder getProcessBuilder() {
return new ProcessBuilder();
}
+ /**
+ * Waits for the server process to exit.
+ * @throws RuntimeException if the process is interrupted while waiting
+ */
public void awaitForExit() {
try {
this.process.waitFor();
@@ -128,6 +157,10 @@ public class StdioServerTransport extends AbstractMcpTransport {
}
}
+ /**
+ * 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(
@@ -135,7 +168,7 @@ public class StdioServerTransport extends AbstractMcpTransport {
String line;
while ((line = processErrorReader.readLine()) != null) {
try {
- System.out.println("Received error line: " + line);
+ logger.error("Received error line: {}", line);
// TODO: handle errors, etc.
this.getErrorSink().tryEmitNext(line);
}
@@ -150,13 +183,17 @@ public class StdioServerTransport extends AbstractMcpTransport {
});
}
+ /**
+ * 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 = deserializeJsonRpcMessage(line);
+ JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, line);
if (!this.getInboundSink().tryEmitNext(message).isSuccess()) {
// TODO: Back off, reschedule, give up?
throw new RuntimeException("Failed to enqueue message");
@@ -173,24 +210,11 @@ public class StdioServerTransport extends AbstractMcpTransport {
});
}
- private JSONRPCMessage deserializeJsonRpcMessage(String jsonText) throws IOException {
-
- var map = this.objectMapper.readValue(jsonText, MAP_TYPE_REF);
-
- // Determine message type based on specific JSON structure
- if (map.containsKey("method") && map.containsKey("id")) {
- return this.objectMapper.convertValue(map, JSONRPCRequest.class);
- }
- else if (map.containsKey("method") && !map.containsKey("id")) {
- return this.objectMapper.convertValue(map, JSONRPCNotification.class);
- }
- else if (map.containsKey("result") || map.containsKey("error")) {
- return this.objectMapper.convertValue(map, JSONRPCResponse.class);
- }
-
- throw new IllegalArgumentException("Cannot deserialize JSONRPCMessage: " + jsonText);
- }
-
+ /**
+ * 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.getOutboundSink()
.asFlux()
@@ -214,11 +238,16 @@ public class StdioServerTransport extends AbstractMcpTransport {
.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 closeGracefully() {
-
return Mono.fromFuture(() -> {
- System.out.println("Sending TERM to process");
+ logger.info("Sending TERM to process");
if (this.process != null) {
this.process.destroy();
return process.onExit();
@@ -228,7 +257,7 @@ public class StdioServerTransport extends AbstractMcpTransport {
}
}).doOnNext(process -> {
if (process.exitValue() != 0) {
- System.out.println("Process terminated with code " + process.exitValue());
+ logger.warn("Process terminated with code " + process.exitValue());
}
}).then(Mono.fromRunnable(() -> {
// The Threads are blocked on readLine so disposeGracefully would not
@@ -239,4 +268,4 @@ public class StdioServerTransport extends AbstractMcpTransport {
})).then().subscribeOn(Schedulers.boundedElastic());
}
-}
\ No newline at end of file
+}
diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/AbstractMcpTransport.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/AbstractMcpTransport.java
index 5e26319..0f0556e 100644
--- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/AbstractMcpTransport.java
+++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/AbstractMcpTransport.java
@@ -19,6 +19,8 @@ package org.springframework.ai.mcp.spec;
import java.util.function.Consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
@@ -34,6 +36,8 @@ import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
// proper encapsulation and state management.
public abstract class AbstractMcpTransport implements McpTransport {
+ private static final Logger logger = LoggerFactory.getLogger(AbstractMcpTransport.class);
+
protected final ObjectMapper objectMapper;
private final Sinks.Many errorSink;
@@ -42,10 +46,9 @@ public abstract class AbstractMcpTransport implements McpTransport {
private final Sinks.Many outboundSink;
- private Consumer inboundMessageHandler = message -> System.out
- .println("Received message: " + message);
+ private Consumer inboundMessageHandler = message -> logger.debug("Received message: {}", message);
- private Consumer errorHandler = error -> System.err.println("Received error: " + error);
+ private Consumer errorHandler = error -> logger.error("Received error: {}", error);
public AbstractMcpTransport() {
this(new ObjectMapper());
diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/DefaultMcpSession.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/DefaultMcpSession.java
index e8d3f4f..f145855 100644
--- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/DefaultMcpSession.java
+++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/DefaultMcpSession.java
@@ -23,6 +23,8 @@ import java.util.concurrent.ConcurrentHashMap;
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.Mono;
import reactor.core.publisher.MonoSink;
@@ -36,6 +38,8 @@ import org.springframework.ai.mcp.client.util.Assert;
*/
public class DefaultMcpSession implements McpSession {
+ private static final Logger logger = LoggerFactory.getLogger(DefaultMcpSession.class);
+
private final ConcurrentHashMap