diff --git a/src/ouroboros/mcp/client/adapter.py b/src/ouroboros/mcp/client/adapter.py index 85d20a833..ea6dc313a 100644 --- a/src/ouroboros/mcp/client/adapter.py +++ b/src/ouroboros/mcp/client/adapter.py @@ -541,6 +541,7 @@ def _parse_tool_result(self, result: Any, _tool_name: str) -> MCPToolResult: return MCPToolResult( content=tuple(content_items), is_error=getattr(result, "isError", False), + meta=getattr(result, "meta", {}) or {}, ) async def list_resources(self) -> Result[Sequence[MCPResourceDefinition], MCPClientError]: diff --git a/src/ouroboros/mcp/server/adapter.py b/src/ouroboros/mcp/server/adapter.py index 3fba78648..c8ce77e42 100644 --- a/src/ouroboros/mcp/server/adapter.py +++ b/src/ouroboros/mcp/server/adapter.py @@ -66,6 +66,21 @@ def _safe_cwd() -> Path: return cwd +def _to_fastmcp_tool_result(tool_result: MCPToolResult) -> Any: + """Convert internal tool results to MCP SDK results without dropping meta.""" + try: + from mcp.types import CallToolResult, TextContent + except ImportError as exc: # pragma: no cover - start() already checks this path. + msg = "mcp package not installed. Install with: pip install 'ouroboros-ai[mcp]'" + raise RuntimeError(msg) from exc + + return CallToolResult( + content=[TextContent(type="text", text=tool_result.text_content)], + isError=tool_result.is_error, + _meta=tool_result.meta or None, + ) + + def _default_interview_state_dir() -> Path: """Return the global interview state directory for MCP handlers.""" from ouroboros.config.models import get_config_dir @@ -920,7 +935,7 @@ async def tool_wrapper(**kwargs: Any) -> Any: if result.is_ok: # Convert MCPToolResult to FastMCP format tool_result = result.value - return tool_result.text_content + return _to_fastmcp_tool_result(tool_result) else: # Raise so FastMCP returns a proper MCP error response # with isError: true, instead of a success with error text. diff --git a/tests/unit/mcp/client/test_adapter.py b/tests/unit/mcp/client/test_adapter.py index 441e7d5f4..755269194 100644 --- a/tests/unit/mcp/client/test_adapter.py +++ b/tests/unit/mcp/client/test_adapter.py @@ -121,6 +121,24 @@ def test_parse_tool_result_text(self) -> None: assert result.content[0].text == "Hello, world!" assert result.is_error is False + def test_parse_tool_result_preserves_meta(self) -> None: + """SDK result metadata is restored into MCPToolResult.meta.""" + adapter = MCPClientAdapter() + mock_result = MagicMock() + mock_content = MagicMock() + mock_content.text = "Question?" + mock_result.content = [mock_content] + mock_result.isError = False + mock_result.meta = { + "internal_reasoning": ["phase: start"], + "interview_reasoning": {"phase": "start"}, + } + + result = adapter._parse_tool_result(mock_result, "ouroboros_interview") + + assert result.meta["internal_reasoning"] == ["phase: start"] + assert result.meta["interview_reasoning"]["phase"] == "start" + class TestMCPClientAdapterRetry: """Test MCPClientAdapter retry behavior.""" diff --git a/tests/unit/mcp/server/test_adapter.py b/tests/unit/mcp/server/test_adapter.py index 154c3007d..84c0fa927 100644 --- a/tests/unit/mcp/server/test_adapter.py +++ b/tests/unit/mcp/server/test_adapter.py @@ -23,6 +23,7 @@ _project_dir_from_artifact, _project_dir_from_seed, _safe_cwd, + _to_fastmcp_tool_result, validate_transport, ) from ouroboros.mcp.types import ( @@ -721,6 +722,18 @@ async def test_call_tool_success(self) -> None: assert result.value.text_content == "Success" handler.handle_mock.assert_called_once_with({"input": "test"}) + def test_fastmcp_tool_result_preserves_meta(self) -> None: + """FastMCP boundary conversion must not drop MCPToolResult.meta.""" + result = MCPToolResult( + content=(MCPContentItem(type=ContentType.TEXT, text="Success"),), + meta={"internal_reasoning": ["phase: start"]}, + ) + + converted = _to_fastmcp_tool_result(result) + + assert converted.content[0].text == "Success" + assert converted.meta == {"internal_reasoning": ["phase: start"]} + async def test_call_tool_scopes_io_journal_recorder_from_runtime_context(self) -> None: """MCP tool calls provide per-call journal identity to shared adapters."""