Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/strands/tools/mcp/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,8 @@ def _handle_tool_result(self, tool_use_id: str, call_tool_result: MCPCallToolRes
result["structuredContent"] = call_tool_result.structuredContent
if call_tool_result.meta:
result["metadata"] = call_tool_result.meta
if call_tool_result.isError is not None:
result["isError"] = call_tool_result.isError

return result

Expand Down
6 changes: 6 additions & 0 deletions src/strands/tools/mcp/mcp_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ class MCPToolResult(ToolResult):
metadata: Optional arbitrary metadata returned by the MCP tool. This field allows
MCP servers to attach custom metadata to tool results (e.g., token usage,
performance metrics, or business-specific tracking information).
isError: Whether the MCP tool reported an application-level error via
``CallToolResult.isError``. ``True`` means the tool executed but its logic
returned a failure. Absent when the tool succeeded or when the error was a
protocol/client exception rather than a tool-reported failure, letting
callers distinguish application errors from transport/protocol errors.
"""

structuredContent: NotRequired[dict[str, Any]]
metadata: NotRequired[dict[str, Any]]
isError: NotRequired[bool]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The type NotRequired[bool] allows both True and False as values, but per the implementation and docstring contract, only True is ever set (and the key is absent otherwise). Using NotRequired[Literal[True]] would encode the actual contract in the type system — the field is either absent or True, never False.

Suggestion: Consider narrowing the type to NotRequired[Literal[True]] to make the semantics explicit:

from typing import Literal
isError: NotRequired[Literal[True]]

This helps callers understand they can simply check if result.get("isError") without worrying about an explicit False value.

13 changes: 13 additions & 0 deletions tests/strands/tools/mcp/test_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ def test_call_tool_sync_status(mock_transport, mock_session, is_error, expected_
assert result["content"][0]["text"] == "Test message"
# No structured content should be present when not provided by MCP
assert result.get("structuredContent") is None
# isError mirrors the MCP server's explicit value; absent only for protocol/client exceptions
assert result.get("isError") is is_error


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


@pytest.mark.asyncio
Expand Down Expand Up @@ -408,6 +412,15 @@ def test_mcp_tool_result_type():

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

# isError is optional — absent by default
assert "isError" not in result

# isError can be set to flag tool-reported application errors
result_with_is_error = MCPToolResult(
status="error", toolUseId="test-789", content=[{"text": "Tool failed"}], isError=True
)
assert result_with_is_error["isError"] is True


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