fastmcp 2.12.5__py3-none-any.whl → 2.14.0__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/__init__.py +2 -23
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +19 -33
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/run.py +13 -8
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +123 -225
- fastmcp/client/client.py +697 -95
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/logging.py +18 -14
- fastmcp/client/messages.py +7 -5
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +117 -30
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +10 -26
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +3 -3
- fastmcp/experimental/server/openapi/__init__.py +20 -21
- fastmcp/experimental/utilities/openapi/__init__.py +16 -47
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +54 -51
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +43 -21
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +161 -61
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -14
- fastmcp/server/auth/auth.py +197 -46
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1469 -298
- fastmcp/server/auth/oidc_proxy.py +91 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +29 -5
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +236 -116
- fastmcp/server/dependencies.py +503 -18
- fastmcp/server/elicitation.py +286 -48
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +71 -20
- fastmcp/server/low_level.py +165 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +15 -10
- fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +72 -48
- fastmcp/server/server.py +1415 -733
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +125 -113
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +138 -55
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -21
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +10 -5
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +8 -8
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
- fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
- fastmcp/utilities/tests.py +92 -5
- fastmcp/utilities/types.py +86 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
- fastmcp-2.14.0.dist-info/RECORD +156 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1083
- fastmcp/utilities/openapi.py +0 -1568
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
- fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/tools/tool.py
CHANGED
|
@@ -9,20 +9,28 @@ from typing import (
|
|
|
9
9
|
Annotated,
|
|
10
10
|
Any,
|
|
11
11
|
Generic,
|
|
12
|
-
Literal,
|
|
13
12
|
TypeAlias,
|
|
14
13
|
get_type_hints,
|
|
15
14
|
)
|
|
16
15
|
|
|
17
16
|
import mcp.types
|
|
18
17
|
import pydantic_core
|
|
19
|
-
from mcp.
|
|
18
|
+
from mcp.shared.tool_name_validation import validate_and_warn_tool_name
|
|
19
|
+
from mcp.types import (
|
|
20
|
+
CallToolResult,
|
|
21
|
+
ContentBlock,
|
|
22
|
+
Icon,
|
|
23
|
+
TextContent,
|
|
24
|
+
ToolAnnotations,
|
|
25
|
+
ToolExecution,
|
|
26
|
+
)
|
|
20
27
|
from mcp.types import Tool as MCPTool
|
|
21
|
-
from pydantic import Field, PydanticSchemaGenerationError
|
|
28
|
+
from pydantic import Field, PydanticSchemaGenerationError, model_validator
|
|
22
29
|
from typing_extensions import TypeVar
|
|
23
30
|
|
|
24
31
|
import fastmcp
|
|
25
|
-
from fastmcp.server.dependencies import get_context
|
|
32
|
+
from fastmcp.server.dependencies import get_context, without_injected_parameters
|
|
33
|
+
from fastmcp.server.tasks.config import TaskConfig
|
|
26
34
|
from fastmcp.utilities.components import FastMCPComponent
|
|
27
35
|
from fastmcp.utilities.json_schema import compress_schema
|
|
28
36
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -32,7 +40,7 @@ from fastmcp.utilities.types import (
|
|
|
32
40
|
Image,
|
|
33
41
|
NotSet,
|
|
34
42
|
NotSetT,
|
|
35
|
-
|
|
43
|
+
create_function_without_params,
|
|
36
44
|
get_cached_typeadapter,
|
|
37
45
|
replace_type,
|
|
38
46
|
)
|
|
@@ -68,6 +76,7 @@ class ToolResult:
|
|
|
68
76
|
self,
|
|
69
77
|
content: list[ContentBlock] | Any | None = None,
|
|
70
78
|
structured_content: dict[str, Any] | Any | None = None,
|
|
79
|
+
meta: dict[str, Any] | None = None,
|
|
71
80
|
):
|
|
72
81
|
if content is None and structured_content is None:
|
|
73
82
|
raise ValueError("Either content or structured_content must be provided")
|
|
@@ -75,6 +84,7 @@ class ToolResult:
|
|
|
75
84
|
content = structured_content
|
|
76
85
|
|
|
77
86
|
self.content: list[ContentBlock] = _convert_to_content(result=content)
|
|
87
|
+
self.meta: dict[str, Any] | None = meta
|
|
78
88
|
|
|
79
89
|
if structured_content is not None:
|
|
80
90
|
try:
|
|
@@ -96,7 +106,15 @@ class ToolResult:
|
|
|
96
106
|
|
|
97
107
|
def to_mcp_result(
|
|
98
108
|
self,
|
|
99
|
-
) ->
|
|
109
|
+
) -> (
|
|
110
|
+
list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult
|
|
111
|
+
):
|
|
112
|
+
if self.meta is not None:
|
|
113
|
+
return CallToolResult(
|
|
114
|
+
structuredContent=self.structured_content,
|
|
115
|
+
content=self.content,
|
|
116
|
+
_meta=self.meta,
|
|
117
|
+
)
|
|
100
118
|
if self.structured_content is None:
|
|
101
119
|
return self.content
|
|
102
120
|
return self.content, self.structured_content
|
|
@@ -115,11 +133,21 @@ class Tool(FastMCPComponent):
|
|
|
115
133
|
ToolAnnotations | None,
|
|
116
134
|
Field(description="Additional annotations about the tool"),
|
|
117
135
|
] = None
|
|
136
|
+
execution: Annotated[
|
|
137
|
+
ToolExecution | None,
|
|
138
|
+
Field(description="Task execution configuration (SEP-1686)"),
|
|
139
|
+
] = None
|
|
118
140
|
serializer: Annotated[
|
|
119
141
|
ToolResultSerializerType | None,
|
|
120
142
|
Field(description="Optional custom serializer for tool results"),
|
|
121
143
|
] = None
|
|
122
144
|
|
|
145
|
+
@model_validator(mode="after")
|
|
146
|
+
def _validate_tool_name(self) -> Tool:
|
|
147
|
+
"""Validate tool name according to MCP specification (SEP-986)."""
|
|
148
|
+
validate_and_warn_tool_name(self.name)
|
|
149
|
+
return self
|
|
150
|
+
|
|
123
151
|
def enable(self) -> None:
|
|
124
152
|
super().enable()
|
|
125
153
|
try:
|
|
@@ -156,7 +184,9 @@ class Tool(FastMCPComponent):
|
|
|
156
184
|
description=overrides.get("description", self.description),
|
|
157
185
|
inputSchema=overrides.get("inputSchema", self.parameters),
|
|
158
186
|
outputSchema=overrides.get("outputSchema", self.output_schema),
|
|
187
|
+
icons=overrides.get("icons", self.icons),
|
|
159
188
|
annotations=overrides.get("annotations", self.annotations),
|
|
189
|
+
execution=overrides.get("execution", self.execution),
|
|
160
190
|
_meta=overrides.get(
|
|
161
191
|
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
162
192
|
),
|
|
@@ -168,13 +198,15 @@ class Tool(FastMCPComponent):
|
|
|
168
198
|
name: str | None = None,
|
|
169
199
|
title: str | None = None,
|
|
170
200
|
description: str | None = None,
|
|
201
|
+
icons: list[Icon] | None = None,
|
|
171
202
|
tags: set[str] | None = None,
|
|
172
203
|
annotations: ToolAnnotations | None = None,
|
|
173
204
|
exclude_args: list[str] | None = None,
|
|
174
|
-
output_schema: dict[str, Any] |
|
|
205
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
175
206
|
serializer: ToolResultSerializerType | None = None,
|
|
176
207
|
meta: dict[str, Any] | None = None,
|
|
177
208
|
enabled: bool | None = None,
|
|
209
|
+
task: bool | TaskConfig | None = None,
|
|
178
210
|
) -> FunctionTool:
|
|
179
211
|
"""Create a Tool from a function."""
|
|
180
212
|
return FunctionTool.from_function(
|
|
@@ -182,6 +214,7 @@ class Tool(FastMCPComponent):
|
|
|
182
214
|
name=name,
|
|
183
215
|
title=title,
|
|
184
216
|
description=description,
|
|
217
|
+
icons=icons,
|
|
185
218
|
tags=tags,
|
|
186
219
|
annotations=annotations,
|
|
187
220
|
exclude_args=exclude_args,
|
|
@@ -189,6 +222,7 @@ class Tool(FastMCPComponent):
|
|
|
189
222
|
serializer=serializer,
|
|
190
223
|
meta=meta,
|
|
191
224
|
enabled=enabled,
|
|
225
|
+
task=task,
|
|
192
226
|
)
|
|
193
227
|
|
|
194
228
|
async def run(self, arguments: dict[str, Any]) -> ToolResult:
|
|
@@ -209,13 +243,13 @@ class Tool(FastMCPComponent):
|
|
|
209
243
|
tool: Tool,
|
|
210
244
|
*,
|
|
211
245
|
name: str | None = None,
|
|
212
|
-
title: str |
|
|
213
|
-
description: str |
|
|
246
|
+
title: str | NotSetT | None = NotSet,
|
|
247
|
+
description: str | NotSetT | None = NotSet,
|
|
214
248
|
tags: set[str] | None = None,
|
|
215
|
-
annotations: ToolAnnotations |
|
|
216
|
-
output_schema: dict[str, Any] |
|
|
249
|
+
annotations: ToolAnnotations | NotSetT | None = NotSet,
|
|
250
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
217
251
|
serializer: ToolResultSerializerType | None = None,
|
|
218
|
-
meta: dict[str, Any] |
|
|
252
|
+
meta: dict[str, Any] | NotSetT | None = NotSet,
|
|
219
253
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
220
254
|
enabled: bool | None = None,
|
|
221
255
|
transform_fn: Callable[..., Any] | None = None,
|
|
@@ -240,6 +274,32 @@ class Tool(FastMCPComponent):
|
|
|
240
274
|
|
|
241
275
|
class FunctionTool(Tool):
|
|
242
276
|
fn: Callable[..., Any]
|
|
277
|
+
task_config: Annotated[
|
|
278
|
+
TaskConfig,
|
|
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
|
|
243
303
|
|
|
244
304
|
@classmethod
|
|
245
305
|
def from_function(
|
|
@@ -248,42 +308,56 @@ class FunctionTool(Tool):
|
|
|
248
308
|
name: str | None = None,
|
|
249
309
|
title: str | None = None,
|
|
250
310
|
description: str | None = None,
|
|
311
|
+
icons: list[Icon] | None = None,
|
|
251
312
|
tags: set[str] | None = None,
|
|
252
313
|
annotations: ToolAnnotations | None = None,
|
|
253
314
|
exclude_args: list[str] | None = None,
|
|
254
|
-
output_schema: dict[str, Any] |
|
|
315
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
255
316
|
serializer: ToolResultSerializerType | None = None,
|
|
256
317
|
meta: dict[str, Any] | None = None,
|
|
257
318
|
enabled: bool | None = None,
|
|
319
|
+
task: bool | TaskConfig | None = None,
|
|
258
320
|
) -> FunctionTool:
|
|
259
321
|
"""Create a Tool from a function."""
|
|
322
|
+
if exclude_args and fastmcp.settings.deprecation_warnings:
|
|
323
|
+
warnings.warn(
|
|
324
|
+
"The `exclude_args` parameter will be deprecated in FastMCP 2.14. "
|
|
325
|
+
"We recommend using dependency injection with `Depends()` instead, which provides "
|
|
326
|
+
"better lifecycle management and is more explicit. "
|
|
327
|
+
"`exclude_args` will continue to work until then. "
|
|
328
|
+
"See https://gofastmcp.com/docs/servers/tools for examples.",
|
|
329
|
+
DeprecationWarning,
|
|
330
|
+
stacklevel=2,
|
|
331
|
+
)
|
|
260
332
|
|
|
261
333
|
parsed_fn = ParsedFunction.from_function(fn, exclude_args=exclude_args)
|
|
334
|
+
func_name = name or parsed_fn.name
|
|
262
335
|
|
|
263
|
-
if
|
|
336
|
+
if func_name == "<lambda>":
|
|
264
337
|
raise ValueError("You must provide a name for lambda functions")
|
|
265
338
|
|
|
339
|
+
# Normalize task to TaskConfig and validate
|
|
340
|
+
if task is None:
|
|
341
|
+
task_config = TaskConfig(mode="forbidden")
|
|
342
|
+
elif isinstance(task, bool):
|
|
343
|
+
task_config = TaskConfig.from_bool(task)
|
|
344
|
+
else:
|
|
345
|
+
task_config = task
|
|
346
|
+
task_config.validate_function(fn, func_name)
|
|
347
|
+
|
|
266
348
|
if isinstance(output_schema, NotSetT):
|
|
267
349
|
final_output_schema = parsed_fn.output_schema
|
|
268
|
-
elif output_schema is False:
|
|
269
|
-
# Handle False as deprecated synonym for None (deprecated in 2.11.4)
|
|
270
|
-
if fastmcp.settings.deprecation_warnings:
|
|
271
|
-
warnings.warn(
|
|
272
|
-
"Passing output_schema=False is deprecated. Use output_schema=None instead.",
|
|
273
|
-
DeprecationWarning,
|
|
274
|
-
stacklevel=2,
|
|
275
|
-
)
|
|
276
|
-
final_output_schema = None
|
|
277
350
|
else:
|
|
278
|
-
# At this point output_schema is not NotSetT
|
|
351
|
+
# At this point output_schema is not NotSetT, so it must be dict | None
|
|
279
352
|
final_output_schema = output_schema
|
|
280
353
|
# Note: explicit schemas (dict) are used as-is without auto-wrapping
|
|
281
354
|
|
|
282
355
|
# Validate that explicit schemas are object type for structured content
|
|
356
|
+
# (resolving $ref references for self-referencing types)
|
|
283
357
|
if final_output_schema is not None and isinstance(final_output_schema, dict):
|
|
284
|
-
if final_output_schema
|
|
358
|
+
if not _is_object_schema(final_output_schema):
|
|
285
359
|
raise ValueError(
|
|
286
|
-
f
|
|
360
|
+
f"Output schemas must represent object types due to MCP spec limitations. Received: {final_output_schema!r}"
|
|
287
361
|
)
|
|
288
362
|
|
|
289
363
|
return cls(
|
|
@@ -291,6 +365,7 @@ class FunctionTool(Tool):
|
|
|
291
365
|
name=name or parsed_fn.name,
|
|
292
366
|
title=title,
|
|
293
367
|
description=description or parsed_fn.description,
|
|
368
|
+
icons=icons,
|
|
294
369
|
parameters=parsed_fn.input_schema,
|
|
295
370
|
output_schema=final_output_schema,
|
|
296
371
|
annotations=annotations,
|
|
@@ -298,21 +373,14 @@ class FunctionTool(Tool):
|
|
|
298
373
|
serializer=serializer,
|
|
299
374
|
meta=meta,
|
|
300
375
|
enabled=enabled if enabled is not None else True,
|
|
376
|
+
task_config=task_config,
|
|
301
377
|
)
|
|
302
378
|
|
|
303
379
|
async def run(self, arguments: dict[str, Any]) -> ToolResult:
|
|
304
380
|
"""Run the tool with arguments."""
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
arguments = arguments.copy()
|
|
308
|
-
|
|
309
|
-
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
|
|
310
|
-
if context_kwarg and context_kwarg not in arguments:
|
|
311
|
-
arguments[context_kwarg] = get_context()
|
|
312
|
-
|
|
313
|
-
type_adapter = get_cached_typeadapter(self.fn)
|
|
381
|
+
wrapper_fn = without_injected_parameters(self.fn)
|
|
382
|
+
type_adapter = get_cached_typeadapter(wrapper_fn)
|
|
314
383
|
result = type_adapter.validate_python(arguments)
|
|
315
|
-
|
|
316
384
|
if inspect.isawaitable(result):
|
|
317
385
|
result = await result
|
|
318
386
|
|
|
@@ -351,6 +419,21 @@ class FunctionTool(Tool):
|
|
|
351
419
|
)
|
|
352
420
|
|
|
353
421
|
|
|
422
|
+
def _is_object_schema(schema: dict[str, Any]) -> bool:
|
|
423
|
+
"""Check if a JSON schema represents an object type."""
|
|
424
|
+
# Direct object type
|
|
425
|
+
if schema.get("type") == "object":
|
|
426
|
+
return True
|
|
427
|
+
|
|
428
|
+
# Schema with properties but no explicit type is treated as object
|
|
429
|
+
if "properties" in schema:
|
|
430
|
+
return True
|
|
431
|
+
|
|
432
|
+
# Self-referencing types use $ref pointing to $defs
|
|
433
|
+
# The referenced type is always an object in our use case
|
|
434
|
+
return "$ref" in schema and "$defs" in schema
|
|
435
|
+
|
|
436
|
+
|
|
354
437
|
@dataclass
|
|
355
438
|
class ParsedFunction:
|
|
356
439
|
fn: Callable[..., Any]
|
|
@@ -367,8 +450,6 @@ class ParsedFunction:
|
|
|
367
450
|
validate: bool = True,
|
|
368
451
|
wrap_non_object_output_schema: bool = True,
|
|
369
452
|
) -> ParsedFunction:
|
|
370
|
-
from fastmcp.server.context import Context
|
|
371
|
-
|
|
372
453
|
if validate:
|
|
373
454
|
sig = inspect.signature(fn)
|
|
374
455
|
# Reject functions with *args or **kwargs
|
|
@@ -404,15 +485,19 @@ class ParsedFunction:
|
|
|
404
485
|
if isinstance(fn, staticmethod):
|
|
405
486
|
fn = fn.__func__
|
|
406
487
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
488
|
+
# Handle injected parameters (Context, Docket dependencies)
|
|
489
|
+
wrapper_fn = without_injected_parameters(fn)
|
|
490
|
+
|
|
491
|
+
# Also handle exclude_args with non-serializable types (issue #2431)
|
|
492
|
+
# This must happen before Pydantic tries to serialize the parameters
|
|
411
493
|
if exclude_args:
|
|
412
|
-
|
|
494
|
+
wrapper_fn = create_function_without_params(wrapper_fn, list(exclude_args))
|
|
413
495
|
|
|
414
|
-
input_type_adapter = get_cached_typeadapter(
|
|
496
|
+
input_type_adapter = get_cached_typeadapter(wrapper_fn)
|
|
415
497
|
input_schema = input_type_adapter.json_schema()
|
|
498
|
+
|
|
499
|
+
# Compress and handle exclude_args
|
|
500
|
+
prune_params = list(exclude_args) if exclude_args else None
|
|
416
501
|
input_schema = compress_schema(
|
|
417
502
|
input_schema, prune_params=prune_params, prune_titles=True
|
|
418
503
|
)
|
|
@@ -441,9 +526,8 @@ class ParsedFunction:
|
|
|
441
526
|
# we ensure that no output schema is automatically generated.
|
|
442
527
|
clean_output_type = replace_type(
|
|
443
528
|
output_type,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
for t in (
|
|
529
|
+
dict.fromkeys( # type: ignore[arg-type]
|
|
530
|
+
(
|
|
447
531
|
Image,
|
|
448
532
|
Audio,
|
|
449
533
|
File,
|
|
@@ -453,8 +537,9 @@ class ParsedFunction:
|
|
|
453
537
|
mcp.types.AudioContent,
|
|
454
538
|
mcp.types.ResourceLink,
|
|
455
539
|
mcp.types.EmbeddedResource,
|
|
456
|
-
)
|
|
457
|
-
|
|
540
|
+
),
|
|
541
|
+
_UnserializableType,
|
|
542
|
+
),
|
|
458
543
|
)
|
|
459
544
|
|
|
460
545
|
try:
|
|
@@ -463,10 +548,9 @@ class ParsedFunction:
|
|
|
463
548
|
|
|
464
549
|
# Generate schema for wrapped type if it's non-object
|
|
465
550
|
# because MCP requires that output schemas are objects
|
|
466
|
-
if
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
):
|
|
551
|
+
# Check if schema is an object type, resolving $ref references
|
|
552
|
+
# (self-referencing types use $ref at root level)
|
|
553
|
+
if wrap_non_object_output_schema and not _is_object_schema(base_schema):
|
|
470
554
|
# Use the wrapped result schema directly
|
|
471
555
|
wrapped_type = _WrappedResult[clean_output_type]
|
|
472
556
|
wrapped_adapter = get_cached_typeadapter(wrapped_type)
|
|
@@ -546,13 +630,12 @@ def _convert_to_content(
|
|
|
546
630
|
|
|
547
631
|
# If any item is a ContentBlock, convert non-ContentBlock items to TextContent
|
|
548
632
|
# without aggregating them
|
|
549
|
-
if any(isinstance(item, ContentBlock) for item in result):
|
|
633
|
+
if any(isinstance(item, ContentBlock | Image | Audio | File) for item in result):
|
|
550
634
|
return [
|
|
551
635
|
_convert_to_single_content_block(item, serializer)
|
|
552
636
|
if not isinstance(item, ContentBlock)
|
|
553
637
|
else item
|
|
554
638
|
for item in result
|
|
555
639
|
]
|
|
556
|
-
|
|
557
640
|
# If none of the items are ContentBlocks, aggregate all items into a single TextContent
|
|
558
641
|
return [TextContent(type="text", text=_serialize_with_fallback(result, serializer))]
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
|
-
from collections.abc import Callable
|
|
5
|
-
from typing import
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
from mcp.types import ToolAnnotations
|
|
8
|
+
from pydantic import ValidationError
|
|
8
9
|
|
|
9
10
|
from fastmcp import settings
|
|
10
11
|
from fastmcp.exceptions import NotFoundError, ToolError
|
|
@@ -16,9 +17,6 @@ from fastmcp.tools.tool_transform import (
|
|
|
16
17
|
)
|
|
17
18
|
from fastmcp.utilities.logging import get_logger
|
|
18
19
|
|
|
19
|
-
if TYPE_CHECKING:
|
|
20
|
-
from fastmcp.server.server import MountedServer
|
|
21
|
-
|
|
22
20
|
logger = get_logger(__name__)
|
|
23
21
|
|
|
24
22
|
|
|
@@ -29,12 +27,15 @@ class ToolManager:
|
|
|
29
27
|
self,
|
|
30
28
|
duplicate_behavior: DuplicateBehavior | None = None,
|
|
31
29
|
mask_error_details: bool | None = None,
|
|
32
|
-
transformations:
|
|
30
|
+
transformations: Mapping[str, ToolTransformConfig] | None = None,
|
|
33
31
|
):
|
|
34
32
|
self._tools: dict[str, Tool] = {}
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
self.mask_error_details: bool = (
|
|
34
|
+
mask_error_details or settings.mask_error_details
|
|
35
|
+
)
|
|
36
|
+
self.transformations: dict[str, ToolTransformConfig] = dict(
|
|
37
|
+
transformations or {}
|
|
38
|
+
)
|
|
38
39
|
|
|
39
40
|
# Default to "warn" if None is provided
|
|
40
41
|
if duplicate_behavior is None:
|
|
@@ -48,56 +49,12 @@ class ToolManager:
|
|
|
48
49
|
|
|
49
50
|
self.duplicate_behavior = duplicate_behavior
|
|
50
51
|
|
|
51
|
-
def
|
|
52
|
-
"""
|
|
53
|
-
self._mounted_servers.append(server)
|
|
54
|
-
|
|
55
|
-
async def _load_tools(self, *, via_server: bool = False) -> dict[str, Tool]:
|
|
56
|
-
"""
|
|
57
|
-
The single, consolidated recursive method for fetching tools. The 'via_server'
|
|
58
|
-
parameter determines the communication path.
|
|
59
|
-
|
|
60
|
-
- via_server=False: Manager-to-manager path for complete, unfiltered inventory
|
|
61
|
-
- via_server=True: Server-to-server path for filtered MCP requests
|
|
62
|
-
"""
|
|
63
|
-
all_tools: dict[str, Tool] = {}
|
|
64
|
-
|
|
65
|
-
for mounted in self._mounted_servers:
|
|
66
|
-
try:
|
|
67
|
-
if via_server:
|
|
68
|
-
# Use the server-to-server filtered path
|
|
69
|
-
child_results = await mounted.server._list_tools()
|
|
70
|
-
else:
|
|
71
|
-
# Use the manager-to-manager unfiltered path
|
|
72
|
-
child_results = await mounted.server._tool_manager.list_tools()
|
|
73
|
-
|
|
74
|
-
# The combination logic is the same for both paths
|
|
75
|
-
child_dict = {t.key: t for t in child_results}
|
|
76
|
-
if mounted.prefix:
|
|
77
|
-
for tool in child_dict.values():
|
|
78
|
-
prefixed_tool = tool.model_copy(
|
|
79
|
-
key=f"{mounted.prefix}_{tool.key}"
|
|
80
|
-
)
|
|
81
|
-
all_tools[prefixed_tool.key] = prefixed_tool
|
|
82
|
-
else:
|
|
83
|
-
all_tools.update(child_dict)
|
|
84
|
-
except Exception as e:
|
|
85
|
-
# Skip failed mounts silently, matches existing behavior
|
|
86
|
-
logger.warning(
|
|
87
|
-
f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
88
|
-
)
|
|
89
|
-
if settings.mounted_components_raise_on_load_error:
|
|
90
|
-
raise
|
|
91
|
-
continue
|
|
92
|
-
|
|
93
|
-
# Finally, add local tools, which always take precedence
|
|
94
|
-
all_tools.update(self._tools)
|
|
95
|
-
|
|
52
|
+
async def _load_tools(self) -> dict[str, Tool]:
|
|
53
|
+
"""Return this manager's local tools with transformations applied."""
|
|
96
54
|
transformed_tools = apply_transformations_to_tools(
|
|
97
|
-
tools=
|
|
55
|
+
tools=self._tools,
|
|
98
56
|
transformations=self.transformations,
|
|
99
57
|
)
|
|
100
|
-
|
|
101
58
|
return transformed_tools
|
|
102
59
|
|
|
103
60
|
async def has_tool(self, key: str) -> bool:
|
|
@@ -114,25 +71,9 @@ class ToolManager:
|
|
|
114
71
|
|
|
115
72
|
async def get_tools(self) -> dict[str, Tool]:
|
|
116
73
|
"""
|
|
117
|
-
Gets the complete, unfiltered inventory of
|
|
118
|
-
"""
|
|
119
|
-
return await self._load_tools(via_server=False)
|
|
120
|
-
|
|
121
|
-
async def list_tools(self) -> list[Tool]:
|
|
74
|
+
Gets the complete, unfiltered inventory of local tools.
|
|
122
75
|
"""
|
|
123
|
-
|
|
124
|
-
"""
|
|
125
|
-
tools_dict = await self._load_tools(via_server=True)
|
|
126
|
-
return list(tools_dict.values())
|
|
127
|
-
|
|
128
|
-
@property
|
|
129
|
-
def _tools_transformed(self) -> list[str]:
|
|
130
|
-
"""Get the local tools."""
|
|
131
|
-
|
|
132
|
-
return [
|
|
133
|
-
transformation.name or tool_name
|
|
134
|
-
for tool_name, transformation in self.transformations.items()
|
|
135
|
-
]
|
|
76
|
+
return await self._load_tools()
|
|
136
77
|
|
|
137
78
|
def add_tool_from_fn(
|
|
138
79
|
self,
|
|
@@ -214,41 +155,18 @@ class ToolManager:
|
|
|
214
155
|
Internal API for servers: Finds and calls a tool, respecting the
|
|
215
156
|
filtered protocol path.
|
|
216
157
|
"""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
raise e
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
except Exception as e:
|
|
233
|
-
logger.exception(f"Error calling tool {key!r}")
|
|
234
|
-
if self.mask_error_details:
|
|
235
|
-
# Mask internal details
|
|
236
|
-
raise ToolError(f"Error calling tool {key!r}") from e
|
|
237
|
-
else:
|
|
238
|
-
# Include original error details
|
|
239
|
-
raise ToolError(f"Error calling tool {key!r}: {e}") from e
|
|
240
|
-
|
|
241
|
-
# 2. Check mounted servers using the filtered protocol path.
|
|
242
|
-
for mounted in reversed(self._mounted_servers):
|
|
243
|
-
tool_key = key
|
|
244
|
-
if mounted.prefix:
|
|
245
|
-
if key.startswith(f"{mounted.prefix}_"):
|
|
246
|
-
tool_key = key.removeprefix(f"{mounted.prefix}_")
|
|
247
|
-
else:
|
|
248
|
-
continue
|
|
249
|
-
try:
|
|
250
|
-
return await mounted.server._call_tool(tool_key, arguments)
|
|
251
|
-
except NotFoundError:
|
|
252
|
-
continue
|
|
253
|
-
|
|
254
|
-
raise NotFoundError(f"Tool {key!r} not found.")
|
|
158
|
+
tool = await self.get_tool(key)
|
|
159
|
+
try:
|
|
160
|
+
return await tool.run(arguments)
|
|
161
|
+
except ValidationError as e:
|
|
162
|
+
logger.exception(f"Error validating tool {key!r}: {e}")
|
|
163
|
+
raise e
|
|
164
|
+
except ToolError as e:
|
|
165
|
+
logger.exception(f"Error calling tool {key!r}")
|
|
166
|
+
raise e
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.exception(f"Error calling tool {key!r}")
|
|
169
|
+
if self.mask_error_details:
|
|
170
|
+
raise ToolError(f"Error calling tool {key!r}") from e
|
|
171
|
+
else:
|
|
172
|
+
raise ToolError(f"Error calling tool {key!r}: {e}") from e
|
fastmcp/tools/tool_transform.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
import warnings
|
|
5
4
|
from collections.abc import Callable
|
|
6
5
|
from contextvars import ContextVar
|
|
6
|
+
from copy import deepcopy
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from typing import Annotated, Any, Literal, cast
|
|
9
9
|
|
|
@@ -13,7 +13,6 @@ from pydantic import ConfigDict
|
|
|
13
13
|
from pydantic.fields import Field
|
|
14
14
|
from pydantic.functional_validators import BeforeValidator
|
|
15
15
|
|
|
16
|
-
import fastmcp
|
|
17
16
|
from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content
|
|
18
17
|
from fastmcp.utilities.components import _convert_set_default_none
|
|
19
18
|
from fastmcp.utilities.json_schema import compress_schema
|
|
@@ -34,7 +33,7 @@ _current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[
|
|
|
34
33
|
)
|
|
35
34
|
|
|
36
35
|
|
|
37
|
-
async def forward(**kwargs) -> ToolResult:
|
|
36
|
+
async def forward(**kwargs: Any) -> ToolResult:
|
|
38
37
|
"""Forward to parent tool with argument transformation applied.
|
|
39
38
|
|
|
40
39
|
This function can only be called from within a transformed tool's custom
|
|
@@ -64,7 +63,7 @@ async def forward(**kwargs) -> ToolResult:
|
|
|
64
63
|
return await tool.forwarding_fn(**kwargs)
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
async def forward_raw(**kwargs) -> ToolResult:
|
|
66
|
+
async def forward_raw(**kwargs: Any) -> ToolResult:
|
|
68
67
|
"""Forward directly to parent tool without transformation.
|
|
69
68
|
|
|
70
69
|
This function bypasses all argument transformation and validation, calling the parent
|
|
@@ -365,15 +364,15 @@ class TransformedTool(Tool):
|
|
|
365
364
|
cls,
|
|
366
365
|
tool: Tool,
|
|
367
366
|
name: str | None = None,
|
|
368
|
-
title: str |
|
|
369
|
-
description: str |
|
|
367
|
+
title: str | NotSetT | None = NotSet,
|
|
368
|
+
description: str | NotSetT | None = NotSet,
|
|
370
369
|
tags: set[str] | None = None,
|
|
371
370
|
transform_fn: Callable[..., Any] | None = None,
|
|
372
371
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
373
|
-
annotations: ToolAnnotations |
|
|
374
|
-
output_schema: dict[str, Any] |
|
|
375
|
-
serializer: Callable[[Any], str] |
|
|
376
|
-
meta: dict[str, Any] |
|
|
372
|
+
annotations: ToolAnnotations | NotSetT | None = NotSet,
|
|
373
|
+
output_schema: dict[str, Any] | NotSetT | None = NotSet,
|
|
374
|
+
serializer: Callable[[Any], str] | NotSetT | None = NotSet,
|
|
375
|
+
meta: dict[str, Any] | NotSetT | None = NotSet,
|
|
377
376
|
enabled: bool | None = None,
|
|
378
377
|
) -> TransformedTool:
|
|
379
378
|
"""Create a transformed tool from a parent tool.
|
|
@@ -486,15 +485,6 @@ class TransformedTool(Tool):
|
|
|
486
485
|
final_output_schema = tool.output_schema
|
|
487
486
|
else:
|
|
488
487
|
final_output_schema = tool.output_schema
|
|
489
|
-
elif output_schema is False:
|
|
490
|
-
# Handle False as deprecated synonym for None (deprecated in 2.11.4)
|
|
491
|
-
if fastmcp.settings.deprecation_warnings:
|
|
492
|
-
warnings.warn(
|
|
493
|
-
"Passing output_schema=False is deprecated. Use output_schema=None instead.",
|
|
494
|
-
DeprecationWarning,
|
|
495
|
-
stacklevel=2,
|
|
496
|
-
)
|
|
497
|
-
final_output_schema = None
|
|
498
488
|
else:
|
|
499
489
|
final_output_schema = cast(dict | None, output_schema)
|
|
500
490
|
|
|
@@ -620,7 +610,8 @@ class TransformedTool(Tool):
|
|
|
620
610
|
"""
|
|
621
611
|
|
|
622
612
|
# Build transformed schema and mapping
|
|
623
|
-
|
|
613
|
+
# Deep copy to prevent compress_schema from mutating parent tool's $defs
|
|
614
|
+
parent_defs = deepcopy(parent_tool.parameters.get("$defs", {}))
|
|
624
615
|
parent_props = parent_tool.parameters.get("properties", {}).copy()
|
|
625
616
|
parent_required = set(parent_tool.parameters.get("required", []))
|
|
626
617
|
|
|
@@ -681,7 +672,7 @@ class TransformedTool(Tool):
|
|
|
681
672
|
schema = compress_schema(schema, prune_defs=True)
|
|
682
673
|
|
|
683
674
|
# Create forwarding function that closes over everything it needs
|
|
684
|
-
async def _forward(**kwargs):
|
|
675
|
+
async def _forward(**kwargs: Any):
|
|
685
676
|
# Validate arguments
|
|
686
677
|
valid_args = set(new_props.keys())
|
|
687
678
|
provided_args = set(kwargs.keys())
|