Skip to content

Commit 41e04ad

Browse files
committed
增加客户端elicitation支持
1 parent 05d70eb commit 41e04ad

File tree

8 files changed

+208
-33
lines changed

8 files changed

+208
-33
lines changed

framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpStreamableClient.java renamed to framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClient.java

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
import io.modelcontextprotocol.spec.McpClientTransport;
1616
import io.modelcontextprotocol.spec.McpSchema;
1717
import modelengine.fel.tool.mcp.client.McpClient;
18+
import modelengine.fel.tool.mcp.client.elicitation.ElicitRequest;
19+
import modelengine.fel.tool.mcp.client.elicitation.ElicitResult;
20+
import modelengine.fel.tool.mcp.client.support.handler.DefaultMcpClientLogHandler;
21+
import modelengine.fel.tool.mcp.client.support.handler.DefaultMcpElicitationHandler;
1822
import modelengine.fel.tool.mcp.entity.Tool;
23+
import modelengine.fitframework.inspection.Nullable;
1924
import modelengine.fitframework.log.Logger;
2025
import modelengine.fitframework.util.StringUtils;
2126
import modelengine.fitframework.util.UuidUtils;
@@ -25,6 +30,7 @@
2530
import java.util.HashMap;
2631
import java.util.List;
2732
import java.util.Map;
33+
import java.util.function.Function;
2834
import java.util.stream.Collectors;
2935

3036
/**
@@ -36,36 +42,47 @@
3642
* @author 黄可欣
3743
* @since 2025-11-03
3844
*/
39-
public class DefaultMcpStreamableClient implements McpClient {
40-
private static final Logger log = Logger.get(DefaultMcpStreamableClient.class);
45+
public class DefaultMcpClient implements McpClient {
46+
private static final Logger log = Logger.get(DefaultMcpClient.class);
4147

4248
private final String clientId;
4349
private final McpSyncClient mcpSyncClient;
44-
private final DefaultMcpClientLogHandler logHandler;
4550

4651
private volatile boolean initialized = false;
4752
private volatile boolean closed = false;
4853

4954
/**
50-
* Constructs a new instance of the DefaultMcpStreamableClient.
55+
* Constructs a new instance of the DefaultMcpClient.
5156
*
5257
* @param baseUri The base URI of the MCP server.
5358
* @param sseEndpoint The endpoint for the Server-Sent Events (SSE) connection.
5459
* @param requestTimeoutSeconds The timeout duration of requests. Units: seconds.
5560
*/
56-
public DefaultMcpStreamableClient(String baseUri, String sseEndpoint, int requestTimeoutSeconds,
57-
McpClientTransport transport) {
61+
public DefaultMcpClient(String baseUri, String sseEndpoint, McpClientTransport transport, int requestTimeoutSeconds,
62+
@Nullable Function<ElicitRequest, ElicitResult> elicitationHandler) {
5863
this.clientId = UuidUtils.randomUuidString();
5964
notBlank(baseUri, "The MCP server base URI cannot be blank.");
6065
notBlank(sseEndpoint, "The MCP server SSE endpoint cannot be blank.");
6166
log.info("Creating MCP client. [clientId={}, baseUri={}]", this.clientId, baseUri);
62-
this.logHandler = new DefaultMcpClientLogHandler(this.clientId);
63-
this.mcpSyncClient = io.modelcontextprotocol.client.McpClient.sync(transport)
64-
.requestTimeout(Duration.ofSeconds(requestTimeoutSeconds))
65-
.capabilities(McpSchema.ClientCapabilities.builder().build())
66-
.loggingConsumer(this.logHandler::handleLoggingMessage)
67-
.jsonSchemaValidator(new DefaultJsonSchemaValidator(new ObjectMapper()))
68-
.build();
67+
DefaultMcpClientLogHandler logHandler = new DefaultMcpClientLogHandler(this.clientId);
68+
if (elicitationHandler != null) {
69+
DefaultMcpElicitationHandler mcpElicitationHandler =
70+
new DefaultMcpElicitationHandler(this.clientId, elicitationHandler);
71+
this.mcpSyncClient = io.modelcontextprotocol.client.McpClient.sync(transport)
72+
.capabilities(McpSchema.ClientCapabilities.builder().elicitation().build())
73+
.loggingConsumer(logHandler::handleLoggingMessage)
74+
.elicitation(mcpElicitationHandler::handleElicitationRequest)
75+
.requestTimeout(Duration.ofSeconds(requestTimeoutSeconds))
76+
.jsonSchemaValidator(new DefaultJsonSchemaValidator(new ObjectMapper()))
77+
.build();
78+
} else {
79+
this.mcpSyncClient = io.modelcontextprotocol.client.McpClient.sync(transport)
80+
.capabilities(McpSchema.ClientCapabilities.builder().build())
81+
.loggingConsumer(logHandler::handleLoggingMessage)
82+
.requestTimeout(Duration.ofSeconds(requestTimeoutSeconds))
83+
.jsonSchemaValidator(new DefaultJsonSchemaValidator(new ObjectMapper()))
84+
.build();
85+
}
6986
}
7087

7188
@Override

framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClientFactory.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
1414
import modelengine.fel.tool.mcp.client.McpClient;
1515
import modelengine.fel.tool.mcp.client.McpClientFactory;
16+
import modelengine.fel.tool.mcp.client.elicitation.ElicitRequest;
17+
import modelengine.fel.tool.mcp.client.elicitation.ElicitResult;
1618
import modelengine.fitframework.annotation.Component;
1719
import modelengine.fitframework.annotation.Value;
20+
import modelengine.fitframework.inspection.Nullable;
21+
22+
import java.util.function.Function;
1823

1924
/**
20-
* Represents a factory for creating instances of the {@link DefaultMcpStreamableClient}.
25+
* Represents a factory for creating instances of the {@link DefaultMcpClient}.
2126
* This class is responsible for initializing and configuring.
2227
*
2328
* @author 季聿阶
@@ -37,26 +42,28 @@ public DefaultMcpClientFactory(@Value("${mcp.client.request.timeout-seconds}") i
3742
}
3843

3944
@Override
40-
public McpClient createStreamable(String baseUri, String sseEndpoint) {
45+
public McpClient createStreamable(String baseUri, String sseEndpoint,
46+
@Nullable Function<ElicitRequest, ElicitResult> elicitationHandler) {
4147
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(baseUri)
4248
.jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper()))
4349
.endpoint(sseEndpoint)
4450
.build();
45-
return new DefaultMcpStreamableClient(baseUri, sseEndpoint, this.requestTimeoutSeconds, transport);
51+
return new DefaultMcpClient(baseUri, sseEndpoint, transport, this.requestTimeoutSeconds, elicitationHandler);
4652
}
4753

4854
@Override
49-
public McpClient createSse(String baseUri, String sseEndpoint) {
55+
public McpClient createSse(String baseUri, String sseEndpoint,
56+
@Nullable Function<ElicitRequest, ElicitResult> elicitationHandler) {
5057
HttpClientSseClientTransport transport = HttpClientSseClientTransport.builder(baseUri)
5158
.jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper()))
5259
.sseEndpoint(sseEndpoint)
5360
.build();
54-
return new DefaultMcpStreamableClient(baseUri, sseEndpoint, this.requestTimeoutSeconds, transport);
61+
return new DefaultMcpClient(baseUri, sseEndpoint, transport, this.requestTimeoutSeconds, elicitationHandler);
5562
}
5663

5764
@Override
5865
@Deprecated
5966
public McpClient create(String baseUri, String sseEndpoint) {
60-
return this.createStreamable(baseUri, sseEndpoint);
67+
return this.createStreamable(baseUri, sseEndpoint, null);
6168
}
6269
}

framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClientLogHandler.java renamed to framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/handler/DefaultMcpClientLogHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*--------------------------------------------------------------------------------------------*/
66

7-
package modelengine.fel.tool.mcp.client.support;
7+
package modelengine.fel.tool.mcp.client.support.handler;
88

99
import io.modelcontextprotocol.spec.McpSchema;
1010
import modelengine.fitframework.log.Logger;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.client.support.handler;
8+
9+
import io.modelcontextprotocol.spec.McpSchema;
10+
import modelengine.fel.tool.mcp.client.elicitation.ElicitRequest;
11+
import modelengine.fel.tool.mcp.client.elicitation.ElicitResult;
12+
import modelengine.fitframework.log.Logger;
13+
14+
import java.util.function.Function;
15+
16+
/**
17+
* Default MCP elicitation handler that delegates to an external handler function.
18+
*
19+
* <p>Converts {@link McpSchema.ElicitRequest} to {@link ElicitRequest},
20+
* calls the user's handler, and converts {@link ElicitResult} back to {@link McpSchema.ElicitResult}.
21+
*
22+
* @author 黄可欣
23+
* @since 2025-11-25
24+
*/
25+
public class DefaultMcpElicitationHandler {
26+
private static final Logger log = Logger.get(DefaultMcpElicitationHandler.class);
27+
private final String clientId;
28+
private final Function<ElicitRequest, ElicitResult> elicitationHandler;
29+
30+
/**
31+
* Constructs a new handler.
32+
*
33+
* @param clientId The client ID.
34+
* @param elicitationHandler The user's handler function that processes {@link ElicitRequest}
35+
* and returns {@link ElicitResult}.
36+
*/
37+
public DefaultMcpElicitationHandler(String clientId, Function<ElicitRequest, ElicitResult> elicitationHandler) {
38+
this.clientId = clientId;
39+
this.elicitationHandler = elicitationHandler;
40+
}
41+
42+
/**
43+
* Handles an elicitation request by converting {@link McpSchema.ElicitRequest} to {@link ElicitRequest},
44+
* delegating to the user's handler, and converting {@link ElicitResult} back to {@link McpSchema.ElicitResult}.
45+
*
46+
* @param request The {@link McpSchema.ElicitRequest} from MCP server.
47+
* @return The {@link McpSchema.ElicitResult} to send back to MCP server.
48+
*/
49+
public McpSchema.ElicitResult handleElicitationRequest(McpSchema.ElicitRequest request) {
50+
log.info("Received elicitation request from MCP server. [clientId={}, message={}, requestSchema={}]",
51+
this.clientId,
52+
request.message(),
53+
request.requestedSchema());
54+
55+
try {
56+
ElicitRequest elicitRequest = new ElicitRequest(request.message(), request.requestedSchema());
57+
ElicitResult result = this.elicitationHandler.apply(elicitRequest);
58+
log.info("Successfully handled elicitation request. [clientId={}, action={}, content={}]",
59+
this.clientId,
60+
result.action(),
61+
result.content());
62+
63+
McpSchema.ElicitResult.Action mcpAction = switch (result.action()) {
64+
case ACCEPT -> McpSchema.ElicitResult.Action.ACCEPT;
65+
case DECLINE -> McpSchema.ElicitResult.Action.DECLINE;
66+
case CANCEL -> McpSchema.ElicitResult.Action.CANCEL;
67+
};
68+
return new McpSchema.ElicitResult(mcpAction, result.content());
69+
} catch (Exception e) {
70+
log.error("Failed to handle elicitation request. [clientId={}, error={}]",
71+
this.clientId,
72+
e.getMessage(),
73+
e);
74+
throw new IllegalStateException("Failed to handle elicitation request: " + e.getMessage(), e);
75+
}
76+
}
77+
}

framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/TestController.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import modelengine.fel.tool.mcp.client.McpClient;
1010
import modelengine.fel.tool.mcp.client.McpClientFactory;
11+
import modelengine.fel.tool.mcp.client.elicitation.ElicitResult;
1112
import modelengine.fel.tool.mcp.entity.Tool;
1213
import modelengine.fit.http.annotation.GetMapping;
1314
import modelengine.fit.http.annotation.PostMapping;
@@ -17,6 +18,7 @@
1718
import modelengine.fitframework.annotation.Component;
1819

1920
import java.io.IOException;
21+
import java.util.Collections;
2022
import java.util.List;
2123
import java.util.Map;
2224

@@ -53,15 +55,25 @@ public TestController(McpClientFactory mcpClientFactory) {
5355
@PostMapping(path = "/initialize")
5456
public String initializeStreamable(@RequestQuery(name = "baseUri") String baseUri,
5557
@RequestQuery(name = "sseEndpoint") String sseEndpoint) {
56-
this.client = this.mcpClientFactory.createStreamable(baseUri, sseEndpoint);
58+
this.client = this.mcpClientFactory.createStreamable(baseUri, sseEndpoint, null);
5759
this.client.initialize();
5860
return "Initialized";
5961
}
6062

6163
@PostMapping(path = "/initialize-sse")
6264
public String initializeSse(@RequestQuery(name = "baseUri") String baseUri,
6365
@RequestQuery(name = "sseEndpoint") String sseEndpoint) {
64-
this.client = this.mcpClientFactory.createSse(baseUri, sseEndpoint);
66+
this.client = this.mcpClientFactory.createSse(baseUri, sseEndpoint, null);
67+
this.client.initialize();
68+
return "Initialized";
69+
}
70+
71+
@PostMapping(path = "/initialize-elicitation")
72+
public String initializeElicitation(@RequestQuery(name = "baseUri") String baseUri,
73+
@RequestQuery(name = "sseEndpoint") String sseEndpoint) {
74+
this.client = this.mcpClientFactory.createStreamable(baseUri,
75+
sseEndpoint,
76+
request -> new ElicitResult(ElicitResult.Action.ACCEPT, Collections.emptyMap()));
6577
this.client.initialize();
6678
return "Initialized";
6779
}

framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClientFactory.java

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,52 @@
66

77
package modelengine.fel.tool.mcp.client;
88

9+
import modelengine.fel.tool.mcp.client.elicitation.ElicitRequest;
10+
import modelengine.fel.tool.mcp.client.elicitation.ElicitResult;
11+
import modelengine.fitframework.inspection.Nullable;
12+
13+
import java.util.function.Function;
14+
915
/**
10-
* Indicates the factory of {@link McpClient}.
11-
* <p>
12-
* Each {@link McpClient} instance created by this factory is designed to connect to a single specified MCP server.
16+
* Factory for creating {@link McpClient} instances.
17+
*
18+
* <p>Each client connects to a single MCP server.
1319
*
1420
* @author 季聿阶
1521
* @since 2025-05-21
1622
*/
1723
public interface McpClientFactory {
1824
/**
19-
* Creates a {@link McpClient} instance with streamable HTTP transport.
25+
* Creates a client with streamable HTTP transport.
2026
*
2127
* @param baseUri The base URI of the MCP server.
2228
* @param sseEndpoint The SSE endpoint of the MCP server.
23-
* @return The connected {@link McpClient} instance.
29+
* @param elicitationFunction The function to handle {@link ElicitRequest} and return {@link ElicitResult}.
30+
* If null, elicitation will not be supported in MCP client.
31+
* @return The created {@link McpClient} instance.
2432
*/
25-
public McpClient createStreamable(String baseUri, String sseEndpoint);
33+
public McpClient createStreamable(String baseUri, String sseEndpoint,
34+
@Nullable Function<ElicitRequest, ElicitResult> elicitationFunction);
2635

2736
/**
28-
* Creates a {@link McpClient} instance with SSE transport.
37+
* Creates a client with SSE transport.
2938
*
3039
* @param baseUri The base URI of the MCP server.
3140
* @param sseEndpoint The SSE endpoint of the MCP server.
32-
* @return The connected {@link McpClient} instance.
41+
* @param elicitationFunction The function to handle {@link ElicitRequest} and return {@link ElicitResult}.
42+
* If null, elicitation will not be supported in MCP client.
43+
* @return The created {@link McpClient} instance.
3344
*/
34-
public McpClient createSse(String baseUri, String sseEndpoint);
45+
public McpClient createSse(String baseUri, String sseEndpoint,
46+
@Nullable Function<ElicitRequest, ElicitResult> elicitationFunction);
3547

3648
/**
37-
* Creates a {@link McpClient} instance with streamable HTTP transport.
49+
* Creates a client with streamable HTTP transport.
3850
*
3951
* @param baseUri The base URI of the MCP server.
4052
* @param sseEndpoint The SSE endpoint of the MCP server.
41-
* @return The connected {@link McpClient} instance.
53+
* @return The created {@link McpClient} instance.
54+
* @deprecated Use {@link #createStreamable(String, String, Function)} instead.
4255
*/
4356
@Deprecated
4457
public McpClient create(String baseUri, String sseEndpoint);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.client.elicitation;
8+
9+
import java.util.Map;
10+
11+
/**
12+
* Represents an elicitation request from an MCP server.
13+
* This is a simplified version that doesn't depend on MCP SDK types.
14+
*
15+
* @param message The message describing what information is needed from the user
16+
* @param requestedSchema The JSON schema defining the expected data structure
17+
* @author 黄可欣
18+
* @since 2025-11-25
19+
*/
20+
public record ElicitRequest(String message, Map<String, Object> requestedSchema) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.client.elicitation;
8+
9+
import java.util.Map;
10+
11+
/**
12+
* Represents the result of handling an elicitation request.
13+
* This is a simplified version that doesn't depend on MCP SDK types.
14+
*
15+
* @param action The action to take
16+
* @param content The user-provided data matching the requested schema
17+
* @author 黄可欣
18+
* @since 2025-11-25
19+
*/
20+
public record ElicitResult(Action action, Map<String, Object> content) {
21+
/**
22+
* Action types for elicitation results.
23+
*/
24+
public enum Action {
25+
ACCEPT,
26+
DECLINE,
27+
CANCEL
28+
}
29+
}

0 commit comments

Comments
 (0)