Allow CallToolResult to be returned directly to support _meta field for OpenAI Apps (#1459)

Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
Brandon Shar 2025-10-21 14:52:08 -04:00 • committed by GitHub
commit 1200ba0082
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 445 additions and 5 deletions

View file

@ -13,6 +13,7 @@ from dirty_equals import IsPartialDict
from pydantic import BaseModel, Field
from mcp.server.fastmcp.utilities.func_metadata import func_metadata
from mcp.types import CallToolResult
class SomeInputModelA(BaseModel):
@ -834,6 +835,93 @@ def test_unstructured_output_unannotated_class():
assert meta.output_schema is None
def test_tool_call_result_is_unstructured_and_not_converted():
def func_returning_call_tool_result() -> CallToolResult:
return CallToolResult(content=[])
meta = func_metadata(func_returning_call_tool_result)
assert meta.output_schema is None
assert isinstance(meta.convert_result(func_returning_call_tool_result()), CallToolResult)
def test_tool_call_result_annotated_is_structured_and_converted():
class PersonClass(BaseModel):
name: str
def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]:
return CallToolResult(content=[], structuredContent={"name": "Brandon"})
meta = func_metadata(func_returning_annotated_tool_call_result)
assert meta.output_schema == {
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
},
"required": ["name"],
"title": "PersonClass",
}
assert isinstance(meta.convert_result(func_returning_annotated_tool_call_result()), CallToolResult)
def test_tool_call_result_annotated_is_structured_and_invalid():
class PersonClass(BaseModel):
name: str
def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]:
return CallToolResult(content=[], structuredContent={"person": "Brandon"})
meta = func_metadata(func_returning_annotated_tool_call_result)
with pytest.raises(ValueError):
meta.convert_result(func_returning_annotated_tool_call_result())
def test_tool_call_result_in_optional_is_rejected():
"""Test that Optional[CallToolResult] raises InvalidSignature"""
from mcp.server.fastmcp.exceptions import InvalidSignature
def func_optional_call_tool_result() -> CallToolResult | None:
return CallToolResult(content=[])
with pytest.raises(InvalidSignature) as exc_info:
func_metadata(func_optional_call_tool_result)
assert "Union or Optional" in str(exc_info.value)
assert "CallToolResult" in str(exc_info.value)
def test_tool_call_result_in_union_is_rejected():
"""Test that Union[str, CallToolResult] raises InvalidSignature"""
from mcp.server.fastmcp.exceptions import InvalidSignature
def func_union_call_tool_result() -> str | CallToolResult:
return CallToolResult(content=[])
with pytest.raises(InvalidSignature) as exc_info:
func_metadata(func_union_call_tool_result)
assert "Union or Optional" in str(exc_info.value)
assert "CallToolResult" in str(exc_info.value)
def test_tool_call_result_in_pipe_union_is_rejected():
"""Test that str | CallToolResult raises InvalidSignature"""
from mcp.server.fastmcp.exceptions import InvalidSignature
def func_pipe_union_call_tool_result() -> str | CallToolResult:
return CallToolResult(content=[])
with pytest.raises(InvalidSignature) as exc_info:
func_metadata(func_pipe_union_call_tool_result)
assert "Union or Optional" in str(exc_info.value)
assert "CallToolResult" in str(exc_info.value)
def test_structured_output_with_field_descriptions():
"""Test that Field descriptions are preserved in structured output"""

View file

@ -391,6 +391,47 @@ async def test_both_content_and_valid_dict_with_output_schema():
assert result.structuredContent == {"sentiment": "positive", "confidence": 0.95}
@pytest.mark.anyio
async def test_tool_call_result():
"""Test returning ToolCallResult when no outputSchema is defined."""
tools = [
Tool(
name="get_info",
description="Get structured information",
inputSchema={
"type": "object",
"properties": {},
},
# No outputSchema for direct return of tool call result
)
]
async def call_tool_handler(name: str, arguments: dict[str, Any]) -> CallToolResult:
if name == "get_info":
return CallToolResult(
content=[TextContent(type="text", text="Results calculated")],
structuredContent={"status": "ok", "data": {"value": 42}},
_meta={"some": "metadata"},
)
else:
raise ValueError(f"Unknown tool: {name}")
async def test_callback(client_session: ClientSession) -> CallToolResult:
return await client_session.call_tool("get_info", {})
result = await run_tool_test(tools, call_tool_handler, test_callback)
# Verify results
assert result is not None
assert not result.isError
assert len(result.content) == 1
assert result.content[0].type == "text"
assert result.content[0].text == "Results calculated"
assert isinstance(result.content[0], TextContent)
assert result.structuredContent == {"status": "ok", "data": {"value": 42}}
assert result.meta == {"some": "metadata"}
@pytest.mark.anyio
async def test_output_schema_type_validation():
"""Test outputSchema validates types correctly."""

View file

@ -44,6 +44,23 @@ async def test_complex_inputs():
assert result.content[2].text == "charlie"
@pytest.mark.anyio
async def test_direct_call_tool_result_return():
"""Test the CallToolResult echo server"""
from examples.fastmcp.direct_call_tool_result_return import mcp
async with client_session(mcp._mcp_server) as client:
result = await client.call_tool("echo", {"text": "hello"})
assert len(result.content) == 1
content = result.content[0]
assert isinstance(content, TextContent)
assert content.text == "hello"
assert result.structuredContent
assert result.structuredContent["text"] == "hello"
assert isinstance(result.meta, dict)
assert result.meta["some"] == "metadata"
@pytest.mark.anyio
async def test_desktop(monkeypatch: pytest.MonkeyPatch):
"""Test the desktop server"""