fastmcp 2.12.1__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 +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- 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 +16 -10
- 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 +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- 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 +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- 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 +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- 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 +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- 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 +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.1.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
|
-
from typing import Annotated, Any, Literal
|
|
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,17 @@ 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
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from fastmcp.server.auth.auth import AuthProvider
|
|
34
|
+
|
|
26
35
|
|
|
27
36
|
class ExtendedEnvSettingsSource(EnvSettingsSource):
|
|
28
37
|
"""
|
|
@@ -79,7 +88,7 @@ class Settings(BaseSettings):
|
|
|
79
88
|
|
|
80
89
|
model_config = ExtendedSettingsConfigDict(
|
|
81
90
|
env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"],
|
|
82
|
-
env_file=
|
|
91
|
+
env_file=ENV_FILE,
|
|
83
92
|
extra="ignore",
|
|
84
93
|
env_nested_delimiter="__",
|
|
85
94
|
nested_model_default_partial_update=True,
|
|
@@ -142,7 +151,7 @@ class Settings(BaseSettings):
|
|
|
142
151
|
)
|
|
143
152
|
return self
|
|
144
153
|
|
|
145
|
-
home: Path = Path
|
|
154
|
+
home: Path = Path(user_data_dir("fastmcp", appauthor=False))
|
|
146
155
|
|
|
147
156
|
test_mode: bool = False
|
|
148
157
|
|
|
@@ -186,7 +195,6 @@ class Settings(BaseSettings):
|
|
|
186
195
|
client_raise_first_exceptiongroup_error: Annotated[
|
|
187
196
|
bool,
|
|
188
197
|
Field(
|
|
189
|
-
default=True,
|
|
190
198
|
description=inspect.cleandoc(
|
|
191
199
|
"""
|
|
192
200
|
Many MCP components operate in anyio taskgroups, and raise
|
|
@@ -202,7 +210,6 @@ class Settings(BaseSettings):
|
|
|
202
210
|
resource_prefix_format: Annotated[
|
|
203
211
|
Literal["protocol", "path"],
|
|
204
212
|
Field(
|
|
205
|
-
default="path",
|
|
206
213
|
description=inspect.cleandoc(
|
|
207
214
|
"""
|
|
208
215
|
When perfixing a resource URI, either use path formatting (resource://prefix/path)
|
|
@@ -232,7 +239,6 @@ class Settings(BaseSettings):
|
|
|
232
239
|
mask_error_details: Annotated[
|
|
233
240
|
bool,
|
|
234
241
|
Field(
|
|
235
|
-
default=False,
|
|
236
242
|
description=inspect.cleandoc(
|
|
237
243
|
"""
|
|
238
244
|
If True, error details from user-supplied functions (tool, resource, prompt)
|
|
@@ -245,6 +251,22 @@ class Settings(BaseSettings):
|
|
|
245
251
|
),
|
|
246
252
|
] = False
|
|
247
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
|
+
|
|
248
270
|
server_dependencies: list[str] = Field(
|
|
249
271
|
default_factory=list,
|
|
250
272
|
description="List of dependencies to install in the server environment",
|
|
@@ -258,7 +280,7 @@ class Settings(BaseSettings):
|
|
|
258
280
|
|
|
259
281
|
# Auth settings
|
|
260
282
|
server_auth: Annotated[
|
|
261
|
-
|
|
283
|
+
str | None,
|
|
262
284
|
Field(
|
|
263
285
|
description=inspect.cleandoc(
|
|
264
286
|
"""
|
|
@@ -266,13 +288,13 @@ class Settings(BaseSettings):
|
|
|
266
288
|
the full module path to an AuthProvider class (e.g.,
|
|
267
289
|
'fastmcp.server.auth.providers.google.GoogleProvider').
|
|
268
290
|
|
|
269
|
-
The specified class will be imported and instantiated automatically
|
|
270
|
-
Any class that inherits from AuthProvider
|
|
271
|
-
custom implementations.
|
|
291
|
+
The specified class will be imported and instantiated automatically
|
|
292
|
+
during FastMCP server creation. Any class that inherits from AuthProvider
|
|
293
|
+
can be used, including custom implementations.
|
|
272
294
|
|
|
273
295
|
If None, no automatic configuration will take place.
|
|
274
296
|
|
|
275
|
-
This setting is *always*
|
|
297
|
+
This setting is *always* overridden by any auth provider passed to the
|
|
276
298
|
FastMCP constructor.
|
|
277
299
|
|
|
278
300
|
Note that most auth providers require additional configuration
|
|
@@ -290,7 +312,6 @@ class Settings(BaseSettings):
|
|
|
290
312
|
include_tags: Annotated[
|
|
291
313
|
set[str] | None,
|
|
292
314
|
Field(
|
|
293
|
-
default=None,
|
|
294
315
|
description=inspect.cleandoc(
|
|
295
316
|
"""
|
|
296
317
|
If provided, only components that match these tags will be
|
|
@@ -303,7 +324,6 @@ class Settings(BaseSettings):
|
|
|
303
324
|
exclude_tags: Annotated[
|
|
304
325
|
set[str] | None,
|
|
305
326
|
Field(
|
|
306
|
-
default=None,
|
|
307
327
|
description=inspect.cleandoc(
|
|
308
328
|
"""
|
|
309
329
|
If provided, components that match these tags will be excluded
|
|
@@ -317,7 +337,6 @@ class Settings(BaseSettings):
|
|
|
317
337
|
include_fastmcp_meta: Annotated[
|
|
318
338
|
bool,
|
|
319
339
|
Field(
|
|
320
|
-
default=True,
|
|
321
340
|
description=inspect.cleandoc(
|
|
322
341
|
"""
|
|
323
342
|
Whether to include FastMCP meta in the server's MCP responses.
|
|
@@ -332,7 +351,6 @@ class Settings(BaseSettings):
|
|
|
332
351
|
mounted_components_raise_on_load_error: Annotated[
|
|
333
352
|
bool,
|
|
334
353
|
Field(
|
|
335
|
-
default=False,
|
|
336
354
|
description=inspect.cleandoc(
|
|
337
355
|
"""
|
|
338
356
|
If True, errors encountered when loading mounted components (tools, resources, prompts)
|
|
@@ -343,6 +361,38 @@ class Settings(BaseSettings):
|
|
|
343
361
|
),
|
|
344
362
|
] = False
|
|
345
363
|
|
|
364
|
+
show_cli_banner: Annotated[
|
|
365
|
+
bool,
|
|
366
|
+
Field(
|
|
367
|
+
description=inspect.cleandoc(
|
|
368
|
+
"""
|
|
369
|
+
If True, the server banner will be displayed when running the server via CLI.
|
|
370
|
+
This setting can be overridden by the --no-banner CLI flag.
|
|
371
|
+
Set to False via FASTMCP_SHOW_CLI_BANNER=false to suppress the banner.
|
|
372
|
+
"""
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
] = True
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def server_auth_class(self) -> AuthProvider | None:
|
|
379
|
+
from fastmcp.utilities.types import get_cached_typeadapter
|
|
380
|
+
|
|
381
|
+
if not self.server_auth:
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
# https://github.com/jlowin/fastmcp/issues/1749
|
|
385
|
+
# Pydantic imports the module in an ImportString during model validation, but we don't want the server
|
|
386
|
+
# auth module imported during settings creation as it imports dependencies we aren't ready for yet.
|
|
387
|
+
# To fix this while limiting breaking changes, we delay the import by only creating the ImportString
|
|
388
|
+
# when the class is actually needed
|
|
389
|
+
|
|
390
|
+
type_adapter = get_cached_typeadapter(ImportString)
|
|
391
|
+
|
|
392
|
+
auth_class = type_adapter.validate_python(self.server_auth)
|
|
393
|
+
|
|
394
|
+
return auth_class
|
|
395
|
+
|
|
346
396
|
|
|
347
397
|
def __getattr__(name: str):
|
|
348
398
|
"""
|
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
|
@@ -10,12 +10,13 @@ from typing import (
|
|
|
10
10
|
Any,
|
|
11
11
|
Generic,
|
|
12
12
|
Literal,
|
|
13
|
+
TypeAlias,
|
|
13
14
|
get_type_hints,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
import mcp.types
|
|
17
18
|
import pydantic_core
|
|
18
|
-
from mcp.types import ContentBlock, TextContent, ToolAnnotations
|
|
19
|
+
from mcp.types import CallToolResult, ContentBlock, Icon, TextContent, ToolAnnotations
|
|
19
20
|
from mcp.types import Tool as MCPTool
|
|
20
21
|
from pydantic import Field, PydanticSchemaGenerationError
|
|
21
22
|
from typing_extensions import TypeVar
|
|
@@ -31,6 +32,7 @@ from fastmcp.utilities.types import (
|
|
|
31
32
|
Image,
|
|
32
33
|
NotSet,
|
|
33
34
|
NotSetT,
|
|
35
|
+
create_function_without_params,
|
|
34
36
|
find_kwarg_by_type,
|
|
35
37
|
get_cached_typeadapter,
|
|
36
38
|
replace_type,
|
|
@@ -55,6 +57,9 @@ class _UnserializableType:
|
|
|
55
57
|
pass
|
|
56
58
|
|
|
57
59
|
|
|
60
|
+
ToolResultSerializerType: TypeAlias = Callable[[Any], str]
|
|
61
|
+
|
|
62
|
+
|
|
58
63
|
def default_serializer(data: Any) -> str:
|
|
59
64
|
return pydantic_core.to_json(data, fallback=str).decode()
|
|
60
65
|
|
|
@@ -64,18 +69,20 @@ class ToolResult:
|
|
|
64
69
|
self,
|
|
65
70
|
content: list[ContentBlock] | Any | None = None,
|
|
66
71
|
structured_content: dict[str, Any] | Any | None = None,
|
|
72
|
+
meta: dict[str, Any] | None = None,
|
|
67
73
|
):
|
|
68
74
|
if content is None and structured_content is None:
|
|
69
75
|
raise ValueError("Either content or structured_content must be provided")
|
|
70
76
|
elif content is None:
|
|
71
77
|
content = structured_content
|
|
72
78
|
|
|
73
|
-
self.content = _convert_to_content(content)
|
|
79
|
+
self.content: list[ContentBlock] = _convert_to_content(result=content)
|
|
80
|
+
self.meta: dict[str, Any] | None = meta
|
|
74
81
|
|
|
75
82
|
if structured_content is not None:
|
|
76
83
|
try:
|
|
77
84
|
structured_content = pydantic_core.to_jsonable_python(
|
|
78
|
-
structured_content
|
|
85
|
+
value=structured_content
|
|
79
86
|
)
|
|
80
87
|
except pydantic_core.PydanticSerializationError as e:
|
|
81
88
|
logger.error(
|
|
@@ -92,7 +99,15 @@ class ToolResult:
|
|
|
92
99
|
|
|
93
100
|
def to_mcp_result(
|
|
94
101
|
self,
|
|
95
|
-
) ->
|
|
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
|
+
)
|
|
96
111
|
if self.structured_content is None:
|
|
97
112
|
return self.content
|
|
98
113
|
return self.content, self.structured_content
|
|
@@ -112,7 +127,7 @@ class Tool(FastMCPComponent):
|
|
|
112
127
|
Field(description="Additional annotations about the tool"),
|
|
113
128
|
] = None
|
|
114
129
|
serializer: Annotated[
|
|
115
|
-
|
|
130
|
+
ToolResultSerializerType | None,
|
|
116
131
|
Field(description="Optional custom serializer for tool results"),
|
|
117
132
|
] = None
|
|
118
133
|
|
|
@@ -138,23 +153,26 @@ class Tool(FastMCPComponent):
|
|
|
138
153
|
include_fastmcp_meta: bool | None = None,
|
|
139
154
|
**overrides: Any,
|
|
140
155
|
) -> MCPTool:
|
|
156
|
+
"""Convert the FastMCP tool to an MCP tool."""
|
|
157
|
+
title = None
|
|
158
|
+
|
|
141
159
|
if self.title:
|
|
142
160
|
title = self.title
|
|
143
161
|
elif self.annotations and self.annotations.title:
|
|
144
162
|
title = self.annotations.title
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"annotations"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
|
|
164
|
+
return MCPTool(
|
|
165
|
+
name=overrides.get("name", self.name),
|
|
166
|
+
title=overrides.get("title", title),
|
|
167
|
+
description=overrides.get("description", self.description),
|
|
168
|
+
inputSchema=overrides.get("inputSchema", self.parameters),
|
|
169
|
+
outputSchema=overrides.get("outputSchema", self.output_schema),
|
|
170
|
+
icons=overrides.get("icons", self.icons),
|
|
171
|
+
annotations=overrides.get("annotations", self.annotations),
|
|
172
|
+
_meta=overrides.get(
|
|
173
|
+
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
|
174
|
+
),
|
|
175
|
+
)
|
|
158
176
|
|
|
159
177
|
@staticmethod
|
|
160
178
|
def from_function(
|
|
@@ -162,11 +180,12 @@ class Tool(FastMCPComponent):
|
|
|
162
180
|
name: str | None = None,
|
|
163
181
|
title: str | None = None,
|
|
164
182
|
description: str | None = None,
|
|
183
|
+
icons: list[Icon] | None = None,
|
|
165
184
|
tags: set[str] | None = None,
|
|
166
185
|
annotations: ToolAnnotations | None = None,
|
|
167
186
|
exclude_args: list[str] | None = None,
|
|
168
|
-
output_schema: dict[str, Any] |
|
|
169
|
-
serializer:
|
|
187
|
+
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
|
|
188
|
+
serializer: ToolResultSerializerType | None = None,
|
|
170
189
|
meta: dict[str, Any] | None = None,
|
|
171
190
|
enabled: bool | None = None,
|
|
172
191
|
) -> FunctionTool:
|
|
@@ -176,6 +195,7 @@ class Tool(FastMCPComponent):
|
|
|
176
195
|
name=name,
|
|
177
196
|
title=title,
|
|
178
197
|
description=description,
|
|
198
|
+
icons=icons,
|
|
179
199
|
tags=tags,
|
|
180
200
|
annotations=annotations,
|
|
181
201
|
exclude_args=exclude_args,
|
|
@@ -203,13 +223,13 @@ class Tool(FastMCPComponent):
|
|
|
203
223
|
tool: Tool,
|
|
204
224
|
*,
|
|
205
225
|
name: str | None = None,
|
|
206
|
-
title: str |
|
|
207
|
-
description: str |
|
|
226
|
+
title: str | NotSetT | None = NotSet,
|
|
227
|
+
description: str | NotSetT | None = NotSet,
|
|
208
228
|
tags: set[str] | None = None,
|
|
209
|
-
annotations: ToolAnnotations |
|
|
210
|
-
output_schema: dict[str, Any] |
|
|
211
|
-
serializer:
|
|
212
|
-
meta: dict[str, Any] |
|
|
229
|
+
annotations: ToolAnnotations | NotSetT | None = NotSet,
|
|
230
|
+
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
|
|
231
|
+
serializer: ToolResultSerializerType | None = None,
|
|
232
|
+
meta: dict[str, Any] | NotSetT | None = NotSet,
|
|
213
233
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
214
234
|
enabled: bool | None = None,
|
|
215
235
|
transform_fn: Callable[..., Any] | None = None,
|
|
@@ -242,15 +262,26 @@ class FunctionTool(Tool):
|
|
|
242
262
|
name: str | None = None,
|
|
243
263
|
title: str | None = None,
|
|
244
264
|
description: str | None = None,
|
|
265
|
+
icons: list[Icon] | None = None,
|
|
245
266
|
tags: set[str] | None = None,
|
|
246
267
|
annotations: ToolAnnotations | None = None,
|
|
247
268
|
exclude_args: list[str] | None = None,
|
|
248
|
-
output_schema: dict[str, Any] |
|
|
249
|
-
serializer:
|
|
269
|
+
output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
|
|
270
|
+
serializer: ToolResultSerializerType | None = None,
|
|
250
271
|
meta: dict[str, Any] | None = None,
|
|
251
272
|
enabled: bool | None = None,
|
|
252
273
|
) -> FunctionTool:
|
|
253
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
|
+
)
|
|
254
285
|
|
|
255
286
|
parsed_fn = ParsedFunction.from_function(fn, exclude_args=exclude_args)
|
|
256
287
|
|
|
@@ -274,10 +305,11 @@ class FunctionTool(Tool):
|
|
|
274
305
|
# Note: explicit schemas (dict) are used as-is without auto-wrapping
|
|
275
306
|
|
|
276
307
|
# Validate that explicit schemas are object type for structured content
|
|
308
|
+
# (resolving $ref references for self-referencing types)
|
|
277
309
|
if final_output_schema is not None and isinstance(final_output_schema, dict):
|
|
278
|
-
if final_output_schema
|
|
310
|
+
if not _is_object_schema(final_output_schema):
|
|
279
311
|
raise ValueError(
|
|
280
|
-
f
|
|
312
|
+
f"Output schemas must represent object types due to MCP spec limitations. Received: {final_output_schema!r}"
|
|
281
313
|
)
|
|
282
314
|
|
|
283
315
|
return cls(
|
|
@@ -285,6 +317,7 @@ class FunctionTool(Tool):
|
|
|
285
317
|
name=name or parsed_fn.name,
|
|
286
318
|
title=title,
|
|
287
319
|
description=description or parsed_fn.description,
|
|
320
|
+
icons=icons,
|
|
288
321
|
parameters=parsed_fn.input_schema,
|
|
289
322
|
output_schema=final_output_schema,
|
|
290
323
|
annotations=annotations,
|
|
@@ -315,30 +348,51 @@ class FunctionTool(Tool):
|
|
|
315
348
|
|
|
316
349
|
unstructured_result = _convert_to_content(result, serializer=self.serializer)
|
|
317
350
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
# it as structured content. If it is not a dict, ignore it.
|
|
328
|
-
if structured_output is None:
|
|
351
|
+
if self.output_schema is None:
|
|
352
|
+
# Do not produce a structured output for MCP Content Types
|
|
353
|
+
if isinstance(result, ContentBlock | Audio | Image | File) or (
|
|
354
|
+
isinstance(result, list | tuple)
|
|
355
|
+
and any(isinstance(item, ContentBlock) for item in result)
|
|
356
|
+
):
|
|
357
|
+
return ToolResult(content=unstructured_result)
|
|
358
|
+
|
|
359
|
+
# Otherwise, try to serialize the result as a dict
|
|
329
360
|
try:
|
|
330
|
-
|
|
331
|
-
if
|
|
332
|
-
|
|
333
|
-
|
|
361
|
+
structured_content = pydantic_core.to_jsonable_python(result)
|
|
362
|
+
if isinstance(structured_content, dict):
|
|
363
|
+
return ToolResult(
|
|
364
|
+
content=unstructured_result,
|
|
365
|
+
structured_content=structured_content,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
except pydantic_core.PydanticSerializationError:
|
|
334
369
|
pass
|
|
335
370
|
|
|
371
|
+
return ToolResult(content=unstructured_result)
|
|
372
|
+
|
|
373
|
+
wrap_result = self.output_schema.get("x-fastmcp-wrap-result")
|
|
374
|
+
|
|
336
375
|
return ToolResult(
|
|
337
376
|
content=unstructured_result,
|
|
338
|
-
structured_content=
|
|
377
|
+
structured_content={"result": result} if wrap_result else result,
|
|
339
378
|
)
|
|
340
379
|
|
|
341
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
|
+
|
|
342
396
|
@dataclass
|
|
343
397
|
class ParsedFunction:
|
|
344
398
|
fn: Callable[..., Any]
|
|
@@ -399,9 +453,18 @@ class ParsedFunction:
|
|
|
399
453
|
if exclude_args:
|
|
400
454
|
prune_params.extend(exclude_args)
|
|
401
455
|
|
|
402
|
-
|
|
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)
|
|
403
464
|
input_schema = input_type_adapter.json_schema()
|
|
404
|
-
input_schema = compress_schema(
|
|
465
|
+
input_schema = compress_schema(
|
|
466
|
+
input_schema, prune_params=prune_params, prune_titles=True
|
|
467
|
+
)
|
|
405
468
|
|
|
406
469
|
output_schema = None
|
|
407
470
|
# Get the return annotation from the signature
|
|
@@ -427,9 +490,8 @@ class ParsedFunction:
|
|
|
427
490
|
# we ensure that no output schema is automatically generated.
|
|
428
491
|
clean_output_type = replace_type(
|
|
429
492
|
output_type,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
for t in (
|
|
493
|
+
dict.fromkeys( # type: ignore[arg-type]
|
|
494
|
+
(
|
|
433
495
|
Image,
|
|
434
496
|
Audio,
|
|
435
497
|
File,
|
|
@@ -439,8 +501,9 @@ class ParsedFunction:
|
|
|
439
501
|
mcp.types.AudioContent,
|
|
440
502
|
mcp.types.ResourceLink,
|
|
441
503
|
mcp.types.EmbeddedResource,
|
|
442
|
-
)
|
|
443
|
-
|
|
504
|
+
),
|
|
505
|
+
_UnserializableType,
|
|
506
|
+
),
|
|
444
507
|
)
|
|
445
508
|
|
|
446
509
|
try:
|
|
@@ -449,10 +512,9 @@ class ParsedFunction:
|
|
|
449
512
|
|
|
450
513
|
# Generate schema for wrapped type if it's non-object
|
|
451
514
|
# because MCP requires that output schemas are objects
|
|
452
|
-
if
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
):
|
|
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):
|
|
456
518
|
# Use the wrapped result schema directly
|
|
457
519
|
wrapped_type = _WrappedResult[clean_output_type]
|
|
458
520
|
wrapped_adapter = get_cached_typeadapter(wrapped_type)
|
|
@@ -461,7 +523,7 @@ class ParsedFunction:
|
|
|
461
523
|
else:
|
|
462
524
|
output_schema = base_schema
|
|
463
525
|
|
|
464
|
-
output_schema = compress_schema(output_schema)
|
|
526
|
+
output_schema = compress_schema(output_schema, prune_titles=True)
|
|
465
527
|
|
|
466
528
|
except PydanticSchemaGenerationError as e:
|
|
467
529
|
if "_UnserializableType" not in str(e):
|
|
@@ -476,65 +538,68 @@ class ParsedFunction:
|
|
|
476
538
|
)
|
|
477
539
|
|
|
478
540
|
|
|
479
|
-
def
|
|
480
|
-
result: Any,
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
541
|
+
def _serialize_with_fallback(
|
|
542
|
+
result: Any, serializer: ToolResultSerializerType | None = None
|
|
543
|
+
) -> str:
|
|
544
|
+
if serializer is not None:
|
|
545
|
+
try:
|
|
546
|
+
return serializer(result)
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logger.warning(
|
|
549
|
+
"Error serializing tool result: %s",
|
|
550
|
+
e,
|
|
551
|
+
exc_info=True,
|
|
552
|
+
)
|
|
485
553
|
|
|
486
|
-
|
|
487
|
-
return []
|
|
554
|
+
return default_serializer(result)
|
|
488
555
|
|
|
489
|
-
if isinstance(result, ContentBlock):
|
|
490
|
-
return [result]
|
|
491
556
|
|
|
492
|
-
|
|
493
|
-
|
|
557
|
+
def _convert_to_single_content_block(
|
|
558
|
+
item: Any,
|
|
559
|
+
serializer: ToolResultSerializerType | None = None,
|
|
560
|
+
) -> ContentBlock:
|
|
561
|
+
if isinstance(item, ContentBlock):
|
|
562
|
+
return item
|
|
494
563
|
|
|
495
|
-
|
|
496
|
-
return
|
|
564
|
+
if isinstance(item, Image):
|
|
565
|
+
return item.to_image_content()
|
|
497
566
|
|
|
498
|
-
|
|
499
|
-
return
|
|
567
|
+
if isinstance(item, Audio):
|
|
568
|
+
return item.to_audio_content()
|
|
500
569
|
|
|
501
|
-
if isinstance(
|
|
502
|
-
|
|
503
|
-
# or a "regular" list that the tool is returning, or a mix of both.
|
|
504
|
-
#
|
|
505
|
-
# so we extract all the MCP types / images and convert them as individual content elements,
|
|
506
|
-
# and aggregate the rest as a single content element
|
|
570
|
+
if isinstance(item, File):
|
|
571
|
+
return item.to_resource_content()
|
|
507
572
|
|
|
508
|
-
|
|
509
|
-
|
|
573
|
+
if isinstance(item, str):
|
|
574
|
+
return TextContent(type="text", text=item)
|
|
510
575
|
|
|
511
|
-
|
|
512
|
-
if isinstance(item, ContentBlock | Image | Audio | File):
|
|
513
|
-
mcp_types.append(_convert_to_content(item)[0])
|
|
514
|
-
else:
|
|
515
|
-
other_content.append(item)
|
|
576
|
+
return TextContent(type="text", text=_serialize_with_fallback(item, serializer))
|
|
516
577
|
|
|
517
|
-
if other_content:
|
|
518
|
-
other_content = _convert_to_content(
|
|
519
|
-
other_content,
|
|
520
|
-
serializer=serializer,
|
|
521
|
-
_process_as_single_item=True,
|
|
522
|
-
)
|
|
523
578
|
|
|
524
|
-
|
|
579
|
+
def _convert_to_content(
|
|
580
|
+
result: Any,
|
|
581
|
+
serializer: ToolResultSerializerType | None = None,
|
|
582
|
+
) -> list[ContentBlock]:
|
|
583
|
+
"""Convert a result to a sequence of content objects."""
|
|
525
584
|
|
|
526
|
-
if
|
|
527
|
-
|
|
528
|
-
result = default_serializer(result)
|
|
529
|
-
else:
|
|
530
|
-
try:
|
|
531
|
-
result = serializer(result)
|
|
532
|
-
except Exception as e:
|
|
533
|
-
logger.warning(
|
|
534
|
-
"Error serializing tool result: %s",
|
|
535
|
-
e,
|
|
536
|
-
exc_info=True,
|
|
537
|
-
)
|
|
538
|
-
result = default_serializer(result)
|
|
585
|
+
if result is None:
|
|
586
|
+
return []
|
|
539
587
|
|
|
540
|
-
|
|
588
|
+
if not isinstance(result, (list | tuple)):
|
|
589
|
+
return [_convert_to_single_content_block(result, serializer)]
|
|
590
|
+
|
|
591
|
+
# If all items are ContentBlocks, return them as is
|
|
592
|
+
if all(isinstance(item, ContentBlock) for item in result):
|
|
593
|
+
return result
|
|
594
|
+
|
|
595
|
+
# If any item is a ContentBlock, convert non-ContentBlock items to TextContent
|
|
596
|
+
# without aggregating them
|
|
597
|
+
if any(isinstance(item, ContentBlock | Image | Audio | File) for item in result):
|
|
598
|
+
return [
|
|
599
|
+
_convert_to_single_content_block(item, serializer)
|
|
600
|
+
if not isinstance(item, ContentBlock)
|
|
601
|
+
else item
|
|
602
|
+
for item in result
|
|
603
|
+
]
|
|
604
|
+
# If none of the items are ContentBlocks, aggregate all items into a single TextContent
|
|
605
|
+
return [TextContent(type="text", text=_serialize_with_fallback(result, serializer))]
|