fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fastmcp/telemetry.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentation for FastMCP.
|
|
2
|
+
|
|
3
|
+
This module provides native OpenTelemetry integration for FastMCP servers and clients.
|
|
4
|
+
It uses only the opentelemetry-api package, so telemetry is a no-op unless the user
|
|
5
|
+
installs an OpenTelemetry SDK and configures exporters.
|
|
6
|
+
|
|
7
|
+
Example usage with SDK:
|
|
8
|
+
```python
|
|
9
|
+
from opentelemetry import trace
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
11
|
+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
|
|
12
|
+
|
|
13
|
+
# Configure the SDK (user responsibility)
|
|
14
|
+
provider = TracerProvider()
|
|
15
|
+
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
|
|
16
|
+
trace.set_tracer_provider(provider)
|
|
17
|
+
|
|
18
|
+
# Now FastMCP will emit traces
|
|
19
|
+
from fastmcp import FastMCP
|
|
20
|
+
mcp = FastMCP("my-server")
|
|
21
|
+
```
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from opentelemetry import context as otel_context
|
|
27
|
+
from opentelemetry import propagate, trace
|
|
28
|
+
from opentelemetry.context import Context
|
|
29
|
+
from opentelemetry.trace import Span, Status, StatusCode, Tracer
|
|
30
|
+
from opentelemetry.trace import get_tracer as otel_get_tracer
|
|
31
|
+
|
|
32
|
+
INSTRUMENTATION_NAME = "fastmcp"
|
|
33
|
+
|
|
34
|
+
TRACE_PARENT_KEY = "fastmcp.traceparent"
|
|
35
|
+
TRACE_STATE_KEY = "fastmcp.tracestate"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_tracer(version: str | None = None) -> Tracer:
|
|
39
|
+
"""Get the FastMCP tracer for creating spans.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
version: Optional version string for the instrumentation
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A tracer instance. Returns a no-op tracer if no SDK is configured.
|
|
46
|
+
"""
|
|
47
|
+
return otel_get_tracer(INSTRUMENTATION_NAME, version)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def inject_trace_context(
|
|
51
|
+
meta: dict[str, Any] | None = None,
|
|
52
|
+
) -> dict[str, Any] | None:
|
|
53
|
+
"""Inject current trace context into a meta dict for MCP request propagation.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
meta: Optional existing meta dict to merge with trace context
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
A new dict containing the original meta (if any) plus trace context keys,
|
|
60
|
+
or None if no trace context to inject and meta was None
|
|
61
|
+
"""
|
|
62
|
+
carrier: dict[str, str] = {}
|
|
63
|
+
propagate.inject(carrier)
|
|
64
|
+
|
|
65
|
+
trace_meta: dict[str, Any] = {}
|
|
66
|
+
if "traceparent" in carrier:
|
|
67
|
+
trace_meta[TRACE_PARENT_KEY] = carrier["traceparent"]
|
|
68
|
+
if "tracestate" in carrier:
|
|
69
|
+
trace_meta[TRACE_STATE_KEY] = carrier["tracestate"]
|
|
70
|
+
|
|
71
|
+
if trace_meta:
|
|
72
|
+
return {**(meta or {}), **trace_meta}
|
|
73
|
+
return meta
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def record_span_error(span: Span, exception: BaseException) -> None:
|
|
77
|
+
"""Record an exception on a span and set error status."""
|
|
78
|
+
span.record_exception(exception)
|
|
79
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def extract_trace_context(meta: dict[str, Any] | None) -> Context:
|
|
83
|
+
"""Extract trace context from an MCP request meta dict.
|
|
84
|
+
|
|
85
|
+
If already in a valid trace (e.g., from HTTP propagation), the existing
|
|
86
|
+
trace context is preserved and meta is not used.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
meta: The meta dict from an MCP request (ctx.request_context.meta)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
An OpenTelemetry Context with the extracted trace context,
|
|
93
|
+
or the current context if no trace context found or already in a trace
|
|
94
|
+
"""
|
|
95
|
+
# Don't override existing trace context (e.g., from HTTP propagation)
|
|
96
|
+
current_span = trace.get_current_span()
|
|
97
|
+
if current_span.get_span_context().is_valid:
|
|
98
|
+
return otel_context.get_current()
|
|
99
|
+
|
|
100
|
+
if not meta:
|
|
101
|
+
return otel_context.get_current()
|
|
102
|
+
|
|
103
|
+
carrier: dict[str, str] = {}
|
|
104
|
+
if TRACE_PARENT_KEY in meta:
|
|
105
|
+
carrier["traceparent"] = str(meta[TRACE_PARENT_KEY])
|
|
106
|
+
if TRACE_STATE_KEY in meta:
|
|
107
|
+
carrier["tracestate"] = str(meta[TRACE_STATE_KEY])
|
|
108
|
+
|
|
109
|
+
if carrier:
|
|
110
|
+
return propagate.extract(carrier)
|
|
111
|
+
return otel_context.get_current()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = [
|
|
115
|
+
"INSTRUMENTATION_NAME",
|
|
116
|
+
"TRACE_PARENT_KEY",
|
|
117
|
+
"TRACE_STATE_KEY",
|
|
118
|
+
"extract_trace_context",
|
|
119
|
+
"get_tracer",
|
|
120
|
+
"inject_trace_context",
|
|
121
|
+
"record_span_error",
|
|
122
|
+
]
|
fastmcp/tools/__init__.py
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
1
|
+
from .function_tool import FunctionTool, tool
|
|
2
|
+
from .tool import Tool, ToolResult
|
|
3
3
|
from .tool_transform import forward, forward_raw
|
|
4
4
|
|
|
5
|
-
__all__ = [
|
|
5
|
+
__all__ = [
|
|
6
|
+
"FunctionTool",
|
|
7
|
+
"Tool",
|
|
8
|
+
"ToolResult",
|
|
9
|
+
"forward",
|
|
10
|
+
"forward_raw",
|
|
11
|
+
"tool",
|
|
12
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Function introspection and schema generation for FastMCP tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Generic, get_type_hints
|
|
9
|
+
|
|
10
|
+
import mcp.types
|
|
11
|
+
from pydantic import PydanticSchemaGenerationError
|
|
12
|
+
from typing_extensions import TypeVar as TypeVarExt
|
|
13
|
+
|
|
14
|
+
from fastmcp.server.dependencies import (
|
|
15
|
+
transform_context_annotations,
|
|
16
|
+
without_injected_parameters,
|
|
17
|
+
)
|
|
18
|
+
from fastmcp.tools.tool import ToolResult
|
|
19
|
+
from fastmcp.utilities.json_schema import compress_schema
|
|
20
|
+
from fastmcp.utilities.logging import get_logger
|
|
21
|
+
from fastmcp.utilities.types import (
|
|
22
|
+
Audio,
|
|
23
|
+
File,
|
|
24
|
+
Image,
|
|
25
|
+
create_function_without_params,
|
|
26
|
+
get_cached_typeadapter,
|
|
27
|
+
replace_type,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
T = TypeVarExt("T", default=Any)
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class _WrappedResult(Generic[T]):
|
|
37
|
+
"""Generic wrapper for non-object return types."""
|
|
38
|
+
|
|
39
|
+
result: T
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _UnserializableType:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_object_schema(schema: dict[str, Any]) -> bool:
|
|
47
|
+
"""Check if a JSON schema represents an object type."""
|
|
48
|
+
# Direct object type
|
|
49
|
+
if schema.get("type") == "object":
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
# Schema with properties but no explicit type is treated as object
|
|
53
|
+
if "properties" in schema:
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
# Self-referencing types use $ref pointing to $defs
|
|
57
|
+
# The referenced type is always an object in our use case
|
|
58
|
+
return "$ref" in schema and "$defs" in schema
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ParsedFunction:
|
|
63
|
+
fn: Callable[..., Any]
|
|
64
|
+
name: str
|
|
65
|
+
description: str | None
|
|
66
|
+
input_schema: dict[str, Any]
|
|
67
|
+
output_schema: dict[str, Any] | None
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_function(
|
|
71
|
+
cls,
|
|
72
|
+
fn: Callable[..., Any],
|
|
73
|
+
exclude_args: list[str] | None = None,
|
|
74
|
+
validate: bool = True,
|
|
75
|
+
wrap_non_object_output_schema: bool = True,
|
|
76
|
+
) -> ParsedFunction:
|
|
77
|
+
if validate:
|
|
78
|
+
sig = inspect.signature(fn)
|
|
79
|
+
# Reject functions with *args or **kwargs
|
|
80
|
+
for param in sig.parameters.values():
|
|
81
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
82
|
+
raise ValueError("Functions with *args are not supported as tools")
|
|
83
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"Functions with **kwargs are not supported as tools"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Reject exclude_args that don't exist in the function or don't have a default value
|
|
89
|
+
if exclude_args:
|
|
90
|
+
for arg_name in exclude_args:
|
|
91
|
+
if arg_name not in sig.parameters:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Parameter '{arg_name}' in exclude_args does not exist in function."
|
|
94
|
+
)
|
|
95
|
+
param = sig.parameters[arg_name]
|
|
96
|
+
if param.default == inspect.Parameter.empty:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Parameter '{arg_name}' in exclude_args must have a default value."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# collect name and doc before we potentially modify the function
|
|
102
|
+
fn_name = getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
103
|
+
fn_doc = inspect.getdoc(fn)
|
|
104
|
+
|
|
105
|
+
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
106
|
+
if not inspect.isroutine(fn):
|
|
107
|
+
fn = fn.__call__
|
|
108
|
+
# if the fn is a staticmethod, we need to work with the underlying function
|
|
109
|
+
if isinstance(fn, staticmethod):
|
|
110
|
+
fn = fn.__func__
|
|
111
|
+
|
|
112
|
+
# Transform Context type annotations to Depends() for unified DI
|
|
113
|
+
fn = transform_context_annotations(fn)
|
|
114
|
+
|
|
115
|
+
# Handle injected parameters (Context, Docket dependencies)
|
|
116
|
+
wrapper_fn = without_injected_parameters(fn)
|
|
117
|
+
|
|
118
|
+
# Also handle exclude_args with non-serializable types (issue #2431)
|
|
119
|
+
# This must happen before Pydantic tries to serialize the parameters
|
|
120
|
+
if exclude_args:
|
|
121
|
+
wrapper_fn = create_function_without_params(wrapper_fn, list(exclude_args))
|
|
122
|
+
|
|
123
|
+
input_type_adapter = get_cached_typeadapter(wrapper_fn)
|
|
124
|
+
input_schema = input_type_adapter.json_schema()
|
|
125
|
+
|
|
126
|
+
# Compress and handle exclude_args
|
|
127
|
+
prune_params = list(exclude_args) if exclude_args else None
|
|
128
|
+
input_schema = compress_schema(
|
|
129
|
+
input_schema, prune_params=prune_params, prune_titles=True
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
output_schema = None
|
|
133
|
+
# Get the return annotation from the signature
|
|
134
|
+
sig = inspect.signature(fn)
|
|
135
|
+
output_type = sig.return_annotation
|
|
136
|
+
|
|
137
|
+
# If the annotation is a string (from __future__ annotations), resolve it
|
|
138
|
+
if isinstance(output_type, str):
|
|
139
|
+
try:
|
|
140
|
+
# Use get_type_hints to resolve the return type
|
|
141
|
+
# include_extras=True preserves Annotated metadata
|
|
142
|
+
type_hints = get_type_hints(fn, include_extras=True)
|
|
143
|
+
output_type = type_hints.get("return", output_type)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
# If resolution fails, keep the string annotation
|
|
146
|
+
logger.debug("Failed to resolve type hint for return annotation: %s", e)
|
|
147
|
+
|
|
148
|
+
if output_type not in (inspect._empty, None, Any, ...):
|
|
149
|
+
# there are a variety of types that we don't want to attempt to
|
|
150
|
+
# serialize because they are either used by FastMCP internally,
|
|
151
|
+
# or are MCP content types that explicitly don't form structured
|
|
152
|
+
# content. By replacing them with an explicitly unserializable type,
|
|
153
|
+
# we ensure that no output schema is automatically generated.
|
|
154
|
+
clean_output_type = replace_type(
|
|
155
|
+
output_type,
|
|
156
|
+
dict.fromkeys(
|
|
157
|
+
(
|
|
158
|
+
Image,
|
|
159
|
+
Audio,
|
|
160
|
+
File,
|
|
161
|
+
ToolResult,
|
|
162
|
+
mcp.types.TextContent,
|
|
163
|
+
mcp.types.ImageContent,
|
|
164
|
+
mcp.types.AudioContent,
|
|
165
|
+
mcp.types.ResourceLink,
|
|
166
|
+
mcp.types.EmbeddedResource,
|
|
167
|
+
),
|
|
168
|
+
_UnserializableType,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
type_adapter = get_cached_typeadapter(clean_output_type)
|
|
174
|
+
base_schema = type_adapter.json_schema(mode="serialization")
|
|
175
|
+
|
|
176
|
+
# Generate schema for wrapped type if it's non-object
|
|
177
|
+
# because MCP requires that output schemas are objects
|
|
178
|
+
# Check if schema is an object type, resolving $ref references
|
|
179
|
+
# (self-referencing types use $ref at root level)
|
|
180
|
+
if wrap_non_object_output_schema and not _is_object_schema(base_schema):
|
|
181
|
+
# Use the wrapped result schema directly
|
|
182
|
+
wrapped_type = _WrappedResult[clean_output_type]
|
|
183
|
+
wrapped_adapter = get_cached_typeadapter(wrapped_type)
|
|
184
|
+
output_schema = wrapped_adapter.json_schema(mode="serialization")
|
|
185
|
+
output_schema["x-fastmcp-wrap-result"] = True
|
|
186
|
+
else:
|
|
187
|
+
output_schema = base_schema
|
|
188
|
+
|
|
189
|
+
output_schema = compress_schema(output_schema, prune_titles=True)
|
|
190
|
+
|
|
191
|
+
except PydanticSchemaGenerationError as e:
|
|
192
|
+
if "_UnserializableType" not in str(e):
|
|
193
|
+
logger.debug(f"Unable to generate schema for type {output_type!r}")
|
|
194
|
+
|
|
195
|
+
return cls(
|
|
196
|
+
fn=fn,
|
|
197
|
+
name=fn_name,
|
|
198
|
+
description=fn_doc,
|
|
199
|
+
input_schema=input_schema,
|
|
200
|
+
output_schema=output_schema or None,
|
|
201
|
+
)
|