code cleaning

This commit is contained in:
Christian Tzolov 2024-12-10 11:46:58 +01:00
commit 1b6d661470
15 changed files with 191 additions and 100 deletions

View file

@ -101,7 +101,7 @@ The SDK follows a layered architecture:
- **McpClient**: Main interface defining the synchronous operations - **McpClient**: Main interface defining the synchronous operations
- **McpAsyncClient**: Async implementation using Project Reactor - **McpAsyncClient**: Async implementation using Project Reactor
- **McpSyncClient**: Synchronous wrapper around the async client - **McpSyncClient**: Synchronous wrapper around the async client
- **McpAsyncTransport**: Transport layer interface - **McpTransport**: Transport layer interface
- **DefaultMcpTransport**: Base transport implementation - **DefaultMcpTransport**: Base transport implementation
- **StdioServerTransport**: Stdio-based server communication - **StdioServerTransport**: Stdio-based server communication

View file

@ -8,7 +8,7 @@ skinparam class {
} }
' Interfaces ' Interfaces
interface McpAsyncTransport { interface McpTransport {
+start(): void +start(): void
+close(): void +close(): void
+setInboudMessageHandler(Consumer<JSONRPCMessage>): void +setInboudMessageHandler(Consumer<JSONRPCMessage>): void
@ -42,16 +42,16 @@ class DefaultMcpTransport {
} }
class McpAsyncClient { class McpAsyncClient {
-transport: McpAsyncTransport -transport: McpTransport
-session: McpAsyncSession -session: McpAsyncSession
+McpAsyncClient(McpAsyncTransport) +McpAsyncClient(McpTransport)
+initialize(): Mono<InitializeResult> +initialize(): Mono<InitializeResult>
+close(): void +close(): void
} }
class McpSyncClient { class McpSyncClient {
-asyncClient: McpAsyncClient -asyncClient: McpAsyncClient
+McpSyncClient(McpAsyncTransport) +McpSyncClient(McpTransport)
+initialize(): InitializeResult +initialize(): InitializeResult
+close(): void +close(): void
} }
@ -79,14 +79,14 @@ class Assert {
} }
' Relationships ' Relationships
McpAsyncTransport <|.. DefaultMcpTransport McpTransport <|.. DefaultMcpTransport
DefaultMcpTransport <|-- StdioServerTransport DefaultMcpTransport <|-- StdioServerTransport
McpClient <|.. McpSyncClient McpClient <|.. McpSyncClient
McpAsyncSession <|.. McpAsyncClient McpAsyncSession <|.. McpAsyncClient
McpSyncClient o-- McpAsyncClient McpSyncClient o-- McpAsyncClient
McpAsyncClient o-- McpAsyncTransport McpAsyncClient o-- McpTransport
McpAsyncClient o-- McpAsyncSession McpAsyncClient o-- McpAsyncSession
StdioServerTransport o-- ServerParameters StdioServerTransport o-- ServerParameters
@ -94,7 +94,7 @@ ServerParameters <.. ServerParametersParser : creates
' Package organization ' Package organization
package "spring.ai.mcp.spec" { package "spring.ai.mcp.spec" {
' McpAsyncTransport ' McpTransport
' McpAsyncSession ' McpAsyncSession
' DefaultMcpTransport ' DefaultMcpTransport
} }

View file

@ -5,23 +5,27 @@ import java.time.Duration;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import spring.ai.experimental.mcp.spec.McpAsyncSession; import spring.ai.experimental.mcp.spec.McpSession;
import spring.ai.experimental.mcp.spec.McpAsyncTransport; import spring.ai.experimental.mcp.spec.McpTransport;
import spring.ai.experimental.mcp.spec.McpSchema; import spring.ai.experimental.mcp.spec.McpSchema;
public class McpAsyncClient extends McpAsyncSession { /**
* @author Dariusz Jędrzejczyk
* @author Christian Tzolov
*/
public class McpAsyncClient extends McpSession {
public McpAsyncClient(McpAsyncTransport transport) { public McpAsyncClient(McpTransport transport) {
this(transport, Duration.ofSeconds(10), new ObjectMapper()); this(transport, Duration.ofSeconds(10), new ObjectMapper());
} }
public McpAsyncClient(McpAsyncTransport transport, Duration requestTimeout, ObjectMapper objectMapper) { public McpAsyncClient(McpTransport transport, Duration requestTimeout, ObjectMapper objectMapper) {
super(requestTimeout, objectMapper, transport); super(requestTimeout, objectMapper, transport);
} }
/** /**
* The initialization phase MUST be the first interaction between client and server. * The initialization phase MUST be the first interaction between client and
* server.
* During this phase, the client and server: * During this phase, the client and server:
* <ul> * <ul>
* <li>Establish protocol version compatibility</li> * <li>Establish protocol version compatibility</li>
@ -29,7 +33,8 @@ public class McpAsyncClient extends McpAsyncSession {
* <li>Share implementation details</li> * <li>Share implementation details</li>
* </ul> * </ul>
* <br/> * <br/>
* The client MUST initiate this phase by sending an initialize request containing: * The client MUST initiate this phase by sending an initialize request
* containing:
* <ul> * <ul>
* <li>The protocol version the client supports</li> * <li>The protocol version the client supports</li>
* <li>The client's capabilities</li> * <li>The client's capabilities</li>
@ -38,7 +43,8 @@ public class McpAsyncClient extends McpAsyncSession {
* *
* The server MUST respond with its own capabilities and information: * The server MUST respond with its own capabilities and information:
* {@link McpSchema.ServerCapabilities}. <br/> * {@link McpSchema.ServerCapabilities}. <br/>
* After successful initialization, the client MUST send an initialized notification * After successful initialization, the client MUST send an initialized
* notification
* to indicate it is ready to begin normal operations. * to indicate it is ready to begin normal operations.
* *
* <br/> * <br/>
@ -46,6 +52,7 @@ public class McpAsyncClient extends McpAsyncSession {
* <a href= * <a href=
* "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization * "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization
* Spec</a> * Spec</a>
*
* @return the initialize result. * @return the initialize result.
*/ */
public Mono<McpSchema.InitializeResult> initialize() { public Mono<McpSchema.InitializeResult> initialize() {
@ -54,8 +61,9 @@ public class McpAsyncClient extends McpAsyncSession {
new McpSchema.ClientCapabilities(null, new McpSchema.ClientCapabilities.RootCapabilities(true), null), new McpSchema.ClientCapabilities(null, new McpSchema.ClientCapabilities.RootCapabilities(true), null),
new McpSchema.Implementation("mcp-java-client", "0.0.1")); // @formatter:on new McpSchema.Implementation("mcp-java-client", "0.0.1")); // @formatter:on
Mono<McpSchema.InitializeResult> result = Mono<McpSchema.InitializeResult> result = this.sendRequest("initialize", initializeRequest,
this.sendRequest("initialize", initializeRequest, new TypeReference<McpSchema.InitializeResult>() {}); new TypeReference<McpSchema.InitializeResult>() {
});
return result.flatMap(initializeResult -> { return result.flatMap(initializeResult -> {
if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeResult.protocolVersion())) { if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeResult.protocolVersion())) {
@ -81,7 +89,8 @@ public class McpAsyncClient extends McpAsyncSession {
*/ */
public Mono<Void> ping() { public Mono<Void> ping() {
return this.sendRequest("ping", null, return this.sendRequest("ping", null,
new TypeReference<Void>() {}); new TypeReference<Void>() {
});
} }
// -------------------------- // --------------------------
@ -89,21 +98,25 @@ public class McpAsyncClient extends McpAsyncSession {
// -------------------------- // --------------------------
/** /**
* Send a tools/call request. * Send a tools/call request.
*
* @param callToolRequest the call tool request. * @param callToolRequest the call tool request.
* @return the call tool result. * @return the call tool result.
*/ */
public Mono<McpSchema.CallToolResult> callTool(McpSchema.CallToolRequest callToolRequest) { public Mono<McpSchema.CallToolResult> callTool(McpSchema.CallToolRequest callToolRequest) {
return this.sendRequest("tools/call", callToolRequest, return this.sendRequest("tools/call", callToolRequest,
new TypeReference<McpSchema.CallToolResult>() {}); new TypeReference<McpSchema.CallToolResult>() {
});
} }
/** /**
* Send a tools/list request. * Send a tools/list request.
*
* @return the list of tools result. * @return the list of tools result.
*/ */
public Mono<McpSchema.ListToolsResult> listTools(String cursor) { public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
return this.sendRequest("tools/list", new McpSchema.PaginatedRequest(cursor), return this.sendRequest("tools/list", new McpSchema.PaginatedRequest(cursor),
new TypeReference<McpSchema.ListToolsResult>() {}); new TypeReference<McpSchema.ListToolsResult>() {
});
} }
// -------------------------- // --------------------------
@ -112,16 +125,19 @@ public class McpAsyncClient extends McpAsyncSession {
/** /**
* Send a resources/list request. * Send a resources/list request.
*
* @param cursor the cursor * @param cursor the cursor
* @return the list of resources result. * @return the list of resources result.
*/ */
public Mono<McpSchema.ListResourcesResult> listResources(String cursor) { public Mono<McpSchema.ListResourcesResult> listResources(String cursor) {
return this.sendRequest("resources/list", new McpSchema.PaginatedRequest(cursor), return this.sendRequest("resources/list", new McpSchema.PaginatedRequest(cursor),
new TypeReference<McpSchema.ListResourcesResult>() {}); new TypeReference<McpSchema.ListResourcesResult>() {
});
} }
/** /**
* Send a resources/read request. * Send a resources/read request.
*
* @param resource the resource to read * @param resource the resource to read
* @return the resource content. * @return the resource content.
*/ */
@ -131,12 +147,14 @@ public class McpAsyncClient extends McpAsyncSession {
/** /**
* Send a resources/read request. * Send a resources/read request.
*
* @param readResourceRequest the read resource request. * @param readResourceRequest the read resource request.
* @return the resource content. * @return the resource content.
*/ */
public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceRequest readResourceRequest) { public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceRequest readResourceRequest) {
return this.sendRequest("resources/read", readResourceRequest, return this.sendRequest("resources/read", readResourceRequest,
new TypeReference<McpSchema.ReadResourceResult>() {}); new TypeReference<McpSchema.ReadResourceResult>() {
});
} }
/** /**
@ -144,16 +162,19 @@ public class McpAsyncClient extends McpAsyncSession {
* templates. Arguments may be auto-completed through the completion API. * templates. Arguments may be auto-completed through the completion API.
* *
* Request a list of resource templates the server has. * Request a list of resource templates the server has.
*
* @param cursor the cursor * @param cursor the cursor
* @return the list of resource templates result. * @return the list of resource templates result.
*/ */
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates(String cursor) { public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates(String cursor) {
return this.sendRequest("resources/templates/list", new McpSchema.PaginatedRequest(cursor), return this.sendRequest("resources/templates/list", new McpSchema.PaginatedRequest(cursor),
new TypeReference<McpSchema.ListResourceTemplatesResult>() {}); new TypeReference<McpSchema.ListResourceTemplatesResult>() {
});
} }
/** /**
* List Changed Notification. When the list of available resources changes, servers * List Changed Notification. When the list of available resources changes,
* servers
* that declared the listChanged capability SHOULD send a notification: * that declared the listChanged capability SHOULD send a notification:
*/ */
public Mono<Void> sendResourcesListChanged() { public Mono<Void> sendResourcesListChanged() {
@ -161,26 +182,34 @@ public class McpAsyncClient extends McpAsyncSession {
} }
/** /**
* Subscriptions. The protocol supports optional subscriptions to resource changes. * Subscriptions. The protocol supports optional subscriptions to resource
* Clients can subscribe to specific resources and receive notifications when they * changes.
* Clients can subscribe to specific resources and receive notifications when
* they
* change. * change.
* *
* Send a resources/subscribe request. * Send a resources/subscribe request.
* @param subscribeRequest the subscribe request contains the uri of the resource to *
* @param subscribeRequest the subscribe request contains the uri of the
* resource to
* subscribe to. * subscribe to.
*/ */
public Mono<Void> subscribeResource(McpSchema.SubscribeRequest subscribeRequest) { public Mono<Void> subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
return this.sendRequest("resources/subscribe", subscribeRequest, return this.sendRequest("resources/subscribe", subscribeRequest,
new TypeReference<Void>() {}); new TypeReference<Void>() {
});
} }
/** /**
* Send a resources/unsubscribe request. * Send a resources/unsubscribe request.
* @param unsubscribeRequest the unsubscribe request contains the uri of the resource *
* @param unsubscribeRequest the unsubscribe request contains the uri of the
* resource
* to unsubscribe from. * to unsubscribe from.
*/ */
public Mono<Void> unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) { public Mono<Void> unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
return this.sendRequest("resources/unsubscribe", unsubscribeRequest, return this.sendRequest("resources/unsubscribe", unsubscribeRequest,
new TypeReference<Void>() {}); new TypeReference<Void>() {
});
} }
} }

View file

@ -18,15 +18,14 @@ package spring.ai.experimental.mcp.client;
import java.time.Duration; import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import spring.ai.experimental.mcp.spec.McpAsyncTransport; import spring.ai.experimental.mcp.spec.McpTransport;
/** /**
* The MCP client is the main entry point for interacting with the Model Context Protocol * The MCP client is the main entry point for interacting with the Model Context Protocol
* (MCP) server. * (MCP) server.
* *
*
* @author Christian Tzolov * @author Christian Tzolov
* @since 1.0.0 * @author Dariusz Jędrzejczyk
*/ */
public class McpClient { public class McpClient {
@ -40,20 +39,20 @@ public class McpClient {
// JSONtoPOJOCodec type // JSONtoPOJOCodec type
// .sync(); // .sync();
public static McpAsyncClient async(McpAsyncTransport transport) { public static McpAsyncClient async(McpTransport transport) {
return new McpAsyncClient(transport); return new McpAsyncClient(transport);
} }
public static McpAsyncClient async(McpAsyncTransport transport, Duration requestTimeout, public static McpAsyncClient async(McpTransport transport, Duration requestTimeout,
ObjectMapper objectMapper) { ObjectMapper objectMapper) {
return new McpAsyncClient(transport, requestTimeout, objectMapper); return new McpAsyncClient(transport, requestTimeout, objectMapper);
} }
public static McpSyncClient sync(McpAsyncTransport transport) { public static McpSyncClient sync(McpTransport transport) {
return new McpSyncClient(async(transport)); return new McpSyncClient(async(transport));
} }
public static McpSyncClient sync(McpAsyncTransport transport, Duration requestTimeout, public static McpSyncClient sync(McpTransport transport, Duration requestTimeout,
ObjectMapper objectMapper) { ObjectMapper objectMapper) {
return new McpSyncClient(async(transport, requestTimeout, objectMapper)); return new McpSyncClient(async(transport, requestTimeout, objectMapper));
} }

View file

@ -5,6 +5,10 @@ import java.time.Duration;
import spring.ai.experimental.mcp.client.util.Assert; import spring.ai.experimental.mcp.client.util.Assert;
import spring.ai.experimental.mcp.spec.McpSchema; import spring.ai.experimental.mcp.spec.McpSchema;
/**
* @author Dariusz Jędrzejczyk
* @author Christian Tzolov
*/
public class McpSyncClient implements AutoCloseable { public class McpSyncClient implements AutoCloseable {
// TODO: Consider providing a client config to set this properly // TODO: Consider providing a client config to set this properly

View file

@ -13,6 +13,9 @@ import spring.ai.experimental.mcp.client.util.Assert;
/** /**
* Server parameters for stdio client. * Server parameters for stdio client.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/ */
@JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonInclude(JsonInclude.Include.NON_ABSENT)
public class ServerParameters { public class ServerParameters {

View file

@ -25,7 +25,7 @@ import spring.ai.experimental.mcp.client.stdio.ServerParametersParser.McpServerC
/** /**
* @author Christian Tzolov * @author Christian Tzolov
* @since 1.0.0 * @author Dariusz Jędrzejczyk
*/ */
public class ServerParametersParser { public class ServerParametersParser {

View file

@ -21,6 +21,9 @@ import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCResponse;
/** /**
* Stdio client for communicating with a server process. * Stdio client for communicating with a server process.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/ */
public class StdioServerTransport extends DefaultMcpTransport { public class StdioServerTransport extends DefaultMcpTransport {

View file

@ -25,9 +25,9 @@ import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCMessage;
/** /**
* @author Christian Tzolov * @author Christian Tzolov
* @since 1.0.0 * @author Dariusz Jędrzejczyk
*/ */
public class DefaultMcpTransport implements McpAsyncTransport { public class DefaultMcpTransport implements McpTransport {
protected final ObjectMapper objectMapper; protected final ObjectMapper objectMapper;
@ -93,7 +93,7 @@ public class DefaultMcpTransport implements McpAsyncTransport {
this.inboundMessageHandler = inboundMessageHandler; this.inboundMessageHandler = inboundMessageHandler;
} }
public void setErrorHandler(Consumer<String> errorHandler) { public void setInboundErrorHandler(Consumer<String> errorHandler) {
this.errorHandler = errorHandler; this.errorHandler = errorHandler;
} }

View file

@ -1,39 +0,0 @@
/*
* 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 spring.ai.experimental.mcp.spec;
import java.util.function.Consumer;
import reactor.core.publisher.Mono;
import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCMessage;
/**
* @author Christian Tzolov
* @since 1.0.0
*/
public interface McpAsyncTransport extends AutoCloseable{
void start();
void close();
void setInboudMessageHandler(Consumer<JSONRPCMessage> messageReader);
void setErrorHandler(Consumer<String> errorReader);
Mono<Void> sendMessage(JSONRPCMessage message);
}

View file

@ -0,0 +1,127 @@
package spring.ai.experimental.mcp.spec;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import spring.ai.experimental.mcp.client.util.Assert;
/**
* Implementation of the MCP client session.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
public class McpSession {
private final ConcurrentHashMap<Object, MonoSink<McpSchema.JSONRPCResponse>> pendingResponses = new ConcurrentHashMap<>();
private final Duration requestTimeout;
private final ObjectMapper objectMapper;
private final McpTransport transport;
public McpSession(Duration requestTimeout, ObjectMapper objectMapper, McpTransport transport) {
Assert.notNull(objectMapper, "The ObjectMapper can not be null");
Assert.notNull(requestTimeout, "The requstTimeout can not be null");
Assert.notNull(transport, "The transport can not be null");
this.requestTimeout = requestTimeout;
this.objectMapper = objectMapper;
this.transport = transport;
this.transport.setInboudMessageHandler(message -> {
switch (message) {
case McpSchema.JSONRPCResponse response -> {
var sink = pendingResponses.remove(response.id());
if (sink == null) {
System.out.println("Unexpected response for unkown id " + response.id());
} else {
sink.success(response);
}
}
case McpSchema.JSONRPCRequest request -> {
System.out.println("Client does not yet support server requests");
}
case McpSchema.JSONRPCNotification notification -> {
System.out.println("Notifications not yet supported");
}
}
});
this.transport.setInboundErrorHandler(error -> System.out.println("Error received " + error));
this.transport.start();
}
public <T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef) {
String requestId = UUID.randomUUID().toString();
return Mono.<McpSchema.JSONRPCResponse>create(sink -> {
this.pendingResponses.put(requestId, sink);
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method,
requestId, requestParams);
try {
// TODO: This is non-blocking, but it's actually a synchronous call,
// perhaps there's no need to make it return Mono?
this.transport.sendMessage(jsonrpcRequest)
// TODO: It's most efficient to create a dedicated
// Subscriber here
.subscribe(v -> {
}, e -> {
this.pendingResponses.remove(requestId);
sink.error(e);
});
} catch (Exception e) {
sink.error(e);
}
}).timeout(this.requestTimeout).handle((jsonRpcResponse, s) -> {
if (jsonRpcResponse.error() != null) {
s.error(new McpError(jsonRpcResponse.error()));
} else {
if (typeRef.getType().getTypeName().equals("java.lang.Void")) {
s.complete();
} else {
s.next(this.objectMapper.convertValue(jsonRpcResponse.result(), typeRef));
}
}
});
}
public static class McpError extends RuntimeException {
public McpError(Object error) {
super(error.toString());
}
}
public Mono<Void> sendNotification(String method) {
return sendNotification(method, null);
}
public Mono<Void> sendNotification(String method, Map<String, Object> params) {
McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION,
method, params);
try {
// TODO: make it non-blocking
this.transport.sendMessage(jsonrpcNotification);
} catch (Exception e) {
return Mono.error(new McpError(e));
}
return Mono.empty();
}
public Mono<Void> closeGracefully(Duration timeout) {
// TODO handle the timeout in transport
return Mono.fromRunnable(this.transport::close);
}
}

View file

@ -0,0 +1,91 @@
/*
* 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 spring.ai.experimental.mcp.spec;
import java.util.function.Consumer;
import reactor.core.publisher.Mono;
import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCMessage;
/**
* Defines the asynchronous transport layer for the Model Context Protocol (MCP).
*
* <p>The McpTransport interface provides the foundation for implementing custom transport
* mechanisms in the Model Context Protocol. It handles the bidirectional communication between
* the client and server components, supporting asynchronous message exchange using JSON-RPC format.</p>
*
* <p>Implementations of this interface are responsible for:</p>
* <ul>
* <li>Managing the lifecycle of the transport connection</li>
* <li>Handling incoming messages and errors from the server</li>
* <li>Sending outbound messages to the server</li>
* </ul>
*
* <p>The transport layer is designed to be protocol-agnostic, allowing for various implementations
* such as WebSocket, HTTP, or custom protocols.</p>
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
public interface McpTransport extends AutoCloseable {
/**
* Initializes and starts the transport connection.
*
* <p>This method should be called before any message exchange can occur. It sets up
* the necessary resources and establishes the connection to the server.</p>
*/
void start();
/**
* Closes the transport connection and releases any associated resources.
*
* <p>This method ensures proper cleanup of resources when the transport is no longer needed.
* It should handle the graceful shutdown of any active connections.</p>
*/
void close();
/**
* Sets the handler for processing incoming messages from the server.
*
* <p>The provided handler will be called whenever a new message is received from the server.
* Messages are expected to follow the JSON-RPC format as defined in the MCP specification.</p>
*
* @param inboundMessageHandler a consumer that processes incoming {@link JSONRPCMessage}s
*/
void setInboudMessageHandler(Consumer<JSONRPCMessage> inboundMessageHandler);
/**
* 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 inboundErrorHandler a consumer that processes error messages
*/
void setInboundErrorHandler(Consumer<String> inboundErrorHandler);
/**
* Sends a message to the server asynchronously.
*
* <p>This method handles the transmission of messages to the server in an asynchronous manner.
* Messages are sent in JSON-RPC format as specified by the MCP protocol.</p>
*
* @param message the {@link JSONRPCMessage} to be sent to the server
* @return a {@link Mono<Void>} that completes when the message has been sent
*/
Mono<Void> sendMessage(JSONRPCMessage message);
}

View file

@ -23,8 +23,6 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.Timeout;
import spring.ai.experimental.mcp.client.McpAsyncClient;
import spring.ai.experimental.mcp.client.McpClient;
import spring.ai.experimental.mcp.client.stdio.ServerParameters; import spring.ai.experimental.mcp.client.stdio.ServerParameters;
import spring.ai.experimental.mcp.client.stdio.StdioServerTransport; import spring.ai.experimental.mcp.client.stdio.StdioServerTransport;
import spring.ai.experimental.mcp.spec.McpSchema.CallToolRequest; import spring.ai.experimental.mcp.spec.McpSchema.CallToolRequest;

View file

@ -16,29 +16,26 @@
package spring.ai.experimental.mcp.client; package spring.ai.experimental.mcp.client;
import java.time.Duration; import java.time.Duration;
import java.util.List;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.Timeout;
import spring.ai.experimental.mcp.client.McpClient;
import spring.ai.experimental.mcp.client.McpSyncClient;
import spring.ai.experimental.mcp.client.stdio.ServerParameters; import spring.ai.experimental.mcp.client.stdio.ServerParameters;
import spring.ai.experimental.mcp.client.stdio.StdioServerTransport; import spring.ai.experimental.mcp.client.stdio.StdioServerTransport;
import spring.ai.experimental.mcp.spec.McpSchema.CallToolRequest; import spring.ai.experimental.mcp.spec.McpSchema.CallToolRequest;
import spring.ai.experimental.mcp.spec.McpSchema.CallToolResult; import spring.ai.experimental.mcp.spec.McpSchema.CallToolResult;
import spring.ai.experimental.mcp.spec.McpSchema.Content;
import spring.ai.experimental.mcp.spec.McpSchema.ListResourcesResult; import spring.ai.experimental.mcp.spec.McpSchema.ListResourcesResult;
import spring.ai.experimental.mcp.spec.McpSchema.ListToolsResult; import spring.ai.experimental.mcp.spec.McpSchema.ListToolsResult;
import spring.ai.experimental.mcp.spec.McpSchema.Resource; import spring.ai.experimental.mcp.spec.McpSchema.Resource;
import spring.ai.experimental.mcp.spec.McpSchema.TextContent; import spring.ai.experimental.mcp.spec.McpSchema.TextContent;
import spring.ai.experimental.mcp.spec.McpSchema.Tool; import spring.ai.experimental.mcp.spec.McpSchema.Tool;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import com.fasterxml.jackson.databind.ObjectMapper; import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** /**
* Unit tests for MCP Client Session functionality. * Unit tests for MCP Client Session functionality.

View file

@ -107,7 +107,7 @@ class DefaultMcpTransportTests {
@Test @Test
void customErrorHandlerShouldReceiveErrors() { void customErrorHandlerShouldReceiveErrors() {
AtomicReference<String> receivedError = new AtomicReference<>(); AtomicReference<String> receivedError = new AtomicReference<>();
transport.setErrorHandler(receivedError::set); transport.setInboundErrorHandler(receivedError::set);
transport.start(); transport.start();
String errorMessage = "Test error"; String errorMessage = "Test error";
@ -124,7 +124,7 @@ class DefaultMcpTransportTests {
AtomicReference<String> receivedError = new AtomicReference<>(); AtomicReference<String> receivedError = new AtomicReference<>();
transport.setInboudMessageHandler(receivedMessage::set); transport.setInboudMessageHandler(receivedMessage::set);
transport.setErrorHandler(receivedError::set); transport.setInboundErrorHandler(receivedError::set);
transport.start(); transport.start();
JSONRPCRequest message = new JSONRPCRequest( JSONRPCRequest message = new JSONRPCRequest(