Skip to content

Commit e46632e

Browse files
authored
[FEL] MCP客户端添加SSE和Elicitation支持 (#384)
* 增加SSE客户端支持 * 增加客户端elicitation支持 * 修改注释 * 完善client接口代码
1 parent 4f66590 commit e46632e

File tree

8 files changed

+288
-36
lines changed

8 files changed

+288
-36
lines changed
Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
import com.fasterxml.jackson.databind.ObjectMapper;
1212

1313
import io.modelcontextprotocol.client.McpSyncClient;
14-
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
15-
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
1614
import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator;
15+
import io.modelcontextprotocol.spec.McpClientTransport;
1716
import io.modelcontextprotocol.spec.McpSchema;
1817
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.McpClientLogHandler;
21+
import modelengine.fel.tool.mcp.client.support.handler.McpElicitationHandler;
1922
import modelengine.fel.tool.mcp.entity.Tool;
23+
import modelengine.fitframework.inspection.Nullable;
2024
import modelengine.fitframework.log.Logger;
2125
import modelengine.fitframework.util.StringUtils;
2226
import modelengine.fitframework.util.UuidUtils;
@@ -26,6 +30,7 @@
2630
import java.util.HashMap;
2731
import java.util.List;
2832
import java.util.Map;
33+
import java.util.function.Function;
2934
import java.util.stream.Collectors;
3035

3136
/**
@@ -37,41 +42,47 @@
3742
* @author 黄可欣
3843
* @since 2025-11-03
3944
*/
40-
public class DefaultMcpStreamableClient implements McpClient {
41-
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);
4247

4348
private final String clientId;
4449
private final McpSyncClient mcpSyncClient;
45-
private final DefaultMcpClientLogHandler logHandler;
4650

4751
private volatile boolean initialized = false;
4852
private volatile boolean closed = false;
4953

5054
/**
51-
* Constructs a new instance of the DefaultMcpStreamableClient.
55+
* Constructs a new instance of the DefaultMcpClient.
5256
*
5357
* @param baseUri The base URI of the MCP server.
5458
* @param sseEndpoint The endpoint for the Server-Sent Events (SSE) connection.
5559
* @param requestTimeoutSeconds The timeout duration of requests. Units: seconds.
5660
*/
57-
public DefaultMcpStreamableClient(String baseUri, String sseEndpoint, int requestTimeoutSeconds) {
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-
ObjectMapper mapper = new ObjectMapper();
63-
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(baseUri)
64-
.jsonMapper(new JacksonMcpJsonMapper(mapper))
65-
.endpoint(sseEndpoint)
66-
.build();
67-
68-
this.logHandler = new DefaultMcpClientLogHandler(this.clientId);
69-
this.mcpSyncClient = io.modelcontextprotocol.client.McpClient.sync(transport)
70-
.requestTimeout(Duration.ofSeconds(requestTimeoutSeconds))
71-
.capabilities(McpSchema.ClientCapabilities.builder().build())
72-
.loggingConsumer(this.logHandler::handleLoggingMessage)
73-
.jsonSchemaValidator(new DefaultJsonSchemaValidator(mapper))
74-
.build();
67+
McpClientLogHandler logHandler = new McpClientLogHandler(this.clientId);
68+
if (elicitationHandler != null) {
69+
McpElicitationHandler mcpElicitationHandler =
70+
new McpElicitationHandler(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+
}
7586
}
7687

7788
@Override

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,23 @@
66

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

9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
11+
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
12+
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
13+
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
914
import modelengine.fel.tool.mcp.client.McpClient;
1015
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;
1118
import modelengine.fitframework.annotation.Component;
1219
import modelengine.fitframework.annotation.Value;
20+
import modelengine.fitframework.inspection.Nullable;
21+
22+
import java.util.function.Function;
1323

1424
/**
15-
* Represents a factory for creating instances of the {@link DefaultMcpStreamableClient}.
25+
* Represents a factory for creating instances of the {@link DefaultMcpClient}.
1626
* This class is responsible for initializing and configuring.
1727
*
1828
* @author 季聿阶
@@ -32,7 +42,22 @@ public DefaultMcpClientFactory(@Value("${mcp.client.request.timeout-seconds}") i
3242
}
3343

3444
@Override
35-
public McpClient create(String baseUri, String sseEndpoint) {
36-
return new DefaultMcpStreamableClient(baseUri, sseEndpoint, requestTimeoutSeconds);
45+
public McpClient createStreamable(String baseUri, String sseEndpoint,
46+
@Nullable Function<ElicitRequest, ElicitResult> elicitationHandler) {
47+
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(baseUri)
48+
.jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper()))
49+
.endpoint(sseEndpoint)
50+
.build();
51+
return new DefaultMcpClient(baseUri, sseEndpoint, transport, this.requestTimeoutSeconds, elicitationHandler);
52+
}
53+
54+
@Override
55+
public McpClient createSse(String baseUri, String sseEndpoint,
56+
@Nullable Function<ElicitRequest, ElicitResult> elicitationHandler) {
57+
HttpClientSseClientTransport transport = HttpClientSseClientTransport.builder(baseUri)
58+
.jsonMapper(new JacksonMcpJsonMapper(new ObjectMapper()))
59+
.sseEndpoint(sseEndpoint)
60+
.build();
61+
return new DefaultMcpClient(baseUri, sseEndpoint, transport, this.requestTimeoutSeconds, elicitationHandler);
3762
}
3863
}
Lines changed: 5 additions & 5 deletions
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;
@@ -16,16 +16,16 @@
1616
* @author 黄可欣
1717
* @since 2025-11-03
1818
*/
19-
public class DefaultMcpClientLogHandler {
20-
private static final Logger log = Logger.get(DefaultMcpClientLogHandler.class);
19+
public class McpClientLogHandler {
20+
private static final Logger log = Logger.get(McpClientLogHandler.class);
2121
private final String clientId;
2222

2323
/**
24-
* Constructs a new instance of DefaultMcpClientLogHandler.
24+
* Constructs a new instance of McpClientLogHandler.
2525
*
2626
* @param clientId The unique identifier of the MCP client.
2727
*/
28-
public DefaultMcpClientLogHandler(String clientId) {
28+
public McpClientLogHandler(String clientId) {
2929
this.clientId = clientId;
3030
}
3131

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+
* 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}.</p>
21+
*
22+
* @author 黄可欣
23+
* @since 2025-11-25
24+
*/
25+
public class McpElicitationHandler {
26+
private static final Logger log = Logger.get(McpElicitationHandler.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 McpElicitationHandler(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: 22 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

@@ -51,9 +53,27 @@ public TestController(McpClientFactory mcpClientFactory) {
5153
* @return A string indicating that the initialization was successful.
5254
*/
5355
@PostMapping(path = "/initialize")
54-
public String initialize(@RequestQuery(name = "baseUri") String baseUri,
56+
public String initializeStreamable(@RequestQuery(name = "baseUri") String baseUri,
5557
@RequestQuery(name = "sseEndpoint") String sseEndpoint) {
56-
this.client = this.mcpClientFactory.create(baseUri, sseEndpoint);
58+
this.client = this.mcpClientFactory.createStreamable(baseUri, sseEndpoint, null);
59+
this.client.initialize();
60+
return "Initialized";
61+
}
62+
63+
@PostMapping(path = "/initialize-sse")
64+
public String initializeSse(@RequestQuery(name = "baseUri") String baseUri,
65+
@RequestQuery(name = "sseEndpoint") String 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()));
5777
this.client.initialize();
5878
return "Initialized";
5979
}

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

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,74 @@
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 with SSE or Streamable HTTP transport.
17+
* <p>Each client connects to a single MCP server.</p>
1318
*
1419
* @author 季聿阶
1520
* @since 2025-05-21
1621
*/
1722
public interface McpClientFactory {
1823
/**
19-
* Creates a {@link McpClient} instance.
24+
* Creates a client with streamable HTTP transport.
25+
*
26+
* @param baseUri The base URI of the MCP server.
27+
* @param sseEndpoint The SSE endpoint of the MCP server.
28+
* @param elicitationFunction The function to handle {@link ElicitRequest} and return {@link ElicitResult}.
29+
* If null, elicitation will not be supported in MCP client.
30+
* @return The created {@link McpClient} instance.
31+
*/
32+
McpClient createStreamable(String baseUri, String sseEndpoint,
33+
@Nullable Function<ElicitRequest, ElicitResult> elicitationFunction);
34+
35+
/**
36+
* Creates a client with SSE transport.
37+
*
38+
* @param baseUri The base URI of the MCP server.
39+
* @param sseEndpoint The SSE endpoint of the MCP server.
40+
* @param elicitationFunction The function to handle {@link ElicitRequest} and return {@link ElicitResult}.
41+
* If null, elicitation will not be supported in MCP client.
42+
* @return The created {@link McpClient} instance.
43+
*/
44+
McpClient createSse(String baseUri, String sseEndpoint,
45+
@Nullable Function<ElicitRequest, ElicitResult> elicitationFunction);
46+
47+
/**
48+
* Creates a client with streamable HTTP transport (default). No elicitation support.
49+
*
50+
* @param baseUri The base URI of the MCP server.
51+
* @param sseEndpoint The SSE endpoint of the MCP server.
52+
* @return The created {@link McpClient} instance.
53+
*/
54+
default McpClient create(String baseUri, String sseEndpoint) {
55+
return this.createStreamable(baseUri, sseEndpoint, null);
56+
}
57+
58+
/**
59+
* Creates a client with streamable HTTP transport. No elicitation support.
60+
*
61+
* @param baseUri The base URI of the MCP server.
62+
* @param sseEndpoint The SSE endpoint of the MCP server.
63+
* @return The created {@link McpClient} instance.
64+
*/
65+
default McpClient createStreamable(String baseUri, String sseEndpoint) {
66+
return this.createStreamable(baseUri, sseEndpoint, null);
67+
}
68+
69+
/**
70+
* Creates a client with SSE transport. No elicitation support.
2071
*
2172
* @param baseUri The base URI of the MCP server.
2273
* @param sseEndpoint The SSE endpoint of the MCP server.
23-
* @return The connected {@link McpClient} instance.
74+
* @return The created {@link McpClient} instance.
2475
*/
25-
McpClient create(String baseUri, String sseEndpoint);
76+
default McpClient createSse(String baseUri, String sseEndpoint) {
77+
return this.createSse(baseUri, sseEndpoint, null);
78+
}
2679
}

0 commit comments

Comments
 (0)