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
19 changes: 19 additions & 0 deletions src/mcp/server/mcpserver/utilities/func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,9 @@ def _try_create_model_and_schema(
model = None
wrap_output = False

if _contains_content_helper_type(type_expr):
return None, None, False

# First handle special case: None
if type_expr is None:
model = _create_wrapped_model(func_name, original_annotation)
Expand Down Expand Up @@ -423,6 +426,22 @@ def _try_create_model_and_schema(
return None, None, False


def _contains_content_helper_type(annotation: Any) -> bool:
"""Return whether an annotation contains an MCPServer content helper type."""
origin = get_origin(annotation)
if origin is None and inspect.isclass(annotation) and issubclass(annotation, Image | Audio):
return True

args = get_args(annotation)
if not args:
return False

if origin is Annotated:
return _contains_content_helper_type(args[0])

return any(_contains_content_helper_type(arg) for arg in args)


_no_default = object()


Expand Down
4 changes: 2 additions & 2 deletions src/mcp/server/mcpserver/utilities/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(

def _get_mime_type(self) -> str:
"""Get MIME type from format or guess from file extension."""
if self._format: # pragma: no cover
if self._format:
return f"image/{self._format.lower()}"

if self.path:
Expand All @@ -46,7 +46,7 @@ def to_image_content(self) -> ImageContent:
if self.path:
with open(self.path, "rb") as f:
data = base64.b64encode(f.read()).decode()
elif self.data is not None: # pragma: no cover
elif self.data is not None:
data = base64.b64encode(self.data).decode()
else: # pragma: no cover
raise ValueError("No image data available")
Expand Down
38 changes: 37 additions & 1 deletion tests/server/mcpserver/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from pydantic import BaseModel, Field

from mcp.server.mcpserver.exceptions import InvalidSignature
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
from mcp.server.mcpserver.utilities.func_metadata import _contains_content_helper_type, func_metadata
from mcp.server.mcpserver.utilities.types import Audio, Image
from mcp.types import CallToolResult


Expand Down Expand Up @@ -716,6 +717,41 @@ def func_optional() -> str | None: # pragma: no cover
}


def test_unstructured_output_content_helper_annotations():
"""Image/Audio helper return annotations use content conversion, not schemas."""

def func_image() -> Image: # pragma: no cover
return Image(data=b"abc", format="png")

def func_image_list() -> list[Image]: # pragma: no cover
return [Image(data=b"abc", format="png")]

def func_nested_helpers() -> tuple[str, list[Image | Audio]]: # pragma: no cover
return ("media", [Image(data=b"abc", format="png"), Audio(data=b"def", format="wav")])

def func_annotated_helper() -> Annotated[tuple[str, Image], "media"]: # pragma: no cover
return ("image", Image(data=b"abc", format="png"))

for func in (
func_image,
func_image_list,
func_nested_helpers,
func_annotated_helper,
):
meta = func_metadata(func)
assert meta.output_schema is None
assert meta.output_model is None
assert meta.wrap_output is False


def test_detects_content_helper_types_in_nested_annotations():
assert _contains_content_helper_type(Image)
assert _contains_content_helper_type(list[Image])
assert _contains_content_helper_type(tuple[str, list[Image | Audio]])
assert _contains_content_helper_type(Annotated[tuple[str, Image], "media"])
assert not _contains_content_helper_type(list[str])


def test_structured_output_dataclass():
"""Test structured output with dataclass return types"""

Expand Down
30 changes: 30 additions & 0 deletions tests/server/mcpserver/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,36 @@ def mixed_content_tool_fn() -> list[ContentBlock]:
]


def mixed_content_with_image_helper_tool_fn() -> tuple[str, Image, AudioContent]:
return (
"Hello",
Image(data=b"abc", format="png"),
AudioContent(type="audio", data="def", mime_type="audio/wav"),
)


async def test_tool_mixed_content_with_image_helper_annotation():
mcp = MCPServer()
mcp.add_tool(mixed_content_with_image_helper_tool_fn)
async with Client(mcp) as client:
tools = await client.list_tools()
tool = next(tool for tool in tools.tools if tool.name == "mixed_content_with_image_helper_tool_fn")
assert tool.output_schema is None

result = await client.call_tool("mixed_content_with_image_helper_tool_fn", {})
assert len(result.content) == 3
content1, content2, content3 = result.content
assert isinstance(content1, TextContent)
assert content1.text == "Hello"
assert isinstance(content2, ImageContent)
assert content2.mime_type == "image/png"
assert content2.data == "YWJj"
assert isinstance(content3, AudioContent)
assert content3.mime_type == "audio/wav"
assert content3.data == "def"
assert result.structured_content is None


class TestServerTools:
async def test_add_tool(self):
mcp = MCPServer()
Expand Down
Loading