Python: Pin meta core (#2597)

* pin core dependency in the meta package

* fix for mcp latest

* fix mypy

* test fix

* test fix

* fix tests for content parsing
This commit is contained in:
Eduard van Valkenburg
2025-12-03 19:19:17 +01:00
committed by GitHub
Unverified
parent 931197ba95
commit a81279d960
4 changed files with 154 additions and 133 deletions
+100 -41
View File
@@ -5,7 +5,7 @@ import logging
import re
import sys
from abc import abstractmethod
from collections.abc import Collection
from collections.abc import Collection, Sequence
from contextlib import AsyncExitStack, _AsyncGeneratorContextManager # type: ignore
from datetime import timedelta
from functools import partial
@@ -22,7 +22,16 @@ from mcp.shared.session import RequestResponder
from pydantic import BaseModel, Field, create_model
from ._tools import AIFunction, HostedMCPSpecificApproval
from ._types import ChatMessage, Contents, DataContent, Role, TextContent, UriContent
from ._types import (
ChatMessage,
Contents,
DataContent,
FunctionCallContent,
FunctionResultContent,
Role,
TextContent,
UriContent,
)
from .exceptions import ToolException, ToolExecutionException
if sys.version_info >= (3, 11):
@@ -61,7 +70,7 @@ def _mcp_prompt_message_to_chat_message(
"""Convert a MCP container type to a Agent Framework type."""
return ChatMessage(
role=Role(value=mcp_type.role),
contents=[_mcp_type_to_ai_content(mcp_type.content)],
contents=_mcp_type_to_ai_content(mcp_type.content),
raw_representation=mcp_type,
)
@@ -87,8 +96,7 @@ def _mcp_call_tool_result_to_ai_contents(
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)
meta_data = mcp_type.meta
# Prepare merged metadata once if present
merged_meta_props = None
@@ -104,53 +112,104 @@ def _mcp_call_tool_result_to_ai_contents(
# Convert each content item and merge metadata
result_contents = []
for item in mcp_type.content:
content = _mcp_type_to_ai_content(item)
contents = _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)
for content in contents:
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.extend(contents)
return result_contents
def _mcp_type_to_ai_content(
mcp_type: types.ImageContent | types.TextContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink,
) -> Contents:
mcp_type: types.ImageContent
| types.TextContent
| types.AudioContent
| types.EmbeddedResource
| types.ResourceLink
| types.ToolUseContent
| types.ToolResultContent
| Sequence[
types.ImageContent
| types.TextContent
| types.AudioContent
| types.EmbeddedResource
| types.ResourceLink
| types.ToolUseContent
| types.ToolResultContent
],
) -> list[Contents]:
"""Convert a MCP type to a Agent Framework type."""
match mcp_type:
case types.TextContent():
return TextContent(text=mcp_type.text, raw_representation=mcp_type)
case types.ImageContent() | types.AudioContent():
return DataContent(
uri=mcp_type.data,
media_type=mcp_type.mimeType,
raw_representation=mcp_type,
)
case types.ResourceLink():
return UriContent(
uri=str(mcp_type.uri),
media_type=mcp_type.mimeType or "application/json",
raw_representation=mcp_type,
)
case _:
match mcp_type.resource:
case types.TextResourceContents():
return TextContent(
text=mcp_type.resource.text,
mcp_types = mcp_type if isinstance(mcp_type, Sequence) else [mcp_type]
return_types: list[Contents] = []
for mcp_type in mcp_types:
match mcp_type:
case types.TextContent():
return_types.append(TextContent(text=mcp_type.text, raw_representation=mcp_type))
case types.ImageContent() | types.AudioContent():
return_types.append(
DataContent(
uri=mcp_type.data,
media_type=mcp_type.mimeType,
raw_representation=mcp_type,
additional_properties=(mcp_type.annotations.model_dump() if mcp_type.annotations else None),
)
case types.BlobResourceContents():
return DataContent(
uri=mcp_type.resource.blob,
media_type=mcp_type.resource.mimeType,
)
case types.ResourceLink():
return_types.append(
UriContent(
uri=str(mcp_type.uri),
media_type=mcp_type.mimeType or "application/json",
raw_representation=mcp_type,
additional_properties=(mcp_type.annotations.model_dump() if mcp_type.annotations else None),
)
)
case types.ToolUseContent():
return_types.append(
FunctionCallContent(
call_id=mcp_type.id,
name=mcp_type.name,
arguments=mcp_type.input,
raw_representation=mcp_type,
)
)
case types.ToolResultContent():
return_types.append(
FunctionResultContent(
call_id=mcp_type.toolUseId,
result=_mcp_type_to_ai_content(mcp_type.content)
if mcp_type.content
else mcp_type.structuredContent,
exception=Exception() if mcp_type.isError else None,
raw_representation=mcp_type,
)
)
case types.EmbeddedResource():
match mcp_type.resource:
case types.TextResourceContents():
return_types.append(
TextContent(
text=mcp_type.resource.text,
raw_representation=mcp_type,
additional_properties=(
mcp_type.annotations.model_dump() if mcp_type.annotations else None
),
)
)
case types.BlobResourceContents():
return_types.append(
DataContent(
uri=mcp_type.resource.blob,
media_type=mcp_type.resource.mimeType,
raw_representation=mcp_type,
additional_properties=(
mcp_type.annotations.model_dump() if mcp_type.annotations else None
),
)
)
return return_types
def _ai_content_to_mcp_types(
+23 -61
View File
@@ -91,9 +91,10 @@ def test_mcp_call_tool_result_to_ai_contents():
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"}
mcp_result = types.CallToolResult(
content=[types.TextContent(type="text", text="Error occurred")],
_meta={"isError": True, "errorCode": "TOOL_ERROR", "errorMessage": "Tool execution failed"},
)
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
@@ -115,15 +116,16 @@ def test_mcp_call_tool_result_with_meta_arbitrary_data():
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",
}
mcp_result = types.CallToolResult(
content=[types.TextContent(type="text", text="Success 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)
@@ -145,8 +147,7 @@ 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}
mcp_result = types.CallToolResult(content=[text_content], _meta={"newField": "newValue", "isError": False})
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
@@ -159,30 +160,6 @@ def test_mcp_call_tool_result_with_meta_merging_existing_properties():
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")])
@@ -200,21 +177,6 @@ def test_mcp_call_tool_result_with_meta_none():
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
@@ -247,7 +209,7 @@ def test_mcp_call_tool_result_regression_successful_workflow():
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")
ai_content = _mcp_type_to_ai_content(mcp_content)
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
assert isinstance(ai_content, TextContent)
assert ai_content.text == "Sample text"
@@ -257,7 +219,7 @@ def test_mcp_content_types_to_ai_content_text():
def test_mcp_content_types_to_ai_content_image():
"""Test conversion of MCP image content to AI content."""
mcp_content = types.ImageContent(type="image", data="data:image/jpeg;base64,abc", mimeType="image/jpeg")
ai_content = _mcp_type_to_ai_content(mcp_content)
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
assert isinstance(ai_content, DataContent)
assert ai_content.uri == "data:image/jpeg;base64,abc"
@@ -268,7 +230,7 @@ def test_mcp_content_types_to_ai_content_image():
def test_mcp_content_types_to_ai_content_audio():
"""Test conversion of MCP audio content to AI content."""
mcp_content = types.AudioContent(type="audio", data="data:audio/wav;base64,def", mimeType="audio/wav")
ai_content = _mcp_type_to_ai_content(mcp_content)
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
assert isinstance(ai_content, DataContent)
assert ai_content.uri == "data:audio/wav;base64,def"
@@ -284,7 +246,7 @@ def test_mcp_content_types_to_ai_content_resource_link():
name="test_resource",
mimeType="application/json",
)
ai_content = _mcp_type_to_ai_content(mcp_content)
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
assert isinstance(ai_content, UriContent)
assert ai_content.uri == "https://example.com/resource"
@@ -300,7 +262,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_text():
text="Embedded text content",
)
mcp_content = types.EmbeddedResource(type="resource", resource=text_resource)
ai_content = _mcp_type_to_ai_content(mcp_content)
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
assert isinstance(ai_content, TextContent)
assert ai_content.text == "Embedded text content"
@@ -316,7 +278,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_blob():
blob="data:application/octet-stream;base64,dGVzdCBkYXRh",
)
mcp_content = types.EmbeddedResource(type="resource", resource=blob_resource)
ai_content = _mcp_type_to_ai_content(mcp_content)
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
assert isinstance(ai_content, DataContent)
assert ai_content.uri == "data:application/octet-stream;base64,dGVzdCBkYXRh"
@@ -650,9 +612,9 @@ async def test_mcp_tool_call_tool_with_meta_integration():
# Create a CallToolResult with _meta field
tool_result = types.CallToolResult(
content=[types.TextContent(type="text", text="Tool executed with metadata")]
content=[types.TextContent(type="text", text="Tool executed with metadata")],
_meta={"executionTime": 1.5, "cost": {"usd": 0.002}, "isError": False, "toolVersion": "1.2.3"},
)
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)