Skip to content

Commit 523d902

Browse files
committed
[app-builder] parse MCP config and query tool list from MCP service
1 parent 35d0a6a commit 523d902

File tree

13 files changed

+304
-24
lines changed

13 files changed

+304
-24
lines changed

app-builder/jane/plugins/aipp-plugin/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@
134134
<groupId>modelengine.fit.jade.waterflow</groupId>
135135
<artifactId>waterflow-graph-service</artifactId>
136136
</dependency>
137+
<dependency>
138+
<groupId>org.fitframework.fel</groupId>
139+
<artifactId>tool-mcp-client-service</artifactId>
140+
</dependency>
137141

138142
<!-- Redis -->
139143
<dependency>

app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ public enum AippErrCode implements ErrorCode, RetCode {
137137
*/
138138
INVALID_FILE_PATH(90002003, "无效文件路径。"),
139139

140+
/**
141+
* 调用 MCP 服务失败。
142+
*/
143+
CALL_MCP_SERVER_FAILED(90002004, "调用 MCP 服务失败,原因:{0}。"),
144+
140145
/**
141146
* json解析失败
142147
*/

app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919
import modelengine.fel.engine.flows.AiProcessFlow;
2020
import modelengine.fel.engine.operators.patterns.AbstractAgent;
2121
import modelengine.fel.engine.operators.prompts.Prompts;
22+
import modelengine.fel.tool.mcp.client.McpClient;
23+
import modelengine.fel.tool.mcp.client.McpClientFactory;
24+
import modelengine.fel.tool.mcp.entity.Tool;
2225
import modelengine.fel.tool.model.transfer.ToolData;
26+
import modelengine.fit.jober.aipp.util.McpUtils;
27+
import modelengine.fitframework.inspection.Validation;
2328
import modelengine.jade.store.service.ToolService;
2429
import modelengine.fit.jade.aipp.model.dto.ModelAccessInfo;
2530
import modelengine.fit.jade.aipp.model.service.AippModelCenter;
@@ -60,6 +65,7 @@
6065
import modelengine.fitframework.util.StringUtils;
6166
import modelengine.fitframework.util.UuidUtils;
6267

68+
import java.io.IOException;
6369
import java.util.ArrayList;
6470
import java.util.Collections;
6571
import java.util.HashMap;
@@ -69,6 +75,7 @@
6975
import java.util.Optional;
7076
import java.util.concurrent.ConcurrentHashMap;
7177
import java.util.stream.Collectors;
78+
import java.util.stream.Stream;
7279

7380
/**
7481
* LLM 组件实现
@@ -101,6 +108,7 @@ public class LlmComponent implements FlowableService {
101108
private final AippModelCenter aippModelCenter;
102109
private final PromptBuilderChain promptBuilderChain;
103110
private final AppTaskInstanceService appTaskInstanceService;
111+
private final McpClientFactory mcpClientFactory;
104112

105113
/**
106114
* 大模型节点构造器,内部通过提供的 agent 和 tool 构建智能体工作流。
@@ -114,6 +122,7 @@ public class LlmComponent implements FlowableService {
114122
* @param aippModelCenter 表示模型中心的 {@link AippModelCenter}。
115123
* @param promptBuilderChain 表示提示器构造器职责链的 {@link PromptBuilderChain}。
116124
* @param appTaskInstanceService 表示任务实例服务的 {@link AppTaskInstanceService}。
125+
* @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。
117126
*/
118127
public LlmComponent(FlowInstanceService flowInstanceService,
119128
@Fit ToolService toolService,
@@ -123,7 +132,8 @@ public LlmComponent(FlowInstanceService flowInstanceService,
123132
@Fit(alias = "json") ObjectSerializer serializer,
124133
AippModelCenter aippModelCenter,
125134
PromptBuilderChain promptBuilderChain,
126-
AppTaskInstanceService appTaskInstanceService) {
135+
AppTaskInstanceService appTaskInstanceService,
136+
McpClientFactory mcpClientFactory) {
127137
this.flowInstanceService = flowInstanceService;
128138
this.toolService = toolService;
129139
this.aippLogService = aippLogService;
@@ -139,6 +149,7 @@ public LlmComponent(FlowInstanceService flowInstanceService,
139149
.close();
140150
this.promptBuilderChain = promptBuilderChain;
141151
this.appTaskInstanceService = appTaskInstanceService;
152+
this.mcpClientFactory = notNull(mcpClientFactory, "The mcp client factory cannot be null.");
142153
}
143154

144155
/**
@@ -177,6 +188,7 @@ public List<Map<String, Object>> handleTask(List<Map<String, Object>> flowData)
177188
StreamMsgSender streamMsgSender =
178189
new StreamMsgSender(this.aippLogStreamService, this.serializer, path, msgId, instId);
179190
streamMsgSender.sendKnowledge(promptMessage.getMetadata(), businessData);
191+
ChatOption chatOption = buildChatOptions(businessData);
180192
agentFlow.converse()
181193
.bind((acc, chunk) -> {
182194
if (firstTokenFlag[0]) {
@@ -195,7 +207,8 @@ public List<Map<String, Object>> handleTask(List<Map<String, Object>> flowData)
195207
.doOnConsume(msg -> llmOutputConsumer(llmMeta, msg, promptMessage.getMetadata()))
196208
.doOnError(throwable -> doOnAgentError(llmMeta,
197209
throwable.getCause() == null ? throwable.getMessage() : throwable.getCause().getMessage()))
198-
.bind(buildChatOptions(businessData))
210+
.bind(chatOption)
211+
.bind(AippConst.TOOLS_KEY, chatOption.tools())
199212
.offer(Tip.fromArray(promptMessage.getSystemMessage(), promptMessage.getHumanMessage()));
200213
log.info("[perf] [{}] handleTask end, instId={}", System.currentTimeMillis(), instId);
201214
return flowData;
@@ -393,10 +406,6 @@ private String getFilePath(Map<String, Object> businessData) {
393406
* @return 返回表示自定义参数。
394407
*/
395408
private ChatOption buildChatOptions(Map<String, Object> businessData) {
396-
List<String> skillNameList = new ArrayList<>(ObjectUtils.cast(businessData.get("tools")));
397-
if (businessData.containsKey("workflows")) {
398-
skillNameList.addAll(ObjectUtils.cast(businessData.get("workflows")));
399-
}
400409
String model = ObjectUtils.cast(businessData.get("model"));
401410
Map<String, String> accessInfo = ObjectUtils.nullIf(ObjectUtils.cast(businessData.get("accessInfo")),
402411
MapBuilder.<String, String>get().put("serviceName", model).put("tag", "INTERNAL").build());
@@ -413,10 +422,50 @@ private ChatOption buildChatOptions(Map<String, Object> businessData) {
413422
.secureConfig(modelAccessInfo.isSystemModel() ? null : SecureConfig.custom().ignoreTrust(true).build())
414423
.apiKey(modelAccessInfo.getAccessKey())
415424
.temperature(ObjectUtils.cast(businessData.get("temperature")))
416-
.tools(this.buildToolInfos(skillNameList))
425+
.tools(this.buildToolInfos(businessData))
417426
.build();
418427
}
419428

429+
private List<ToolInfo> buildToolInfos(Map<String, Object> businessData) {
430+
List<String> skillNameList = new ArrayList<>(ObjectUtils.cast(businessData.get("tools")));
431+
if (businessData.containsKey("workflows")) {
432+
skillNameList.addAll(ObjectUtils.cast(businessData.get("workflows")));
433+
}
434+
Map<String, Object> mcpServersConfig = ObjectUtils.cast(businessData.get(AippConst.MCP_SERVERS_KEY));
435+
436+
return Stream.concat(this.buildToolInfos(skillNameList).stream(),
437+
this.buildMcpToolInfos(mcpServersConfig).stream()).collect(Collectors.toList());
438+
}
439+
440+
private List<ToolInfo> buildMcpToolInfos(Map<String, Object> mcpServersConfig) {
441+
List<ToolInfo> result = new ArrayList<>();
442+
ObjectUtils.nullIf(mcpServersConfig, new HashMap<String, Object>()).forEach((serverName, value) -> {
443+
Map<String, Object> serverConfig = ObjectUtils.cast(value);
444+
String url = Validation.notBlank(ObjectUtils.cast(serverConfig.get(AippConst.MCP_SERVER_URL_KEY)),
445+
"The mcp url should not be empty.");
446+
447+
try (McpClient mcpClient = this.mcpClientFactory.create(McpUtils.getBaseUrl(url),
448+
McpUtils.getSseEndpoint(url))) {
449+
mcpClient.initialize();
450+
List<Tool> tools = mcpClient.getTools();
451+
result.addAll(tools.stream()
452+
.map(tool -> ToolInfo.custom()
453+
.name(buildUniqueToolName(AippConst.MCP_SERVER_TYPE, serverName, tool.getName()))
454+
.description(tool.getDescription())
455+
.parameters(tool.getInputSchema())
456+
.extensions(MapBuilder.<String, Object>get()
457+
.put(AippConst.MCP_SERVER_KEY, serverConfig)
458+
.put(AippConst.TOOL_REAL_NAME, tool.getName())
459+
.build())
460+
.build())
461+
.toList());
462+
} catch (IOException exception) {
463+
throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, exception.getMessage());
464+
}
465+
});
466+
return result;
467+
}
468+
420469
private List<ToolInfo> buildToolInfos(List<String> skillNameList) {
421470
return skillNameList.stream()
422471
.map(this.toolService::getTool)
@@ -427,12 +476,21 @@ private List<ToolInfo> buildToolInfos(List<String> skillNameList) {
427476

428477
private ToolInfo buildToolInfo(ToolData toolData) {
429478
return ToolInfo.custom()
430-
.name(toolData.getUniqueName())
479+
.name(buildUniqueToolName(AippConst.STORE_SERVER_TYPE,
480+
AippConst.STORE_SERVER_NAME,
481+
toolData.getUniqueName()))
431482
.description(toolData.getDescription())
432483
.parameters(new HashMap<>(toolData.getSchema()))
484+
.extensions(MapBuilder.<String, Object>get()
485+
.put(AippConst.TOOL_REAL_NAME, toolData.getUniqueName())
486+
.build())
433487
.build();
434488
}
435489

490+
private static String buildUniqueToolName(String type, String serverName, String toolName) {
491+
return String.format("%s_%s_%s", type, serverName, toolName);
492+
}
493+
436494
public static boolean checkEnableLog(Map<String, Object> businessData) {
437495
Object value = businessData.get(AippConst.BS_LLM_ENABLE_LOG);
438496
if (value == null) {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.fit.jober.aipp.util;
8+
9+
import modelengine.fitframework.inspection.Validation;
10+
11+
/**
12+
* 大模型上下文协议相关工具方法。
13+
*
14+
* @author 宋永坦
15+
* @since 2025-07-11
16+
*/
17+
public class McpUtils {
18+
private static final String SSE_ENDPOINT_SPLIT_DELIMITER = "/";
19+
20+
/**
21+
* 获取 {@code baseUrl} 部分。
22+
*
23+
* @param url 目标地址。
24+
* @return {@code baseUrl} 部分。
25+
* @throws IllegalArgumentException 当目标地址不包含 {@code sseEndpoint} 时。
26+
*/
27+
public static String getBaseUrl(String url) {
28+
String[] splits = url.split(SSE_ENDPOINT_SPLIT_DELIMITER);
29+
Validation.greaterThan(splits.length, 3, "The url is wrong. [url={0}]", url);
30+
return url.substring(0, url.length() - splits[splits.length - 1].length() - 1);
31+
}
32+
33+
/**
34+
* 获取 {@code sseEndpoint} 部分。
35+
*
36+
* @param url 目标地址。
37+
* @return {@code sseEndpoint} 部分。
38+
* @throws IllegalArgumentException 当目标地址不包含 {@code sseEndpoint} 时。
39+
*/
40+
public static String getSseEndpoint(String url) {
41+
String[] splits = url.split(SSE_ENDPOINT_SPLIT_DELIMITER);
42+
Validation.greaterThan(splits.length, 3, "The url is wrong. [url={0}]", url);
43+
return SSE_ENDPOINT_SPLIT_DELIMITER + splits[splits.length - 1];
44+
}
45+
}

app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_en.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
90002001=The file has expired or is damaged.
2121
90002002=Failed to parse the file.
2222
90002003=Invalid file path.
23+
90002004=Failed to call MCP service. reason: {0}.
2324
90002900=JSON parsing failed. Cause: {0}.
2425
90002901=JSON encoding failed. Cause: {0}.
2526
90002902=Failed to obtain historical records.

app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_zh.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
90002001=文件过期或损坏。
2121
90002002=解析文件内容失败。
2222
90002003=无效文件路径。
23+
90002004=调用 MCP 服务失败,原因:{0}。
2324
90002900=json解码失败,原因:{0}。
2425
90002901=json编码失败,原因:{0}。
2526
90002902=获取历史记录失败。

0 commit comments

Comments
 (0)