Files
temp/features/steps/tdd_mcp_error_content_key_steps.py
freemo b96138b88e fix(mcp): extract error message from content[0].text per MCP 1.4.0 protocol
MCPToolAdapter.invoke() was reading error messages from result.get('error',
'unknown error'), but the MCP 1.4.0 protocol returns errors in the content
field as a list of content items with type and text keys. This caused every
error from a real MCP 1.4.0-compliant server to be silently replaced with
the string 'unknown error'.

Changes:
- src/cleveragents/mcp/adapter.py: extract error_text from content[0].text
  with safe guards (isinstance check, length check) and fallback to
  'unknown error' when content is absent or empty
- features/mocks/mock_mcp_transport.py: return MCP 1.4.0-compliant error
  responses using content list format instead of the non-standard error key
- features/tdd_mcp_error_content_key.feature: Behave scenario verifying
  correct error extraction from MCP 1.4.0 content arrays (written as TDD
  issue-capture, @tdd_expected_fail removed after fix applied)
- features/steps/tdd_mcp_error_content_key_steps.py: step definitions for
  the new scenario including _MCP14ErrorTransport mock subclass

All 51 MCP adapter scenarios pass. Typecheck: 0 errors. Lint: clean.

ISSUES CLOSED: #2158
2026-04-03 19:03:15 +00:00

72 lines
2.6 KiB
Python

"""Step definitions for features/tdd_mcp_error_content_key.feature.
TDD issue-capture scenario for #2158: MCPToolAdapter.invoke() must extract
error messages from content[0].text per MCP 1.4.0 protocol.
"""
from __future__ import annotations
from typing import Any
from behave import given, then
from behave.runner import Context
from cleveragents.mcp.adapter import MCPServerConfig, MCPToolAdapter
from features.mocks.mock_mcp_transport import MockMCPTransport
class _MCP14ErrorTransport(MockMCPTransport):
"""Mock transport that returns MCP 1.4.0-compliant error responses.
Returns ``{"isError": True, "content": [{"type": "text", "text": "..."}]}``
instead of the non-standard ``{"isError": True, "error": "..."}`` format.
"""
def __init__(self, tool_name: str, error_text: str) -> None:
tools = [
{
"name": tool_name,
"description": f"Mock tool {tool_name}",
"inputSchema": {},
}
]
super().__init__(tools=tools)
self._mcp14_error_tool = tool_name
self._mcp14_error_text = error_text
def call(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
if method == "tools/call":
tool_name = params.get("name", "")
if tool_name == self._mcp14_error_tool:
return {
"isError": True,
"content": [{"type": "text", "text": self._mcp14_error_text}],
}
return super().call(method, params)
@given(
'a connected MCP adapter with a tool returning MCP 1.4.0 content-based error "{tool_name}"'
)
def step_mcp14_error_tool(context: Context, tool_name: str) -> None:
"""Set up a connected MCP adapter whose tool returns a MCP 1.4.0 content-based error."""
transport = _MCP14ErrorTransport(
tool_name=tool_name,
error_text="File not found: /path/to/file",
)
config = MCPServerConfig(name="test-server", transport="stdio", command="echo")
context.mcp_adapter = MCPToolAdapter(config=config, transport=transport)
context.mcp_adapter.connect()
context.mcp_adapter.discover_tools()
@then('the invocation error should contain "{expected}"')
def step_invoke_error_contains(context: Context, expected: str) -> None:
"""Assert the invocation error message contains the expected substring."""
result = context.mcp_invoke_result
assert result is not None, "No invocation result found"
assert result.error is not None, "Expected an error but got None"
assert expected in result.error, (
f"Expected error to contain {expected!r}, got: {result.error!r}"
)