From 0443b4e0349190988fb5ca5415158d07e206ccfb Mon Sep 17 00:00:00 2001 From: loveTsong <271667068@qq.com> Date: Mon, 2 Mar 2026 15:24:47 +0800 Subject: [PATCH 1/7] [app-builder] refactor: replace MCP client with LangChain4j MCP module --- app-builder/plugins/aipp-plugin/pom.xml | 4 +- .../jober/aipp/fel/FelComponentConfig.java | 6 +- .../fit/jober/aipp/fel/WaterFlowAgent.java | 29 +++--- .../fit/jober/aipp/fitable/LlmComponent.java | 44 +++++---- .../jober/aipp/util/LangChain4jMcpClient.java | 92 +++++++++++++++++++ .../jober/aipp/fel/WaterFlowAgentTest.java | 45 +++++---- .../jober/aipp/fitable/LlmComponentTest.java | 73 ++++++++------- common/dependency/pom.xml | 7 +- 8 files changed, 203 insertions(+), 97 deletions(-) create mode 100644 app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClient.java diff --git a/app-builder/plugins/aipp-plugin/pom.xml b/app-builder/plugins/aipp-plugin/pom.xml index 1eeafc765c..a87acc058b 100644 --- a/app-builder/plugins/aipp-plugin/pom.xml +++ b/app-builder/plugins/aipp-plugin/pom.xml @@ -134,8 +134,8 @@ waterflow-graph-service - org.fitframework.fel - tool-mcp-client-service + dev.langchain4j + langchain4j-mcp diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java index f0830c87c1..8baf99fe2e 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java @@ -9,7 +9,6 @@ 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.fel.tool.service.ToolExecuteService; import modelengine.fit.jober.aipp.constants.AippConst; import modelengine.fitframework.annotation.Bean; @@ -29,12 +28,11 @@ public class FelComponentConfig { * * @param toolExecuteService 表示工具调用服务的 {@link ToolExecuteService}。 * @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 ToolExecuteService toolExecuteService, ChatModel chatModel, McpClientFactory mcpClientFactory) { - return new WaterFlowAgent(toolExecuteService, chatModel, mcpClientFactory); + public AbstractAgent getWaterFlowAgent(@Fit ToolExecuteService toolExecuteService, ChatModel chatModel) { + return new WaterFlowAgent(toolExecuteService, chatModel); } } \ No newline at end of file diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java index b8fdb37fa1..bbbb30e72f 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java @@ -6,9 +6,6 @@ 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; @@ -22,20 +19,19 @@ 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.fel.tool.service.ToolExecuteService; 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.LangChain4jMcpClient; 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.log.Logger; 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; @@ -54,23 +50,24 @@ public class WaterFlowAgent extends AbstractAgent { private final String agentMsgKey; private final ToolExecuteService toolExecuteService; - private final McpClientFactory mcpClientFactory; + private java.util.function.Function mcpClientFactory = LangChain4jMcpClient::new; /** * {@link WaterFlowAgent} 的构造方法。 * * @param toolExecuteService 表示工具调用服务的 {@link ToolExecuteService}。 * @param chatStreamModel 表示流式对话大模型的 {@link ChatModel}。 - * @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。 */ - public WaterFlowAgent(@Fit ToolExecuteService toolExecuteService, ChatModel chatStreamModel, - McpClientFactory mcpClientFactory) { + public WaterFlowAgent(@Fit ToolExecuteService toolExecuteService, ChatModel chatStreamModel) { super(new ChatFlowModel(chatStreamModel, null)); this.toolExecuteService = Validation.notNull(toolExecuteService, "The tool execute service cannot be null."); - this.mcpClientFactory = Validation.notNull(mcpClientFactory, "The mcp client factory cannot be null."); this.agentMsgKey = AGENT_MSG_KEY; } + void setMcpClientFactory(java.util.function.Function mcpClientFactory) { + this.mcpClientFactory = mcpClientFactory; + } + @Override protected Prompt doToolCall(List toolCalls, StateContext ctx) { Validation.notNull(ctx, "The state context cannot be null."); @@ -136,12 +133,10 @@ private ChatMessage callTool(ToolCall toolCall, Map toolsMap, 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) { + try (LangChain4jMcpClient mcpClient = this.mcpClientFactory.apply(url)) { + String result = mcpClient.callTool(toolRealName, toolCall.arguments()); + return new ToolMessage(toolCall.id(), result); + } catch (Exception exception) { throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, exception.getMessage()); } } diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java index 476f76791e..18ab2a7383 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java @@ -20,13 +20,10 @@ 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.domains.appversion.service.AppVersionService; import modelengine.fit.jober.aipp.enums.MetaInstStatusEnum; -import modelengine.fit.jober.aipp.util.McpUtils; +import modelengine.fit.jober.aipp.util.LangChain4jMcpClient; import modelengine.fitframework.inspection.Validation; import modelengine.jade.store.service.ToolService; import modelengine.fit.jade.aipp.formatter.OutputFormatterChain; @@ -70,6 +67,9 @@ import modelengine.fitframework.util.StringUtils; import modelengine.fitframework.util.UuidUtils; +import dev.langchain4j.model.chat.request.json.JsonObjectSchema; +import dev.langchain4j.agent.tool.ToolSpecification; + import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -113,7 +113,6 @@ public class LlmComponent implements FlowableService { private final AippModelCenter aippModelCenter; private final PromptBuilderChain promptBuilderChain; private final AppTaskInstanceService appTaskInstanceService; - private final McpClientFactory mcpClientFactory; private final OutputFormatterChain formatterChain; private final AppVersionService appVersionService; @@ -129,7 +128,6 @@ 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, @@ -141,7 +139,7 @@ public LlmComponent(FlowInstanceService flowInstanceService, PromptBuilderChain promptBuilderChain, AppTaskInstanceService appTaskInstanceService, OutputFormatterChain formatterChain, - McpClientFactory mcpClientFactory, AppVersionService appVersionService) { + AppVersionService appVersionService) { this.flowInstanceService = flowInstanceService; this.toolService = toolService; this.aippLogService = aippLogService; @@ -157,7 +155,6 @@ public LlmComponent(FlowInstanceService flowInstanceService, .close(); this.promptBuilderChain = promptBuilderChain; this.appTaskInstanceService = appTaskInstanceService; - this.mcpClientFactory = notNull(mcpClientFactory, "The mcp client factory cannot be null."); this.formatterChain = formatterChain; this.appVersionService = appVersionService; } @@ -482,12 +479,12 @@ private List buildMcpToolInfos(Map mcpServersConfig) { String url = Validation.notBlank(ObjectUtils.cast(serverConfig.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(); - List tools = mcpClient.getTools(); - result.addAll(tools.stream().map(tool -> buildMcpToolInfo(serverName, tool, serverConfig)).toList()); - } catch (IOException exception) { + try (LangChain4jMcpClient mcpClient = new LangChain4jMcpClient(url)) { + List tools = mcpClient.getTools(); + result.addAll(tools.stream() + .map(tool -> buildMcpToolInfo(serverName, tool, serverConfig)) + .toList()); + } catch (Exception exception) { throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, exception.getMessage()); } }); @@ -515,14 +512,23 @@ private ToolInfo buildToolInfo(ToolData toolData) { .build(); } - private static ToolInfo buildMcpToolInfo(String serverName, Tool tool, Map serverConfig) { + private static ToolInfo buildMcpToolInfo(String serverName, + ToolSpecification tool, Map serverConfig) { + JsonObjectSchema toolParams = tool.parameters(); + Map parametersMap = new HashMap<>(); + if (toolParams != null) { + parametersMap.put("type", "object"); + parametersMap.put("properties", toolParams.properties()); + parametersMap.put("required", toolParams.required()); + } + return ToolInfo.custom() - .name(buildUniqueToolName(AippConst.MCP_SERVER_TYPE, serverName, tool.getName())) - .description(tool.getDescription()) - .parameters(tool.getInputSchema()) + .name(buildUniqueToolName(AippConst.MCP_SERVER_TYPE, serverName, tool.name())) + .description(tool.description()) + .parameters(parametersMap) .extensions(MapBuilder.get() .put(AippConst.MCP_SERVER_KEY, serverConfig) - .put(AippConst.TOOL_REAL_NAME, tool.getName()) + .put(AippConst.TOOL_REAL_NAME, tool.name()) .build()) .build(); } diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClient.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClient.java new file mode 100644 index 0000000000..1ddc13b271 --- /dev/null +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClient.java @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * 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.util; + +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.mcp.client.DefaultMcpClient; +import dev.langchain4j.mcp.client.McpClient; +import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport; + +import modelengine.fit.jober.aipp.common.exception.AippErrCode; +import modelengine.fit.jober.aipp.common.exception.AippException; +import modelengine.fitframework.util.StringUtils; + +import java.util.List; + +/** + * LangChain4jMcpClient is a client for calling ModelEngine's MCP server. + * + * @author songyongtan + * @since 2026-03-01 + */ +public class LangChain4jMcpClient implements AutoCloseable { + private final McpClient mcpClient; + private final String url; + + /** + * 构造函数,用于初始化LangChain4jMcpClient对象。 + * + * @param url MCP服务器的URL,格式为http://host:port + */ + public LangChain4jMcpClient(String url) { + this.url = url; + + HttpMcpTransport transport = new HttpMcpTransport.Builder() + .sseUrl(url) + .build(); + + this.mcpClient = new DefaultMcpClient.Builder() + .transport(transport) + .build(); + } + + /** + * 获取MCP服务器上注册的所有工具。 + * + * @return 包含所有工具规范的列表 + */ + public List getTools() { + try { + return this.mcpClient.listTools(); + } catch (Exception e) { + throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, + StringUtils.format("Failed to get tools from MCP server. [url={0}]", this.url), e); + } + } + + /** + * 调用MCP服务器上的指定工具。 + * + * @param toolName 要调用的工具名称 + * @param arguments 工具调用的参数,格式为JSON字符串 + * @return 工具调用的结果,格式为JSON字符串 + */ + public String callTool(String toolName, String arguments) { + try { + ToolExecutionRequest request = ToolExecutionRequest.builder() + .name(toolName) + .arguments(arguments) + .build(); + return this.mcpClient.executeTool(request); + } catch (Exception e) { + throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, + StringUtils.format("Failed to call tool. [toolName={0}, url={1}]", toolName, this.url), e); + } + } + + @Override + public void close() { + try { + this.mcpClient.close(); + } catch (Exception e) { + throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, + StringUtils.format("Failed to close MCP client. [url={0}]", this.url), e); + } + } +} diff --git a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java index 11a965d68e..6ee1a57677 100644 --- a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java +++ b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java @@ -16,10 +16,9 @@ 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.fel.tool.service.ToolExecuteService; import modelengine.fit.jober.aipp.constants.AippConst; +import modelengine.fit.jober.aipp.util.LangChain4jMcpClient; import modelengine.fitframework.flowable.Choir; import modelengine.fitframework.util.MapBuilder; @@ -27,7 +26,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockedConstruction; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.mockito.InjectMocks; +import modelengine.fit.jober.aipp.util.McpUtils; import java.util.Collections; import java.util.HashMap; @@ -37,16 +42,21 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mockConstruction; /** * {@link WaterFlowAgent} 的测试。 */ @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class WaterFlowAgentTest { private static final String TEXT_STEP = "textStep"; private static final String TOOL_CALL_STEP = "toolCallStep"; @@ -55,13 +65,11 @@ class WaterFlowAgentTest { private ToolExecuteService toolExecuteService; @Mock private ChatModel chatModel; - @Mock - private McpClientFactory mcpClientFactory; @Test void shouldGetResultWhenRunFlowGivenNoToolCall() { WaterFlowAgent waterFlowAgent = - new WaterFlowAgent(this.toolExecuteService, this.chatModel, this.mcpClientFactory); + new WaterFlowAgent(this.toolExecuteService, this.chatModel); String expectResult = "0123"; doAnswer(invocation -> Choir.create(emitter -> { @@ -81,7 +89,7 @@ void shouldGetResultWhenRunFlowGivenNoToolCall() { @Test void shouldGetResultWhenRunFlowGivenStoreToolCall() { - WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.toolExecuteService, this.chatModel, this.mcpClientFactory); + WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.toolExecuteService, this.chatModel); String expectResult = "tool result:0123"; String realName = "realName"; @@ -105,19 +113,17 @@ void shouldGetResultWhenRunFlowGivenStoreToolCall() { .bind(AippConst.TOOLS_KEY, Collections.singletonList(toolInfo)) .offer(ChatMessages.from(new HumanMessage("hi"))).await(); - verify(this.mcpClientFactory, times(0)).create(any(), any()); assertEquals(expectResult, result.text()); } @Test void shouldGetResultWhenRunFlowGivenMcpToolCall() { - WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.toolExecuteService, this.chatModel, this.mcpClientFactory); + WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.toolExecuteService, this.chatModel); - String expectResult = "\"tool result:\"0123"; + String expectResult = "tool result:0123"; String realName = "realName"; - String baseUrl = "http://localhost"; - String sseEndpoint = "/sse"; - ToolInfo toolInfo = buildMcpToolInfo(realName, baseUrl, sseEndpoint); + String url = "http://localhost/sse"; + ToolInfo toolInfo = buildMcpToolInfo(realName, url); ToolCall toolCall = ToolCall.custom().id("id").name(toolInfo.name()).arguments("{}").build(); List toolCalls = Collections.singletonList(toolCall); AtomicReference step = new AtomicReference<>(TOOL_CALL_STEP); @@ -128,10 +134,13 @@ void shouldGetResultWhenRunFlowGivenMcpToolCall() { return result; }).when(chatModel).generate(any(), any()); Map toolContext = MapBuilder.get().put("key", "value").build(); - McpClient mcpClient = mock(McpClient.class); - when(this.mcpClientFactory.create(baseUrl, sseEndpoint)).thenReturn(mcpClient); - when(mcpClient.callTool(realName, new HashMap<>())).thenReturn("tool result:"); - + + LangChain4jMcpClient mockMcpClient = mock(LangChain4jMcpClient.class); + when(mockMcpClient.callTool(realName, "{}")).thenReturn("tool result:"); + doNothing().when(mockMcpClient).close(); + + waterFlowAgent.setMcpClientFactory(clientUrl -> mockMcpClient); + AiProcessFlow flow = waterFlowAgent.buildFlow(); ChatMessage result = flow.converse() .bind(ChatOption.custom().build()) @@ -169,7 +178,7 @@ private static ToolInfo buildToolInfo(String realName) { .build(); } - private static ToolInfo buildMcpToolInfo(String realName, String baseUrl, String sseEndpoint) { + private static ToolInfo buildMcpToolInfo(String realName, String url) { return ToolInfo.custom() .name("tool1") .description("desc") @@ -177,7 +186,7 @@ private static ToolInfo buildMcpToolInfo(String realName, String baseUrl, Strin .extensions(MapBuilder.get() .put(AippConst.TOOL_REAL_NAME, realName) .put(AippConst.MCP_SERVER_KEY, - MapBuilder.get().put(AippConst.MCP_SERVER_URL_KEY, baseUrl + sseEndpoint).build()) + MapBuilder.get().put(AippConst.MCP_SERVER_URL_KEY, url).build()) .build()) .build(); } diff --git a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java index f1b011cd1b..139dc63025 100644 --- a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java +++ b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java @@ -9,18 +9,18 @@ import static modelengine.fit.jober.aipp.TestUtils.mockFailAsyncJob; import static modelengine.fit.jober.aipp.TestUtils.mockResumeFlow; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mockConstruction; +import dev.langchain4j.model.chat.request.json.JsonObjectSchema; import modelengine.fel.core.chat.ChatOption; import modelengine.fel.core.tool.ToolInfo; import modelengine.fel.engine.operators.models.ChatFlowModel; import modelengine.fel.tool.ToolSchema; -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.fel.tool.service.ToolExecuteService; import modelengine.fit.jade.aipp.formatter.OutputFormatterChain; @@ -40,6 +40,7 @@ import modelengine.fit.jober.aipp.service.AippLogService; import modelengine.fit.jober.aipp.service.AippLogStreamService; import modelengine.fit.jober.aipp.util.JsonUtils; +import modelengine.fit.jober.aipp.util.LangChain4jMcpClient; import modelengine.fit.waterflow.domain.context.StateContext; import modelengine.fel.core.chat.ChatMessage; @@ -61,11 +62,14 @@ import modelengine.fitframework.util.ObjectUtils; import modelengine.jade.store.service.ToolService; +import dev.langchain4j.agent.tool.ToolSpecification; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -104,8 +108,6 @@ public class LlmComponentTest { @Mock private AippModelCenter aippModelCenter; @Mock - private McpClientFactory mcpClientFactory; - @Mock private OutputFormatterChain formatterChain; static class PromptBuilderStub implements PromptBuilder { @@ -168,7 +170,7 @@ protected AiProcessFlow buildFlow() { } private AbstractAgent getWaterFlowAgent(ChatModel model) { - return new WaterFlowAgent(this.toolExecuteService, model, this.mcpClientFactory); + return new WaterFlowAgent(this.toolExecuteService, model); } private ChatModel buildChatStreamModel(String exceptionMsg) { @@ -242,8 +244,7 @@ void shouldFailedWhenNoTool() throws InterruptedException { this.aippModelCenter, this.promptBuilderChain, this.appTaskInstanceService, - this.formatterChain, - this.mcpClientFactory, null); + this.formatterChain, null); // mock CountDownLatch countDownLatch = mockFailAsyncJob(flowInstanceService); @@ -372,30 +373,35 @@ void shouldGetMcpToolsWhenHandleTaskGivenMcpServersConfig() throws InterruptedEx Map mcpServerInfo = MapBuilder.get().put("url", url).build(); String serverName = "server1"; businessData.put(AippConst.MCP_SERVERS_KEY, MapBuilder.get().put(serverName, mcpServerInfo).build()); - McpClient mcpCLient = Mockito.mock(McpClient.class); - doNothing().when(mcpCLient).initialize(); - Tool tool = new Tool(); - tool.setName("tool1"); - tool.setDescription("desc"); - tool.setInputSchema(new HashMap<>()); - when(mcpCLient.getTools()).thenReturn(Arrays.asList(tool)); - when(this.mcpClientFactory.create(baseUrl, sseEndpoint)).thenReturn(mcpCLient); - - // when - llmComponent.handleTask(TestUtils.buildFlowDataWithExtraConfig(businessData, null)); - - // then - countDownLatch.await(); - ArgumentCaptor chatOptionCaptor = ArgumentCaptor.forClass(ChatOption.class); - Mockito.verify(chatModel).generate(any(), chatOptionCaptor.capture()); - ChatOption capturedChatOptions = chatOptionCaptor.getValue(); - Assertions.assertTrue(CollectionUtils.isNotEmpty(capturedChatOptions.tools())); - Assertions.assertEquals(1, capturedChatOptions.tools().size()); - ToolInfo toolInfo = capturedChatOptions.tools().get(0); - Assertions.assertEquals("mcp_" + serverName + "_" + tool.getName(), toolInfo.name()); - Assertions.assertEquals(tool.getDescription(), toolInfo.description()); - Assertions.assertEquals(tool.getInputSchema(), toolInfo.parameters()); - Assertions.assertEquals(mcpServerInfo, toolInfo.extensions().get(AippConst.MCP_SERVER_KEY)); + + ToolSpecification tool = ToolSpecification.builder() + .name("tool1") + .description("desc") + .parameters(JsonObjectSchema.builder() + .addProperties(new HashMap<>()) + .required(List.of()) + .build()) + .build(); + + try (MockedConstruction mockedConstruction = mockConstruction(LangChain4jMcpClient.class, + (mock, context) -> { + when(mock.getTools()).thenReturn(Arrays.asList(tool)); + })) { + // when + llmComponent.handleTask(TestUtils.buildFlowDataWithExtraConfig(businessData, null)); + + // then + countDownLatch.await(); + ArgumentCaptor chatOptionCaptor = ArgumentCaptor.forClass(ChatOption.class); + Mockito.verify(chatModel).generate(any(), chatOptionCaptor.capture()); + ChatOption capturedChatOptions = chatOptionCaptor.getValue(); + Assertions.assertTrue(CollectionUtils.isNotEmpty(capturedChatOptions.tools())); + Assertions.assertEquals(1, capturedChatOptions.tools().size()); + ToolInfo toolInfo = capturedChatOptions.tools().get(0); + Assertions.assertEquals("mcp_" + serverName + "_" + tool.name(), toolInfo.name()); + Assertions.assertEquals(tool.description(), toolInfo.description()); + Assertions.assertEquals(mcpServerInfo, toolInfo.extensions().get(AippConst.MCP_SERVER_KEY)); + } } private LlmComponent getLlmComponent(final AbstractAgent agent) { @@ -408,8 +414,7 @@ private LlmComponent getLlmComponent(final AbstractAgent agent) { this.aippModelCenter, this.promptBuilderChain, this.appTaskInstanceService, - this.formatterChain, - this.mcpClientFactory, null); + this.formatterChain, null); } private void prepareModel() { diff --git a/common/dependency/pom.xml b/common/dependency/pom.xml index 53b56d2e51..437e8783f2 100644 --- a/common/dependency/pom.xml +++ b/common/dependency/pom.xml @@ -59,6 +59,7 @@ 1.12.468 5.7.1 1.1.0 + 1.0.0-beta1 3.22.0 @@ -230,9 +231,9 @@ ${fel.version} - org.fitframework.fel - tool-mcp-client-service - ${fel.version} + dev.langchain4j + langchain4j-mcp + ${langchain4j.version} From 11b5e347bdeb31fb544c7e81fd5bebb7e23ff81e Mon Sep 17 00:00:00 2001 From: loveTsong <271667068@qq.com> Date: Mon, 2 Mar 2026 15:43:55 +0800 Subject: [PATCH 2/7] [aipp-plugin] refactor: use factory pattern for MCP client injection in tests --- .../fit/jober/aipp/fitable/LlmComponent.java | 7 +++- .../jober/aipp/fitable/LlmComponentTest.java | 42 +++++++++---------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java index 18ab2a7383..3c11a7adab 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java @@ -115,6 +115,7 @@ public class LlmComponent implements FlowableService { private final AppTaskInstanceService appTaskInstanceService; private final OutputFormatterChain formatterChain; private final AppVersionService appVersionService; + private java.util.function.Function mcpClientFactory = LangChain4jMcpClient::new; /** * 大模型节点构造器,内部通过提供的 agent 和 tool 构建智能体工作流。 @@ -479,7 +480,7 @@ private List buildMcpToolInfos(Map mcpServersConfig) { String url = Validation.notBlank(ObjectUtils.cast(serverConfig.get(AippConst.MCP_SERVER_URL_KEY)), "The mcp url should not be empty."); - try (LangChain4jMcpClient mcpClient = new LangChain4jMcpClient(url)) { + try (LangChain4jMcpClient mcpClient = this.mcpClientFactory.apply(url)) { List tools = mcpClient.getTools(); result.addAll(tools.stream() .map(tool -> buildMcpToolInfo(serverName, tool, serverConfig)) @@ -491,6 +492,10 @@ private List buildMcpToolInfos(Map mcpServersConfig) { return result; } + void setMcpClientFactory(java.util.function.Function mcpClientFactory) { + this.mcpClientFactory = mcpClientFactory; + } + private List buildToolInfos(List skillNameList) { return skillNameList.stream() .map(this.toolService::getTool) diff --git a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java index 139dc63025..1c8c86ccaa 100644 --- a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java +++ b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java @@ -12,9 +12,9 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.mockConstruction; import dev.langchain4j.model.chat.request.json.JsonObjectSchema; import modelengine.fel.core.chat.ChatOption; @@ -69,7 +69,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.MockedConstruction; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -383,25 +382,26 @@ void shouldGetMcpToolsWhenHandleTaskGivenMcpServersConfig() throws InterruptedEx .build()) .build(); - try (MockedConstruction mockedConstruction = mockConstruction(LangChain4jMcpClient.class, - (mock, context) -> { - when(mock.getTools()).thenReturn(Arrays.asList(tool)); - })) { - // when - llmComponent.handleTask(TestUtils.buildFlowDataWithExtraConfig(businessData, null)); - - // then - countDownLatch.await(); - ArgumentCaptor chatOptionCaptor = ArgumentCaptor.forClass(ChatOption.class); - Mockito.verify(chatModel).generate(any(), chatOptionCaptor.capture()); - ChatOption capturedChatOptions = chatOptionCaptor.getValue(); - Assertions.assertTrue(CollectionUtils.isNotEmpty(capturedChatOptions.tools())); - Assertions.assertEquals(1, capturedChatOptions.tools().size()); - ToolInfo toolInfo = capturedChatOptions.tools().get(0); - Assertions.assertEquals("mcp_" + serverName + "_" + tool.name(), toolInfo.name()); - Assertions.assertEquals(tool.description(), toolInfo.description()); - Assertions.assertEquals(mcpServerInfo, toolInfo.extensions().get(AippConst.MCP_SERVER_KEY)); - } + LangChain4jMcpClient mockMcpClient = mock(LangChain4jMcpClient.class); + when(mockMcpClient.getTools()).thenReturn(Arrays.asList(tool)); + doNothing().when(mockMcpClient).close(); + + llmComponent.setMcpClientFactory(clientUrl -> mockMcpClient); + + // when + llmComponent.handleTask(TestUtils.buildFlowDataWithExtraConfig(businessData, null)); + + // then + countDownLatch.await(); + ArgumentCaptor chatOptionCaptor = ArgumentCaptor.forClass(ChatOption.class); + Mockito.verify(chatModel).generate(any(), chatOptionCaptor.capture()); + ChatOption capturedChatOptions = chatOptionCaptor.getValue(); + Assertions.assertTrue(CollectionUtils.isNotEmpty(capturedChatOptions.tools())); + Assertions.assertEquals(1, capturedChatOptions.tools().size()); + ToolInfo toolInfo = capturedChatOptions.tools().get(0); + Assertions.assertEquals("mcp_" + serverName + "_" + tool.name(), toolInfo.name()); + Assertions.assertEquals(tool.description(), toolInfo.description()); + Assertions.assertEquals(mcpServerInfo, toolInfo.extensions().get(AippConst.MCP_SERVER_KEY)); } private LlmComponent getLlmComponent(final AbstractAgent agent) { From b42b3af11450b5d024af3a38ad0c07f63ff0ac13 Mon Sep 17 00:00:00 2001 From: loveTsong <271667068@qq.com> Date: Mon, 2 Mar 2026 15:59:44 +0800 Subject: [PATCH 3/7] [aipp-plugin] refactor: use McpClientFactory interface for dependency injection --- .../fit/jober/aipp/fel/FelComponentConfig.java | 7 +++++-- .../fit/jober/aipp/fel/WaterFlowAgent.java | 10 +++++----- .../fit/jober/aipp/fitable/LlmComponent.java | 14 ++++++++------ .../aipp/util/DefaultMcpClientFactory.java | 11 +++++++++++ .../fit/jober/aipp/util/McpClientFactory.java | 8 ++++++++ .../fit/jober/aipp/fel/WaterFlowAgentTest.java | 16 ++++++++-------- .../jober/aipp/fitable/LlmComponentTest.java | 17 ++++++++++++----- 7 files changed, 57 insertions(+), 26 deletions(-) create mode 100644 app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java create mode 100644 app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java index 8baf99fe2e..ed55d0ef48 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java @@ -11,6 +11,7 @@ import modelengine.fel.engine.operators.patterns.AbstractAgent; import modelengine.fel.tool.service.ToolExecuteService; import modelengine.fit.jober.aipp.constants.AippConst; +import modelengine.fit.jober.aipp.util.McpClientFactory; import modelengine.fitframework.annotation.Bean; import modelengine.fitframework.annotation.Component; import modelengine.fitframework.annotation.Fit; @@ -28,11 +29,13 @@ public class FelComponentConfig { * * @param toolExecuteService 表示工具调用服务的 {@link ToolExecuteService}。 * @param chatModel 表示模型流式服务的 {@link ChatModel}。 + * @param mcpClientFactory 表示 MCP 客户端工厂的 {@link McpClientFactory}。 * @return 返回 WaterFlow 场景的 Agent 服务的 {@link AbstractAgent}{@code <}{@link Prompt}{@code , * }{@link Prompt}{@code >}。 */ @Bean(AippConst.WATER_FLOW_AGENT_BEAN) - public AbstractAgent getWaterFlowAgent(@Fit ToolExecuteService toolExecuteService, ChatModel chatModel) { - return new WaterFlowAgent(toolExecuteService, chatModel); + public AbstractAgent getWaterFlowAgent(@Fit ToolExecuteService toolExecuteService, ChatModel chatModel, + McpClientFactory mcpClientFactory) { + return new WaterFlowAgent(toolExecuteService, chatModel, mcpClientFactory); } } \ No newline at end of file diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java index bbbb30e72f..3bbb5ea8e3 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java @@ -24,6 +24,7 @@ import modelengine.fit.jober.aipp.common.exception.AippException; import modelengine.fit.jober.aipp.constants.AippConst; import modelengine.fit.jober.aipp.util.LangChain4jMcpClient; +import modelengine.fit.jober.aipp.util.McpClientFactory; import modelengine.fit.jober.aipp.util.McpUtils; import modelengine.fit.waterflow.domain.context.StateContext; import modelengine.fitframework.annotation.Fit; @@ -50,21 +51,20 @@ public class WaterFlowAgent extends AbstractAgent { private final String agentMsgKey; private final ToolExecuteService toolExecuteService; - private java.util.function.Function mcpClientFactory = LangChain4jMcpClient::new; + private final McpClientFactory mcpClientFactory; /** * {@link WaterFlowAgent} 的构造方法。 * * @param toolExecuteService 表示工具调用服务的 {@link ToolExecuteService}。 * @param chatStreamModel 表示流式对话大模型的 {@link ChatModel}。 + * @param mcpClientFactory 表示 MCP 客户端工厂的 {@link McpClientFactory}。 */ - public WaterFlowAgent(@Fit ToolExecuteService toolExecuteService, ChatModel chatStreamModel) { + public WaterFlowAgent(@Fit ToolExecuteService toolExecuteService, ChatModel chatStreamModel, + McpClientFactory mcpClientFactory) { super(new ChatFlowModel(chatStreamModel, null)); this.toolExecuteService = Validation.notNull(toolExecuteService, "The tool execute service cannot be null."); this.agentMsgKey = AGENT_MSG_KEY; - } - - void setMcpClientFactory(java.util.function.Function mcpClientFactory) { this.mcpClientFactory = mcpClientFactory; } diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java index 3c11a7adab..9487076a32 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java @@ -24,6 +24,7 @@ import modelengine.fit.jober.aipp.domains.appversion.service.AppVersionService; import modelengine.fit.jober.aipp.enums.MetaInstStatusEnum; import modelengine.fit.jober.aipp.util.LangChain4jMcpClient; +import modelengine.fit.jober.aipp.util.McpClientFactory; import modelengine.fitframework.inspection.Validation; import modelengine.jade.store.service.ToolService; import modelengine.fit.jade.aipp.formatter.OutputFormatterChain; @@ -115,7 +116,7 @@ public class LlmComponent implements FlowableService { private final AppTaskInstanceService appTaskInstanceService; private final OutputFormatterChain formatterChain; private final AppVersionService appVersionService; - private java.util.function.Function mcpClientFactory = LangChain4jMcpClient::new; + private final McpClientFactory mcpClientFactory; /** * 大模型节点构造器,内部通过提供的 agent 和 tool 构建智能体工作流。 @@ -129,6 +130,9 @@ public class LlmComponent implements FlowableService { * @param aippModelCenter 表示模型中心的 {@link AippModelCenter}。 * @param promptBuilderChain 表示提示器构造器职责链的 {@link PromptBuilderChain}。 * @param appTaskInstanceService 表示任务实例服务的 {@link AppTaskInstanceService}。 + * @param formatterChain 表示输出格式化器链的 {@link OutputFormatterChain}。 + * @param appVersionService 表示应用版本服务的 {@link AppVersionService}。 + * @param mcpClientFactory 表示 MCP 客户端工厂的 {@link McpClientFactory}。 */ public LlmComponent(FlowInstanceService flowInstanceService, @Fit ToolService toolService, @@ -140,13 +144,15 @@ public LlmComponent(FlowInstanceService flowInstanceService, PromptBuilderChain promptBuilderChain, AppTaskInstanceService appTaskInstanceService, OutputFormatterChain formatterChain, - AppVersionService appVersionService) { + AppVersionService appVersionService, + McpClientFactory mcpClientFactory) { this.flowInstanceService = flowInstanceService; this.toolService = toolService; this.aippLogService = aippLogService; this.aippLogStreamService = aippLogStreamService; this.serializer = notNull(serializer, "The serializer cannot be nul."); this.aippModelCenter = aippModelCenter; + this.mcpClientFactory = mcpClientFactory; // handleTask从入口开始处理,callback从agent node开始处理 this.agentFlow = AiFlows.create() @@ -492,10 +498,6 @@ private List buildMcpToolInfos(Map mcpServersConfig) { return result; } - void setMcpClientFactory(java.util.function.Function mcpClientFactory) { - this.mcpClientFactory = mcpClientFactory; - } - private List buildToolInfos(List skillNameList) { return skillNameList.stream() .map(this.toolService::getTool) diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java new file mode 100644 index 0000000000..87839fa7f8 --- /dev/null +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java @@ -0,0 +1,11 @@ +package modelengine.fit.jober.aipp.util; + +import modelengine.fitframework.annotation.Component; + +@Component +public class DefaultMcpClientFactory implements McpClientFactory { + @Override + public LangChain4jMcpClient apply(String url) { + return new LangChain4jMcpClient(url); + } +} diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java new file mode 100644 index 0000000000..e472e30d18 --- /dev/null +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java @@ -0,0 +1,8 @@ +package modelengine.fit.jober.aipp.util; + +import modelengine.fitframework.annotation.Component; + +import java.util.function.Function; + +public interface McpClientFactory extends Function { +} diff --git a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java index 6ee1a57677..c862a7e2ae 100644 --- a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java +++ b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java @@ -19,6 +19,7 @@ import modelengine.fel.tool.service.ToolExecuteService; import modelengine.fit.jober.aipp.constants.AippConst; import modelengine.fit.jober.aipp.util.LangChain4jMcpClient; +import modelengine.fit.jober.aipp.util.McpClientFactory; import modelengine.fitframework.flowable.Choir; import modelengine.fitframework.util.MapBuilder; @@ -26,8 +27,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.MockedConstruction; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @@ -50,7 +50,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.mockConstruction; /** * {@link WaterFlowAgent} 的测试。 @@ -69,7 +68,7 @@ class WaterFlowAgentTest { @Test void shouldGetResultWhenRunFlowGivenNoToolCall() { WaterFlowAgent waterFlowAgent = - new WaterFlowAgent(this.toolExecuteService, this.chatModel); + new WaterFlowAgent(this.toolExecuteService, this.chatModel, mock(McpClientFactory.class)); String expectResult = "0123"; doAnswer(invocation -> Choir.create(emitter -> { @@ -89,7 +88,7 @@ void shouldGetResultWhenRunFlowGivenNoToolCall() { @Test void shouldGetResultWhenRunFlowGivenStoreToolCall() { - WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.toolExecuteService, this.chatModel); + WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.toolExecuteService, this.chatModel, mock(McpClientFactory.class)); String expectResult = "tool result:0123"; String realName = "realName"; @@ -118,8 +117,6 @@ void shouldGetResultWhenRunFlowGivenStoreToolCall() { @Test void shouldGetResultWhenRunFlowGivenMcpToolCall() { - WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.toolExecuteService, this.chatModel); - String expectResult = "tool result:0123"; String realName = "realName"; String url = "http://localhost/sse"; @@ -139,7 +136,10 @@ void shouldGetResultWhenRunFlowGivenMcpToolCall() { when(mockMcpClient.callTool(realName, "{}")).thenReturn("tool result:"); doNothing().when(mockMcpClient).close(); - waterFlowAgent.setMcpClientFactory(clientUrl -> mockMcpClient); + McpClientFactory mockFactory = mock(McpClientFactory.class); + when(mockFactory.apply(any())).thenReturn(mockMcpClient); + + WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.toolExecuteService, this.chatModel, mockFactory); AiProcessFlow flow = waterFlowAgent.buildFlow(); ChatMessage result = flow.converse() diff --git a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java index 1c8c86ccaa..e196c678fc 100644 --- a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java +++ b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java @@ -41,6 +41,7 @@ import modelengine.fit.jober.aipp.service.AippLogStreamService; import modelengine.fit.jober.aipp.util.JsonUtils; import modelengine.fit.jober.aipp.util.LangChain4jMcpClient; +import modelengine.fit.jober.aipp.util.McpClientFactory; import modelengine.fit.waterflow.domain.context.StateContext; import modelengine.fel.core.chat.ChatMessage; @@ -169,7 +170,7 @@ protected AiProcessFlow buildFlow() { } private AbstractAgent getWaterFlowAgent(ChatModel model) { - return new WaterFlowAgent(this.toolExecuteService, model); + return new WaterFlowAgent(this.toolExecuteService, model, mock(McpClientFactory.class)); } private ChatModel buildChatStreamModel(String exceptionMsg) { @@ -243,7 +244,7 @@ void shouldFailedWhenNoTool() throws InterruptedException { this.aippModelCenter, this.promptBuilderChain, this.appTaskInstanceService, - this.formatterChain, null); + this.formatterChain, null, mock(McpClientFactory.class)); // mock CountDownLatch countDownLatch = mockFailAsyncJob(flowInstanceService); @@ -362,7 +363,6 @@ void shouldGetMcpToolsWhenHandleTaskGivenMcpServersConfig() throws InterruptedEx })).when(chatModel).generate(any(), any()); AbstractAgent agent = this.getWaterFlowAgent(chatModel); - LlmComponent llmComponent = getLlmComponent(agent); CountDownLatch countDownLatch = mockResumeFlow(flowInstanceService); Map businessData = buildLlmTestData(); @@ -386,7 +386,10 @@ void shouldGetMcpToolsWhenHandleTaskGivenMcpServersConfig() throws InterruptedEx when(mockMcpClient.getTools()).thenReturn(Arrays.asList(tool)); doNothing().when(mockMcpClient).close(); - llmComponent.setMcpClientFactory(clientUrl -> mockMcpClient); + McpClientFactory mockFactory = mock(McpClientFactory.class); + when(mockFactory.apply(any())).thenReturn(mockMcpClient); + + LlmComponent llmComponent = getLlmComponent(agent, mockFactory); // when llmComponent.handleTask(TestUtils.buildFlowDataWithExtraConfig(businessData, null)); @@ -405,6 +408,10 @@ void shouldGetMcpToolsWhenHandleTaskGivenMcpServersConfig() throws InterruptedEx } private LlmComponent getLlmComponent(final AbstractAgent agent) { + return getLlmComponent(agent, mock(McpClientFactory.class)); + } + + private LlmComponent getLlmComponent(final AbstractAgent agent, McpClientFactory mcpClientFactory) { return new LlmComponent(this.flowInstanceService, this.toolService, agent, @@ -414,7 +421,7 @@ private LlmComponent getLlmComponent(final AbstractAgent agent) { this.aippModelCenter, this.promptBuilderChain, this.appTaskInstanceService, - this.formatterChain, null); + this.formatterChain, null, mcpClientFactory); } private void prepareModel() { From 522f16dd5a77ae0d9169c08d39e8ba8fe85e4f23 Mon Sep 17 00:00:00 2001 From: loveTsong <271667068@qq.com> Date: Mon, 2 Mar 2026 17:19:48 +0800 Subject: [PATCH 4/7] [aipp-plugin] docs: add copyright and javadoc for McpClientFactory classes --- .../jober/aipp/util/DefaultMcpClientFactory.java | 12 ++++++++++++ .../fit/jober/aipp/util/McpClientFactory.java | 14 +++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java index 87839fa7f8..96ee14f5eb 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java @@ -1,7 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * 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.util; import modelengine.fitframework.annotation.Component; +/** + * MCP 客户端工厂的默认实现,使用 {@link LangChain4jMcpClient} 创建客户端实例。 + * + * @author songyongtan + * @since 2026-03-02 + */ @Component public class DefaultMcpClientFactory implements McpClientFactory { @Override diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java index e472e30d18..e77863926f 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java @@ -1,8 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * 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.util; -import modelengine.fitframework.annotation.Component; +import modelengine.fit.jober.aipp.util.LangChain4jMcpClient; import java.util.function.Function; +/** + * MCP 客户端工厂接口,用于创建 {@link LangChain4jMcpClient} 实例。 + * + * @author songyongtan + * @since 2026-03-02 + */ public interface McpClientFactory extends Function { } From e48fee06e082bc8d89f4455893dc1580f156ce7d Mon Sep 17 00:00:00 2001 From: loveTsong <271667068@qq.com> Date: Mon, 2 Mar 2026 20:30:03 +0800 Subject: [PATCH 5/7] [aipp-plugin] refactor: use custom create method instead of Function.apply --- .../fit/jober/aipp/fel/WaterFlowAgent.java | 2 +- .../fit/jober/aipp/fitable/LlmComponent.java | 2 +- .../jober/aipp/util/DefaultMcpClientFactory.java | 2 +- .../fit/jober/aipp/util/McpClientFactory.java | 13 ++++++++----- .../fit/jober/aipp/fel/WaterFlowAgentTest.java | 2 +- .../fit/jober/aipp/fitable/LlmComponentTest.java | 2 +- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java index 3bbb5ea8e3..7cb9eed2e3 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java @@ -133,7 +133,7 @@ private ChatMessage callTool(ToolCall toolCall, Map toolsMap, if (mcpServerConfig != null) { String url = Validation.notBlank(ObjectUtils.cast(mcpServerConfig.get(AippConst.MCP_SERVER_URL_KEY)), "The mcp url should not be empty."); - try (LangChain4jMcpClient mcpClient = this.mcpClientFactory.apply(url)) { + try (LangChain4jMcpClient mcpClient = this.mcpClientFactory.create(url)) { String result = mcpClient.callTool(toolRealName, toolCall.arguments()); return new ToolMessage(toolCall.id(), result); } catch (Exception exception) { diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java index 9487076a32..a27d4010dd 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java @@ -486,7 +486,7 @@ private List buildMcpToolInfos(Map mcpServersConfig) { String url = Validation.notBlank(ObjectUtils.cast(serverConfig.get(AippConst.MCP_SERVER_URL_KEY)), "The mcp url should not be empty."); - try (LangChain4jMcpClient mcpClient = this.mcpClientFactory.apply(url)) { + try (LangChain4jMcpClient mcpClient = this.mcpClientFactory.create(url)) { List tools = mcpClient.getTools(); result.addAll(tools.stream() .map(tool -> buildMcpToolInfo(serverName, tool, serverConfig)) diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java index 96ee14f5eb..5ba32e7839 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/DefaultMcpClientFactory.java @@ -17,7 +17,7 @@ @Component public class DefaultMcpClientFactory implements McpClientFactory { @Override - public LangChain4jMcpClient apply(String url) { + public LangChain4jMcpClient create(String url) { return new LangChain4jMcpClient(url); } } diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java index e77863926f..cd799ced59 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpClientFactory.java @@ -6,15 +6,18 @@ package modelengine.fit.jober.aipp.util; -import modelengine.fit.jober.aipp.util.LangChain4jMcpClient; - -import java.util.function.Function; - /** * MCP 客户端工厂接口,用于创建 {@link LangChain4jMcpClient} 实例。 * * @author songyongtan * @since 2026-03-02 */ -public interface McpClientFactory extends Function { +public interface McpClientFactory { + /** + * 创建 MCP 客户端实例。 + * + * @param url 表示 MCP 服务器的 URL。 + * @return 返回创建的 {@link LangChain4jMcpClient} 实例。 + */ + LangChain4jMcpClient create(String url); } diff --git a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java index c862a7e2ae..83bf6ca78e 100644 --- a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java +++ b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java @@ -137,7 +137,7 @@ void shouldGetResultWhenRunFlowGivenMcpToolCall() { doNothing().when(mockMcpClient).close(); McpClientFactory mockFactory = mock(McpClientFactory.class); - when(mockFactory.apply(any())).thenReturn(mockMcpClient); + when(mockFactory.create(any())).thenReturn(mockMcpClient); WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.toolExecuteService, this.chatModel, mockFactory); diff --git a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java index e196c678fc..e641bb4c5b 100644 --- a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java +++ b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java @@ -387,7 +387,7 @@ void shouldGetMcpToolsWhenHandleTaskGivenMcpServersConfig() throws InterruptedEx doNothing().when(mockMcpClient).close(); McpClientFactory mockFactory = mock(McpClientFactory.class); - when(mockFactory.apply(any())).thenReturn(mockMcpClient); + when(mockFactory.create(any())).thenReturn(mockMcpClient); LlmComponent llmComponent = getLlmComponent(agent, mockFactory); From 3970c6388bf6482d35f0712f5852183074622562 Mon Sep 17 00:00:00 2001 From: loveTsong <271667068@qq.com> Date: Tue, 3 Mar 2026 09:53:57 +0800 Subject: [PATCH 6/7] [dependency] upgrade LangChain4j to version 1.11.0-beta19 --- .../modelengine/fit/jober/aipp/util/LangChain4jMcpClient.java | 2 +- common/dependency/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClient.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClient.java index 1ddc13b271..b436da369e 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClient.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClient.java @@ -73,7 +73,7 @@ public String callTool(String toolName, String arguments) { .name(toolName) .arguments(arguments) .build(); - return this.mcpClient.executeTool(request); + return mcpClient.executeTool(request).resultText(); } catch (Exception e) { throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, StringUtils.format("Failed to call tool. [toolName={0}, url={1}]", toolName, this.url), e); diff --git a/common/dependency/pom.xml b/common/dependency/pom.xml index 437e8783f2..a72fa96fed 100644 --- a/common/dependency/pom.xml +++ b/common/dependency/pom.xml @@ -59,7 +59,7 @@ 1.12.468 5.7.1 1.1.0 - 1.0.0-beta1 + 1.11.0-beta19 3.22.0 From f8f4836af4fe3416d3a9b070d79d950a9ff60508 Mon Sep 17 00:00:00 2001 From: loveTsong <271667068@qq.com> Date: Wed, 4 Mar 2026 09:17:18 +0800 Subject: [PATCH 7/7] [aipp-plugin] test: add unit tests for LangChain4jMcpClient --- .../aipp/util/LangChain4jMcpClientTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClientTest.java diff --git a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClientTest.java b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClientTest.java new file mode 100644 index 0000000000..6ff507d142 --- /dev/null +++ b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/util/LangChain4jMcpClientTest.java @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * 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.util; + +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.mcp.client.McpClient; +import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport; +import dev.langchain4j.service.tool.ToolExecutionResult; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedConstruction; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +public class LangChain4jMcpClientTest { + private static final String TEST_URL = "http://localhost:8080/sse"; + private static final String TOOL_NAME = "testTool"; + private static final String TOOL_ARGUMENTS = "{\"key\":\"value\"}"; + private static final String TOOL_RESULT = "{\"result\":\"success\"}"; + + private MockedConstruction mcpClientMockedConstruction; + private MockedConstruction transportMockedConstruction; + private LangChain4jMcpClient client; + + @BeforeEach + void setUp() { + transportMockedConstruction = mockConstruction(HttpMcpTransport.class); + mcpClientMockedConstruction = mockConstruction(dev.langchain4j.mcp.client.DefaultMcpClient.class, + (mock, context) -> { + when(mock.listTools()).thenReturn(Arrays.asList( + ToolSpecification.builder() + .name(TOOL_NAME) + .description("Test tool") + .build() + )); + + ToolExecutionResult mockResult = mock(ToolExecutionResult.class); + lenient().when(mockResult.resultText()).thenReturn(TOOL_RESULT); + lenient().when(mock.executeTool(any(ToolExecutionRequest.class))).thenReturn(mockResult); + }); + client = new LangChain4jMcpClient(TEST_URL); + } + + @AfterEach + void tearDown() { + if (client != null) { + client.close(); + } + if (mcpClientMockedConstruction != null) { + mcpClientMockedConstruction.close(); + } + if (transportMockedConstruction != null) { + transportMockedConstruction.close(); + } + } + + @Test + @DisplayName("测试获取工具列表成功") + void shouldGetToolsWhenListTools() { + List tools = Assertions.assertDoesNotThrow(() -> client.getTools()); + + Assertions.assertNotNull(tools); + Assertions.assertEquals(1, tools.size()); + Assertions.assertEquals(TOOL_NAME, tools.get(0).name()); + } + + @Test + @DisplayName("测试调用工具成功") + void shouldCallToolWhenExecuteTool() { + String result = Assertions.assertDoesNotThrow(() -> client.callTool(TOOL_NAME, TOOL_ARGUMENTS)); + + Assertions.assertNotNull(result); + Assertions.assertEquals(TOOL_RESULT, result); + } + + @Test + @DisplayName("测试关闭客户端成功") + void shouldCloseClientWhenClose() { + Assertions.assertDoesNotThrow(() -> client.close()); + } + + @Test + @DisplayName("测试使用try-with-resources自动关闭客户端") + void shouldAutoCloseWhenUsingTryWithResources() { + Assertions.assertDoesNotThrow(() -> { + try (LangChain4jMcpClient autoCloseClient = new LangChain4jMcpClient(TEST_URL)) { + List tools = autoCloseClient.getTools(); + Assertions.assertNotNull(tools); + } + }); + } +}