From 4e0be20dc2236f274e67625d64645c1b20021b8a Mon Sep 17 00:00:00 2001 From: pragnyanramtha Date: Fri, 15 May 2026 22:41:13 +0000 Subject: [PATCH 1/4] Fix Image helper schema generation --- .../mcpserver/utilities/func_metadata.py | 18 +++++++++++ tests/server/mcpserver/test_func_metadata.py | 28 +++++++++++++++++ tests/server/mcpserver/test_server.py | 30 +++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index 4a76106371..99cca4d0ed 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -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) @@ -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() diff --git a/tests/server/mcpserver/test_func_metadata.py b/tests/server/mcpserver/test_func_metadata.py index c57d1ee9f0..f8bf276cc7 100644 --- a/tests/server/mcpserver/test_func_metadata.py +++ b/tests/server/mcpserver/test_func_metadata.py @@ -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 @@ -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""" diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 3457ec944a..a1ff745a03 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -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() From b65095dfd6972215f7d0dc71db513cbf2254b773 Mon Sep 17 00:00:00 2001 From: pragnyanramtha Date: Fri, 15 May 2026 23:00:42 +0000 Subject: [PATCH 2/4] Fix helper annotation detection on Python 3.10 --- src/mcp/server/mcpserver/utilities/func_metadata.py | 8 ++++++-- tests/server/mcpserver/test_func_metadata.py | 10 +++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index 99cca4d0ed..ff8720703d 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -428,8 +428,12 @@ def _try_create_model_and_schema( 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 + if inspect.isclass(annotation): + try: + if issubclass(annotation, Image | Audio): + return True + except TypeError: + pass args = get_args(annotation) if not args: diff --git a/tests/server/mcpserver/test_func_metadata.py b/tests/server/mcpserver/test_func_metadata.py index f8bf276cc7..9954fcd9eb 100644 --- a/tests/server/mcpserver/test_func_metadata.py +++ b/tests/server/mcpserver/test_func_metadata.py @@ -13,7 +13,7 @@ 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 @@ -744,6 +744,14 @@ def func_annotated_helper() -> Annotated[tuple[str, Image], "media"]: # pragma: 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""" From 709d7d0a26a974e53cf0e045edf771317381725d Mon Sep 17 00:00:00 2001 From: pragnyanramtha Date: Fri, 15 May 2026 23:04:36 +0000 Subject: [PATCH 3/4] Avoid helper checks on parameterized generics --- src/mcp/server/mcpserver/utilities/func_metadata.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index ff8720703d..a4c9a25453 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -428,18 +428,15 @@ def _try_create_model_and_schema( def _contains_content_helper_type(annotation: Any) -> bool: """Return whether an annotation contains an MCPServer content helper type.""" - if inspect.isclass(annotation): - try: - if issubclass(annotation, Image | Audio): - return True - except TypeError: - pass + 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 get_origin(annotation) is Annotated: + if origin is Annotated: return _contains_content_helper_type(args[0]) return any(_contains_content_helper_type(arg) for arg in args) From 96da282fd565123d3d60edbe8082339c4a99ab0f Mon Sep 17 00:00:00 2001 From: pragnyanramtha Date: Fri, 15 May 2026 23:21:09 +0000 Subject: [PATCH 4/4] Remove covered Image helper pragmas --- src/mcp/server/mcpserver/utilities/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/mcpserver/utilities/types.py b/src/mcp/server/mcpserver/utilities/types.py index f092b245a8..1a5b45dd4c 100644 --- a/src/mcp/server/mcpserver/utilities/types.py +++ b/src/mcp/server/mcpserver/utilities/types.py @@ -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: @@ -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")