feat: 完成 Agent MCP 对接

- 增加 MCP 连接类型、环境检测接口和容器运行环境支持

- 将 Agent 编排改为绑定整体 MCP 并编译为 runtime McpSpec

- 优化 MCP 工具展示、审批、草稿试运行和画布回显稳定性
This commit is contained in:
2026-05-29 11:09:21 +08:00
parent e39f7521e2
commit cc3bb9cff0
33 changed files with 2405 additions and 127 deletions

View File

@@ -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 连接方式"));
}
}
}

View File

@@ -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"));
}
}