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:
parent
40acbc596c
commit
1200ba0082
2 changed files with 445 additions and 5 deletions
|
|
@ -1,9 +1,10 @@
|
|||
import inspect
|
||||
import json
|
||||
import types
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from itertools import chain
|
||||
from types import GenericAlias
|
||||
from typing import Annotated, Any, ForwardRef, cast, get_args, get_origin, get_type_hints
|
||||
from typing import Annotated, Any, ForwardRef, Union, cast, get_args, get_origin, get_type_hints
|
||||
|
||||
import pydantic_core
|
||||
from pydantic import (
|
||||
|
|
@ -22,7 +23,7 @@ from pydantic_core import PydanticUndefined
|
|||
from mcp.server.fastmcp.exceptions import InvalidSignature
|
||||
from mcp.server.fastmcp.utilities.logging import get_logger
|
||||
from mcp.server.fastmcp.utilities.types import Audio, Image
|
||||
from mcp.types import ContentBlock, TextContent
|
||||
from mcp.types import CallToolResult, ContentBlock, TextContent
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
|
@ -104,6 +105,12 @@ class FuncMetadata(BaseModel):
|
|||
from function return values, whereas the lowlevel server simply serializes
|
||||
the structured output.
|
||||
"""
|
||||
if isinstance(result, CallToolResult):
|
||||
if self.output_schema is not None:
|
||||
assert self.output_model is not None, "Output model must be set if output schema is defined"
|
||||
self.output_model.model_validate(result.structuredContent)
|
||||
return result
|
||||
|
||||
unstructured_content = _convert_to_content(result)
|
||||
|
||||
if self.output_schema is None:
|
||||
|
|
@ -268,6 +275,26 @@ def func_metadata(
|
|||
output_info = FieldInfo.from_annotation(_get_typed_annotation(sig.return_annotation, globalns))
|
||||
annotation = output_info.annotation
|
||||
|
||||
# Reject CallToolResult in Union types (including Optional)
|
||||
# Handle both typing.Union (Union[X, Y]) and types.UnionType (X | Y)
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is types.UnionType:
|
||||
args = get_args(annotation)
|
||||
# Check if CallToolResult appears in the union (excluding None for Optional check)
|
||||
if any(isinstance(arg, type) and issubclass(arg, CallToolResult) for arg in args if arg is not type(None)):
|
||||
raise InvalidSignature(
|
||||
f"Function {func.__name__}: CallToolResult cannot be used in Union or Optional types. "
|
||||
"To return empty results, use: CallToolResult(content=[])"
|
||||
)
|
||||
|
||||
# if the typehint is CallToolResult, the user either intends to return without validation
|
||||
# or they provided validation as Annotated metadata
|
||||
if isinstance(annotation, type) and issubclass(annotation, CallToolResult):
|
||||
if output_info.metadata:
|
||||
annotation = output_info.metadata[0]
|
||||
else:
|
||||
return FuncMetadata(arg_model=arguments_model)
|
||||
|
||||
output_model, output_schema, wrap_output = _try_create_model_and_schema(annotation, func.__name__, output_info)
|
||||
|
||||
if output_model is None and structured_output is True:
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ class Server(Generic[LifespanResultT, RequestT]):
|
|||
def decorator(
|
||||
func: Callable[
|
||||
...,
|
||||
Awaitable[UnstructuredContent | StructuredContent | CombinationContent],
|
||||
Awaitable[UnstructuredContent | StructuredContent | CombinationContent | types.CallToolResult],
|
||||
],
|
||||
):
|
||||
logger.debug("Registering handler for CallToolRequest")
|
||||
|
|
@ -504,7 +504,9 @@ class Server(Generic[LifespanResultT, RequestT]):
|
|||
# output normalization
|
||||
unstructured_content: UnstructuredContent
|
||||
maybe_structured_content: StructuredContent | None
|
||||
if isinstance(results, tuple) and len(results) == 2:
|
||||
if isinstance(results, types.CallToolResult):
|
||||
return types.ServerResult(results)
|
||||
elif isinstance(results, tuple) and len(results) == 2:
|
||||
# tool returned both structured and unstructured content
|
||||
unstructured_content, maybe_structured_content = cast(CombinationContent, results)
|
||||
elif isinstance(results, dict):
|
||||
|
|
|
|||
Loading…
Reference in a new issue