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
4 changes: 4 additions & 0 deletions src/strands/tools/mcp/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,10 @@ def _handle_tool_result(self, tool_use_id: str, call_tool_result: MCPCallToolRes
if call_tool_result.structuredContent:
result["structuredContent"] = call_tool_result.structuredContent

call_tool_result_dict = call_tool_result.model_dump()
if call_tool_result_dict.get("meta"):
result["meta"] = call_tool_result_dict["meta"]

return result

async def _async_background_thread(self) -> None:
Expand Down
4 changes: 4 additions & 0 deletions src/strands/tools/mcp/mcp_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ class MCPToolResult(ToolResult):
structuredContent: Optional JSON object containing structured data returned
by the MCP tool. This allows MCP tools to return complex data structures
that can be processed programmatically by agents or other tools.
meta: 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).
"""

structuredContent: NotRequired[Dict[str, Any]]
meta: NotRequired[Dict[str, Any]]
62 changes: 62 additions & 0 deletions tests/strands/tools/mcp/test_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,3 +723,65 @@ async def test_handle_error_message_non_exception():

# This should not raise an exception
await client._handle_error_message("normal message")


def test_call_tool_sync_with_meta_field(mock_transport, mock_session):
"""Test that call_tool_sync correctly handles meta field."""
mock_content = MCPTextContent(type="text", text="Test message")
meta_data = {"tokenUsage": {"inputTokens": 100, "outputTokens": 50}, "executionTime": 1.5}
mock_session.call_tool.return_value = MCPCallToolResult(isError=False, content=[mock_content], meta=meta_data)

with MCPClient(mock_transport["transport_callable"]) as client:
result = client.call_tool_sync(tool_use_id="test-123", name="test_tool", arguments={"param": "value"})

mock_session.call_tool.assert_called_once_with("test_tool", {"param": "value"}, None)

assert result["status"] == "success"
assert result["toolUseId"] == "test-123"
assert len(result["content"]) == 1
assert result["content"][0]["text"] == "Test message"
assert "meta" in result
assert result["meta"] == meta_data
assert result["meta"]["tokenUsage"]["inputTokens"] == 100
assert result["meta"]["tokenUsage"]["outputTokens"] == 50
assert result["meta"]["executionTime"] == 1.5


def test_call_tool_sync_without_meta_field(mock_transport, mock_session):
"""Test that call_tool_sync works correctly when no meta field is provided."""
mock_content = MCPTextContent(type="text", text="Test message")
mock_session.call_tool.return_value = MCPCallToolResult(
isError=False,
content=[mock_content],
)

with MCPClient(mock_transport["transport_callable"]) as client:
result = client.call_tool_sync(tool_use_id="test-123", name="test_tool", arguments={"param": "value"})

assert result["status"] == "success"
assert result["toolUseId"] == "test-123"
assert len(result["content"]) == 1
assert result["content"][0]["text"] == "Test message"
assert result.get("meta") is None


def test_call_tool_sync_with_meta_and_structured_content(mock_transport, mock_session):
"""Test that call_tool_sync correctly handles both meta and structuredContent fields."""
mock_content = MCPTextContent(type="text", text="Test message")
meta_data = {"tokenUsage": {"inputTokens": 100, "outputTokens": 50}}
structured_content = {"result": 42, "status": "completed"}
mock_session.call_tool.return_value = MCPCallToolResult(
isError=False, content=[mock_content], meta=meta_data, structuredContent=structured_content
)

with MCPClient(mock_transport["transport_callable"]) as client:
result = client.call_tool_sync(tool_use_id="test-123", name="test_tool", arguments={"param": "value"})

mock_session.call_tool.assert_called_once_with("test_tool", {"param": "value"}, None)

assert result["status"] == "success"
assert result["toolUseId"] == "test-123"
assert "meta" in result
assert result["meta"] == meta_data
assert "structuredContent" in result
assert result["structuredContent"] == structured_content
Loading