diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java index 8b13f235a..5873dd098 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java @@ -26,10 +26,12 @@ import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; import io.modelcontextprotocol.spec.McpSchema.ElicitResult; +import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; @@ -38,6 +40,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -78,6 +81,13 @@ * .streamableHttpTransport("https://mcp.example.com/http") * .queryParams(Map.of("token", "abc123", "env", "prod")) * .buildSync(); + * + * // StdIO transport with custom protocol versions + * McpClientWrapper client = McpClientBuilder.create("mcp") + * .stdioTransport("python", "server.py") + * .protocolVersions("2024-11-05", "2025-03-26") + * .buildAsync() + * .block(); * } */ public class McpClientBuilder { @@ -91,6 +101,7 @@ public class McpClientBuilder { private Duration initializationTimeout = DEFAULT_INIT_TIMEOUT; private Function> asyncElicitationHandler; private Function syncElicitationHandler; + private List protocolVersions; private McpClientBuilder(String name) { this.name = name; @@ -294,6 +305,34 @@ public McpClientBuilder initializationTimeout(Duration timeout) { return this; } + /** + * Sets the MCP protocol versions that the client supports. + * + *

By default, the client only supports "2024-11-05". If the MCP server responds + * with a different protocol version during initialization (e.g., "2025-03-26"), + * the connection will fail with "Unsupported protocol version". + * + *

Use this method to declare support for additional protocol versions: + *

{@code
+     * McpClientWrapper client = McpClientBuilder.create("server")
+     *         .stdioTransport("python", "server.py")
+     *         .protocolVersions("2024-11-05", "2025-03-26")
+     *         .buildAsync()
+     *         .block();
+     * }
+ * + * @param versions one or more protocol version strings (e.g., "2024-11-05", "2025-03-26") + * @return this builder + * @throws IllegalArgumentException if no versions are provided + */ + public McpClientBuilder protocolVersions(String... versions) { + if (versions == null || versions.length == 0) { + throw new IllegalArgumentException("At least one protocol version must be specified"); + } + this.protocolVersions = Collections.unmodifiableList(Arrays.asList(versions)); + return this; + } + /** * Registers an asynchronous elicitation handler for processing elicit requests * from the server. @@ -381,6 +420,11 @@ public Mono buildAsync() { () -> { McpClientTransport transport = transportConfig.createTransport(); + if (protocolVersions != null) { + transport = + new ProtocolVersionOverrideTransport(transport, protocolVersions); + } + McpSchema.Implementation clientInfo = new McpSchema.Implementation( "agentscope-java", "AgentScope Java Framework", VERSION); @@ -417,6 +461,10 @@ public McpClientWrapper buildSync() { McpClientTransport transport = transportConfig.createTransport(); + if (protocolVersions != null) { + transport = new ProtocolVersionOverrideTransport(transport, protocolVersions); + } + McpSchema.Implementation clientInfo = new McpSchema.Implementation( "agentscope-java", "AgentScope Java Framework", Version.VERSION); @@ -460,6 +508,64 @@ private interface TransportConfig { McpClientTransport createTransport(); } + /** + * A transport decorator that overrides the protocol versions reported to the MCP client. + * + *

The MCP Java SDK's {@code LifecycleInitializer} performs a strict check: + * the server's protocol version must be contained in the client's supported versions list. + * By default, transports only report support for "2024-11-05", causing connections to fail + * with servers that respond with newer versions (e.g., "2025-03-26"). + * + *

This decorator wraps any {@link McpClientTransport} and overrides + * {@link #protocolVersions()} to return a user-specified list, while delegating + * all other operations to the underlying transport. + */ + private static class ProtocolVersionOverrideTransport implements McpClientTransport { + + private final McpClientTransport delegate; + private final List versions; + + ProtocolVersionOverrideTransport(McpClientTransport delegate, List versions) { + this.delegate = delegate; + this.versions = versions; + } + + @Override + public List protocolVersions() { + return versions; + } + + @Override + public Mono connect(Function, Mono> handler) { + return delegate.connect(handler); + } + + @Override + public void setExceptionHandler(Consumer handler) { + delegate.setExceptionHandler(handler); + } + + @Override + public Mono closeGracefully() { + return delegate.closeGracefully(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public Mono sendMessage(JSONRPCMessage message) { + return delegate.sendMessage(message); + } + + @Override + public T unmarshalFrom(Object data, TypeRef typeRef) { + return delegate.unmarshalFrom(data, typeRef); + } + } + private static class StdioTransportConfig implements TransportConfig { private final String command; private final List args; diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpClientBuilderTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpClientBuilderTest.java index 3ca370462..adcbc9ded 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpClientBuilderTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpClientBuilderTest.java @@ -1296,4 +1296,124 @@ void testElicitation_BothHandlersSet() { assertNotNull(syncWrapper); assertTrue(syncWrapper instanceof McpSyncClientWrapper); } + + // ==================== Protocol Versions Tests ==================== + + @Test + void testProtocolVersions_SingleVersion() { + McpClientBuilder builder = + McpClientBuilder.create("pv-client") + .stdioTransport("echo", "test") + .protocolVersions("2025-03-26"); + + assertNotNull(builder); + } + + @Test + void testProtocolVersions_MultipleVersions() { + McpClientBuilder builder = + McpClientBuilder.create("pv-client") + .stdioTransport("echo", "test") + .protocolVersions("2024-11-05", "2025-03-26", "2025-06-18"); + + assertNotNull(builder); + } + + @Test + void testProtocolVersions_NullThrows() { + assertThrows( + IllegalArgumentException.class, + () -> + McpClientBuilder.create("pv-client") + .stdioTransport("echo", "test") + .protocolVersions((String[]) null)); + } + + @Test + void testProtocolVersions_EmptyThrows() { + assertThrows( + IllegalArgumentException.class, + () -> + McpClientBuilder.create("pv-client") + .stdioTransport("echo", "test") + .protocolVersions()); + } + + @Test + void testProtocolVersions_BuildAsyncWithVersions() { + McpClientBuilder builder = + McpClientBuilder.create("pv-async") + .stdioTransport("echo", "test") + .protocolVersions("2024-11-05", "2025-03-26"); + + McpClientWrapper wrapper = builder.buildAsync().block(); + assertNotNull(wrapper); + assertTrue(wrapper instanceof McpAsyncClientWrapper); + } + + @Test + void testProtocolVersions_BuildSyncWithVersions() { + McpClientBuilder builder = + McpClientBuilder.create("pv-sync") + .stdioTransport("echo", "test") + .protocolVersions("2024-11-05", "2025-03-26"); + + McpClientWrapper wrapper = builder.buildSync(); + assertNotNull(wrapper); + assertTrue(wrapper instanceof McpSyncClientWrapper); + } + + @Test + void testProtocolVersions_WithSseTransport() { + McpClientBuilder builder = + McpClientBuilder.create("pv-sse") + .sseTransport("https://mcp.example.com/sse") + .protocolVersions("2024-11-05", "2025-03-26"); + + assertNotNull(builder); + } + + @Test + void testProtocolVersions_WithStreamableHttpTransport() { + McpClientBuilder builder = + McpClientBuilder.create("pv-http") + .streamableHttpTransport("https://mcp.example.com/http") + .protocolVersions("2024-11-05", "2025-03-26"); + + assertNotNull(builder); + } + + @Test + @SuppressWarnings("unchecked") + void testProtocolVersions_OverrideTransportIsApplied() throws Exception { + // Verify that when protocolVersions is set, the transport is wrapped + McpClientBuilder builder = + McpClientBuilder.create("pv-verify") + .stdioTransport("echo", "test") + .protocolVersions("2024-11-05", "2025-03-26"); + + // Use reflection to access the protocolVersions field + Field pvField = McpClientBuilder.class.getDeclaredField("protocolVersions"); + pvField.setAccessible(true); + List versions = (List) pvField.get(builder); + + assertNotNull(versions); + assertEquals(2, versions.size()); + assertEquals("2024-11-05", versions.get(0)); + assertEquals("2025-03-26", versions.get(1)); + } + + @Test + void testProtocolVersions_WithoutSettingUsesDefault() throws Exception { + // Verify that when protocolVersions is NOT set, the field remains null + McpClientBuilder builder = + McpClientBuilder.create("pv-default").stdioTransport("echo", "test"); + + Field pvField = McpClientBuilder.class.getDeclaredField("protocolVersions"); + pvField.setAccessible(true); + Object versions = pvField.get(builder); + + // Should be null, meaning the transport's default protocolVersions() is used + assertEquals(null, versions); + } } diff --git a/docs/en/task/mcp.md b/docs/en/task/mcp.md index 18514c5ec..2ac80bede 100644 --- a/docs/en/task/mcp.md +++ b/docs/en/task/mcp.md @@ -280,6 +280,30 @@ System.out.println("Received elicit request: " + request.message()); ## Managing MCP Clients +### Protocol Version Configuration + +By default, the MCP client only supports protocol version `2024-11-05`. If the MCP server responds with a different protocol version during initialization (e.g., `2025-03-26`), the connection will fail with "Unsupported protocol version". + +Use `protocolVersions()` to declare support for additional protocol versions: + +```java +// Support multiple protocol versions +McpClientWrapper client = McpClientBuilder.create("mcp") + .stdioTransport("python", "server.py") + .protocolVersions("2024-11-05", "2025-03-26") + .buildAsync() + .block(); + +// Works with any transport type +McpClientWrapper sseClient = McpClientBuilder.create("mcp") + .sseTransport("https://mcp.example.com/sse") + .protocolVersions("2024-11-05", "2025-03-26", "2025-06-18") + .buildAsync() + .block(); +``` + +> **Note**: This is useful when connecting to third-party MCP servers that may use newer protocol versions. The MCP specification defines protocol version negotiation as a client-server handshake where the server may respond with a different version than the client requested. + ### List Tools from MCP Server ```java diff --git a/docs/zh/task/mcp.md b/docs/zh/task/mcp.md index 52b793450..6c31e65f3 100644 --- a/docs/zh/task/mcp.md +++ b/docs/zh/task/mcp.md @@ -280,6 +280,30 @@ System.out.println("Received elicit request: " + request.message()); ## 管理 MCP 客户端 +### 协议版本配置 + +默认情况下,MCP 客户端仅支持协议版本 `2024-11-05`。如果 MCP 服务器在初始化时返回不同的协议版本(例如 `2025-03-26`),连接将失败并报错 "Unsupported protocol version"。 + +使用 `protocolVersions()` 声明支持的协议版本: + +```java +// 支持多个协议版本 +McpClientWrapper client = McpClientBuilder.create("mcp") + .stdioTransport("python", "server.py") + .protocolVersions("2024-11-05", "2025-03-26") + .buildAsync() + .block(); + +// 适用于任何传输类型 +McpClientWrapper sseClient = McpClientBuilder.create("mcp") + .sseTransport("https://mcp.example.com/sse") + .protocolVersions("2024-11-05", "2025-03-26", "2025-06-18") + .buildAsync() + .block(); +``` + +> **注意**:当连接使用较新协议版本的第三方 MCP 服务器时,此功能非常有用。MCP 规范将协议版本协商定义为客户端-服务器握手过程,服务器可能返回与客户端请求不同的版本。 + ### 列出 MCP 服务器的工具 ```java