Skip to content

Commit ed261e8

Browse files
authored
[app-builder] support mcp tools (#323)
* [app-builder] parse MCP config and query tool list from MCP service * [app-builder] support MCP tool invocation in LLM nodes * [app-builder] clean code
1 parent 5225119 commit ed261e8

File tree

16 files changed

+561
-42
lines changed

16 files changed

+561
-42
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/fel/FelComponentConfig.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import modelengine.fel.core.chat.ChatModel;
1010
import modelengine.fel.core.chat.Prompt;
1111
import modelengine.fel.engine.operators.patterns.AbstractAgent;
12+
import modelengine.fel.tool.mcp.client.McpClientFactory;
1213
import modelengine.fit.jade.tool.SyncToolCall;
1314
import modelengine.fit.jober.aipp.constants.AippConst;
1415
import modelengine.fitframework.annotation.Bean;
@@ -28,11 +29,12 @@ public class FelComponentConfig {
2829
*
2930
* @param syncToolCall 表示同步工具调用服务的 {@link SyncToolCall}。
3031
* @param chatModel 表示模型流式服务的 {@link ChatModel}。
32+
* @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。
3133
* @return 返回 WaterFlow 场景的 Agent 服务的 {@link AbstractAgent}{@code <}{@link Prompt}{@code ,
3234
* }{@link Prompt}{@code >}。
3335
*/
3436
@Bean(AippConst.WATER_FLOW_AGENT_BEAN)
35-
public AbstractAgent getWaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatModel) {
36-
return new WaterFlowAgent(syncToolCall, chatModel);
37+
public AbstractAgent getWaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatModel, McpClientFactory mcpClientFactory) {
38+
return new WaterFlowAgent(syncToolCall, chatModel, mcpClientFactory);
3739
}
3840
}

app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,40 @@
66

77
package modelengine.fit.jober.aipp.fel;
88

9+
import com.alibaba.fastjson.JSON;
10+
import com.alibaba.fastjson.JSONObject;
11+
912
import modelengine.fel.core.chat.ChatMessage;
1013
import modelengine.fel.core.chat.ChatModel;
1114
import modelengine.fel.core.chat.Prompt;
1215
import modelengine.fel.core.chat.support.ChatMessages;
1316
import modelengine.fel.core.chat.support.FlatChatMessage;
1417
import modelengine.fel.core.chat.support.ToolMessage;
1518
import modelengine.fel.core.tool.ToolCall;
19+
import modelengine.fel.core.tool.ToolInfo;
1620
import modelengine.fel.engine.flows.AiFlows;
1721
import modelengine.fel.engine.flows.AiProcessFlow;
1822
import modelengine.fel.engine.operators.models.ChatChunk;
1923
import modelengine.fel.engine.operators.models.ChatFlowModel;
2024
import modelengine.fel.engine.operators.patterns.AbstractAgent;
25+
import modelengine.fel.tool.mcp.client.McpClient;
26+
import modelengine.fel.tool.mcp.client.McpClientFactory;
2127
import modelengine.fit.jade.tool.SyncToolCall;
28+
import modelengine.fit.jober.aipp.common.exception.AippErrCode;
29+
import modelengine.fit.jober.aipp.common.exception.AippException;
2230
import modelengine.fit.jober.aipp.constants.AippConst;
31+
import modelengine.fit.jober.aipp.util.McpUtils;
2332
import modelengine.fit.waterflow.domain.context.StateContext;
2433
import modelengine.fitframework.annotation.Fit;
2534
import modelengine.fitframework.inspection.Validation;
35+
import modelengine.fitframework.util.CollectionUtils;
2636
import modelengine.fitframework.util.ObjectUtils;
2737

38+
import java.io.IOException;
2839
import java.util.Collections;
2940
import java.util.List;
3041
import java.util.Map;
42+
import java.util.function.Function;
3143
import java.util.stream.Collectors;
3244

3345
/**
@@ -42,28 +54,30 @@ public class WaterFlowAgent extends AbstractAgent {
4254

4355
private final String agentMsgKey;
4456
private final SyncToolCall syncToolCall;
57+
private final McpClientFactory mcpClientFactory;
4558

4659
/**
4760
* {@link WaterFlowAgent} 的构造方法。
4861
*
4962
* @param syncToolCall 表示工具调用服务的 {@link SyncToolCall}。
5063
* @param chatStreamModel 表示流式对话大模型的 {@link ChatModel}。
64+
* @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。
5165
*/
52-
public WaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatStreamModel) {
66+
public WaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatStreamModel,
67+
McpClientFactory mcpClientFactory) {
5368
super(new ChatFlowModel(chatStreamModel, null));
54-
this.syncToolCall = Validation.notNull(syncToolCall, "The tool sync tool call cannot be null.");
69+
this.syncToolCall = Validation.notNull(syncToolCall, "The tool sync tool call cannot be null.");
70+
this.mcpClientFactory = Validation.notNull(mcpClientFactory, "The mcp client factory cannot be null.");
5571
this.agentMsgKey = AGENT_MSG_KEY;
5672
}
5773

5874
@Override
5975
protected Prompt doToolCall(List<ToolCall> toolCalls, StateContext ctx) {
6076
Validation.notNull(ctx, "The state context cannot be null.");
61-
Map<String, Object> toolContext = ObjectUtils.getIfNull(ctx.getState(AippConst.TOOL_CONTEXT_KEY),
62-
Collections::emptyMap);
63-
return toolCalls.stream()
64-
.map(toolCall -> (ChatMessage) new ToolMessage(toolCall.id(),
65-
this.syncToolCall.call(toolCall.name(), toolCall.arguments(), toolContext)))
66-
.collect(Collectors.collectingAndThen(Collectors.toList(), ChatMessages::from));
77+
return ChatMessages.from(this.callTools(toolCalls, ctx)
78+
.stream()
79+
.map(message -> (ChatMessage) FlatChatMessage.from(message))
80+
.collect(Collectors.toList()));
6781
}
6882

6983
@Override
@@ -87,18 +101,53 @@ public AiProcessFlow<Prompt, ChatMessage> buildFlow() {
87101
private ChatMessage handleTool(ChatMessage input, StateContext ctx) {
88102
Validation.notNull(ctx, "The state context cannot be null.");
89103
Validation.notNull(input, "The input message cannot be null.");
90-
91-
Map<String, Object> toolContext = ObjectUtils.getIfNull(ctx.getState(AippConst.TOOL_CONTEXT_KEY),
92-
Collections::emptyMap);
93104
ChatMessages lastRequest = ctx.getState(this.agentMsgKey);
94105
lastRequest.add(input);
95-
input.toolCalls().forEach(toolCall -> {
96-
lastRequest.add(FlatChatMessage.from(new ToolMessage(toolCall.id(),
97-
this.syncToolCall.call(toolCall.name(), toolCall.arguments(), toolContext))));
98-
});
106+
lastRequest.addAll(this.callTools(input.toolCalls(), ctx));
99107
return input;
100108
}
101109

110+
private List<ChatMessage> callTools(List<ToolCall> toolCalls, StateContext ctx) {
111+
if (CollectionUtils.isEmpty(toolCalls)) {
112+
return Collections.emptyList();
113+
}
114+
List<ToolInfo> tools = ctx.getState(AippConst.TOOLS_KEY);
115+
Validation.notEmpty(tools, "Missing tool detected during call.");
116+
Map<String, ToolInfo> toolsMap = tools.stream().collect(Collectors.toMap(ToolInfo::name, Function.identity()));
117+
Map<String, Object> toolContext =
118+
ObjectUtils.getIfNull(ctx.getState(AippConst.TOOL_CONTEXT_KEY), Collections::emptyMap);
119+
return toolCalls.stream()
120+
.map(toolCall -> this.callTool(toolCall, toolsMap, toolContext))
121+
.collect(Collectors.toList());
122+
}
123+
124+
private ChatMessage callTool(ToolCall toolCall, Map<String, ToolInfo> toolsMap, Map<String, Object> toolContext) {
125+
ToolInfo toolInfo = toolsMap.get(toolCall.name());
126+
if (toolInfo == null) {
127+
throw new IllegalStateException(String.format("The tool call's tool is not exist. [toolName=%s]",
128+
toolCall.name()));
129+
}
130+
Map<String, Object> extensions = Validation.notNull(toolInfo.extensions(),
131+
"The tool call's extension is not exist. [toolName={0}]", toolCall.name());
132+
String toolRealName = Validation.notBlank(ObjectUtils.cast(extensions.get(AippConst.TOOL_REAL_NAME)),
133+
"Can not find the tool real name. [toolName={0}]",
134+
toolCall.name());
135+
Map<String, Object> mcpServerConfig = ObjectUtils.cast(extensions.get(AippConst.MCP_SERVER_KEY));
136+
if (mcpServerConfig != null) {
137+
String url = Validation.notBlank(ObjectUtils.cast(mcpServerConfig.get(AippConst.MCP_SERVER_URL_KEY)),
138+
"The mcp url should not be empty.");
139+
try (McpClient mcpClient = this.mcpClientFactory.create(McpUtils.getBaseUrl(url),
140+
McpUtils.getSseEndpoint(url))) {
141+
mcpClient.initialize();
142+
Object result = mcpClient.callTool(toolRealName, JSONObject.parseObject(toolCall.arguments()));
143+
return new ToolMessage(toolCall.id(), JSON.toJSONString(result));
144+
} catch (IOException exception) {
145+
throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, exception.getMessage());
146+
}
147+
}
148+
return new ToolMessage(toolCall.id(), this.syncToolCall.call(toolRealName, toolCall.arguments(), toolContext));
149+
}
150+
102151
private ChatMessages getAgentMsg(ChatMessage input, StateContext ctx) {
103152
Validation.notNull(ctx, "The state context cannot be null.");
104153
return ctx.getState(this.agentMsgKey);

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

Lines changed: 74 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 = this.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,40 @@ 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().map(tool -> buildMcpToolInfo(serverName, tool, serverConfig)).toList());
452+
} catch (IOException exception) {
453+
throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, exception.getMessage());
454+
}
455+
});
456+
return result;
457+
}
458+
420459
private List<ToolInfo> buildToolInfos(List<String> skillNameList) {
421460
return skillNameList.stream()
422461
.map(this.toolService::getTool)
@@ -427,12 +466,39 @@ private List<ToolInfo> buildToolInfos(List<String> skillNameList) {
427466

428467
private ToolInfo buildToolInfo(ToolData toolData) {
429468
return ToolInfo.custom()
430-
.name(toolData.getUniqueName())
469+
.name(buildUniqueToolName(AippConst.STORE_SERVER_TYPE,
470+
AippConst.STORE_SERVER_NAME,
471+
toolData.getUniqueName()))
431472
.description(toolData.getDescription())
432473
.parameters(new HashMap<>(toolData.getSchema()))
474+
.extensions(MapBuilder.<String, Object>get()
475+
.put(AippConst.TOOL_REAL_NAME, toolData.getUniqueName())
476+
.build())
477+
.build();
478+
}
479+
480+
private static ToolInfo buildMcpToolInfo(String serverName, Tool tool, Map<String, Object> serverConfig) {
481+
return ToolInfo.custom()
482+
.name(buildUniqueToolName(AippConst.MCP_SERVER_TYPE, serverName, tool.getName()))
483+
.description(tool.getDescription())
484+
.parameters(tool.getInputSchema())
485+
.extensions(MapBuilder.<String, Object>get()
486+
.put(AippConst.MCP_SERVER_KEY, serverConfig)
487+
.put(AippConst.TOOL_REAL_NAME, tool.getName())
488+
.build())
433489
.build();
434490
}
435491

492+
private static String buildUniqueToolName(String type, String serverName, String toolName) {
493+
return StringUtils.format("{0}_{1}_{2}", type, serverName, toolName);
494+
}
495+
496+
/**
497+
* 判断是否启用日志。
498+
*
499+
* @param businessData 表示业务上下文数据的 {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}。
500+
* @return 表示是否启用日志的 {@code boolean}。
501+
*/
436502
public static boolean checkEnableLog(Map<String, Object> businessData) {
437503
Object value = businessData.get(AippConst.BS_LLM_ENABLE_LOG);
438504
if (value == null) {

0 commit comments

Comments
 (0)