Skip to content
Merged
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
48 changes: 46 additions & 2 deletions python/packages/core/agent_framework/_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,52 @@ def _mcp_prompt_message_to_chat_message(
def _mcp_call_tool_result_to_ai_contents(
mcp_type: types.CallToolResult,
) -> list[Contents]:
"""Convert a MCP container type to a Agent Framework type."""
return [_mcp_type_to_ai_content(item) for item in mcp_type.content]
"""Convert a MCP container type to a Agent Framework type.

This function extracts the complete _meta field from CallToolResult objects
and merges all metadata into the additional_properties field of converted
content items.

Note: The _meta field from CallToolResult is applied to ALL content items
in the result, as the Agent Framework's content model doesn't have a
result-level metadata container. This ensures metadata is preserved but
means it will be duplicated across multiple content items if present.

Args:
mcp_type: The MCP CallToolResult object to convert.

Returns:
A list of Agent Framework content items with metadata merged into
additional_properties.
"""
# Extract _meta field using getattr for compatibility
meta_data = getattr(mcp_type, "_meta", None)

# Prepare merged metadata once if present
merged_meta_props = None
if meta_data:
merged_meta_props = {}
if hasattr(meta_data, "__dict__"):
merged_meta_props.update(meta_data.__dict__)
elif isinstance(meta_data, dict):
merged_meta_props.update(meta_data)
else:
merged_meta_props["_meta"] = meta_data

# Convert each content item and merge metadata
result_contents = []
for item in mcp_type.content:
content = _mcp_type_to_ai_content(item)

if merged_meta_props:
existing_props = getattr(content, "additional_properties", None) or {}
# Merge with content-specific properties, letting content-specific props override
final_props = merged_meta_props.copy()
final_props.update(existing_props)
content.additional_properties = final_props
result_contents.append(content)

return result_contents


def _mcp_type_to_ai_content(
Expand Down
208 changes: 208 additions & 0 deletions python/packages/core/tests/core/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,162 @@ def test_mcp_call_tool_result_to_ai_contents():
assert ai_contents[1].media_type == "image/png"


def test_mcp_call_tool_result_with_meta_error():
"""Test conversion from MCP tool result with _meta field containing isError=True."""
# Create a mock CallToolResult with _meta field containing error information
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="Error occurred")])
# Simulate _meta field with isError=True
mcp_result._meta = {"isError": True, "errorCode": "TOOL_ERROR", "errorMessage": "Tool execution failed"}

ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)

assert len(ai_contents) == 1
assert isinstance(ai_contents[0], TextContent)
assert ai_contents[0].text == "Error occurred"

# Check that _meta data is merged into additional_properties
assert ai_contents[0].additional_properties is not None
assert ai_contents[0].additional_properties["isError"] is True
assert ai_contents[0].additional_properties["errorCode"] == "TOOL_ERROR"
assert ai_contents[0].additional_properties["errorMessage"] == "Tool execution failed"


def test_mcp_call_tool_result_with_meta_arbitrary_data():
"""Test conversion from MCP tool result with _meta field containing arbitrary metadata.

Note: The _meta field is optional and can contain any structure that a specific
MCP server chooses to provide. This test uses example metadata to verify that
whatever is provided gets preserved in additional_properties.
"""
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="Success result")])
# Example _meta field - different MCP servers may provide completely different structures
mcp_result._meta = {
"serverVersion": "2.1.0",
"executionId": "exec_abc123",
"metrics": {"responseTime": 1.25, "memoryUsed": "64MB"},
"source": "example-mcp-server",
"customField": "arbitrary_value",
}

ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)

assert len(ai_contents) == 1
assert isinstance(ai_contents[0], TextContent)
assert ai_contents[0].text == "Success result"

# Check that _meta data is preserved in additional_properties
props = ai_contents[0].additional_properties
assert props is not None
assert props["serverVersion"] == "2.1.0"
assert props["executionId"] == "exec_abc123"
assert props["metrics"] == {"responseTime": 1.25, "memoryUsed": "64MB"}
assert props["source"] == "example-mcp-server"
assert props["customField"] == "arbitrary_value"


def test_mcp_call_tool_result_with_meta_merging_existing_properties():
"""Test that _meta data merges correctly with existing additional_properties."""
# Create content with existing additional_properties
text_content = types.TextContent(type="text", text="Test content")
mcp_result = types.CallToolResult(content=[text_content])
mcp_result._meta = {"newField": "newValue", "isError": False}

ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)

assert len(ai_contents) == 1
content = ai_contents[0]

# Check that _meta data is present in additional_properties
assert content.additional_properties is not None
assert content.additional_properties["newField"] == "newValue"
assert content.additional_properties["isError"] is False


def test_mcp_call_tool_result_with_meta_object_attributes():
"""Test conversion when _meta is an object with attributes rather than a dict."""

class MetaObject:
def __init__(self):
self.isError = True
self.requestId = "req-12345"
self.executionTime = 2.5

mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="Object meta test")])
mcp_result._meta = MetaObject()

ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)

assert len(ai_contents) == 1
content = ai_contents[0]

# Check that object attributes are extracted correctly
assert content.additional_properties is not None
assert content.additional_properties["isError"] is True
assert content.additional_properties["requestId"] == "req-12345"
assert content.additional_properties["executionTime"] == 2.5


def test_mcp_call_tool_result_with_meta_none():
"""Test that missing _meta field is handled gracefully."""
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="No meta test")])
# No _meta field set

ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)

assert len(ai_contents) == 1
assert isinstance(ai_contents[0], TextContent)
assert ai_contents[0].text == "No meta test"

# Should handle gracefully when no _meta field exists
# additional_properties may be None or empty dict
props = ai_contents[0].additional_properties
assert props is None or props == {}


def test_mcp_call_tool_result_with_meta_non_dict_value():
"""Test conversion when _meta contains a non-dict value."""
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="Non-dict meta test")])
mcp_result._meta = "simple string meta"

ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)

assert len(ai_contents) == 1
content = ai_contents[0]

# Non-dict _meta should be stored under '_meta' key
assert content.additional_properties is not None
assert content.additional_properties["_meta"] == "simple string meta"


def test_mcp_call_tool_result_regression_successful_workflow():
"""Regression test to ensure existing successful workflows remain unchanged."""
# Test the original successful workflow still works
mcp_result = types.CallToolResult(
content=[
types.TextContent(type="text", text="Success message"),
types.ImageContent(type="image", data="data:image/jpeg;base64,abc123", mimeType="image/jpeg"),
]
)

ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)

# Verify basic conversion still works correctly
assert len(ai_contents) == 2

text_content = ai_contents[0]
assert isinstance(text_content, TextContent)
assert text_content.text == "Success message"

image_content = ai_contents[1]
assert isinstance(image_content, DataContent)
assert image_content.uri == "data:image/jpeg;base64,abc123"
assert image_content.media_type == "image/jpeg"

# Should have no additional_properties when no _meta field
assert text_content.additional_properties is None or text_content.additional_properties == {}
assert image_content.additional_properties is None or image_content.additional_properties == {}


def test_mcp_content_types_to_ai_content_text():
"""Test conversion of MCP text content to AI content."""
mcp_content = types.TextContent(type="text", text="Sample text")
Expand Down Expand Up @@ -440,6 +596,58 @@ def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
assert server.functions[0].name == "test_prompt"


async def test_mcp_tool_call_tool_with_meta_integration():
"""Test that call_tool method properly integrates with enhanced metadata extraction."""

class TestServer(MCPTool):
async def connect(self):
self.session = Mock(spec=ClientSession)
self.session.list_tools = AsyncMock(
return_value=types.ListToolsResult(
tools=[
types.Tool(
name="test_tool",
description="Test tool",
inputSchema={
"type": "object",
"properties": {"param": {"type": "string"}},
"required": ["param"],
},
)
]
)
)

# Create a CallToolResult with _meta field
tool_result = types.CallToolResult(
content=[types.TextContent(type="text", text="Tool executed with metadata")]
)
tool_result._meta = {"executionTime": 1.5, "cost": {"usd": 0.002}, "isError": False, "toolVersion": "1.2.3"}

self.session.call_tool = AsyncMock(return_value=tool_result)

def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
return None

server = TestServer(name="test_server")
async with server:
await server.load_tools()
func = server.functions[0]
result = await func.invoke(param="test_value")

assert len(result) == 1
assert isinstance(result[0], TextContent)
assert result[0].text == "Tool executed with metadata"

# Verify that _meta data is present in additional_properties
props = result[0].additional_properties
assert props is not None
assert props["executionTime"] == 1.5
assert props["cost"] == {"usd": 0.002}
assert props["isError"] is False
assert props["toolVersion"] == "1.2.3"


async def test_local_mcp_server_function_execution():
"""Test function execution through MCP server."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

This sample demonstrates integrating Model Context Protocol (MCP) tools with
OpenAI Chat Client for extended functionality and external service access.

The Agent Framework now supports enhanced metadata extraction from MCP tool
results, including error states, token usage, costs, and other arbitrary
metadata through the _meta field of CallToolResult objects.
"""


Expand Down