code cleaning
This commit is contained in:
parent
b378ab73dd
commit
1b6d661470
15 changed files with 191 additions and 100 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>() {
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue