Skip to content

Commit 4e0be20

Browse files
Fix Image helper schema generation
1 parent 161834d commit 4e0be20

3 files changed

Lines changed: 76 additions & 0 deletions

File tree

src/mcp/server/mcpserver/utilities/func_metadata.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@ def _try_create_model_and_schema(
340340
model = None
341341
wrap_output = False
342342

343+
if _contains_content_helper_type(type_expr):
344+
return None, None, False
345+
343346
# First handle special case: None
344347
if type_expr is None:
345348
model = _create_wrapped_model(func_name, original_annotation)
@@ -423,6 +426,21 @@ def _try_create_model_and_schema(
423426
return None, None, False
424427

425428

429+
def _contains_content_helper_type(annotation: Any) -> bool:
430+
"""Return whether an annotation contains an MCPServer content helper type."""
431+
if isinstance(annotation, type) and issubclass(annotation, Image | Audio):
432+
return True
433+
434+
args = get_args(annotation)
435+
if not args:
436+
return False
437+
438+
if get_origin(annotation) is Annotated:
439+
return _contains_content_helper_type(args[0])
440+
441+
return any(_contains_content_helper_type(arg) for arg in args)
442+
443+
426444
_no_default = object()
427445

428446

tests/server/mcpserver/test_func_metadata.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from mcp.server.mcpserver.exceptions import InvalidSignature
1616
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
17+
from mcp.server.mcpserver.utilities.types import Audio, Image
1718
from mcp.types import CallToolResult
1819

1920

@@ -716,6 +717,33 @@ def func_optional() -> str | None: # pragma: no cover
716717
}
717718

718719

720+
def test_unstructured_output_content_helper_annotations():
721+
"""Image/Audio helper return annotations use content conversion, not schemas."""
722+
723+
def func_image() -> Image: # pragma: no cover
724+
return Image(data=b"abc", format="png")
725+
726+
def func_image_list() -> list[Image]: # pragma: no cover
727+
return [Image(data=b"abc", format="png")]
728+
729+
def func_nested_helpers() -> tuple[str, list[Image | Audio]]: # pragma: no cover
730+
return ("media", [Image(data=b"abc", format="png"), Audio(data=b"def", format="wav")])
731+
732+
def func_annotated_helper() -> Annotated[tuple[str, Image], "media"]: # pragma: no cover
733+
return ("image", Image(data=b"abc", format="png"))
734+
735+
for func in (
736+
func_image,
737+
func_image_list,
738+
func_nested_helpers,
739+
func_annotated_helper,
740+
):
741+
meta = func_metadata(func)
742+
assert meta.output_schema is None
743+
assert meta.output_model is None
744+
assert meta.wrap_output is False
745+
746+
719747
def test_structured_output_dataclass():
720748
"""Test structured output with dataclass return types"""
721749

tests/server/mcpserver/test_server.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,36 @@ def mixed_content_tool_fn() -> list[ContentBlock]:
236236
]
237237

238238

239+
def mixed_content_with_image_helper_tool_fn() -> tuple[str, Image, AudioContent]:
240+
return (
241+
"Hello",
242+
Image(data=b"abc", format="png"),
243+
AudioContent(type="audio", data="def", mime_type="audio/wav"),
244+
)
245+
246+
247+
async def test_tool_mixed_content_with_image_helper_annotation():
248+
mcp = MCPServer()
249+
mcp.add_tool(mixed_content_with_image_helper_tool_fn)
250+
async with Client(mcp) as client:
251+
tools = await client.list_tools()
252+
tool = next(tool for tool in tools.tools if tool.name == "mixed_content_with_image_helper_tool_fn")
253+
assert tool.output_schema is None
254+
255+
result = await client.call_tool("mixed_content_with_image_helper_tool_fn", {})
256+
assert len(result.content) == 3
257+
content1, content2, content3 = result.content
258+
assert isinstance(content1, TextContent)
259+
assert content1.text == "Hello"
260+
assert isinstance(content2, ImageContent)
261+
assert content2.mime_type == "image/png"
262+
assert content2.data == "YWJj"
263+
assert isinstance(content3, AudioContent)
264+
assert content3.mime_type == "audio/wav"
265+
assert content3.data == "def"
266+
assert result.structured_content is None
267+
268+
239269
class TestServerTools:
240270
async def test_add_tool(self):
241271
mcp = MCPServer()

0 commit comments

Comments
 (0)