Skip to content

Commit e535c2b

Browse files
miniceMliangxingguang
authored andcommitted
feat(mcp): 添加elicitation功能支持 (agentscope-ai#798)
agentscope-ai#797 - 在McpClientBuilder中添加asyncElicitation和syncElicitation方法 - 更新文档说明elicitation特性和使用示例 - 添加完整的单元测试验证elicitation功能 ## AgentScope-Java Version <revision>1.0.10-SNAPSHOT</revision> ## Description 为 McpClientBuilder 添加了 MCP 官方 SDK 定义的 elicitation 功能支持。 ### 核心变更 修改了 McpClientBuilder.java: 新增导入 io.modelcontextprotocol.spec.McpSchema.ElicitRequest io.modelcontextprotocol.spec.McpSchema.ElicitResult java.util.function.Function 新增字段 asyncElicitationHandler: 用于异步客户端的 elicitation handler syncElicitationHandler: 用于同步客户端的 elicitation handler 新增方法 asyncElicitation(Function<ElicitRequest, Mono> handler): 注册异步 elicitation handler syncElicitation(Function<ElicitRequest, ElicitResult> handler): 注册同步 elicitation handler 修改 buildAsync() 方法 当 asyncElicitationHandler 不为 null 时,在 ClientCapabilities.builder() 中调用 .elicitation() 当 asyncElicitationHandler 不为 null 时,在 McpClient.async() builder 中调用 .elicitation(asyncElicitationHandler) 修改 buildSync() 方法 当 syncElicitationHandler 不为 null 时,在 ClientCapabilities.builder() 中调用 .elicitation() 当 syncElicitationHandler 不为 null 时,在 McpClient.sync() builder 中调用 .elicitation(syncElicitationHandler) 新增单元测试 McpClientBuilderTest 类中相关测试方法 测试命令 ``` mvn test -pl agentscope-core -Dtest='McpClientBuilderTest#test*Elicitation*' ``` 更新了中英两版 mcp.md 文件,添加了使用示例。
1 parent 728fc5b commit e535c2b

4 files changed

Lines changed: 352 additions & 52 deletions

File tree

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

Lines changed: 153 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import io.modelcontextprotocol.json.McpJsonMapper;
2929
import io.modelcontextprotocol.spec.McpClientTransport;
3030
import io.modelcontextprotocol.spec.McpSchema;
31+
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
32+
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
3133
import java.net.URI;
3234
import java.net.URLDecoder;
3335
import java.net.URLEncoder;
@@ -40,6 +42,7 @@
4042
import java.util.List;
4143
import java.util.Map;
4244
import java.util.function.Consumer;
45+
import java.util.function.Function;
4346
import java.util.stream.Collectors;
4447
import reactor.core.publisher.Mono;
4548

@@ -48,33 +51,33 @@
4851
*
4952
* <p>Supports three transport types:
5053
* <ul>
51-
* <li>StdIO - for local process communication</li>
52-
* <li>SSE - for HTTP Server-Sent Events (stateful)</li>
53-
* <li>StreamableHTTP - for HTTP streaming (stateless)</li>
54+
* <li>StdIO - for local process communication</li>
55+
* <li>SSE - for HTTP Server-Sent Events (stateful)</li>
56+
* <li>StreamableHTTP - for HTTP streaming (stateless)</li>
5457
* </ul>
5558
*
5659
* <p>Example usage:
5760
* <pre>{@code
5861
* // StdIO transport
5962
* McpClientWrapper client = McpClientBuilder.create("git-mcp")
60-
* .stdioTransport("python", "-m", "mcp_server_git")
61-
* .buildAsync()
62-
* .block();
63+
* .stdioTransport("python", "-m", "mcp_server_git")
64+
* .buildAsync()
65+
* .block();
6366
*
6467
* // SSE transport with headers and query parameters
6568
* McpClientWrapper client = McpClientBuilder.create("remote-mcp")
66-
* .sseTransport("https://mcp.example.com/sse")
67-
* .header("Authorization", "Bearer " + token)
68-
* .queryParam("queryKey", "queryValue")
69-
* .timeout(Duration.ofSeconds(60))
70-
* .buildAsync()
71-
* .block();
69+
* .sseTransport("https://mcp.example.com/sse")
70+
* .header("Authorization", "Bearer " + token)
71+
* .queryParam("queryKey", "queryValue")
72+
* .timeout(Duration.ofSeconds(60))
73+
* .buildAsync()
74+
* .block();
7275
*
7376
* // HTTP transport with multiple query parameters
7477
* McpClientWrapper client = McpClientBuilder.create("http-mcp")
75-
* .streamableHttpTransport("https://mcp.example.com/http")
76-
* .queryParams(Map.of("token", "abc123", "env", "prod"))
77-
* .buildSync();
78+
* .streamableHttpTransport("https://mcp.example.com/http")
79+
* .queryParams(Map.of("token", "abc123", "env", "prod"))
80+
* .buildSync();
7881
* }</pre>
7982
*/
8083
public class McpClientBuilder {
@@ -86,6 +89,8 @@ public class McpClientBuilder {
8689
private TransportConfig transportConfig;
8790
private Duration requestTimeout = DEFAULT_REQUEST_TIMEOUT;
8891
private Duration initializationTimeout = DEFAULT_INIT_TIMEOUT;
92+
private Function<ElicitRequest, Mono<ElicitResult>> asyncElicitationHandler;
93+
private Function<ElicitRequest, ElicitResult> syncElicitationHandler;
8994

9095
private McpClientBuilder(String name) {
9196
this.name = name;
@@ -108,7 +113,7 @@ public static McpClientBuilder create(String name) {
108113
* Configures StdIO transport for local process communication.
109114
*
110115
* @param command the executable command
111-
* @param args command arguments
116+
* @param args command arguments
112117
* @return this builder
113118
*/
114119
public McpClientBuilder stdioTransport(String command, String... args) {
@@ -120,8 +125,8 @@ public McpClientBuilder stdioTransport(String command, String... args) {
120125
* Configures StdIO transport with environment variables.
121126
*
122127
* @param command the executable command
123-
* @param args command arguments list
124-
* @param env environment variables
128+
* @param args command arguments list
129+
* @param env environment variables
125130
* @return this builder
126131
*/
127132
public McpClientBuilder stdioTransport(
@@ -148,11 +153,11 @@ public McpClientBuilder sseTransport(String url) {
148153
* <p>Example usage for HTTP/2:
149154
* <pre>{@code
150155
* McpClientWrapper client = McpClientBuilder.create("mcp")
151-
* .sseTransport("https://example.com/sse")
156+
* .sseTransport("https://example.com/sse")
152157
* .customizeSseClient(clientBuilder ->
153158
* clientBuilder.version(java.net.http.HttpClient.Version.HTTP_2))
154-
* .buildAsync()
155-
* .block();
159+
* .buildAsync()
160+
* .block();
156161
* }</pre>
157162
*
158163
* @param customizer consumer to customize the HttpClient.Builder
@@ -177,17 +182,21 @@ public McpClientBuilder streamableHttpTransport(String url) {
177182
}
178183

179184
/**
180-
* Customizes the HTTP client for StreamableHTTP transport (only applicable after calling streamableHttpTransport).
181-
* This allows advanced HTTP client configuration like HTTP/2, custom timeouts, SSL settings, etc.
185+
* Customizes the HTTP client for StreamableHTTP transport (only applicable
186+
* after calling streamableHttpTransport).
187+
* This allows advanced HTTP client configuration like HTTP/2, custom timeouts,
188+
* SSL settings, etc.
189+
*
190+
* <p>
191+
* Example usage for HTTP/2:
182192
*
183-
* <p>Example usage for HTTP/2:
184193
* <pre>{@code
185194
* McpClientWrapper client = McpClientBuilder.create("mcp")
186-
* .streamableHttpTransport("https://example.com/http")
187-
* .customizeStreamableHttpClient(clientBuilder ->
188-
* clientBuilder.version(java.net.http.HttpClient.Version.HTTP_2))
189-
* .buildAsync()
190-
* .block();
195+
* .streamableHttpTransport("https://example.com/http")
196+
* .customizeStreamableHttpClient(
197+
* clientBuilder -> clientBuilder.version(java.net.http.HttpClient.Version.HTTP_2))
198+
* .buildAsync()
199+
* .block();
191200
* }</pre>
192201
*
193202
* @param customizer consumer to customize the HttpClient.Builder
@@ -203,7 +212,7 @@ public McpClientBuilder customizeStreamableHttpClient(Consumer<HttpClient.Builde
203212
/**
204213
* Adds an HTTP header (only applicable for HTTP transports).
205214
*
206-
* @param key header name
215+
* @param key header name
207216
* @param value header value
208217
* @return this builder
209218
*/
@@ -230,11 +239,12 @@ public McpClientBuilder headers(Map<String, String> headers) {
230239
/**
231240
* Adds a query parameter to the URL (only applicable for HTTP transports).
232241
*
233-
* <p>Query parameters added via this method will be merged with any existing
242+
* <p>
243+
* Query parameters added via this method will be merged with any existing
234244
* query parameters in the URL. If the same parameter key exists in both the URL
235245
* and the added parameters, the added parameter will take precedence.
236246
*
237-
* @param key query parameter name
247+
* @param key query parameter name
238248
* @param value query parameter value
239249
* @return this builder
240250
*/
@@ -248,7 +258,8 @@ public McpClientBuilder queryParam(String key, String value) {
248258
/**
249259
* Sets multiple query parameters (only applicable for HTTP transports).
250260
*
251-
* <p>This method replaces any previously added query parameters.
261+
* <p>
262+
* This method replaces any previously added query parameters.
252263
* Query parameters in the original URL are still preserved and merged.
253264
*
254265
* @param queryParams map of query parameter name-value pairs
@@ -283,6 +294,79 @@ public McpClientBuilder initializationTimeout(Duration timeout) {
283294
return this;
284295
}
285296

297+
/**
298+
* Registers an asynchronous elicitation handler for processing elicit requests
299+
* from the server.
300+
*
301+
* <p>
302+
* When an elicitation handler is registered, the client will automatically
303+
* enable
304+
* the elicitation capability in ClientCapabilities.
305+
*
306+
* <p>
307+
* This method is for use with {@link #buildAsync()}. The handler returns a
308+
* {@link Mono}
309+
* for asynchronous processing.
310+
*
311+
* <p>
312+
* Example usage:
313+
*
314+
* <pre>{@code
315+
* McpClientWrapper client = McpClientBuilder.create("mcp")
316+
* .stdioTransport("python", "-m", "mcp_server")
317+
* .asyncElicitation(request -> {
318+
* // Handle elicitation request asynchronously
319+
* return Mono.just(ElicitResult.builder()...build());
320+
* })
321+
* .buildAsync()
322+
* .block();
323+
* }
324+
* </pre>
325+
*
326+
* @param handler function to handle elicit requests asynchronously
327+
* @return this builder
328+
*/
329+
public McpClientBuilder asyncElicitation(Function<ElicitRequest, Mono<ElicitResult>> handler) {
330+
this.asyncElicitationHandler = handler;
331+
return this;
332+
}
333+
334+
/**
335+
* Registers a synchronous elicitation handler for processing elicit requests
336+
* from the server.
337+
*
338+
* <p>
339+
* When an elicitation handler is registered, the client will automatically
340+
* enable
341+
* the elicitation capability in ClientCapabilities.
342+
*
343+
* <p>
344+
* This method is for use with {@link #buildSync()}. The handler returns an
345+
* {@link ElicitResult}
346+
* directly for synchronous processing.
347+
*
348+
* <p>
349+
* Example usage:
350+
*
351+
* <pre>{@code
352+
* McpClientWrapper client = McpClientBuilder.create("mcp")
353+
* .stdioTransport("python", "-m", "mcp_server")
354+
* .syncElicitation(request -> {
355+
* // Handle elicitation request synchronously
356+
* return ElicitResult.builder()...build();
357+
* })
358+
* .buildSync();
359+
* }
360+
* </pre>
361+
*
362+
* @param handler function to handle elicit requests synchronously
363+
* @return this builder
364+
*/
365+
public McpClientBuilder syncElicitation(Function<ElicitRequest, ElicitResult> handler) {
366+
this.syncElicitationHandler = handler;
367+
return this;
368+
}
369+
286370
/**
287371
* Builds an asynchronous MCP client wrapper.
288372
*
@@ -302,15 +386,20 @@ public Mono<McpClientWrapper> buildAsync() {
302386
"agentscope-java", "AgentScope Java Framework", VERSION);
303387

304388
McpSchema.ClientCapabilities clientCapabilities =
305-
McpSchema.ClientCapabilities.builder().build();
389+
buildCapabilities(asyncElicitationHandler != null);
306390

307-
McpAsyncClient mcpClient =
391+
var clientBuilder =
308392
McpClient.async(transport)
309393
.requestTimeout(requestTimeout)
310394
.initializationTimeout(initializationTimeout)
311395
.clientInfo(clientInfo)
312-
.capabilities(clientCapabilities)
313-
.build();
396+
.capabilities(clientCapabilities);
397+
398+
if (asyncElicitationHandler != null) {
399+
clientBuilder = clientBuilder.elicitation(asyncElicitationHandler);
400+
}
401+
402+
McpAsyncClient mcpClient = clientBuilder.build();
314403

315404
return new McpAsyncClientWrapper(name, mcpClient);
316405
});
@@ -333,19 +422,38 @@ public McpClientWrapper buildSync() {
333422
"agentscope-java", "AgentScope Java Framework", Version.VERSION);
334423

335424
McpSchema.ClientCapabilities clientCapabilities =
336-
McpSchema.ClientCapabilities.builder().build();
425+
buildCapabilities(syncElicitationHandler != null);
337426

338-
McpSyncClient mcpClient =
427+
var clientBuilder =
339428
McpClient.sync(transport)
340429
.requestTimeout(requestTimeout)
341430
.initializationTimeout(initializationTimeout)
342431
.clientInfo(clientInfo)
343-
.capabilities(clientCapabilities)
344-
.build();
432+
.capabilities(clientCapabilities);
433+
434+
if (syncElicitationHandler != null) {
435+
clientBuilder = clientBuilder.elicitation(syncElicitationHandler);
436+
}
437+
438+
McpSyncClient mcpClient = clientBuilder.build();
345439

346440
return new McpSyncClientWrapper(name, mcpClient);
347441
}
348442

443+
/**
444+
* Builds client capabilities
445+
* @param withElicitation whether to include elicitation capability
446+
* @return client capabilities
447+
*/
448+
private McpSchema.ClientCapabilities buildCapabilities(boolean withElicitation) {
449+
McpSchema.ClientCapabilities.Builder capabilitiesBuilder =
450+
McpSchema.ClientCapabilities.builder();
451+
if (withElicitation) {
452+
capabilitiesBuilder.elicitation();
453+
}
454+
return capabilitiesBuilder.build();
455+
}
456+
349457
// ==================== Internal Transport Configuration Classes ====================
350458

351459
private interface TransportConfig {
@@ -419,8 +527,10 @@ public void setQueryParams(Map<String, String> queryParams) {
419527
}
420528

421529
/**
422-
* Extracts the endpoint path from URL, merging with additional query parameters.
423-
* Query parameters from the original URL are merged with additionally configured parameters.
530+
* Extracts the endpoint path from URL, merging with additional query
531+
* parameters.
532+
* Query parameters from the original URL are merged with additionally
533+
* configured parameters.
424534
* Additional parameters take precedence over URL parameters with the same key.
425535
*
426536
* @return endpoint path with query parameters (e.g., "/api/sse?token=abc")

0 commit comments

Comments
 (0)