fastmcp 2.12.5__py3-none-any.whl → 2.13.2__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 -2
- fastmcp/cli/cli.py +11 -11
- 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/run.py +13 -8
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +115 -217
- fastmcp/client/client.py +105 -39
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +80 -25
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +6 -6
- 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/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +14 -15
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +22 -19
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +14 -9
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +70 -43
- 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 +1510 -289
- fastmcp/server/auth/oidc_proxy.py +84 -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/bearer.py +1 -1
- 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 +27 -3
- 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 +177 -51
- fastmcp/server/dependencies.py +39 -12
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +56 -17
- fastmcp/server/low_level.py +121 -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.py +10 -6
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +725 -242
- fastmcp/settings.py +24 -10
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +70 -23
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -10
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +118 -8
- 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 +4 -4
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +85 -4
- fastmcp/utilities/types.py +78 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/settings.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import os
|
|
4
5
|
import warnings
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
7
8
|
|
|
9
|
+
from platformdirs import user_data_dir
|
|
8
10
|
from pydantic import Field, ImportString, field_validator
|
|
9
11
|
from pydantic.fields import FieldInfo
|
|
10
12
|
from pydantic_settings import (
|
|
@@ -19,10 +21,14 @@ from fastmcp.utilities.logging import get_logger
|
|
|
19
21
|
|
|
20
22
|
logger = get_logger(__name__)
|
|
21
23
|
|
|
24
|
+
ENV_FILE = os.getenv("FASTMCP_ENV_FILE", ".env")
|
|
25
|
+
|
|
22
26
|
LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
23
27
|
|
|
24
28
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
25
29
|
|
|
30
|
+
TEN_MB_IN_BYTES = 1024 * 1024 * 10
|
|
31
|
+
|
|
26
32
|
if TYPE_CHECKING:
|
|
27
33
|
from fastmcp.server.auth.auth import AuthProvider
|
|
28
34
|
|
|
@@ -82,7 +88,7 @@ class Settings(BaseSettings):
|
|
|
82
88
|
|
|
83
89
|
model_config = ExtendedSettingsConfigDict(
|
|
84
90
|
env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"],
|
|
85
|
-
env_file=
|
|
91
|
+
env_file=ENV_FILE,
|
|
86
92
|
extra="ignore",
|
|
87
93
|
env_nested_delimiter="__",
|
|
88
94
|
nested_model_default_partial_update=True,
|
|
@@ -145,7 +151,7 @@ class Settings(BaseSettings):
|
|
|
145
151
|
)
|
|
146
152
|
return self
|
|
147
153
|
|
|
148
|
-
home: Path = Path
|
|
154
|
+
home: Path = Path(user_data_dir("fastmcp", appauthor=False))
|
|
149
155
|
|
|
150
156
|
test_mode: bool = False
|
|
151
157
|
|
|
@@ -189,7 +195,6 @@ class Settings(BaseSettings):
|
|
|
189
195
|
client_raise_first_exceptiongroup_error: Annotated[
|
|
190
196
|
bool,
|
|
191
197
|
Field(
|
|
192
|
-
default=True,
|
|
193
198
|
description=inspect.cleandoc(
|
|
194
199
|
"""
|
|
195
200
|
Many MCP components operate in anyio taskgroups, and raise
|
|
@@ -205,7 +210,6 @@ class Settings(BaseSettings):
|
|
|
205
210
|
resource_prefix_format: Annotated[
|
|
206
211
|
Literal["protocol", "path"],
|
|
207
212
|
Field(
|
|
208
|
-
default="path",
|
|
209
213
|
description=inspect.cleandoc(
|
|
210
214
|
"""
|
|
211
215
|
When perfixing a resource URI, either use path formatting (resource://prefix/path)
|
|
@@ -235,7 +239,6 @@ class Settings(BaseSettings):
|
|
|
235
239
|
mask_error_details: Annotated[
|
|
236
240
|
bool,
|
|
237
241
|
Field(
|
|
238
|
-
default=False,
|
|
239
242
|
description=inspect.cleandoc(
|
|
240
243
|
"""
|
|
241
244
|
If True, error details from user-supplied functions (tool, resource, prompt)
|
|
@@ -248,6 +251,22 @@ class Settings(BaseSettings):
|
|
|
248
251
|
),
|
|
249
252
|
] = False
|
|
250
253
|
|
|
254
|
+
strict_input_validation: Annotated[
|
|
255
|
+
bool,
|
|
256
|
+
Field(
|
|
257
|
+
description=inspect.cleandoc(
|
|
258
|
+
"""
|
|
259
|
+
If True, tool inputs are strictly validated against the input
|
|
260
|
+
JSON schema. For example, providing the string \"10\" to an
|
|
261
|
+
integer field will raise an error. If False, compatible inputs
|
|
262
|
+
will be coerced to match the schema, which can increase
|
|
263
|
+
compatibility. For example, providing the string \"10\" to an
|
|
264
|
+
integer field will be coerced to 10. Defaults to False.
|
|
265
|
+
"""
|
|
266
|
+
),
|
|
267
|
+
),
|
|
268
|
+
] = False
|
|
269
|
+
|
|
251
270
|
server_dependencies: list[str] = Field(
|
|
252
271
|
default_factory=list,
|
|
253
272
|
description="List of dependencies to install in the server environment",
|
|
@@ -293,7 +312,6 @@ class Settings(BaseSettings):
|
|
|
293
312
|
include_tags: Annotated[
|
|
294
313
|
set[str] | None,
|
|
295
314
|
Field(
|
|
296
|
-
default=None,
|
|
297
315
|
description=inspect.cleandoc(
|
|
298
316
|
"""
|
|
299
317
|
If provided, only components that match these tags will be
|
|
@@ -306,7 +324,6 @@ class Settings(BaseSettings):
|
|
|
306
324
|
exclude_tags: Annotated[
|
|
307
325
|
set[str] | None,
|
|
308
326
|
Field(
|
|
309
|
-
default=None,
|
|
310
327
|
description=inspect.cleandoc(
|
|
311
328
|
"""
|
|
312
329
|
If provided, components that match these tags will be excluded
|
|
@@ -320,7 +337,6 @@ class Settings(BaseSettings):
|
|
|
320
337
|
include_fastmcp_meta: Annotated[
|
|
321
338
|
bool,
|
|
322
339
|
Field(
|
|
323
|
-
default=True,
|
|
324
340
|
description=inspect.cleandoc(
|
|
325
341
|
"""
|
|
326
342
|
Whether to include FastMCP meta in the server's MCP responses.
|
|
@@ -335,7 +351,6 @@ class Settings(BaseSettings):
|
|
|
335
351
|
mounted_components_raise_on_load_error: Annotated[
|
|
336
352
|
bool,
|
|
337
353
|
Field(
|
|
338
|
-
default=False,
|
|
339
354
|
description=inspect.cleandoc(
|
|
340
355
|
"""
|
|
341
356
|
If True, errors encountered when loading mounted components (tools, resources, prompts)
|
|
@@ -349,7 +364,6 @@ class Settings(BaseSettings):
|
|
|
349
364
|
show_cli_banner: Annotated[
|
|
350
365
|
bool,
|
|
351
366
|
Field(
|
|
352
|
-
default=True,
|
|
353
367
|
description=inspect.cleandoc(
|
|
354
368
|
"""
|
|
355
369
|
If True, the server banner will be displayed when running the server via CLI.
|
fastmcp/tools/__init__.py
CHANGED
|
@@ -2,4 +2,4 @@ from .tool import Tool, FunctionTool
|
|
|
2
2
|
from .tool_manager import ToolManager
|
|
3
3
|
from .tool_transform import forward, forward_raw
|
|
4
4
|
|
|
5
|
-
__all__ = ["
|
|
5
|
+
__all__ = ["FunctionTool", "Tool", "ToolManager", "forward", "forward_raw"]
|
fastmcp/tools/tool.py
CHANGED
|
@@ -16,7 +16,7 @@ from typing import (
|
|
|
16
16
|
|
|
17
17
|
import mcp.types
|
|
18
18
|
import pydantic_core
|
|
19
|
-
from mcp.types import ContentBlock, TextContent, ToolAnnotations
|
|
19
|
+
from mcp.types import CallToolResult, ContentBlock, Icon, TextContent, ToolAnnotations
|
|
20
20
|
from mcp.types import Tool as MCPTool
|
|
21
21
|
from pydantic import Field, PydanticSchemaGenerationError
|
|
22
22
|
from typing_extensions import TypeVar
|
|
@@ -32,6 +32,7 @@ from fastmcp.utilities.types import (
|
|
|
32
32
|
Image,
|
|
33
33
|
NotSet,
|
|
34
34
|
NotSetT,
|
|
35
|
+
create_function_without_params,
|
|
35
36
|
find_kwarg_by_type,
|
|
36
37
|
get_cached_typeadapter,
|
|
37
38
|
replace_type,
|
|
@@ -68,6 +69,7 @@ class ToolResult:
|
|
|
68
69
|
self,
|
|
69
70
|
content: list[ContentBlock] | Any | None = None,
|
|
70
71
|
structured_content: dict[str, Any] | Any | None = None,
|
|
72
|
+
meta: dict[str, Any] | None = None,
|
|
71
73
|
):
|
|
72
74
|
if content is None and structured_content is None:
|
|
73
75
|
raise ValueError("Either content or structured_content must be provided")
|
|
@@ -75,6 +77,7 @@ class ToolResult:
|
|
|
75
77
|
content = structured_content
|
|
76
78
|
|
|
77
79
|
self.content: list[ContentBlock] = _convert_to_content(result=content)
|
|
80
|
+
self.meta: dict[str, Any] | None = meta
|
|
78
81
|
|
|
79
82
|
if structured_content is not None:
|
|
80
83
|
try:
|
|
@@ -96,7 +99,15 @@ class ToolResult:
|
|
|
96
99
|
|
|
97
100
|
def to_mcp_result(
|
|
98
101
|
self,
|
|
99
|
-
) ->
|
|
102
|
+
) -> (
|
|
103
|
+
list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult
|
|
104
|
+
):
|
|
105
|
+
if self.meta is not None:
|
|
106
|
+
return CallToolResult(
|
|
107
|
+
structuredContent=self.structured_content,
|
|
108
|
+
content=self.content,
|
|
109
|
+
_meta=self.meta,
|
|
110
|
+
)
|
|
100
111
|
if self.structured_content is None:
|
|
101
112
|
return self.content
|
|
102
113
|
return self.content, self.structured_content
|
|
@@ -156,6 +167,7 @@ class Tool(FastMCPComponent):
|
|
|
156
167
|
description=overrides.get("description", self.description),
|
|
157
168
|
inputSchema=overrides.get("inputSchema", self.parameters),
|
|
158
169
|
outputSchema=overrides.get("outputSchema", self.output_schema),
|
|
170
|
+
icons=overrides.get("icons", self.icons),
|
|
159
171
|
annotations=overrides.get("annotations", self.annotations),
|
|
160
172
|
_meta=overrides.get(
|
|
161
173
|
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
@@ -168,10 +180,11 @@ class Tool(FastMCPComponent):
|
|
|
168
180
|
name: str | None = None,
|
|
169
181
|
title: str | None = None,
|
|
170
182
|
description: str | None = None,
|
|
183
|
+
icons: list[Icon] | None = None,
|
|
171
184
|
tags: set[str] | None = None,
|
|
172
185
|
annotations: ToolAnnotations | None = None,
|
|
173
186
|
exclude_args: list[str] | None = None,
|
|
174
|
-
output_schema: dict[str, Any] |
|
|
187
|
+
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
|
|
175
188
|
serializer: ToolResultSerializerType | None = None,
|
|
176
189
|
meta: dict[str, Any] | None = None,
|
|
177
190
|
enabled: bool | None = None,
|
|
@@ -182,6 +195,7 @@ class Tool(FastMCPComponent):
|
|
|
182
195
|
name=name,
|
|
183
196
|
title=title,
|
|
184
197
|
description=description,
|
|
198
|
+
icons=icons,
|
|
185
199
|
tags=tags,
|
|
186
200
|
annotations=annotations,
|
|
187
201
|
exclude_args=exclude_args,
|
|
@@ -209,13 +223,13 @@ class Tool(FastMCPComponent):
|
|
|
209
223
|
tool: Tool,
|
|
210
224
|
*,
|
|
211
225
|
name: str | None = None,
|
|
212
|
-
title: str |
|
|
213
|
-
description: str |
|
|
226
|
+
title: str | NotSetT | None = NotSet,
|
|
227
|
+
description: str | NotSetT | None = NotSet,
|
|
214
228
|
tags: set[str] | None = None,
|
|
215
|
-
annotations: ToolAnnotations |
|
|
216
|
-
output_schema: dict[str, Any] |
|
|
229
|
+
annotations: ToolAnnotations | NotSetT | None = NotSet,
|
|
230
|
+
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
|
|
217
231
|
serializer: ToolResultSerializerType | None = None,
|
|
218
|
-
meta: dict[str, Any] |
|
|
232
|
+
meta: dict[str, Any] | NotSetT | None = NotSet,
|
|
219
233
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
220
234
|
enabled: bool | None = None,
|
|
221
235
|
transform_fn: Callable[..., Any] | None = None,
|
|
@@ -248,15 +262,26 @@ class FunctionTool(Tool):
|
|
|
248
262
|
name: str | None = None,
|
|
249
263
|
title: str | None = None,
|
|
250
264
|
description: str | None = None,
|
|
265
|
+
icons: list[Icon] | None = None,
|
|
251
266
|
tags: set[str] | None = None,
|
|
252
267
|
annotations: ToolAnnotations | None = None,
|
|
253
268
|
exclude_args: list[str] | None = None,
|
|
254
|
-
output_schema: dict[str, Any] |
|
|
269
|
+
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
|
|
255
270
|
serializer: ToolResultSerializerType | None = None,
|
|
256
271
|
meta: dict[str, Any] | None = None,
|
|
257
272
|
enabled: bool | None = None,
|
|
258
273
|
) -> FunctionTool:
|
|
259
274
|
"""Create a Tool from a function."""
|
|
275
|
+
if exclude_args and fastmcp.settings.deprecation_warnings:
|
|
276
|
+
warnings.warn(
|
|
277
|
+
"The `exclude_args` parameter will be deprecated in FastMCP 2.14. "
|
|
278
|
+
"We recommend using dependency injection with `Depends()` instead, which provides "
|
|
279
|
+
"better lifecycle management and is more explicit. "
|
|
280
|
+
"`exclude_args` will continue to work until then. "
|
|
281
|
+
"See https://gofastmcp.com/docs/servers/tools for examples.",
|
|
282
|
+
DeprecationWarning,
|
|
283
|
+
stacklevel=2,
|
|
284
|
+
)
|
|
260
285
|
|
|
261
286
|
parsed_fn = ParsedFunction.from_function(fn, exclude_args=exclude_args)
|
|
262
287
|
|
|
@@ -280,10 +305,11 @@ class FunctionTool(Tool):
|
|
|
280
305
|
# Note: explicit schemas (dict) are used as-is without auto-wrapping
|
|
281
306
|
|
|
282
307
|
# Validate that explicit schemas are object type for structured content
|
|
308
|
+
# (resolving $ref references for self-referencing types)
|
|
283
309
|
if final_output_schema is not None and isinstance(final_output_schema, dict):
|
|
284
|
-
if final_output_schema
|
|
310
|
+
if not _is_object_schema(final_output_schema):
|
|
285
311
|
raise ValueError(
|
|
286
|
-
f
|
|
312
|
+
f"Output schemas must represent object types due to MCP spec limitations. Received: {final_output_schema!r}"
|
|
287
313
|
)
|
|
288
314
|
|
|
289
315
|
return cls(
|
|
@@ -291,6 +317,7 @@ class FunctionTool(Tool):
|
|
|
291
317
|
name=name or parsed_fn.name,
|
|
292
318
|
title=title,
|
|
293
319
|
description=description or parsed_fn.description,
|
|
320
|
+
icons=icons,
|
|
294
321
|
parameters=parsed_fn.input_schema,
|
|
295
322
|
output_schema=final_output_schema,
|
|
296
323
|
annotations=annotations,
|
|
@@ -351,6 +378,21 @@ class FunctionTool(Tool):
|
|
|
351
378
|
)
|
|
352
379
|
|
|
353
380
|
|
|
381
|
+
def _is_object_schema(schema: dict[str, Any]) -> bool:
|
|
382
|
+
"""Check if a JSON schema represents an object type."""
|
|
383
|
+
# Direct object type
|
|
384
|
+
if schema.get("type") == "object":
|
|
385
|
+
return True
|
|
386
|
+
|
|
387
|
+
# Schema with properties but no explicit type is treated as object
|
|
388
|
+
if "properties" in schema:
|
|
389
|
+
return True
|
|
390
|
+
|
|
391
|
+
# Self-referencing types use $ref pointing to $defs
|
|
392
|
+
# The referenced type is always an object in our use case
|
|
393
|
+
return "$ref" in schema and "$defs" in schema
|
|
394
|
+
|
|
395
|
+
|
|
354
396
|
@dataclass
|
|
355
397
|
class ParsedFunction:
|
|
356
398
|
fn: Callable[..., Any]
|
|
@@ -411,7 +453,14 @@ class ParsedFunction:
|
|
|
411
453
|
if exclude_args:
|
|
412
454
|
prune_params.extend(exclude_args)
|
|
413
455
|
|
|
414
|
-
|
|
456
|
+
# Create a function without excluded parameters in annotations
|
|
457
|
+
# This prevents Pydantic from trying to serialize non-serializable types
|
|
458
|
+
# before we can exclude them in compress_schema
|
|
459
|
+
fn_for_typeadapter = fn
|
|
460
|
+
if prune_params:
|
|
461
|
+
fn_for_typeadapter = create_function_without_params(fn, prune_params)
|
|
462
|
+
|
|
463
|
+
input_type_adapter = get_cached_typeadapter(fn_for_typeadapter)
|
|
415
464
|
input_schema = input_type_adapter.json_schema()
|
|
416
465
|
input_schema = compress_schema(
|
|
417
466
|
input_schema, prune_params=prune_params, prune_titles=True
|
|
@@ -441,9 +490,8 @@ class ParsedFunction:
|
|
|
441
490
|
# we ensure that no output schema is automatically generated.
|
|
442
491
|
clean_output_type = replace_type(
|
|
443
492
|
output_type,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
for t in (
|
|
493
|
+
dict.fromkeys( # type: ignore[arg-type]
|
|
494
|
+
(
|
|
447
495
|
Image,
|
|
448
496
|
Audio,
|
|
449
497
|
File,
|
|
@@ -453,8 +501,9 @@ class ParsedFunction:
|
|
|
453
501
|
mcp.types.AudioContent,
|
|
454
502
|
mcp.types.ResourceLink,
|
|
455
503
|
mcp.types.EmbeddedResource,
|
|
456
|
-
)
|
|
457
|
-
|
|
504
|
+
),
|
|
505
|
+
_UnserializableType,
|
|
506
|
+
),
|
|
458
507
|
)
|
|
459
508
|
|
|
460
509
|
try:
|
|
@@ -463,10 +512,9 @@ class ParsedFunction:
|
|
|
463
512
|
|
|
464
513
|
# Generate schema for wrapped type if it's non-object
|
|
465
514
|
# because MCP requires that output schemas are objects
|
|
466
|
-
if
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
):
|
|
515
|
+
# Check if schema is an object type, resolving $ref references
|
|
516
|
+
# (self-referencing types use $ref at root level)
|
|
517
|
+
if wrap_non_object_output_schema and not _is_object_schema(base_schema):
|
|
470
518
|
# Use the wrapped result schema directly
|
|
471
519
|
wrapped_type = _WrappedResult[clean_output_type]
|
|
472
520
|
wrapped_adapter = get_cached_typeadapter(wrapped_type)
|
|
@@ -546,13 +594,12 @@ def _convert_to_content(
|
|
|
546
594
|
|
|
547
595
|
# If any item is a ContentBlock, convert non-ContentBlock items to TextContent
|
|
548
596
|
# without aggregating them
|
|
549
|
-
if any(isinstance(item, ContentBlock) for item in result):
|
|
597
|
+
if any(isinstance(item, ContentBlock | Image | Audio | File) for item in result):
|
|
550
598
|
return [
|
|
551
599
|
_convert_to_single_content_block(item, serializer)
|
|
552
600
|
if not isinstance(item, ContentBlock)
|
|
553
601
|
else item
|
|
554
602
|
for item in result
|
|
555
603
|
]
|
|
556
|
-
|
|
557
604
|
# If none of the items are ContentBlocks, aggregate all items into a single TextContent
|
|
558
605
|
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
|
@@ -4,6 +4,7 @@ import inspect
|
|
|
4
4
|
import warnings
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from contextvars import ContextVar
|
|
7
|
+
from copy import deepcopy
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from typing import Annotated, Any, Literal, cast
|
|
9
10
|
|
|
@@ -34,7 +35,7 @@ _current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[
|
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
|
|
37
|
-
async def forward(**kwargs) -> ToolResult:
|
|
38
|
+
async def forward(**kwargs: Any) -> ToolResult:
|
|
38
39
|
"""Forward to parent tool with argument transformation applied.
|
|
39
40
|
|
|
40
41
|
This function can only be called from within a transformed tool's custom
|
|
@@ -64,7 +65,7 @@ async def forward(**kwargs) -> ToolResult:
|
|
|
64
65
|
return await tool.forwarding_fn(**kwargs)
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
async def forward_raw(**kwargs) -> ToolResult:
|
|
68
|
+
async def forward_raw(**kwargs: Any) -> ToolResult:
|
|
68
69
|
"""Forward directly to parent tool without transformation.
|
|
69
70
|
|
|
70
71
|
This function bypasses all argument transformation and validation, calling the parent
|
|
@@ -365,15 +366,15 @@ class TransformedTool(Tool):
|
|
|
365
366
|
cls,
|
|
366
367
|
tool: Tool,
|
|
367
368
|
name: str | None = None,
|
|
368
|
-
title: str |
|
|
369
|
-
description: str |
|
|
369
|
+
title: str | NotSetT | None = NotSet,
|
|
370
|
+
description: str | NotSetT | None = NotSet,
|
|
370
371
|
tags: set[str] | None = None,
|
|
371
372
|
transform_fn: Callable[..., Any] | None = None,
|
|
372
373
|
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] |
|
|
374
|
+
annotations: ToolAnnotations | NotSetT | None = NotSet,
|
|
375
|
+
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
|
|
376
|
+
serializer: Callable[[Any], str] | NotSetT | None = NotSet,
|
|
377
|
+
meta: dict[str, Any] | NotSetT | None = NotSet,
|
|
377
378
|
enabled: bool | None = None,
|
|
378
379
|
) -> TransformedTool:
|
|
379
380
|
"""Create a transformed tool from a parent tool.
|
|
@@ -620,7 +621,8 @@ class TransformedTool(Tool):
|
|
|
620
621
|
"""
|
|
621
622
|
|
|
622
623
|
# Build transformed schema and mapping
|
|
623
|
-
|
|
624
|
+
# Deep copy to prevent compress_schema from mutating parent tool's $defs
|
|
625
|
+
parent_defs = deepcopy(parent_tool.parameters.get("$defs", {}))
|
|
624
626
|
parent_props = parent_tool.parameters.get("properties", {}).copy()
|
|
625
627
|
parent_required = set(parent_tool.parameters.get("required", []))
|
|
626
628
|
|
|
@@ -681,7 +683,7 @@ class TransformedTool(Tool):
|
|
|
681
683
|
schema = compress_schema(schema, prune_defs=True)
|
|
682
684
|
|
|
683
685
|
# Create forwarding function that closes over everything it needs
|
|
684
|
-
async def _forward(**kwargs):
|
|
686
|
+
async def _forward(**kwargs: Any):
|
|
685
687
|
# Validate arguments
|
|
686
688
|
valid_args = set(new_props.keys())
|
|
687
689
|
provided_args = set(kwargs.keys())
|