diff --git a/apps/api/src/plugins/mcp/interfaces.py b/apps/api/src/plugins/mcp/interfaces.py index f2ee317e..6c2d75c2 100644 --- a/apps/api/src/plugins/mcp/interfaces.py +++ b/apps/api/src/plugins/mcp/interfaces.py @@ -30,7 +30,9 @@ class MCPTool: name: str description: str input_schema: dict[str, Any] - server_name: str + # 2026-05-06 Codex: 部分舊 provider 的 list_tools() 尚未傳 server_name。 + # 先給 DTO 預設值,registry 會以 provider.name 補正,避免啟動登記直接 crash。 + server_name: str = "" @dataclass diff --git a/apps/api/src/services/mcp_tool_registry.py b/apps/api/src/services/mcp_tool_registry.py index 5d8e0b34..4a22e6ad 100644 --- a/apps/api/src/services/mcp_tool_registry.py +++ b/apps/api/src/services/mcp_tool_registry.py @@ -21,8 +21,8 @@ MASTER §3.1.3 (B) AI 自主工具選擇 from __future__ import annotations -from dataclasses import dataclass, field -from enum import Enum +from dataclasses import dataclass, field, replace +from enum import StrEnum from typing import Any import structlog @@ -33,7 +33,7 @@ from src.plugins.mcp.registry import AuditedMCPToolProvider logger = structlog.get_logger(__name__) -class SensorDimension(str, Enum): +class SensorDimension(StrEnum): """8D 感官維度分類""" D1_K8S_STATE = "d1_k8s_state" D2_LOGS = "d2_logs" @@ -111,6 +111,8 @@ class MCPToolRegistry: count = 0 for tool in tools: + if not tool.server_name: + tool = replace(tool, server_name=provider.name) audited_provider = AuditedMCPToolProvider(provider) reg = _classify_tool(tool, audited_provider) self._tools.append(reg) diff --git a/apps/api/tests/test_mcp_tool_result_compat.py b/apps/api/tests/test_mcp_tool_result_compat.py index 686ce92f..baefeb06 100644 --- a/apps/api/tests/test_mcp_tool_result_compat.py +++ b/apps/api/tests/test_mcp_tool_result_compat.py @@ -1,4 +1,7 @@ -from src.plugins.mcp.interfaces import MCPToolResult +import pytest + +from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult +from src.services.mcp_tool_registry import MCPToolRegistry def test_mcp_tool_result_accepts_legacy_data_alias() -> None: @@ -26,3 +29,31 @@ def test_mcp_tool_result_allows_failure_without_execution_id() -> None: assert result.execution_id.startswith("mcp-") assert result.error == "blocked" + + +def test_mcp_tool_allows_legacy_missing_server_name() -> None: + tool = MCPTool(name="legacy_tool", description="legacy", input_schema={}) + + assert tool.server_name == "" + + +@pytest.mark.asyncio +async def test_mcp_registry_stamps_missing_server_name_from_provider() -> None: + class LegacyProvider(MCPToolProvider): + @property + def name(self) -> str: + return "legacy_provider" + + async def list_tools(self) -> list[MCPTool]: + return [MCPTool(name="legacy_tool", description="legacy", input_schema={})] + + async def execute(self, tool_name: str, parameters: dict) -> MCPToolResult: + return MCPToolResult(success=True, output={"tool": tool_name, "parameters": parameters}) + + registry = MCPToolRegistry() + + count = await registry.register_provider(LegacyProvider()) + + assert count == 1 + registered = registry.get_all_tools()[0] + assert registered.tool.server_name == "legacy_provider" diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 4b8b894d..469195f9 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -5,7 +5,8 @@ **本次修補**: - `MCPToolResult` 對舊 provider 介面做向後相容:`data` 自動映射到 `output`。 - 缺少 `execution_id` 時自動產生 `mcp-`,避免失敗/blocked 回傳因建構 DTO 就爆掉。 -- 補回歸測試,鎖住 `data` alias、明確 `output` 優先,以及 failure without execution_id。 +- `MCPTool` 對舊 provider 介面做向後相容:允許 `server_name` 暫缺,並由 `MCPToolRegistry.register_provider()` 以 `provider.name` 補正。 +- 補回歸測試,鎖住 `data` alias、明確 `output` 優先、failure without execution_id,以及舊 provider 缺 `server_name` 時仍可登記工具。 **後續**: - 這是相容層止血;後續仍應逐步把 provider call-sites 改成明確 `output=` 與穩定 `execution_id`。