mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-22 16:53:13 +08:00
feat(agent): 建立严格工具身份与调用契约 (#6280)
This commit is contained in:
@@ -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 返回内容。"""
|
||||
|
||||
Reference in New Issue
Block a user