feat: 完成 Agent MCP 对接
- 增加 MCP 连接类型、环境检测接口和容器运行环境支持 - 将 Agent 编排改为绑定整体 MCP 并编译为 runtime McpSpec - 优化 MCP 工具展示、审批、草稿试运行和画布回显稳定性
This commit is contained in:
@@ -37,6 +37,18 @@ public class McpBase extends DateEntity implements Serializable {
|
||||
@Column(comment = "完整MCP配置JSON")
|
||||
private String configJson;
|
||||
|
||||
/**
|
||||
* MCP连接方式
|
||||
*/
|
||||
@Column(comment = "MCP连接方式")
|
||||
private String transportType;
|
||||
|
||||
/**
|
||||
* 是否启用工具调用审批
|
||||
*/
|
||||
@Column(comment = "是否启用工具调用审批")
|
||||
private Boolean approvalRequired;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
@@ -111,6 +123,22 @@ public class McpBase extends DateEntity implements Serializable {
|
||||
this.configJson = configJson;
|
||||
}
|
||||
|
||||
public String getTransportType() {
|
||||
return transportType;
|
||||
}
|
||||
|
||||
public void setTransportType(String transportType) {
|
||||
this.transportType = transportType;
|
||||
}
|
||||
|
||||
public Boolean getApprovalRequired() {
|
||||
return approvalRequired;
|
||||
}
|
||||
|
||||
public void setApprovalRequired(Boolean approvalRequired) {
|
||||
this.approvalRequired = approvalRequired;
|
||||
}
|
||||
|
||||
public BigInteger getDeptId() {
|
||||
return deptId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package tech.easyflow.ai.mcp;
|
||||
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* MCP 连接方式。
|
||||
*/
|
||||
public enum McpTransportType {
|
||||
|
||||
/**
|
||||
* 标准输入输出进程通信。
|
||||
*/
|
||||
STDIO("stdio"),
|
||||
|
||||
/**
|
||||
* HTTP SSE 通信。
|
||||
*/
|
||||
SSE("http-sse"),
|
||||
|
||||
/**
|
||||
* Streamable HTTP 通信。
|
||||
*/
|
||||
HTTP("http-stream");
|
||||
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* 创建 MCP 连接方式。
|
||||
*
|
||||
* @param value 配置值
|
||||
*/
|
||||
McpTransportType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置值。
|
||||
*
|
||||
* @return 配置值
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析连接方式。
|
||||
*
|
||||
* @param value 连接方式文本
|
||||
* @return MCP 连接方式
|
||||
*/
|
||||
public static McpTransportType from(String value) {
|
||||
if (StringUtil.noText(value)) {
|
||||
return STDIO;
|
||||
}
|
||||
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||
return switch (normalized) {
|
||||
case "stdio" -> STDIO;
|
||||
case "sse", "http-sse" -> SSE;
|
||||
case "http", "http-stream", "streamable-http" -> HTTP;
|
||||
default -> throw new BusinessException("不支持的 MCP 连接方式: " + value);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.easyagents.mcp.client.McpEnvironmentCheckResult;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.ai.entity.BotMcp;
|
||||
@@ -30,4 +31,6 @@ public interface McpService extends IService<Mcp> {
|
||||
Mcp getMcpTools(String id);
|
||||
|
||||
Page<Mcp> pageTools(Page<Mcp> mcpPage);
|
||||
|
||||
McpEnvironmentCheckResult checkMcp(String configJson);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package tech.easyflow.ai.service.impl;
|
||||
import com.easyagents.core.model.chat.tool.Parameter;
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.easyagents.mcp.client.McpClientManager;
|
||||
import com.easyagents.mcp.client.McpEnvironmentCheckResult;
|
||||
import com.easyagents.mcp.client.McpEnvironmentChecker;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
@@ -16,6 +18,7 @@ import tech.easyflow.ai.easyagents.tool.McpTool;
|
||||
import tech.easyflow.ai.entity.BotMcp;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.mapper.McpMapper;
|
||||
import tech.easyflow.ai.mcp.McpTransportType;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.ai.utils.CommonFiledUtil;
|
||||
import tech.easyflow.common.constant.enums.EnumRes;
|
||||
@@ -37,7 +40,8 @@ import java.util.*;
|
||||
@Service
|
||||
public class McpServiceImpl extends ServiceImpl<McpMapper, Mcp> implements McpService {
|
||||
private final McpClientManager mcpClientManager = McpClientManager.getInstance();
|
||||
protected Logger Log = LoggerFactory.getLogger(DocumentServiceImpl.class);
|
||||
private final McpEnvironmentChecker mcpEnvironmentChecker = new McpEnvironmentChecker();
|
||||
protected Logger Log = LoggerFactory.getLogger(McpServiceImpl.class);
|
||||
|
||||
@Override
|
||||
public Result<?> saveMcp(Mcp entity) {
|
||||
@@ -49,6 +53,8 @@ public class McpServiceImpl extends ServiceImpl<McpMapper, Mcp> implements McpS
|
||||
if (!StringUtil.hasText(serverName)) {
|
||||
return Result.fail("未找到mcp服务名称", serverName);
|
||||
}
|
||||
entity.setTransportType(getFirstMcpTransportType(entity.getConfigJson()));
|
||||
entity.setApprovalRequired(Boolean.TRUE.equals(entity.getApprovalRequired()));
|
||||
try {
|
||||
mcpClientManager.registerFromJson(entity.getConfigJson());
|
||||
} catch (Exception e) {
|
||||
@@ -79,6 +85,8 @@ public class McpServiceImpl extends ServiceImpl<McpMapper, Mcp> implements McpS
|
||||
if (!StringUtil.hasText(serverName)) {
|
||||
return Result.fail("未找到mcp服务名称", serverName);
|
||||
}
|
||||
entity.setTransportType(getFirstMcpTransportType(entity.getConfigJson()));
|
||||
entity.setApprovalRequired(Boolean.TRUE.equals(entity.getApprovalRequired()));
|
||||
if (entity.getStatus()) {
|
||||
try {
|
||||
mcpClientManager.registerFromJson(entity.getConfigJson());
|
||||
@@ -121,6 +129,7 @@ public class McpServiceImpl extends ServiceImpl<McpMapper, Mcp> implements McpS
|
||||
records.forEach(mcp -> {
|
||||
boolean clientOnline = mcpClientManager.isClientOnline(getFirstMcpServerName(mcp.getConfigJson()));
|
||||
mcp.setClientOnline(clientOnline);
|
||||
mcp.setTransportType(resolveMcpTransportType(mcp));
|
||||
}
|
||||
);
|
||||
page.getData().setRecords(records);
|
||||
@@ -130,6 +139,9 @@ public class McpServiceImpl extends ServiceImpl<McpMapper, Mcp> implements McpS
|
||||
@Override
|
||||
public Mcp getMcpTools(String id) {
|
||||
Mcp mcp = this.getById(id);
|
||||
if (mcp != null) {
|
||||
mcp.setTransportType(resolveMcpTransportType(mcp));
|
||||
}
|
||||
if (mcp != null && mcp.getStatus()) {
|
||||
McpSyncClient mcpClient = getMcpClient(mcp, mcpClientManager);
|
||||
List<McpSchema.Tool> tools = null;
|
||||
@@ -209,9 +221,27 @@ public class McpServiceImpl extends ServiceImpl<McpMapper, Mcp> implements McpS
|
||||
return firstServerName.orElse(null);
|
||||
}
|
||||
|
||||
public static String getFirstMcpTransportType(String mcpJson) {
|
||||
JSONObject rootJson = JSON.parseObject(mcpJson);
|
||||
JSONObject mcpServersJson = rootJson.getJSONObject("mcpServers");
|
||||
if (mcpServersJson == null || mcpServersJson.isEmpty()) {
|
||||
return McpTransportType.STDIO.getValue();
|
||||
}
|
||||
Optional<String> firstServerName = mcpServersJson.keySet().stream().findFirst();
|
||||
if (firstServerName.isEmpty()) {
|
||||
return McpTransportType.STDIO.getValue();
|
||||
}
|
||||
JSONObject serverJson = mcpServersJson.getJSONObject(firstServerName.get());
|
||||
if (serverJson == null) {
|
||||
return McpTransportType.STDIO.getValue();
|
||||
}
|
||||
return McpTransportType.from(serverJson.getString("transport")).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<Mcp> pageTools(Page<Mcp> page) {
|
||||
page.getRecords().forEach(mcp -> {
|
||||
mcp.setTransportType(resolveMcpTransportType(mcp));
|
||||
// mcp 未启用,不查询工具
|
||||
if (!mcp.getStatus()) {
|
||||
return;
|
||||
@@ -235,6 +265,11 @@ public class McpServiceImpl extends ServiceImpl<McpMapper, Mcp> implements McpS
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpEnvironmentCheckResult checkMcp(String configJson) {
|
||||
return mcpEnvironmentChecker.check(configJson);
|
||||
}
|
||||
|
||||
private Result<?> validateMcpConfig(Mcp entity) {
|
||||
if (entity == null || !StringUtil.hasText(entity.getConfigJson())) {
|
||||
Log.error("MCP 配置不能为空");
|
||||
@@ -242,4 +277,14 @@ public class McpServiceImpl extends ServiceImpl<McpMapper, Mcp> implements McpS
|
||||
}
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private String resolveMcpTransportType(Mcp mcp) {
|
||||
if (mcp == null) {
|
||||
return McpTransportType.STDIO.getValue();
|
||||
}
|
||||
if (StringUtil.hasText(mcp.getTransportType())) {
|
||||
return McpTransportType.from(mcp.getTransportType()).getValue();
|
||||
}
|
||||
return getFirstMcpTransportType(mcp.getConfigJson());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package tech.easyflow.ai.mcp;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.service.impl.McpServiceImpl;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
/**
|
||||
* {@link McpTransportType} 单元测试。
|
||||
*/
|
||||
public class McpTransportTypeTest {
|
||||
|
||||
/**
|
||||
* 应兼容解析 MCP 配置中常见的连接方式文本。
|
||||
*/
|
||||
@Test
|
||||
public void fromShouldParseSupportedTransportTypes() {
|
||||
Assert.assertEquals(McpTransportType.STDIO, McpTransportType.from("stdio"));
|
||||
Assert.assertEquals(McpTransportType.SSE, McpTransportType.from("sse"));
|
||||
Assert.assertEquals(McpTransportType.SSE, McpTransportType.from("http-sse"));
|
||||
Assert.assertEquals(McpTransportType.HTTP, McpTransportType.from("http"));
|
||||
Assert.assertEquals(McpTransportType.HTTP, McpTransportType.from("http-stream"));
|
||||
Assert.assertEquals(McpTransportType.HTTP, McpTransportType.from("streamable-http"));
|
||||
Assert.assertEquals(McpTransportType.STDIO, McpTransportType.from(null));
|
||||
Assert.assertEquals(McpTransportType.STDIO, McpTransportType.from(" "));
|
||||
}
|
||||
|
||||
/**
|
||||
* 应从 MCP 配置 JSON 中推断首个 server 的连接方式。
|
||||
*/
|
||||
@Test
|
||||
public void getFirstMcpTransportTypeShouldInferFromConfigJson() {
|
||||
Assert.assertEquals("stdio", McpServiceImpl.getFirstMcpTransportType("""
|
||||
{"mcpServers":{"everything":{"command":"npx","args":["-y","@modelcontextprotocol/server-everything"]}}}
|
||||
"""));
|
||||
Assert.assertEquals("http-sse", McpServiceImpl.getFirstMcpTransportType("""
|
||||
{"mcpServers":{"remote":{"transport":"http-sse","url":"http://127.0.0.1:3000/sse"}}}
|
||||
"""));
|
||||
Assert.assertEquals("http-stream", McpServiceImpl.getFirstMcpTransportType("""
|
||||
{"mcpServers":{"remote":{"transport":"http-stream","url":"http://127.0.0.1:3000/mcp"}}}
|
||||
"""));
|
||||
}
|
||||
|
||||
/**
|
||||
* 不支持的连接方式应直接失败,避免保存无法启动的 MCP 配置。
|
||||
*/
|
||||
@Test
|
||||
public void getFirstMcpTransportTypeShouldRejectUnsupportedTransportType() {
|
||||
try {
|
||||
McpServiceImpl.getFirstMcpTransportType("""
|
||||
{"mcpServers":{"remote":{"transport":"websocket","url":"ws://127.0.0.1:3000/mcp"}}}
|
||||
""");
|
||||
Assert.fail("expected BusinessException");
|
||||
} catch (BusinessException exception) {
|
||||
Assert.assertTrue(exception.getMessage().contains("不支持的 MCP 连接方式"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,4 +70,25 @@ public class ChatAssistantAccumulatorTest {
|
||||
Assert.assertEquals(1, secondToolCalls.size());
|
||||
Assert.assertEquals("call-2", secondToolCalls.get(0).get("id"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具展示名应进入展示链和 assistant toolCalls,但不覆盖真实工具名。
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldKeepToolDisplayNameWithoutOverridingToolName() {
|
||||
ChatAssistantAccumulator accumulator = new ChatAssistantAccumulator();
|
||||
accumulator.appendToolCall("call-1", "mcp_123_search", "知识库 MCP - search", "{\"q\":\"java\"}");
|
||||
accumulator.appendToolResult("call-1", "mcp_123_search", "知识库 MCP - search", "{\"ok\":true}");
|
||||
|
||||
Map<String, Object> payload = accumulator.buildPayload(null);
|
||||
List<Map<String, Object>> chains = (List<Map<String, Object>>) payload.get("chains");
|
||||
List<Map<String, Object>> messageChain = (List<Map<String, Object>>) payload.get("messageChain");
|
||||
List<Map<String, Object>> toolCalls = (List<Map<String, Object>>) messageChain.get(0).get("toolCalls");
|
||||
|
||||
Assert.assertEquals("mcp_123_search", chains.get(0).get("name"));
|
||||
Assert.assertEquals("知识库 MCP - search", chains.get(0).get("toolDisplayName"));
|
||||
Assert.assertEquals("mcp_123_search", toolCalls.get(0).get("name"));
|
||||
Assert.assertEquals("知识库 MCP - search", toolCalls.get(0).get("toolDisplayName"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user