Skip to content

Commit aef14b7

Browse files
author
Damon
committed
feat(mcp): expose protocolVersions configuration in McpClientBuilder
Allow users to specify supported MCP protocol versions when building MCP clients via a new protocolVersions(String...) builder method. This enables compatibility with MCP servers that respond with newer protocol versions (e.g., 2025-03-26) during initialization, which previously caused 'Unsupported protocol version' errors. Implementation uses a ProtocolVersionOverrideTransport decorator that wraps any McpClientTransport and overrides protocolVersions(), working with all transport types (StdIO, SSE, StreamableHTTP). Closes #1173
1 parent de01c66 commit aef14b7

4 files changed

Lines changed: 274 additions & 0 deletions

File tree

agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
import io.modelcontextprotocol.client.transport.ServerParameters;
2727
import io.modelcontextprotocol.client.transport.StdioClientTransport;
2828
import io.modelcontextprotocol.json.McpJsonMapper;
29+
import io.modelcontextprotocol.json.TypeRef;
2930
import io.modelcontextprotocol.spec.McpClientTransport;
3031
import io.modelcontextprotocol.spec.McpSchema;
3132
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
3233
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
34+
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
3335
import java.net.URI;
3436
import java.net.URLDecoder;
3537
import java.net.URLEncoder;
@@ -38,6 +40,7 @@
3840
import java.time.Duration;
3941
import java.util.ArrayList;
4042
import java.util.Arrays;
43+
import java.util.Collections;
4144
import java.util.HashMap;
4245
import java.util.List;
4346
import java.util.Map;
@@ -78,6 +81,13 @@
7881
* .streamableHttpTransport("https://mcp.example.com/http")
7982
* .queryParams(Map.of("token", "abc123", "env", "prod"))
8083
* .buildSync();
84+
*
85+
* // StdIO transport with custom protocol versions
86+
* McpClientWrapper client = McpClientBuilder.create("mcp")
87+
* .stdioTransport("python", "server.py")
88+
* .protocolVersions("2024-11-05", "2025-03-26")
89+
* .buildAsync()
90+
* .block();
8191
* }</pre>
8292
*/
8393
public class McpClientBuilder {
@@ -91,6 +101,7 @@ public class McpClientBuilder {
91101
private Duration initializationTimeout = DEFAULT_INIT_TIMEOUT;
92102
private Function<ElicitRequest, Mono<ElicitResult>> asyncElicitationHandler;
93103
private Function<ElicitRequest, ElicitResult> syncElicitationHandler;
104+
private List<String> protocolVersions;
94105

95106
private McpClientBuilder(String name) {
96107
this.name = name;
@@ -294,6 +305,34 @@ public McpClientBuilder initializationTimeout(Duration timeout) {
294305
return this;
295306
}
296307

308+
/**
309+
* Sets the MCP protocol versions that the client supports.
310+
*
311+
* <p>By default, the client only supports "2024-11-05". If the MCP server responds
312+
* with a different protocol version during initialization (e.g., "2025-03-26"),
313+
* the connection will fail with "Unsupported protocol version".
314+
*
315+
* <p>Use this method to declare support for additional protocol versions:
316+
* <pre>{@code
317+
* McpClientWrapper client = McpClientBuilder.create("server")
318+
* .stdioTransport("python", "server.py")
319+
* .protocolVersions("2024-11-05", "2025-03-26")
320+
* .buildAsync()
321+
* .block();
322+
* }</pre>
323+
*
324+
* @param versions one or more protocol version strings (e.g., "2024-11-05", "2025-03-26")
325+
* @return this builder
326+
* @throws IllegalArgumentException if no versions are provided
327+
*/
328+
public McpClientBuilder protocolVersions(String... versions) {
329+
if (versions == null || versions.length == 0) {
330+
throw new IllegalArgumentException("At least one protocol version must be specified");
331+
}
332+
this.protocolVersions = Collections.unmodifiableList(Arrays.asList(versions));
333+
return this;
334+
}
335+
297336
/**
298337
* Registers an asynchronous elicitation handler for processing elicit requests
299338
* from the server.
@@ -381,6 +420,11 @@ public Mono<McpClientWrapper> buildAsync() {
381420
() -> {
382421
McpClientTransport transport = transportConfig.createTransport();
383422

423+
if (protocolVersions != null) {
424+
transport =
425+
new ProtocolVersionOverrideTransport(transport, protocolVersions);
426+
}
427+
384428
McpSchema.Implementation clientInfo =
385429
new McpSchema.Implementation(
386430
"agentscope-java", "AgentScope Java Framework", VERSION);
@@ -417,6 +461,10 @@ public McpClientWrapper buildSync() {
417461

418462
McpClientTransport transport = transportConfig.createTransport();
419463

464+
if (protocolVersions != null) {
465+
transport = new ProtocolVersionOverrideTransport(transport, protocolVersions);
466+
}
467+
420468
McpSchema.Implementation clientInfo =
421469
new McpSchema.Implementation(
422470
"agentscope-java", "AgentScope Java Framework", Version.VERSION);
@@ -460,6 +508,64 @@ private interface TransportConfig {
460508
McpClientTransport createTransport();
461509
}
462510

511+
/**
512+
* A transport decorator that overrides the protocol versions reported to the MCP client.
513+
*
514+
* <p>The MCP Java SDK's {@code LifecycleInitializer} performs a strict check:
515+
* the server's protocol version must be contained in the client's supported versions list.
516+
* By default, transports only report support for "2024-11-05", causing connections to fail
517+
* with servers that respond with newer versions (e.g., "2025-03-26").
518+
*
519+
* <p>This decorator wraps any {@link McpClientTransport} and overrides
520+
* {@link #protocolVersions()} to return a user-specified list, while delegating
521+
* all other operations to the underlying transport.
522+
*/
523+
private static class ProtocolVersionOverrideTransport implements McpClientTransport {
524+
525+
private final McpClientTransport delegate;
526+
private final List<String> versions;
527+
528+
ProtocolVersionOverrideTransport(McpClientTransport delegate, List<String> versions) {
529+
this.delegate = delegate;
530+
this.versions = versions;
531+
}
532+
533+
@Override
534+
public List<String> protocolVersions() {
535+
return versions;
536+
}
537+
538+
@Override
539+
public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> handler) {
540+
return delegate.connect(handler);
541+
}
542+
543+
@Override
544+
public void setExceptionHandler(Consumer<Throwable> handler) {
545+
delegate.setExceptionHandler(handler);
546+
}
547+
548+
@Override
549+
public Mono<Void> closeGracefully() {
550+
return delegate.closeGracefully();
551+
}
552+
553+
@Override
554+
public void close() {
555+
delegate.close();
556+
}
557+
558+
@Override
559+
public Mono<Void> sendMessage(JSONRPCMessage message) {
560+
return delegate.sendMessage(message);
561+
}
562+
563+
@Override
564+
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
565+
return delegate.unmarshalFrom(data, typeRef);
566+
}
567+
}
568+
463569
private static class StdioTransportConfig implements TransportConfig {
464570
private final String command;
465571
private final List<String> args;

agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpClientBuilderTest.java

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,4 +1296,124 @@ void testElicitation_BothHandlersSet() {
12961296
assertNotNull(syncWrapper);
12971297
assertTrue(syncWrapper instanceof McpSyncClientWrapper);
12981298
}
1299+
1300+
// ==================== Protocol Versions Tests ====================
1301+
1302+
@Test
1303+
void testProtocolVersions_SingleVersion() {
1304+
McpClientBuilder builder =
1305+
McpClientBuilder.create("pv-client")
1306+
.stdioTransport("echo", "test")
1307+
.protocolVersions("2025-03-26");
1308+
1309+
assertNotNull(builder);
1310+
}
1311+
1312+
@Test
1313+
void testProtocolVersions_MultipleVersions() {
1314+
McpClientBuilder builder =
1315+
McpClientBuilder.create("pv-client")
1316+
.stdioTransport("echo", "test")
1317+
.protocolVersions("2024-11-05", "2025-03-26", "2025-06-18");
1318+
1319+
assertNotNull(builder);
1320+
}
1321+
1322+
@Test
1323+
void testProtocolVersions_NullThrows() {
1324+
assertThrows(
1325+
IllegalArgumentException.class,
1326+
() ->
1327+
McpClientBuilder.create("pv-client")
1328+
.stdioTransport("echo", "test")
1329+
.protocolVersions((String[]) null));
1330+
}
1331+
1332+
@Test
1333+
void testProtocolVersions_EmptyThrows() {
1334+
assertThrows(
1335+
IllegalArgumentException.class,
1336+
() ->
1337+
McpClientBuilder.create("pv-client")
1338+
.stdioTransport("echo", "test")
1339+
.protocolVersions());
1340+
}
1341+
1342+
@Test
1343+
void testProtocolVersions_BuildAsyncWithVersions() {
1344+
McpClientBuilder builder =
1345+
McpClientBuilder.create("pv-async")
1346+
.stdioTransport("echo", "test")
1347+
.protocolVersions("2024-11-05", "2025-03-26");
1348+
1349+
McpClientWrapper wrapper = builder.buildAsync().block();
1350+
assertNotNull(wrapper);
1351+
assertTrue(wrapper instanceof McpAsyncClientWrapper);
1352+
}
1353+
1354+
@Test
1355+
void testProtocolVersions_BuildSyncWithVersions() {
1356+
McpClientBuilder builder =
1357+
McpClientBuilder.create("pv-sync")
1358+
.stdioTransport("echo", "test")
1359+
.protocolVersions("2024-11-05", "2025-03-26");
1360+
1361+
McpClientWrapper wrapper = builder.buildSync();
1362+
assertNotNull(wrapper);
1363+
assertTrue(wrapper instanceof McpSyncClientWrapper);
1364+
}
1365+
1366+
@Test
1367+
void testProtocolVersions_WithSseTransport() {
1368+
McpClientBuilder builder =
1369+
McpClientBuilder.create("pv-sse")
1370+
.sseTransport("https://mcp.example.com/sse")
1371+
.protocolVersions("2024-11-05", "2025-03-26");
1372+
1373+
assertNotNull(builder);
1374+
}
1375+
1376+
@Test
1377+
void testProtocolVersions_WithStreamableHttpTransport() {
1378+
McpClientBuilder builder =
1379+
McpClientBuilder.create("pv-http")
1380+
.streamableHttpTransport("https://mcp.example.com/http")
1381+
.protocolVersions("2024-11-05", "2025-03-26");
1382+
1383+
assertNotNull(builder);
1384+
}
1385+
1386+
@Test
1387+
@SuppressWarnings("unchecked")
1388+
void testProtocolVersions_OverrideTransportIsApplied() throws Exception {
1389+
// Verify that when protocolVersions is set, the transport is wrapped
1390+
McpClientBuilder builder =
1391+
McpClientBuilder.create("pv-verify")
1392+
.stdioTransport("echo", "test")
1393+
.protocolVersions("2024-11-05", "2025-03-26");
1394+
1395+
// Use reflection to access the protocolVersions field
1396+
Field pvField = McpClientBuilder.class.getDeclaredField("protocolVersions");
1397+
pvField.setAccessible(true);
1398+
List<String> versions = (List<String>) pvField.get(builder);
1399+
1400+
assertNotNull(versions);
1401+
assertEquals(2, versions.size());
1402+
assertEquals("2024-11-05", versions.get(0));
1403+
assertEquals("2025-03-26", versions.get(1));
1404+
}
1405+
1406+
@Test
1407+
void testProtocolVersions_WithoutSettingUsesDefault() throws Exception {
1408+
// Verify that when protocolVersions is NOT set, the field remains null
1409+
McpClientBuilder builder =
1410+
McpClientBuilder.create("pv-default").stdioTransport("echo", "test");
1411+
1412+
Field pvField = McpClientBuilder.class.getDeclaredField("protocolVersions");
1413+
pvField.setAccessible(true);
1414+
Object versions = pvField.get(builder);
1415+
1416+
// Should be null, meaning the transport's default protocolVersions() is used
1417+
assertEquals(null, versions);
1418+
}
12991419
}

docs/en/task/mcp.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,30 @@ System.out.println("Received elicit request: " + request.message());
280280

281281
## Managing MCP Clients
282282

283+
### Protocol Version Configuration
284+
285+
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".
286+
287+
Use `protocolVersions()` to declare support for additional protocol versions:
288+
289+
```java
290+
// Support multiple protocol versions
291+
McpClientWrapper client = McpClientBuilder.create("mcp")
292+
.stdioTransport("python", "server.py")
293+
.protocolVersions("2024-11-05", "2025-03-26")
294+
.buildAsync()
295+
.block();
296+
297+
// Works with any transport type
298+
McpClientWrapper sseClient = McpClientBuilder.create("mcp")
299+
.sseTransport("https://mcp.example.com/sse")
300+
.protocolVersions("2024-11-05", "2025-03-26", "2025-06-18")
301+
.buildAsync()
302+
.block();
303+
```
304+
305+
> **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.
306+
283307
### List Tools from MCP Server
284308

285309
```java

docs/zh/task/mcp.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,30 @@ System.out.println("Received elicit request: " + request.message());
280280

281281
## 管理 MCP 客户端
282282

283+
### 协议版本配置
284+
285+
默认情况下,MCP 客户端仅支持协议版本 `2024-11-05`。如果 MCP 服务器在初始化时返回不同的协议版本(例如 `2025-03-26`),连接将失败并报错 "Unsupported protocol version"。
286+
287+
使用 `protocolVersions()` 声明支持的协议版本:
288+
289+
```java
290+
// 支持多个协议版本
291+
McpClientWrapper client = McpClientBuilder.create("mcp")
292+
.stdioTransport("python", "server.py")
293+
.protocolVersions("2024-11-05", "2025-03-26")
294+
.buildAsync()
295+
.block();
296+
297+
// 适用于任何传输类型
298+
McpClientWrapper sseClient = McpClientBuilder.create("mcp")
299+
.sseTransport("https://mcp.example.com/sse")
300+
.protocolVersions("2024-11-05", "2025-03-26", "2025-06-18")
301+
.buildAsync()
302+
.block();
303+
```
304+
305+
> **注意**:当连接使用较新协议版本的第三方 MCP 服务器时,此功能非常有用。MCP 规范将协议版本协商定义为客户端-服务器握手过程,服务器可能返回与客户端请求不同的版本。
306+
283307
### 列出 MCP 服务器的工具
284308

285309
```java

0 commit comments

Comments
 (0)