feat(agent): 建立严格工具身份与调用契约 (#6280)

This commit is contained in:
InfinityPacer
2026-08-13 04:07:59 +08:00
committed by GitHub
parent 36297572fb
commit 4ee7ee2589
21 changed files with 1866 additions and 64 deletions

View File

@@ -1,10 +1,16 @@
import sys
import textwrap
from unittest.mock import AsyncMock, patch
import pytest
from app.agent.mcp import AgentMcpManager, AgentMcpToolSpec
from app.agent.tools.impl.mcp import McpExternalTool
from app.agent.tools.catalog import ToolCatalogSnapshot
from app.agent.tools.impl.mcp import (
McpExternalTool,
create_external_mcp_tools,
select_legacy_mcp_tools,
)
from app.schemas.agent import AgentMcpServerConfig
@@ -80,6 +86,122 @@ async def test_stdio_mcp_server_lists_tools(tmp_path):
assert tools[0].input_schema["properties"]["text"]["type"] == "string"
@pytest.mark.anyio
async def test_enabled_specs_preserve_cross_server_name_collisions() -> None:
"""跨 MCP 服务器生成相同 Agent 名时必须把全部身份交给目录判断。"""
manager = AgentMcpManager()
first = AgentMcpServerConfig(id="one", name="one", transport="stdio", command="one")
second = AgentMcpServerConfig(id="two", name="two", transport="stdio", command="two")
def _spec(server):
return AgentMcpToolSpec(
server=server,
name="echo",
agent_tool_name="shared_echo",
description="echo",
input_schema={"type": "object", "properties": {}},
)
with patch.object(manager, "get_servers", return_value=[first, second]), patch.object(
manager,
"list_server_tools",
new=AsyncMock(side_effect=lambda server: [_spec(server)]),
):
specs = await manager.list_enabled_tool_specs()
assert [spec.server.id for spec in specs] == ["one", "two"]
@pytest.mark.anyio
async def test_mcp_catalog_uses_server_id_and_legacy_execution_keeps_first() -> None:
"""目录应区分同显示名服务器,普通执行仍保留首个同名工具。"""
first_server = AgentMcpServerConfig(
id="one",
name="Shared Name",
transport="stdio",
command="one",
)
second_server = AgentMcpServerConfig(
id="two",
name="Shared Name",
transport="stdio",
command="two",
)
def _spec(server):
return AgentMcpToolSpec(
server=server,
name="echo",
agent_tool_name="shared_echo",
description="echo",
input_schema={"type": "object", "properties": {}},
)
tools = await create_external_mcp_tools(
session_id="session",
user_id="user",
specs=[_spec(first_server), _spec(second_server)],
)
catalog = ToolCatalogSnapshot.from_tools(
tools,
plugin_revision=0,
factory_revision="factory-v1",
)
assert [entry.source for entry in catalog.collisions["shared_echo"]] == [
"mcp:one",
"mcp:two",
]
assert len({entry.identity for entry in catalog.collisions["shared_echo"]}) == 2
assert select_legacy_mcp_tools(tools) == [tools[0]]
@pytest.mark.anyio
async def test_mcp_catalog_distinguishes_normalized_names_within_server() -> None:
"""同一服务内规范化重名的原始工具仍应具有不同绑定身份。"""
server = AgentMcpServerConfig(
id="shared",
name="Shared",
transport="stdio",
command="shared",
)
def _spec(name: str) -> AgentMcpToolSpec:
return AgentMcpToolSpec(
server=server,
name=name,
agent_tool_name="mcp_shared_foo_bar",
description="same description",
input_schema={"type": "object", "properties": {}},
)
tools = await create_external_mcp_tools(
session_id="session",
user_id="user",
specs=[_spec("foo-bar"), _spec("foo_bar")],
)
catalog = ToolCatalogSnapshot.from_tools(
tools,
plugin_revision=0,
factory_revision="factory-v1",
)
first_only = ToolCatalogSnapshot.from_tools(
[tools[0]],
plugin_revision=0,
factory_revision="factory-v1",
)
second_only = ToolCatalogSnapshot.from_tools(
[tools[1]],
plugin_revision=0,
factory_revision="factory-v1",
)
collisions = catalog.collisions["mcp_shared_foo_bar"]
assert len({entry.identity for entry in collisions}) == 2
assert len({entry.revision.implementation for entry in collisions}) == 2
assert first_only.signature != second_only.signature
@pytest.mark.anyio
async def test_stdio_mcp_server_calls_tool(tmp_path):
"""stdio MCP 工具应能通过 tools/call 返回内容。"""