Skip to content

Commit 3e08d5e

Browse files
authored
feat(mcp): preserve CallToolResult.isError flag in MCPToolResult (#2118)
1 parent 50439e0 commit 3e08d5e

3 files changed

Lines changed: 21 additions & 0 deletions

File tree

src/strands/tools/mcp/mcp_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,8 @@ def _handle_tool_result(self, tool_use_id: str, call_tool_result: MCPCallToolRes
754754
result["structuredContent"] = call_tool_result.structuredContent
755755
if call_tool_result.meta:
756756
result["metadata"] = call_tool_result.meta
757+
if call_tool_result.isError is not None:
758+
result["isError"] = call_tool_result.isError
757759

758760
return result
759761

src/strands/tools/mcp/mcp_types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,13 @@ class MCPToolResult(ToolResult):
6161
metadata: Optional arbitrary metadata returned by the MCP tool. This field allows
6262
MCP servers to attach custom metadata to tool results (e.g., token usage,
6363
performance metrics, or business-specific tracking information).
64+
isError: Whether the MCP tool reported an application-level error via
65+
``CallToolResult.isError``. ``True`` means the tool executed but its logic
66+
returned a failure. Absent when the tool succeeded or when the error was a
67+
protocol/client exception rather than a tool-reported failure, letting
68+
callers distinguish application errors from transport/protocol errors.
6469
"""
6570

6671
structuredContent: NotRequired[dict[str, Any]]
6772
metadata: NotRequired[dict[str, Any]]
73+
isError: NotRequired[bool]

tests/strands/tools/mcp/test_mcp_client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ def test_call_tool_sync_status(mock_transport, mock_session, is_error, expected_
132132
assert result["content"][0]["text"] == "Test message"
133133
# No structured content should be present when not provided by MCP
134134
assert result.get("structuredContent") is None
135+
# isError mirrors the MCP server's explicit value; absent only for protocol/client exceptions
136+
assert result.get("isError") is is_error
135137

136138

137139
def test_call_tool_sync_session_not_active():
@@ -261,6 +263,8 @@ async def mock_awaitable():
261263
assert result["toolUseId"] == "test-123"
262264
assert len(result["content"]) == 1
263265
assert result["content"][0]["text"] == "Test message"
266+
# isError mirrors the MCP server's explicit value; absent only for protocol/client exceptions
267+
assert result.get("isError") is is_error
264268

265269

266270
@pytest.mark.asyncio
@@ -408,6 +412,15 @@ def test_mcp_tool_result_type():
408412

409413
assert result_with_structured["structuredContent"] == {"key": "value"}
410414

415+
# isError is optional — absent by default
416+
assert "isError" not in result
417+
418+
# isError can be set to flag tool-reported application errors
419+
result_with_is_error = MCPToolResult(
420+
status="error", toolUseId="test-789", content=[{"text": "Tool failed"}], isError=True
421+
)
422+
assert result_with_is_error["isError"] is True
423+
411424

412425
def test_call_tool_sync_without_structured_content(mock_transport, mock_session):
413426
"""Test that call_tool_sync works correctly when no structured content is provided."""

0 commit comments

Comments
 (0)