From 523d90233cb366cbaaca2775567f4df44eb4a264 Mon Sep 17 00:00:00 2001
From: song <271667068@qq.com>
Date: Thu, 10 Jul 2025 22:32:00 +0800
Subject: [PATCH 1/3] [app-builder] parse MCP config and query tool list from
MCP service
---
app-builder/jane/plugins/aipp-plugin/pom.xml | 4 +
.../aipp/common/exception/AippErrCode.java | 5 +
.../fit/jober/aipp/fitable/LlmComponent.java | 74 ++++++++-
.../fit/jober/aipp/util/McpUtils.java | 45 ++++++
.../resources/i18n/messages_en.properties | 1 +
.../resources/i18n/messages_zh.properties | 1 +
.../jober/aipp/fitable/LlmComponentTest.java | 141 ++++++++++++++++--
.../fit/jober/aipp/constants/AippConst.java | 41 +++++
.../aipp/common/exception/AippErrCode.java | 5 +
.../pages/appDetail/overview/common/config.ts | 4 +
common/dependency/pom.xml | 5 +
.../main/resources/i18n/aipp_en.properties | 1 +
.../main/resources/i18n/aipp_zh.properties | 1 +
13 files changed, 304 insertions(+), 24 deletions(-)
create mode 100644 app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpUtils.java
diff --git a/app-builder/jane/plugins/aipp-plugin/pom.xml b/app-builder/jane/plugins/aipp-plugin/pom.xml
index 7722071770..7e82ceca76 100644
--- a/app-builder/jane/plugins/aipp-plugin/pom.xml
+++ b/app-builder/jane/plugins/aipp-plugin/pom.xml
@@ -134,6 +134,10 @@
modelengine.fit.jade.waterflow
waterflow-graph-service
+
+ org.fitframework.fel
+ tool-mcp-client-service
+
diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java
index 2c03e79b57..ecd511011d 100644
--- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java
+++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java
@@ -137,6 +137,11 @@ public enum AippErrCode implements ErrorCode, RetCode {
*/
INVALID_FILE_PATH(90002003, "无效文件路径。"),
+ /**
+ * 调用 MCP 服务失败。
+ */
+ CALL_MCP_SERVER_FAILED(90002004, "调用 MCP 服务失败,原因:{0}。"),
+
/**
* json解析失败
*/
diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java
index a7f0b611f4..86d28862f9 100644
--- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java
+++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java
@@ -19,7 +19,12 @@
import modelengine.fel.engine.flows.AiProcessFlow;
import modelengine.fel.engine.operators.patterns.AbstractAgent;
import modelengine.fel.engine.operators.prompts.Prompts;
+import modelengine.fel.tool.mcp.client.McpClient;
+import modelengine.fel.tool.mcp.client.McpClientFactory;
+import modelengine.fel.tool.mcp.entity.Tool;
import modelengine.fel.tool.model.transfer.ToolData;
+import modelengine.fit.jober.aipp.util.McpUtils;
+import modelengine.fitframework.inspection.Validation;
import modelengine.jade.store.service.ToolService;
import modelengine.fit.jade.aipp.model.dto.ModelAccessInfo;
import modelengine.fit.jade.aipp.model.service.AippModelCenter;
@@ -60,6 +65,7 @@
import modelengine.fitframework.util.StringUtils;
import modelengine.fitframework.util.UuidUtils;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -69,6 +75,7 @@
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* LLM 组件实现
@@ -101,6 +108,7 @@ public class LlmComponent implements FlowableService {
private final AippModelCenter aippModelCenter;
private final PromptBuilderChain promptBuilderChain;
private final AppTaskInstanceService appTaskInstanceService;
+ private final McpClientFactory mcpClientFactory;
/**
* 大模型节点构造器,内部通过提供的 agent 和 tool 构建智能体工作流。
@@ -114,6 +122,7 @@ public class LlmComponent implements FlowableService {
* @param aippModelCenter 表示模型中心的 {@link AippModelCenter}。
* @param promptBuilderChain 表示提示器构造器职责链的 {@link PromptBuilderChain}。
* @param appTaskInstanceService 表示任务实例服务的 {@link AppTaskInstanceService}。
+ * @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。
*/
public LlmComponent(FlowInstanceService flowInstanceService,
@Fit ToolService toolService,
@@ -123,7 +132,8 @@ public LlmComponent(FlowInstanceService flowInstanceService,
@Fit(alias = "json") ObjectSerializer serializer,
AippModelCenter aippModelCenter,
PromptBuilderChain promptBuilderChain,
- AppTaskInstanceService appTaskInstanceService) {
+ AppTaskInstanceService appTaskInstanceService,
+ McpClientFactory mcpClientFactory) {
this.flowInstanceService = flowInstanceService;
this.toolService = toolService;
this.aippLogService = aippLogService;
@@ -139,6 +149,7 @@ public LlmComponent(FlowInstanceService flowInstanceService,
.close();
this.promptBuilderChain = promptBuilderChain;
this.appTaskInstanceService = appTaskInstanceService;
+ this.mcpClientFactory = notNull(mcpClientFactory, "The mcp client factory cannot be null.");
}
/**
@@ -177,6 +188,7 @@ public List
+
+ org.fitframework.fel
+ tool-mcp-client-service
+ ${fel.version}
+
modelengine.fit.jade.service
diff --git a/common/plugins/http-interceptor/src/main/resources/i18n/aipp_en.properties b/common/plugins/http-interceptor/src/main/resources/i18n/aipp_en.properties
index adcb2ff4fb..ceb9bdff5c 100644
--- a/common/plugins/http-interceptor/src/main/resources/i18n/aipp_en.properties
+++ b/common/plugins/http-interceptor/src/main/resources/i18n/aipp_en.properties
@@ -20,6 +20,7 @@
90002001=The file has expired or is damaged.
90002002=Failed to parse the file.
90002003=Invalid file path.
+90002004=Failed to call MCP service. reason: {0}.
90002900=JSON parsing failed. Cause: {0}.
90002901=JSON encoding failed. Cause: {0}.
90002902=Failed to obtain historical records.
diff --git a/common/plugins/http-interceptor/src/main/resources/i18n/aipp_zh.properties b/common/plugins/http-interceptor/src/main/resources/i18n/aipp_zh.properties
index 875ac29637..c44f6ebca6 100644
--- a/common/plugins/http-interceptor/src/main/resources/i18n/aipp_zh.properties
+++ b/common/plugins/http-interceptor/src/main/resources/i18n/aipp_zh.properties
@@ -20,6 +20,7 @@
90002001=文件过期或损坏。
90002002=解析文件内容失败。
90002003=无效文件路径。
+90002004=调用 MCP 服务失败,原因:{0}。
90002900=json解码失败,原因:{0}。
90002901=json编码失败,原因:{0}。
90002902=获取历史记录失败。
From b039d559e15146ebb42d8106bf7b61f0c018cc73 Mon Sep 17 00:00:00 2001
From: song <271667068@qq.com>
Date: Mon, 14 Jul 2025 16:25:24 +0800
Subject: [PATCH 2/3] [app-builder] support MCP tool invocation in LLM nodes
---
.../jober/aipp/fel/FelComponentConfig.java | 6 +-
.../fit/jober/aipp/fel/WaterFlowAgent.java | 79 ++++++--
.../jober/aipp/fel/WaterFlowAgentTest.java | 183 ++++++++++++++++++
.../jober/aipp/fitable/LlmComponentTest.java | 2 +-
4 files changed, 252 insertions(+), 18 deletions(-)
create mode 100644 app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java
diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java
index e9671c9d31..ab9296f730 100644
--- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java
+++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java
@@ -9,6 +9,7 @@
import modelengine.fel.core.chat.ChatModel;
import modelengine.fel.core.chat.Prompt;
import modelengine.fel.engine.operators.patterns.AbstractAgent;
+import modelengine.fel.tool.mcp.client.McpClientFactory;
import modelengine.fit.jade.tool.SyncToolCall;
import modelengine.fit.jober.aipp.constants.AippConst;
import modelengine.fitframework.annotation.Bean;
@@ -28,11 +29,12 @@ public class FelComponentConfig {
*
* @param syncToolCall 表示同步工具调用服务的 {@link SyncToolCall}。
* @param chatModel 表示模型流式服务的 {@link ChatModel}。
+ * @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。
* @return 返回 WaterFlow 场景的 Agent 服务的 {@link AbstractAgent}{@code <}{@link Prompt}{@code ,
* }{@link Prompt}{@code >}。
*/
@Bean(AippConst.WATER_FLOW_AGENT_BEAN)
- public AbstractAgent getWaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatModel) {
- return new WaterFlowAgent(syncToolCall, chatModel);
+ public AbstractAgent getWaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatModel, McpClientFactory mcpClientFactory) {
+ return new WaterFlowAgent(syncToolCall, chatModel, mcpClientFactory);
}
}
\ No newline at end of file
diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java
index 72b95ff911..4a8a822608 100644
--- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java
+++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java
@@ -6,6 +6,9 @@
package modelengine.fit.jober.aipp.fel;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+
import modelengine.fel.core.chat.ChatMessage;
import modelengine.fel.core.chat.ChatModel;
import modelengine.fel.core.chat.Prompt;
@@ -13,21 +16,30 @@
import modelengine.fel.core.chat.support.FlatChatMessage;
import modelengine.fel.core.chat.support.ToolMessage;
import modelengine.fel.core.tool.ToolCall;
+import modelengine.fel.core.tool.ToolInfo;
import modelengine.fel.engine.flows.AiFlows;
import modelengine.fel.engine.flows.AiProcessFlow;
import modelengine.fel.engine.operators.models.ChatChunk;
import modelengine.fel.engine.operators.models.ChatFlowModel;
import modelengine.fel.engine.operators.patterns.AbstractAgent;
+import modelengine.fel.tool.mcp.client.McpClient;
+import modelengine.fel.tool.mcp.client.McpClientFactory;
import modelengine.fit.jade.tool.SyncToolCall;
+import modelengine.fit.jober.aipp.common.exception.AippErrCode;
+import modelengine.fit.jober.aipp.common.exception.AippException;
import modelengine.fit.jober.aipp.constants.AippConst;
+import modelengine.fit.jober.aipp.util.McpUtils;
import modelengine.fit.waterflow.domain.context.StateContext;
import modelengine.fitframework.annotation.Fit;
import modelengine.fitframework.inspection.Validation;
+import modelengine.fitframework.util.CollectionUtils;
import modelengine.fitframework.util.ObjectUtils;
+import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -42,28 +54,30 @@ public class WaterFlowAgent extends AbstractAgent {
private final String agentMsgKey;
private final SyncToolCall syncToolCall;
+ private final McpClientFactory mcpClientFactory;
/**
* {@link WaterFlowAgent} 的构造方法。
*
* @param syncToolCall 表示工具调用服务的 {@link SyncToolCall}。
* @param chatStreamModel 表示流式对话大模型的 {@link ChatModel}。
+ * @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。
*/
- public WaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatStreamModel) {
+ public WaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatStreamModel,
+ McpClientFactory mcpClientFactory) {
super(new ChatFlowModel(chatStreamModel, null));
- this.syncToolCall = Validation.notNull(syncToolCall, "The tool sync tool call cannot be null.");
+ this.syncToolCall = Validation.notNull(syncToolCall, "The tool sync tool call cannot be null.");
+ this.mcpClientFactory = Validation.notNull(mcpClientFactory, "The mcp client factory cannot be null.");
this.agentMsgKey = AGENT_MSG_KEY;
}
@Override
protected Prompt doToolCall(List toolCalls, StateContext ctx) {
Validation.notNull(ctx, "The state context cannot be null.");
- Map toolContext = ObjectUtils.getIfNull(ctx.getState(AippConst.TOOL_CONTEXT_KEY),
- Collections::emptyMap);
- return toolCalls.stream()
- .map(toolCall -> (ChatMessage) new ToolMessage(toolCall.id(),
- this.syncToolCall.call(toolCall.name(), toolCall.arguments(), toolContext)))
- .collect(Collectors.collectingAndThen(Collectors.toList(), ChatMessages::from));
+ return ChatMessages.from(this.callTools(toolCalls, ctx)
+ .stream()
+ .map(message -> (ChatMessage) FlatChatMessage.from(message))
+ .collect(Collectors.toList()));
}
@Override
@@ -87,18 +101,53 @@ public AiProcessFlow buildFlow() {
private ChatMessage handleTool(ChatMessage input, StateContext ctx) {
Validation.notNull(ctx, "The state context cannot be null.");
Validation.notNull(input, "The input message cannot be null.");
-
- Map toolContext = ObjectUtils.getIfNull(ctx.getState(AippConst.TOOL_CONTEXT_KEY),
- Collections::emptyMap);
ChatMessages lastRequest = ctx.getState(this.agentMsgKey);
lastRequest.add(input);
- input.toolCalls().forEach(toolCall -> {
- lastRequest.add(FlatChatMessage.from(new ToolMessage(toolCall.id(),
- this.syncToolCall.call(toolCall.name(), toolCall.arguments(), toolContext))));
- });
+ lastRequest.addAll(this.callTools(input.toolCalls(), ctx));
return input;
}
+ private List callTools(List toolCalls, StateContext ctx) {
+ if (CollectionUtils.isEmpty(toolCalls)) {
+ return Collections.emptyList();
+ }
+ List tools = ctx.getState(AippConst.TOOLS_KEY);
+ Validation.notEmpty(tools, "Missing tool detected during call.");
+ Map toolsMap = tools.stream().collect(Collectors.toMap(ToolInfo::name, Function.identity()));
+ Map toolContext =
+ ObjectUtils.getIfNull(ctx.getState(AippConst.TOOL_CONTEXT_KEY), Collections::emptyMap);
+ return toolCalls.stream()
+ .map(toolCall -> this.callTool(toolCall, toolsMap, toolContext))
+ .collect(Collectors.toList());
+ }
+
+ private ChatMessage callTool(ToolCall toolCall, Map toolsMap, Map toolContext) {
+ ToolInfo toolInfo = toolsMap.get(toolCall.name());
+ if (toolInfo == null) {
+ throw new IllegalStateException(String.format("The tool call's tool is not exist. [toolName=%s]",
+ toolCall.name()));
+ }
+ Map extensions = Validation.notNull(toolInfo.extensions(),
+ "The tool call's extension is not exist. [toolName={0}]", toolCall.name());
+ String toolRealName = Validation.notBlank(ObjectUtils.cast(extensions.get(AippConst.TOOL_REAL_NAME)),
+ "Can not find the tool real name. [toolName={0}]",
+ toolCall.name());
+ Map mcpServerConfig = ObjectUtils.cast(extensions.get(AippConst.MCP_SERVER_KEY));
+ if (mcpServerConfig != null) {
+ String url = Validation.notBlank(ObjectUtils.cast(mcpServerConfig.get(AippConst.MCP_SERVER_URL_KEY)),
+ "The mcp url should not be empty.");
+ try (McpClient mcpClient = this.mcpClientFactory.create(McpUtils.getBaseUrl(url),
+ McpUtils.getSseEndpoint(url))) {
+ mcpClient.initialize();
+ Object result = mcpClient.callTool(toolRealName, JSONObject.parseObject(toolCall.arguments()));
+ return new ToolMessage(toolCall.id(), JSON.toJSONString(result));
+ } catch (IOException exception) {
+ throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, exception.getMessage());
+ }
+ }
+ return new ToolMessage(toolCall.id(), this.syncToolCall.call(toolRealName, toolCall.arguments(), toolContext));
+ }
+
private ChatMessages getAgentMsg(ChatMessage input, StateContext ctx) {
Validation.notNull(ctx, "The state context cannot be null.");
return ctx.getState(this.agentMsgKey);
diff --git a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java
new file mode 100644
index 0000000000..ff4507c2a4
--- /dev/null
+++ b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java
@@ -0,0 +1,183 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
+ * This file is a part of the ModelEngine Project.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+package modelengine.fit.jober.aipp.fel;
+
+import modelengine.fel.core.chat.ChatMessage;
+import modelengine.fel.core.chat.ChatModel;
+import modelengine.fel.core.chat.ChatOption;
+import modelengine.fel.core.chat.Prompt;
+import modelengine.fel.core.chat.support.AiMessage;
+import modelengine.fel.core.chat.support.ChatMessages;
+import modelengine.fel.core.chat.support.HumanMessage;
+import modelengine.fel.core.tool.ToolCall;
+import modelengine.fel.core.tool.ToolInfo;
+import modelengine.fel.engine.flows.AiProcessFlow;
+import modelengine.fel.tool.mcp.client.McpClient;
+import modelengine.fel.tool.mcp.client.McpClientFactory;
+import modelengine.fit.jade.tool.SyncToolCall;
+import modelengine.fit.jober.aipp.constants.AippConst;
+import modelengine.fitframework.flowable.Choir;
+import modelengine.fitframework.util.MapBuilder;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link WaterFlowAgent} 的测试。
+ */
+@ExtendWith(MockitoExtension.class)
+class WaterFlowAgentTest {
+ private static final String TEXT_STEP = "textStep";
+ private static final String TOOL_CALL_STEP = "toolCallStep";
+
+ @Mock
+ private SyncToolCall syncToolCall;
+ @Mock
+ private ChatModel chatModel;
+ @Mock
+ private McpClientFactory mcpClientFactory;
+
+ @Test
+ void shouldGetResultWhenRunFlowGivenNoToolCall() {
+ WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.syncToolCall, this.chatModel, this.mcpClientFactory);
+
+ String expectResult = "0123";
+ doAnswer(invocation -> Choir.create(emitter -> {
+ for (int i = 0; i < 4; i++) {
+ emitter.emit(new AiMessage(String.valueOf(i)));
+ }
+ emitter.complete();
+ })).when(chatModel).generate(any(), any());
+
+ AiProcessFlow flow = waterFlowAgent.buildFlow();
+ ChatMessage result = flow.converse()
+ .bind(ChatOption.custom().build())
+ .offer(ChatMessages.from(new HumanMessage("hi"))).await();
+
+ assertEquals(expectResult, result.text());
+ }
+
+ @Test
+ void shouldGetResultWhenRunFlowGivenStoreToolCall() {
+ WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.syncToolCall, this.chatModel, this.mcpClientFactory);
+
+ String expectResult = "tool result:0123";
+ String realName = "realName";
+ ToolInfo toolInfo = buildToolInfo(realName);
+ ToolCall toolCall = ToolCall.custom().id("id").name(toolInfo.name()).arguments("{}").build();
+ List toolCalls = Collections.singletonList(toolCall);
+ AtomicReference step = new AtomicReference<>(TOOL_CALL_STEP);
+ doAnswer(invocation -> {
+ Prompt prompt = invocation.getArgument(0);
+ Choir