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/tools/tool.py
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import inspect
|
|
4
3
|
import warnings
|
|
5
4
|
from collections.abc import Callable
|
|
6
|
-
from dataclasses import dataclass
|
|
7
5
|
from typing import (
|
|
8
6
|
TYPE_CHECKING,
|
|
9
7
|
Annotated,
|
|
10
8
|
Any,
|
|
11
|
-
|
|
9
|
+
ClassVar,
|
|
12
10
|
TypeAlias,
|
|
13
|
-
|
|
11
|
+
overload,
|
|
14
12
|
)
|
|
15
13
|
|
|
16
14
|
import mcp.types
|
|
@@ -25,14 +23,10 @@ from mcp.types import (
|
|
|
25
23
|
ToolExecution,
|
|
26
24
|
)
|
|
27
25
|
from mcp.types import Tool as MCPTool
|
|
28
|
-
from pydantic import
|
|
29
|
-
from typing_extensions import TypeVar
|
|
26
|
+
from pydantic import BaseModel, Field, model_validator
|
|
30
27
|
|
|
31
|
-
import
|
|
32
|
-
from fastmcp.server.dependencies import get_context, without_injected_parameters
|
|
33
|
-
from fastmcp.server.tasks.config import TaskConfig
|
|
28
|
+
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
|
|
34
29
|
from fastmcp.utilities.components import FastMCPComponent
|
|
35
|
-
from fastmcp.utilities.json_schema import compress_schema, resolve_root_ref
|
|
36
30
|
from fastmcp.utilities.logging import get_logger
|
|
37
31
|
from fastmcp.utilities.types import (
|
|
38
32
|
Audio,
|
|
@@ -40,28 +34,22 @@ from fastmcp.utilities.types import (
|
|
|
40
34
|
Image,
|
|
41
35
|
NotSet,
|
|
42
36
|
NotSetT,
|
|
43
|
-
create_function_without_params,
|
|
44
|
-
get_cached_typeadapter,
|
|
45
|
-
replace_type,
|
|
46
37
|
)
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
logger = get_logger(__name__)
|
|
52
|
-
|
|
53
|
-
T = TypeVar("T", default=Any)
|
|
39
|
+
# Runtime type alias for auth checks to avoid circular imports with authorization.py
|
|
40
|
+
# AuthCheck is Callable[[AuthContext], bool] but we use Any to avoid the import
|
|
41
|
+
AuthCheckCallable: TypeAlias = Callable[[Any], bool]
|
|
54
42
|
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from docket import Docket
|
|
45
|
+
from docket.execution import Execution
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"""Generic wrapper for non-object return types."""
|
|
59
|
-
|
|
60
|
-
result: T
|
|
47
|
+
from fastmcp.tools.function_tool import FunctionTool
|
|
48
|
+
from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
|
|
61
49
|
|
|
50
|
+
# Re-export from function_tool module
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
pass
|
|
52
|
+
logger = get_logger(__name__)
|
|
65
53
|
|
|
66
54
|
|
|
67
55
|
ToolResultSerializerType: TypeAlias = Callable[[Any], str]
|
|
@@ -71,7 +59,17 @@ def default_serializer(data: Any) -> str:
|
|
|
71
59
|
return pydantic_core.to_json(data, fallback=str).decode()
|
|
72
60
|
|
|
73
61
|
|
|
74
|
-
class ToolResult:
|
|
62
|
+
class ToolResult(BaseModel):
|
|
63
|
+
content: list[ContentBlock] = Field(
|
|
64
|
+
description="List of content blocks for the tool result"
|
|
65
|
+
)
|
|
66
|
+
structured_content: dict[str, Any] | None = Field(
|
|
67
|
+
default=None, description="Structured content matching the tool's output schema"
|
|
68
|
+
)
|
|
69
|
+
meta: dict[str, Any] | None = Field(
|
|
70
|
+
default=None, description="Runtime metadata about the tool execution"
|
|
71
|
+
)
|
|
72
|
+
|
|
75
73
|
def __init__(
|
|
76
74
|
self,
|
|
77
75
|
content: list[ContentBlock] | Any | None = None,
|
|
@@ -83,8 +81,7 @@ class ToolResult:
|
|
|
83
81
|
elif content is None:
|
|
84
82
|
content = structured_content
|
|
85
83
|
|
|
86
|
-
|
|
87
|
-
self.meta: dict[str, Any] | None = meta
|
|
84
|
+
converted_content: list[ContentBlock] = _convert_to_content(result=content)
|
|
88
85
|
|
|
89
86
|
if structured_content is not None:
|
|
90
87
|
try:
|
|
@@ -102,7 +99,10 @@ class ToolResult:
|
|
|
102
99
|
f"Got {type(structured_content).__name__}: {structured_content!r}. "
|
|
103
100
|
"Tools should wrap non-dict values based on their output_schema."
|
|
104
101
|
)
|
|
105
|
-
|
|
102
|
+
|
|
103
|
+
super().__init__(
|
|
104
|
+
content=converted_content, structured_content=structured_content, meta=meta
|
|
105
|
+
)
|
|
106
106
|
|
|
107
107
|
def to_mcp_result(
|
|
108
108
|
self,
|
|
@@ -113,7 +113,7 @@ class ToolResult:
|
|
|
113
113
|
return CallToolResult(
|
|
114
114
|
structuredContent=self.structured_content,
|
|
115
115
|
content=self.content,
|
|
116
|
-
_meta=self.meta,
|
|
116
|
+
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
|
117
117
|
)
|
|
118
118
|
if self.structured_content is None:
|
|
119
119
|
return self.content
|
|
@@ -123,6 +123,8 @@ class ToolResult:
|
|
|
123
123
|
class Tool(FastMCPComponent):
|
|
124
124
|
"""Internal tool registration info."""
|
|
125
125
|
|
|
126
|
+
KEY_PREFIX: ClassVar[str] = "tool"
|
|
127
|
+
|
|
126
128
|
parameters: Annotated[
|
|
127
129
|
dict[str, Any], Field(description="JSON schema for tool parameters")
|
|
128
130
|
]
|
|
@@ -139,7 +141,19 @@ class Tool(FastMCPComponent):
|
|
|
139
141
|
] = None
|
|
140
142
|
serializer: Annotated[
|
|
141
143
|
ToolResultSerializerType | None,
|
|
142
|
-
Field(
|
|
144
|
+
Field(
|
|
145
|
+
description="Deprecated. Return ToolResult from your tools for full control over serialization."
|
|
146
|
+
),
|
|
147
|
+
] = None
|
|
148
|
+
auth: Annotated[
|
|
149
|
+
AuthCheckCallable | list[AuthCheckCallable] | None,
|
|
150
|
+
Field(description="Authorization checks for this tool", exclude=True),
|
|
151
|
+
] = None
|
|
152
|
+
timeout: Annotated[
|
|
153
|
+
float | None,
|
|
154
|
+
Field(
|
|
155
|
+
description="Execution timeout in seconds. If None, no timeout is applied."
|
|
156
|
+
),
|
|
143
157
|
] = None
|
|
144
158
|
|
|
145
159
|
@model_validator(mode="after")
|
|
@@ -148,26 +162,8 @@ class Tool(FastMCPComponent):
|
|
|
148
162
|
validate_and_warn_tool_name(self.name)
|
|
149
163
|
return self
|
|
150
164
|
|
|
151
|
-
def enable(self) -> None:
|
|
152
|
-
super().enable()
|
|
153
|
-
try:
|
|
154
|
-
context = get_context()
|
|
155
|
-
context._queue_tool_list_changed() # type: ignore[private-use]
|
|
156
|
-
except RuntimeError:
|
|
157
|
-
pass # No context available
|
|
158
|
-
|
|
159
|
-
def disable(self) -> None:
|
|
160
|
-
super().disable()
|
|
161
|
-
try:
|
|
162
|
-
context = get_context()
|
|
163
|
-
context._queue_tool_list_changed() # type: ignore[private-use]
|
|
164
|
-
except RuntimeError:
|
|
165
|
-
pass # No context available
|
|
166
|
-
|
|
167
165
|
def to_mcp_tool(
|
|
168
166
|
self,
|
|
169
|
-
*,
|
|
170
|
-
include_fastmcp_meta: bool | None = None,
|
|
171
167
|
**overrides: Any,
|
|
172
168
|
) -> MCPTool:
|
|
173
169
|
"""Convert the FastMCP tool to an MCP tool."""
|
|
@@ -187,15 +183,18 @@ class Tool(FastMCPComponent):
|
|
|
187
183
|
icons=overrides.get("icons", self.icons),
|
|
188
184
|
annotations=overrides.get("annotations", self.annotations),
|
|
189
185
|
execution=overrides.get("execution", self.execution),
|
|
190
|
-
_meta=overrides.get(
|
|
191
|
-
"_meta", self.get_meta(
|
|
186
|
+
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
|
187
|
+
"_meta", self.get_meta()
|
|
192
188
|
),
|
|
193
189
|
)
|
|
194
190
|
|
|
195
|
-
@
|
|
191
|
+
@classmethod
|
|
196
192
|
def from_function(
|
|
193
|
+
cls,
|
|
197
194
|
fn: Callable[..., Any],
|
|
195
|
+
*,
|
|
198
196
|
name: str | None = None,
|
|
197
|
+
version: str | int | None = None,
|
|
199
198
|
title: str | None = None,
|
|
200
199
|
description: str | None = None,
|
|
201
200
|
icons: list[Icon] | None = None,
|
|
@@ -203,15 +202,19 @@ class Tool(FastMCPComponent):
|
|
|
203
202
|
annotations: ToolAnnotations | None = None,
|
|
204
203
|
exclude_args: list[str] | None = None,
|
|
205
204
|
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
206
|
-
serializer: ToolResultSerializerType | None = None,
|
|
205
|
+
serializer: ToolResultSerializerType | None = None, # Deprecated
|
|
207
206
|
meta: dict[str, Any] | None = None,
|
|
208
|
-
enabled: bool | None = None,
|
|
209
207
|
task: bool | TaskConfig | None = None,
|
|
208
|
+
timeout: float | None = None,
|
|
209
|
+
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
|
|
210
210
|
) -> FunctionTool:
|
|
211
211
|
"""Create a Tool from a function."""
|
|
212
|
+
from fastmcp.tools.function_tool import FunctionTool
|
|
213
|
+
|
|
212
214
|
return FunctionTool.from_function(
|
|
213
215
|
fn=fn,
|
|
214
216
|
name=name,
|
|
217
|
+
version=version,
|
|
215
218
|
title=title,
|
|
216
219
|
description=description,
|
|
217
220
|
icons=icons,
|
|
@@ -221,8 +224,9 @@ class Tool(FastMCPComponent):
|
|
|
221
224
|
output_schema=output_schema,
|
|
222
225
|
serializer=serializer,
|
|
223
226
|
meta=meta,
|
|
224
|
-
enabled=enabled,
|
|
225
227
|
task=task,
|
|
228
|
+
timeout=timeout,
|
|
229
|
+
auth=auth,
|
|
226
230
|
)
|
|
227
231
|
|
|
228
232
|
async def run(self, arguments: dict[str, Any]) -> ToolResult:
|
|
@@ -237,6 +241,127 @@ class Tool(FastMCPComponent):
|
|
|
237
241
|
"""
|
|
238
242
|
raise NotImplementedError("Subclasses must implement run()")
|
|
239
243
|
|
|
244
|
+
def convert_result(self, raw_value: Any) -> ToolResult:
|
|
245
|
+
"""Convert a raw result to ToolResult.
|
|
246
|
+
|
|
247
|
+
Handles ToolResult passthrough and converts raw values using the tool's
|
|
248
|
+
attributes (serializer, output_schema) for proper conversion.
|
|
249
|
+
"""
|
|
250
|
+
if isinstance(raw_value, ToolResult):
|
|
251
|
+
return raw_value
|
|
252
|
+
|
|
253
|
+
content = _convert_to_content(raw_value, serializer=self.serializer)
|
|
254
|
+
|
|
255
|
+
# Skip structured content for ContentBlock types only if no output_schema
|
|
256
|
+
# (if output_schema exists, MCP SDK requires structured_content)
|
|
257
|
+
if self.output_schema is None and (
|
|
258
|
+
isinstance(raw_value, ContentBlock | Audio | Image | File)
|
|
259
|
+
or (
|
|
260
|
+
isinstance(raw_value, list | tuple)
|
|
261
|
+
and any(isinstance(item, ContentBlock) for item in raw_value)
|
|
262
|
+
)
|
|
263
|
+
):
|
|
264
|
+
return ToolResult(content=content)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
structured = pydantic_core.to_jsonable_python(raw_value)
|
|
268
|
+
except pydantic_core.PydanticSerializationError:
|
|
269
|
+
return ToolResult(content=content)
|
|
270
|
+
|
|
271
|
+
if self.output_schema is None:
|
|
272
|
+
# No schema - only use structured_content for dicts
|
|
273
|
+
if isinstance(structured, dict):
|
|
274
|
+
return ToolResult(content=content, structured_content=structured)
|
|
275
|
+
return ToolResult(content=content)
|
|
276
|
+
|
|
277
|
+
# Has output_schema - wrap if x-fastmcp-wrap-result is set
|
|
278
|
+
wrap_result = self.output_schema.get("x-fastmcp-wrap-result")
|
|
279
|
+
return ToolResult(
|
|
280
|
+
content=content,
|
|
281
|
+
structured_content={"result": structured} if wrap_result else structured,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@overload
|
|
285
|
+
async def _run(
|
|
286
|
+
self,
|
|
287
|
+
arguments: dict[str, Any],
|
|
288
|
+
task_meta: None = None,
|
|
289
|
+
) -> ToolResult: ...
|
|
290
|
+
|
|
291
|
+
@overload
|
|
292
|
+
async def _run(
|
|
293
|
+
self,
|
|
294
|
+
arguments: dict[str, Any],
|
|
295
|
+
task_meta: TaskMeta,
|
|
296
|
+
) -> mcp.types.CreateTaskResult: ...
|
|
297
|
+
|
|
298
|
+
async def _run(
|
|
299
|
+
self,
|
|
300
|
+
arguments: dict[str, Any],
|
|
301
|
+
task_meta: TaskMeta | None = None,
|
|
302
|
+
) -> ToolResult | mcp.types.CreateTaskResult:
|
|
303
|
+
"""Server entry point that handles task routing.
|
|
304
|
+
|
|
305
|
+
This allows ANY Tool subclass to support background execution by setting
|
|
306
|
+
task_config.mode to "supported" or "required". The server calls this
|
|
307
|
+
method instead of run() directly.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
arguments: Tool arguments
|
|
311
|
+
task_meta: If provided, execute as background task and return
|
|
312
|
+
CreateTaskResult. If None (default), execute synchronously and
|
|
313
|
+
return ToolResult.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
ToolResult when task_meta is None.
|
|
317
|
+
CreateTaskResult when task_meta is provided.
|
|
318
|
+
|
|
319
|
+
Subclasses can override this to customize task routing behavior.
|
|
320
|
+
For example, FastMCPProviderTool overrides to delegate to child
|
|
321
|
+
middleware without submitting to Docket.
|
|
322
|
+
"""
|
|
323
|
+
from fastmcp.server.tasks.routing import check_background_task
|
|
324
|
+
|
|
325
|
+
task_result = await check_background_task(
|
|
326
|
+
component=self,
|
|
327
|
+
task_type="tool",
|
|
328
|
+
arguments=arguments,
|
|
329
|
+
task_meta=task_meta,
|
|
330
|
+
)
|
|
331
|
+
if task_result:
|
|
332
|
+
return task_result
|
|
333
|
+
|
|
334
|
+
return await self.run(arguments)
|
|
335
|
+
|
|
336
|
+
def register_with_docket(self, docket: Docket) -> None:
|
|
337
|
+
"""Register this tool with docket for background execution."""
|
|
338
|
+
if not self.task_config.supports_tasks():
|
|
339
|
+
return
|
|
340
|
+
docket.register(self.run, names=[self.key])
|
|
341
|
+
|
|
342
|
+
async def add_to_docket( # type: ignore[override]
|
|
343
|
+
self,
|
|
344
|
+
docket: Docket,
|
|
345
|
+
arguments: dict[str, Any],
|
|
346
|
+
*,
|
|
347
|
+
fn_key: str | None = None,
|
|
348
|
+
task_key: str | None = None,
|
|
349
|
+
**kwargs: Any,
|
|
350
|
+
) -> Execution:
|
|
351
|
+
"""Schedule this tool for background execution via docket.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
docket: The Docket instance
|
|
355
|
+
arguments: Tool arguments
|
|
356
|
+
fn_key: Function lookup key in Docket registry (defaults to self.key)
|
|
357
|
+
task_key: Redis storage key for the result
|
|
358
|
+
**kwargs: Additional kwargs passed to docket.add()
|
|
359
|
+
"""
|
|
360
|
+
lookup_key = fn_key or self.key
|
|
361
|
+
if task_key:
|
|
362
|
+
kwargs["key"] = task_key
|
|
363
|
+
return await docket.add(lookup_key, **kwargs)(arguments)
|
|
364
|
+
|
|
240
365
|
@classmethod
|
|
241
366
|
def from_tool(
|
|
242
367
|
cls,
|
|
@@ -248,10 +373,9 @@ class Tool(FastMCPComponent):
|
|
|
248
373
|
tags: set[str] | None = None,
|
|
249
374
|
annotations: ToolAnnotations | NotSetT | None = NotSet,
|
|
250
375
|
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
251
|
-
serializer: ToolResultSerializerType | None = None,
|
|
376
|
+
serializer: ToolResultSerializerType | None = None, # Deprecated
|
|
252
377
|
meta: dict[str, Any] | NotSetT | None = NotSet,
|
|
253
378
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
254
|
-
enabled: bool | None = None,
|
|
255
379
|
transform_fn: Callable[..., Any] | None = None,
|
|
256
380
|
) -> TransformedTool:
|
|
257
381
|
from fastmcp.tools.tool_transform import TransformedTool
|
|
@@ -268,312 +392,13 @@ class Tool(FastMCPComponent):
|
|
|
268
392
|
output_schema=output_schema,
|
|
269
393
|
serializer=serializer,
|
|
270
394
|
meta=meta,
|
|
271
|
-
enabled=enabled,
|
|
272
395
|
)
|
|
273
396
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
Field(description="Background task execution configuration (SEP-1686)."),
|
|
280
|
-
] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
|
|
281
|
-
|
|
282
|
-
def to_mcp_tool(
|
|
283
|
-
self,
|
|
284
|
-
*,
|
|
285
|
-
include_fastmcp_meta: bool | None = None,
|
|
286
|
-
**overrides: Any,
|
|
287
|
-
) -> MCPTool:
|
|
288
|
-
"""Convert the FastMCP tool to an MCP tool.
|
|
289
|
-
|
|
290
|
-
Extends the base implementation to add task execution mode if enabled.
|
|
291
|
-
"""
|
|
292
|
-
# Get base MCP tool from parent
|
|
293
|
-
mcp_tool = super().to_mcp_tool(
|
|
294
|
-
include_fastmcp_meta=include_fastmcp_meta, **overrides
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
# Add task execution mode per SEP-1686
|
|
298
|
-
# Only set execution if not overridden and mode is not "forbidden"
|
|
299
|
-
if self.task_config.mode != "forbidden" and "execution" not in overrides:
|
|
300
|
-
mcp_tool.execution = ToolExecution(taskSupport=self.task_config.mode)
|
|
301
|
-
|
|
302
|
-
return mcp_tool
|
|
303
|
-
|
|
304
|
-
@classmethod
|
|
305
|
-
def from_function(
|
|
306
|
-
cls,
|
|
307
|
-
fn: Callable[..., Any],
|
|
308
|
-
name: str | None = None,
|
|
309
|
-
title: str | None = None,
|
|
310
|
-
description: str | None = None,
|
|
311
|
-
icons: list[Icon] | None = None,
|
|
312
|
-
tags: set[str] | None = None,
|
|
313
|
-
annotations: ToolAnnotations | None = None,
|
|
314
|
-
exclude_args: list[str] | None = None,
|
|
315
|
-
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
316
|
-
serializer: ToolResultSerializerType | None = None,
|
|
317
|
-
meta: dict[str, Any] | None = None,
|
|
318
|
-
enabled: bool | None = None,
|
|
319
|
-
task: bool | TaskConfig | None = None,
|
|
320
|
-
) -> FunctionTool:
|
|
321
|
-
"""Create a Tool from a function."""
|
|
322
|
-
if exclude_args and fastmcp.settings.deprecation_warnings:
|
|
323
|
-
warnings.warn(
|
|
324
|
-
"The `exclude_args` parameter is deprecated as of FastMCP 2.14. "
|
|
325
|
-
"Use dependency injection with `Depends()` instead for better lifecycle management. "
|
|
326
|
-
"See https://gofastmcp.com/servers/dependencies for examples.",
|
|
327
|
-
DeprecationWarning,
|
|
328
|
-
stacklevel=2,
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
parsed_fn = ParsedFunction.from_function(fn, exclude_args=exclude_args)
|
|
332
|
-
func_name = name or parsed_fn.name
|
|
333
|
-
|
|
334
|
-
if func_name == "<lambda>":
|
|
335
|
-
raise ValueError("You must provide a name for lambda functions")
|
|
336
|
-
|
|
337
|
-
# Normalize task to TaskConfig and validate
|
|
338
|
-
if task is None:
|
|
339
|
-
task_config = TaskConfig(mode="forbidden")
|
|
340
|
-
elif isinstance(task, bool):
|
|
341
|
-
task_config = TaskConfig.from_bool(task)
|
|
342
|
-
else:
|
|
343
|
-
task_config = task
|
|
344
|
-
task_config.validate_function(fn, func_name)
|
|
345
|
-
|
|
346
|
-
if isinstance(output_schema, NotSetT):
|
|
347
|
-
final_output_schema = parsed_fn.output_schema
|
|
348
|
-
else:
|
|
349
|
-
# At this point output_schema is not NotSetT, so it must be dict | None
|
|
350
|
-
final_output_schema = output_schema
|
|
351
|
-
# Note: explicit schemas (dict) are used as-is without auto-wrapping
|
|
352
|
-
|
|
353
|
-
# Validate that explicit schemas are object type for structured content
|
|
354
|
-
# (resolving $ref references for self-referencing types)
|
|
355
|
-
if final_output_schema is not None and isinstance(final_output_schema, dict):
|
|
356
|
-
if not _is_object_schema(final_output_schema):
|
|
357
|
-
raise ValueError(
|
|
358
|
-
f"Output schemas must represent object types due to MCP spec limitations. Received: {final_output_schema!r}"
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
return cls(
|
|
362
|
-
fn=parsed_fn.fn,
|
|
363
|
-
name=name or parsed_fn.name,
|
|
364
|
-
title=title,
|
|
365
|
-
description=description or parsed_fn.description,
|
|
366
|
-
icons=icons,
|
|
367
|
-
parameters=parsed_fn.input_schema,
|
|
368
|
-
output_schema=final_output_schema,
|
|
369
|
-
annotations=annotations,
|
|
370
|
-
tags=tags or set(),
|
|
371
|
-
serializer=serializer,
|
|
372
|
-
meta=meta,
|
|
373
|
-
enabled=enabled if enabled is not None else True,
|
|
374
|
-
task_config=task_config,
|
|
375
|
-
)
|
|
376
|
-
|
|
377
|
-
async def run(self, arguments: dict[str, Any]) -> ToolResult:
|
|
378
|
-
"""Run the tool with arguments."""
|
|
379
|
-
wrapper_fn = without_injected_parameters(self.fn)
|
|
380
|
-
type_adapter = get_cached_typeadapter(wrapper_fn)
|
|
381
|
-
result = type_adapter.validate_python(arguments)
|
|
382
|
-
if inspect.isawaitable(result):
|
|
383
|
-
result = await result
|
|
384
|
-
|
|
385
|
-
if isinstance(result, ToolResult):
|
|
386
|
-
return result
|
|
387
|
-
|
|
388
|
-
unstructured_result = _convert_to_content(result, serializer=self.serializer)
|
|
389
|
-
|
|
390
|
-
if self.output_schema is None:
|
|
391
|
-
# Do not produce a structured output for MCP Content Types
|
|
392
|
-
if isinstance(result, ContentBlock | Audio | Image | File) or (
|
|
393
|
-
isinstance(result, list | tuple)
|
|
394
|
-
and any(isinstance(item, ContentBlock) for item in result)
|
|
395
|
-
):
|
|
396
|
-
return ToolResult(content=unstructured_result)
|
|
397
|
-
|
|
398
|
-
# Otherwise, try to serialize the result as a dict
|
|
399
|
-
try:
|
|
400
|
-
structured_content = pydantic_core.to_jsonable_python(result)
|
|
401
|
-
if isinstance(structured_content, dict):
|
|
402
|
-
return ToolResult(
|
|
403
|
-
content=unstructured_result,
|
|
404
|
-
structured_content=structured_content,
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
except pydantic_core.PydanticSerializationError:
|
|
408
|
-
pass
|
|
409
|
-
|
|
410
|
-
return ToolResult(content=unstructured_result)
|
|
411
|
-
|
|
412
|
-
wrap_result = self.output_schema.get("x-fastmcp-wrap-result")
|
|
413
|
-
|
|
414
|
-
return ToolResult(
|
|
415
|
-
content=unstructured_result,
|
|
416
|
-
structured_content={"result": result} if wrap_result else result,
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def _is_object_schema(schema: dict[str, Any]) -> bool:
|
|
421
|
-
"""Check if a JSON schema represents an object type."""
|
|
422
|
-
# Direct object type
|
|
423
|
-
if schema.get("type") == "object":
|
|
424
|
-
return True
|
|
425
|
-
|
|
426
|
-
# Schema with properties but no explicit type is treated as object
|
|
427
|
-
if "properties" in schema:
|
|
428
|
-
return True
|
|
429
|
-
|
|
430
|
-
# Self-referencing types use $ref pointing to $defs
|
|
431
|
-
# The referenced type is always an object in our use case
|
|
432
|
-
return "$ref" in schema and "$defs" in schema
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
@dataclass
|
|
436
|
-
class ParsedFunction:
|
|
437
|
-
fn: Callable[..., Any]
|
|
438
|
-
name: str
|
|
439
|
-
description: str | None
|
|
440
|
-
input_schema: dict[str, Any]
|
|
441
|
-
output_schema: dict[str, Any] | None
|
|
442
|
-
|
|
443
|
-
@classmethod
|
|
444
|
-
def from_function(
|
|
445
|
-
cls,
|
|
446
|
-
fn: Callable[..., Any],
|
|
447
|
-
exclude_args: list[str] | None = None,
|
|
448
|
-
validate: bool = True,
|
|
449
|
-
wrap_non_object_output_schema: bool = True,
|
|
450
|
-
) -> ParsedFunction:
|
|
451
|
-
if validate:
|
|
452
|
-
sig = inspect.signature(fn)
|
|
453
|
-
# Reject functions with *args or **kwargs
|
|
454
|
-
for param in sig.parameters.values():
|
|
455
|
-
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
456
|
-
raise ValueError("Functions with *args are not supported as tools")
|
|
457
|
-
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
458
|
-
raise ValueError(
|
|
459
|
-
"Functions with **kwargs are not supported as tools"
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
# Reject exclude_args that don't exist in the function or don't have a default value
|
|
463
|
-
if exclude_args:
|
|
464
|
-
for arg_name in exclude_args:
|
|
465
|
-
if arg_name not in sig.parameters:
|
|
466
|
-
raise ValueError(
|
|
467
|
-
f"Parameter '{arg_name}' in exclude_args does not exist in function."
|
|
468
|
-
)
|
|
469
|
-
param = sig.parameters[arg_name]
|
|
470
|
-
if param.default == inspect.Parameter.empty:
|
|
471
|
-
raise ValueError(
|
|
472
|
-
f"Parameter '{arg_name}' in exclude_args must have a default value."
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
# collect name and doc before we potentially modify the function
|
|
476
|
-
fn_name = getattr(fn, "__name__", None) or fn.__class__.__name__
|
|
477
|
-
fn_doc = inspect.getdoc(fn)
|
|
478
|
-
|
|
479
|
-
# if the fn is a callable class, we need to get the __call__ method from here out
|
|
480
|
-
if not inspect.isroutine(fn):
|
|
481
|
-
fn = fn.__call__
|
|
482
|
-
# if the fn is a staticmethod, we need to work with the underlying function
|
|
483
|
-
if isinstance(fn, staticmethod):
|
|
484
|
-
fn = fn.__func__
|
|
485
|
-
|
|
486
|
-
# Handle injected parameters (Context, Docket dependencies)
|
|
487
|
-
wrapper_fn = without_injected_parameters(fn)
|
|
488
|
-
|
|
489
|
-
# Also handle exclude_args with non-serializable types (issue #2431)
|
|
490
|
-
# This must happen before Pydantic tries to serialize the parameters
|
|
491
|
-
if exclude_args:
|
|
492
|
-
wrapper_fn = create_function_without_params(wrapper_fn, list(exclude_args))
|
|
493
|
-
|
|
494
|
-
input_type_adapter = get_cached_typeadapter(wrapper_fn)
|
|
495
|
-
input_schema = input_type_adapter.json_schema()
|
|
496
|
-
|
|
497
|
-
# Compress and handle exclude_args
|
|
498
|
-
prune_params = list(exclude_args) if exclude_args else None
|
|
499
|
-
input_schema = compress_schema(
|
|
500
|
-
input_schema, prune_params=prune_params, prune_titles=True
|
|
501
|
-
)
|
|
502
|
-
|
|
503
|
-
output_schema = None
|
|
504
|
-
# Get the return annotation from the signature
|
|
505
|
-
sig = inspect.signature(fn)
|
|
506
|
-
output_type = sig.return_annotation
|
|
507
|
-
|
|
508
|
-
# If the annotation is a string (from __future__ annotations), resolve it
|
|
509
|
-
if isinstance(output_type, str):
|
|
510
|
-
try:
|
|
511
|
-
# Use get_type_hints to resolve the return type
|
|
512
|
-
# include_extras=True preserves Annotated metadata
|
|
513
|
-
type_hints = get_type_hints(fn, include_extras=True)
|
|
514
|
-
output_type = type_hints.get("return", output_type)
|
|
515
|
-
except Exception:
|
|
516
|
-
# If resolution fails, keep the string annotation
|
|
517
|
-
pass
|
|
518
|
-
|
|
519
|
-
if output_type not in (inspect._empty, None, Any, ...):
|
|
520
|
-
# there are a variety of types that we don't want to attempt to
|
|
521
|
-
# serialize because they are either used by FastMCP internally,
|
|
522
|
-
# or are MCP content types that explicitly don't form structured
|
|
523
|
-
# content. By replacing them with an explicitly unserializable type,
|
|
524
|
-
# we ensure that no output schema is automatically generated.
|
|
525
|
-
clean_output_type = replace_type(
|
|
526
|
-
output_type,
|
|
527
|
-
dict.fromkeys( # type: ignore[arg-type]
|
|
528
|
-
(
|
|
529
|
-
Image,
|
|
530
|
-
Audio,
|
|
531
|
-
File,
|
|
532
|
-
ToolResult,
|
|
533
|
-
mcp.types.TextContent,
|
|
534
|
-
mcp.types.ImageContent,
|
|
535
|
-
mcp.types.AudioContent,
|
|
536
|
-
mcp.types.ResourceLink,
|
|
537
|
-
mcp.types.EmbeddedResource,
|
|
538
|
-
),
|
|
539
|
-
_UnserializableType,
|
|
540
|
-
),
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
try:
|
|
544
|
-
type_adapter = get_cached_typeadapter(clean_output_type)
|
|
545
|
-
base_schema = type_adapter.json_schema(mode="serialization")
|
|
546
|
-
|
|
547
|
-
# Generate schema for wrapped type if it's non-object
|
|
548
|
-
# because MCP requires that output schemas are objects
|
|
549
|
-
# Check if schema is an object type, resolving $ref references
|
|
550
|
-
# (self-referencing types use $ref at root level)
|
|
551
|
-
if wrap_non_object_output_schema and not _is_object_schema(base_schema):
|
|
552
|
-
# Use the wrapped result schema directly
|
|
553
|
-
wrapped_type = _WrappedResult[clean_output_type]
|
|
554
|
-
wrapped_adapter = get_cached_typeadapter(wrapped_type)
|
|
555
|
-
output_schema = wrapped_adapter.json_schema(mode="serialization")
|
|
556
|
-
output_schema["x-fastmcp-wrap-result"] = True
|
|
557
|
-
else:
|
|
558
|
-
output_schema = base_schema
|
|
559
|
-
|
|
560
|
-
output_schema = compress_schema(output_schema, prune_titles=True)
|
|
561
|
-
|
|
562
|
-
# Resolve root-level $ref to meet MCP spec requirement for type: object
|
|
563
|
-
# Self-referential Pydantic models generate schemas with $ref at root
|
|
564
|
-
output_schema = resolve_root_ref(output_schema)
|
|
565
|
-
|
|
566
|
-
except PydanticSchemaGenerationError as e:
|
|
567
|
-
if "_UnserializableType" not in str(e):
|
|
568
|
-
logger.debug(f"Unable to generate schema for type {output_type!r}")
|
|
569
|
-
|
|
570
|
-
return cls(
|
|
571
|
-
fn=fn,
|
|
572
|
-
name=fn_name,
|
|
573
|
-
description=fn_doc,
|
|
574
|
-
input_schema=input_schema,
|
|
575
|
-
output_schema=output_schema or None,
|
|
576
|
-
)
|
|
397
|
+
def get_span_attributes(self) -> dict[str, Any]:
|
|
398
|
+
return super().get_span_attributes() | {
|
|
399
|
+
"fastmcp.component.type": "tool",
|
|
400
|
+
"fastmcp.provider.type": "LocalProvider",
|
|
401
|
+
}
|
|
577
402
|
|
|
578
403
|
|
|
579
404
|
def _serialize_with_fallback(
|
|
@@ -641,3 +466,31 @@ def _convert_to_content(
|
|
|
641
466
|
]
|
|
642
467
|
# If none of the items are ContentBlocks, aggregate all items into a single TextContent
|
|
643
468
|
return [TextContent(type="text", text=_serialize_with_fallback(result, serializer))]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
__all__ = ["Tool", "ToolResult"]
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def __getattr__(name: str) -> Any:
|
|
475
|
+
"""Deprecated re-exports for backwards compatibility."""
|
|
476
|
+
deprecated_exports = {
|
|
477
|
+
"FunctionTool": "FunctionTool",
|
|
478
|
+
"ParsedFunction": "ParsedFunction",
|
|
479
|
+
"tool": "tool",
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if name in deprecated_exports:
|
|
483
|
+
import fastmcp
|
|
484
|
+
|
|
485
|
+
if fastmcp.settings.deprecation_warnings:
|
|
486
|
+
warnings.warn(
|
|
487
|
+
f"Importing {name} from fastmcp.tools.tool is deprecated. "
|
|
488
|
+
f"Import from fastmcp.tools.function_tool instead.",
|
|
489
|
+
DeprecationWarning,
|
|
490
|
+
stacklevel=2,
|
|
491
|
+
)
|
|
492
|
+
from fastmcp.tools import function_tool
|
|
493
|
+
|
|
494
|
+
return getattr(function_tool, name)
|
|
495
|
+
|
|
496
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|