Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 18 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,21 @@ 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."""
if isinstance(annotation, type) and issubclass(annotation, Image | Audio):
return True

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

if get_origin(annotation) 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
28 changes: 28 additions & 0 deletions tests/server/mcpserver/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

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


Expand Down Expand Up @@ -716,6 +717,33 @@ 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_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