fastmcp 2.10.5__py3-none-any.whl → 2.11.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 +7 -2
- fastmcp/cli/cli.py +128 -33
- fastmcp/cli/install/__init__.py +2 -2
- fastmcp/cli/install/claude_code.py +42 -1
- fastmcp/cli/install/claude_desktop.py +42 -1
- fastmcp/cli/install/cursor.py +42 -1
- fastmcp/cli/install/{mcp_config.py → mcp_json.py} +51 -7
- fastmcp/cli/run.py +127 -1
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/auth/oauth.py +68 -99
- fastmcp/client/oauth_callback.py +18 -0
- fastmcp/client/transports.py +69 -15
- fastmcp/contrib/component_manager/example.py +2 -2
- fastmcp/experimental/server/openapi/README.md +266 -0
- fastmcp/experimental/server/openapi/__init__.py +38 -0
- fastmcp/experimental/server/openapi/components.py +348 -0
- fastmcp/experimental/server/openapi/routing.py +132 -0
- fastmcp/experimental/server/openapi/server.py +466 -0
- fastmcp/experimental/utilities/openapi/README.md +239 -0
- fastmcp/experimental/utilities/openapi/__init__.py +68 -0
- fastmcp/experimental/utilities/openapi/director.py +208 -0
- fastmcp/experimental/utilities/openapi/formatters.py +355 -0
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
- fastmcp/experimental/utilities/openapi/models.py +85 -0
- fastmcp/experimental/utilities/openapi/parser.py +618 -0
- fastmcp/experimental/utilities/openapi/schemas.py +538 -0
- fastmcp/mcp_config.py +125 -88
- fastmcp/prompts/prompt.py +11 -1
- fastmcp/prompts/prompt_manager.py +1 -1
- fastmcp/resources/resource.py +21 -1
- fastmcp/resources/resource_manager.py +2 -2
- fastmcp/resources/template.py +20 -1
- fastmcp/server/auth/__init__.py +17 -2
- fastmcp/server/auth/auth.py +144 -7
- fastmcp/server/auth/providers/bearer.py +25 -473
- fastmcp/server/auth/providers/in_memory.py +4 -2
- fastmcp/server/auth/providers/jwt.py +538 -0
- fastmcp/server/auth/providers/workos.py +170 -0
- fastmcp/server/auth/registry.py +52 -0
- fastmcp/server/context.py +110 -26
- fastmcp/server/dependencies.py +9 -2
- fastmcp/server/http.py +62 -30
- fastmcp/server/middleware/middleware.py +3 -23
- fastmcp/server/openapi.py +26 -13
- fastmcp/server/proxy.py +89 -8
- fastmcp/server/server.py +170 -62
- fastmcp/settings.py +83 -18
- fastmcp/tools/tool.py +41 -6
- fastmcp/tools/tool_manager.py +39 -3
- fastmcp/tools/tool_transform.py +122 -6
- fastmcp/utilities/components.py +35 -2
- fastmcp/utilities/json_schema.py +136 -98
- fastmcp/utilities/json_schema_type.py +1 -3
- fastmcp/utilities/mcp_config.py +28 -0
- fastmcp/utilities/openapi.py +306 -30
- fastmcp/utilities/tests.py +54 -6
- fastmcp/utilities/types.py +89 -11
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
- fastmcp-2.11.0.dist-info/RECORD +108 -0
- fastmcp/server/auth/providers/bearer_env.py +0 -63
- fastmcp/utilities/cache.py +0 -26
- fastmcp-2.10.5.dist-info/RECORD +0 -93
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/settings.py
CHANGED
|
@@ -5,7 +5,7 @@ import warnings
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Annotated, Any, Literal
|
|
7
7
|
|
|
8
|
-
from pydantic import Field,
|
|
8
|
+
from pydantic import Field, field_validator
|
|
9
9
|
from pydantic.fields import FieldInfo
|
|
10
10
|
from pydantic_settings import (
|
|
11
11
|
BaseSettings,
|
|
@@ -55,6 +55,25 @@ class ExtendedSettingsConfigDict(SettingsConfigDict, total=False):
|
|
|
55
55
|
env_prefixes: list[str] | None
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
class ExperimentalSettings(BaseSettings):
|
|
59
|
+
model_config = SettingsConfigDict(
|
|
60
|
+
env_prefix="FASTMCP_EXPERIMENTAL_",
|
|
61
|
+
extra="ignore",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
enable_new_openapi_parser: Annotated[
|
|
65
|
+
bool,
|
|
66
|
+
Field(
|
|
67
|
+
description=inspect.cleandoc(
|
|
68
|
+
"""
|
|
69
|
+
Whether to use the new OpenAPI parser. This parser was introduced
|
|
70
|
+
for testing in 2.11 and will become the default soon.
|
|
71
|
+
"""
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
] = False
|
|
75
|
+
|
|
76
|
+
|
|
58
77
|
class Settings(BaseSettings):
|
|
59
78
|
"""FastMCP settings."""
|
|
60
79
|
|
|
@@ -64,8 +83,35 @@ class Settings(BaseSettings):
|
|
|
64
83
|
extra="ignore",
|
|
65
84
|
env_nested_delimiter="__",
|
|
66
85
|
nested_model_default_partial_update=True,
|
|
86
|
+
validate_assignment=True,
|
|
67
87
|
)
|
|
68
88
|
|
|
89
|
+
def get_setting(self, attr: str) -> Any:
|
|
90
|
+
"""
|
|
91
|
+
Get a setting. If the setting contains one or more `__`, it will be
|
|
92
|
+
treated as a nested setting.
|
|
93
|
+
"""
|
|
94
|
+
settings = self
|
|
95
|
+
while "__" in attr:
|
|
96
|
+
parent_attr, attr = attr.split("__", 1)
|
|
97
|
+
if not hasattr(settings, parent_attr):
|
|
98
|
+
raise AttributeError(f"Setting {parent_attr} does not exist.")
|
|
99
|
+
settings = getattr(settings, parent_attr)
|
|
100
|
+
return getattr(settings, attr)
|
|
101
|
+
|
|
102
|
+
def set_setting(self, attr: str, value: Any) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Set a setting. If the setting contains one or more `__`, it will be
|
|
105
|
+
treated as a nested setting.
|
|
106
|
+
"""
|
|
107
|
+
settings = self
|
|
108
|
+
while "__" in attr:
|
|
109
|
+
parent_attr, attr = attr.split("__", 1)
|
|
110
|
+
if not hasattr(settings, parent_attr):
|
|
111
|
+
raise AttributeError(f"Setting {parent_attr} does not exist.")
|
|
112
|
+
settings = getattr(settings, parent_attr)
|
|
113
|
+
setattr(settings, attr, value)
|
|
114
|
+
|
|
69
115
|
@classmethod
|
|
70
116
|
def settings_customise_sources(
|
|
71
117
|
cls,
|
|
@@ -99,7 +145,18 @@ class Settings(BaseSettings):
|
|
|
99
145
|
home: Path = Path.home() / ".fastmcp"
|
|
100
146
|
|
|
101
147
|
test_mode: bool = False
|
|
148
|
+
|
|
102
149
|
log_level: LOG_LEVEL = "INFO"
|
|
150
|
+
|
|
151
|
+
@field_validator("log_level", mode="before")
|
|
152
|
+
@classmethod
|
|
153
|
+
def normalize_log_level(cls, v):
|
|
154
|
+
if isinstance(v, str):
|
|
155
|
+
return v.upper()
|
|
156
|
+
return v
|
|
157
|
+
|
|
158
|
+
experimental: ExperimentalSettings = ExperimentalSettings()
|
|
159
|
+
|
|
103
160
|
enable_rich_tracebacks: Annotated[
|
|
104
161
|
bool,
|
|
105
162
|
Field(
|
|
@@ -162,17 +219,6 @@ class Settings(BaseSettings):
|
|
|
162
219
|
),
|
|
163
220
|
] = None
|
|
164
221
|
|
|
165
|
-
@model_validator(mode="after")
|
|
166
|
-
def setup_logging(self) -> Self:
|
|
167
|
-
"""Finalize the settings."""
|
|
168
|
-
from fastmcp.utilities.logging import configure_logging
|
|
169
|
-
|
|
170
|
-
configure_logging(
|
|
171
|
-
self.log_level, enable_rich_tracebacks=self.enable_rich_tracebacks
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
return self
|
|
175
|
-
|
|
176
222
|
# HTTP settings
|
|
177
223
|
host: str = "127.0.0.1"
|
|
178
224
|
port: int = 8000
|
|
@@ -213,19 +259,23 @@ class Settings(BaseSettings):
|
|
|
213
259
|
)
|
|
214
260
|
|
|
215
261
|
# Auth settings
|
|
216
|
-
|
|
217
|
-
|
|
262
|
+
server_auth: Annotated[
|
|
263
|
+
str | None,
|
|
218
264
|
Field(
|
|
219
265
|
description=inspect.cleandoc(
|
|
220
266
|
"""
|
|
221
|
-
Configure the authentication provider
|
|
222
|
-
|
|
223
|
-
|
|
267
|
+
Configure the authentication provider for the server. Auth
|
|
268
|
+
providers are registered with a specific key, and providing that
|
|
269
|
+
key here will cause the server to automatically configure the
|
|
270
|
+
provider from the environment.
|
|
224
271
|
|
|
225
272
|
If None, no automatic configuration will take place.
|
|
226
273
|
|
|
227
274
|
This setting is *always* overriden by any auth provider passed to the
|
|
228
275
|
FastMCP constructor.
|
|
276
|
+
|
|
277
|
+
Note that most auth providers require additional configuration
|
|
278
|
+
that must be provided via env vars.
|
|
229
279
|
"""
|
|
230
280
|
),
|
|
231
281
|
),
|
|
@@ -258,6 +308,21 @@ class Settings(BaseSettings):
|
|
|
258
308
|
),
|
|
259
309
|
] = None
|
|
260
310
|
|
|
311
|
+
include_fastmcp_meta: Annotated[
|
|
312
|
+
bool,
|
|
313
|
+
Field(
|
|
314
|
+
default=True,
|
|
315
|
+
description=inspect.cleandoc(
|
|
316
|
+
"""
|
|
317
|
+
Whether to include FastMCP meta in the server's MCP responses.
|
|
318
|
+
If True, a `_fastmcp` key will be added to the `meta` field of
|
|
319
|
+
all MCP component responses. This key will contain a dict of
|
|
320
|
+
various FastMCP-specific metadata, such as tags.
|
|
321
|
+
"""
|
|
322
|
+
),
|
|
323
|
+
),
|
|
324
|
+
] = True
|
|
325
|
+
|
|
261
326
|
|
|
262
327
|
def __getattr__(name: str):
|
|
263
328
|
"""
|
|
@@ -270,7 +335,7 @@ def __getattr__(name: str):
|
|
|
270
335
|
# Deprecated in 2.10.2
|
|
271
336
|
if settings.deprecation_warnings:
|
|
272
337
|
warnings.warn(
|
|
273
|
-
"`from fastmcp.settings import settings` is deprecated. use `
|
|
338
|
+
"`from fastmcp.settings import settings` is deprecated. use `fastmcp.settings` instead.",
|
|
274
339
|
DeprecationWarning,
|
|
275
340
|
stacklevel=2,
|
|
276
341
|
)
|
fastmcp/tools/tool.py
CHANGED
|
@@ -3,7 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
import inspect
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import (
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
Annotated,
|
|
9
|
+
Any,
|
|
10
|
+
Generic,
|
|
11
|
+
Literal,
|
|
12
|
+
TypeVar,
|
|
13
|
+
get_type_hints,
|
|
14
|
+
)
|
|
7
15
|
|
|
8
16
|
import mcp.types
|
|
9
17
|
import pydantic_core
|
|
@@ -122,7 +130,12 @@ class Tool(FastMCPComponent):
|
|
|
122
130
|
except RuntimeError:
|
|
123
131
|
pass # No context available
|
|
124
132
|
|
|
125
|
-
def to_mcp_tool(
|
|
133
|
+
def to_mcp_tool(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
include_fastmcp_meta: bool | None = None,
|
|
137
|
+
**overrides: Any,
|
|
138
|
+
) -> MCPTool:
|
|
126
139
|
if self.title:
|
|
127
140
|
title = self.title
|
|
128
141
|
elif self.annotations and self.annotations.title:
|
|
@@ -137,6 +150,7 @@ class Tool(FastMCPComponent):
|
|
|
137
150
|
"outputSchema": self.output_schema,
|
|
138
151
|
"annotations": self.annotations,
|
|
139
152
|
"title": title,
|
|
153
|
+
"_meta": self.get_meta(include_fastmcp_meta=include_fastmcp_meta),
|
|
140
154
|
}
|
|
141
155
|
return MCPTool(**kwargs | overrides)
|
|
142
156
|
|
|
@@ -151,6 +165,7 @@ class Tool(FastMCPComponent):
|
|
|
151
165
|
exclude_args: list[str] | None = None,
|
|
152
166
|
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
153
167
|
serializer: Callable[[Any], str] | None = None,
|
|
168
|
+
meta: dict[str, Any] | None = None,
|
|
154
169
|
enabled: bool | None = None,
|
|
155
170
|
) -> FunctionTool:
|
|
156
171
|
"""Create a Tool from a function."""
|
|
@@ -164,6 +179,7 @@ class Tool(FastMCPComponent):
|
|
|
164
179
|
exclude_args=exclude_args,
|
|
165
180
|
output_schema=output_schema,
|
|
166
181
|
serializer=serializer,
|
|
182
|
+
meta=meta,
|
|
167
183
|
enabled=enabled,
|
|
168
184
|
)
|
|
169
185
|
|
|
@@ -185,12 +201,14 @@ class Tool(FastMCPComponent):
|
|
|
185
201
|
tool: Tool,
|
|
186
202
|
transform_fn: Callable[..., Any] | None = None,
|
|
187
203
|
name: str | None = None,
|
|
204
|
+
title: str | None | NotSetT = NotSet,
|
|
188
205
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
189
|
-
description: str | None =
|
|
206
|
+
description: str | None | NotSetT = NotSet,
|
|
190
207
|
tags: set[str] | None = None,
|
|
191
208
|
annotations: ToolAnnotations | None = None,
|
|
192
209
|
output_schema: dict[str, Any] | None | Literal[False] = None,
|
|
193
210
|
serializer: Callable[[Any], str] | None = None,
|
|
211
|
+
meta: dict[str, Any] | None | NotSetT = NotSet,
|
|
194
212
|
enabled: bool | None = None,
|
|
195
213
|
) -> TransformedTool:
|
|
196
214
|
from fastmcp.tools.tool_transform import TransformedTool
|
|
@@ -199,12 +217,14 @@ class Tool(FastMCPComponent):
|
|
|
199
217
|
tool=tool,
|
|
200
218
|
transform_fn=transform_fn,
|
|
201
219
|
name=name,
|
|
220
|
+
title=title,
|
|
202
221
|
transform_args=transform_args,
|
|
203
222
|
description=description,
|
|
204
223
|
tags=tags,
|
|
205
224
|
annotations=annotations,
|
|
206
225
|
output_schema=output_schema,
|
|
207
226
|
serializer=serializer,
|
|
227
|
+
meta=meta,
|
|
208
228
|
enabled=enabled,
|
|
209
229
|
)
|
|
210
230
|
|
|
@@ -224,6 +244,7 @@ class FunctionTool(Tool):
|
|
|
224
244
|
exclude_args: list[str] | None = None,
|
|
225
245
|
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
226
246
|
serializer: Callable[[Any], str] | None = None,
|
|
247
|
+
meta: dict[str, Any] | None = None,
|
|
227
248
|
enabled: bool | None = None,
|
|
228
249
|
) -> FunctionTool:
|
|
229
250
|
"""Create a Tool from a function."""
|
|
@@ -256,6 +277,7 @@ class FunctionTool(Tool):
|
|
|
256
277
|
annotations=annotations,
|
|
257
278
|
tags=tags or set(),
|
|
258
279
|
serializer=serializer,
|
|
280
|
+
meta=meta,
|
|
259
281
|
enabled=enabled if enabled is not None else True,
|
|
260
282
|
)
|
|
261
283
|
|
|
@@ -369,7 +391,20 @@ class ParsedFunction:
|
|
|
369
391
|
input_schema = compress_schema(input_schema, prune_params=prune_params)
|
|
370
392
|
|
|
371
393
|
output_schema = None
|
|
372
|
-
|
|
394
|
+
# Get the return annotation from the signature
|
|
395
|
+
sig = inspect.signature(fn)
|
|
396
|
+
output_type = sig.return_annotation
|
|
397
|
+
|
|
398
|
+
# If the annotation is a string (from __future__ annotations), resolve it
|
|
399
|
+
if isinstance(output_type, str):
|
|
400
|
+
try:
|
|
401
|
+
# Use get_type_hints to resolve the return type
|
|
402
|
+
# include_extras=True preserves Annotated metadata
|
|
403
|
+
type_hints = get_type_hints(fn, include_extras=True)
|
|
404
|
+
output_type = type_hints.get("return", output_type)
|
|
405
|
+
except Exception:
|
|
406
|
+
# If resolution fails, keep the string annotation
|
|
407
|
+
pass
|
|
373
408
|
|
|
374
409
|
if output_type not in (inspect._empty, None, Any, ...):
|
|
375
410
|
# there are a variety of types that we don't want to attempt to
|
|
@@ -397,7 +432,7 @@ class ParsedFunction:
|
|
|
397
432
|
|
|
398
433
|
try:
|
|
399
434
|
type_adapter = get_cached_typeadapter(clean_output_type)
|
|
400
|
-
base_schema = type_adapter.json_schema()
|
|
435
|
+
base_schema = type_adapter.json_schema(mode="serialization")
|
|
401
436
|
|
|
402
437
|
# Generate schema for wrapped type if it's non-object
|
|
403
438
|
# because MCP requires that output schemas are objects
|
|
@@ -408,7 +443,7 @@ class ParsedFunction:
|
|
|
408
443
|
# Use the wrapped result schema directly
|
|
409
444
|
wrapped_type = _WrappedResult[clean_output_type]
|
|
410
445
|
wrapped_adapter = get_cached_typeadapter(wrapped_type)
|
|
411
|
-
output_schema = wrapped_adapter.json_schema()
|
|
446
|
+
output_schema = wrapped_adapter.json_schema(mode="serialization")
|
|
412
447
|
output_schema["x-fastmcp-wrap-result"] = True
|
|
413
448
|
else:
|
|
414
449
|
output_schema = base_schema
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -10,6 +10,10 @@ from fastmcp import settings
|
|
|
10
10
|
from fastmcp.exceptions import NotFoundError, ToolError
|
|
11
11
|
from fastmcp.settings import DuplicateBehavior
|
|
12
12
|
from fastmcp.tools.tool import Tool, ToolResult
|
|
13
|
+
from fastmcp.tools.tool_transform import (
|
|
14
|
+
ToolTransformConfig,
|
|
15
|
+
apply_transformations_to_tools,
|
|
16
|
+
)
|
|
13
17
|
from fastmcp.utilities.logging import get_logger
|
|
14
18
|
|
|
15
19
|
if TYPE_CHECKING:
|
|
@@ -25,10 +29,12 @@ class ToolManager:
|
|
|
25
29
|
self,
|
|
26
30
|
duplicate_behavior: DuplicateBehavior | None = None,
|
|
27
31
|
mask_error_details: bool | None = None,
|
|
32
|
+
transformations: dict[str, ToolTransformConfig] | None = None,
|
|
28
33
|
):
|
|
29
34
|
self._tools: dict[str, Tool] = {}
|
|
30
35
|
self._mounted_servers: list[MountedServer] = []
|
|
31
36
|
self.mask_error_details = mask_error_details or settings.mask_error_details
|
|
37
|
+
self.transformations = transformations or {}
|
|
32
38
|
|
|
33
39
|
# Default to "warn" if None is provided
|
|
34
40
|
if duplicate_behavior is None:
|
|
@@ -76,13 +82,19 @@ class ToolManager:
|
|
|
76
82
|
except Exception as e:
|
|
77
83
|
# Skip failed mounts silently, matches existing behavior
|
|
78
84
|
logger.warning(
|
|
79
|
-
f"Failed to get tools from mounted
|
|
85
|
+
f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
80
86
|
)
|
|
81
87
|
continue
|
|
82
88
|
|
|
83
89
|
# Finally, add local tools, which always take precedence
|
|
84
90
|
all_tools.update(self._tools)
|
|
85
|
-
|
|
91
|
+
|
|
92
|
+
transformed_tools = apply_transformations_to_tools(
|
|
93
|
+
tools=all_tools,
|
|
94
|
+
transformations=self.transformations,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return transformed_tools
|
|
86
98
|
|
|
87
99
|
async def has_tool(self, key: str) -> bool:
|
|
88
100
|
"""Check if a tool exists."""
|
|
@@ -109,6 +121,15 @@ class ToolManager:
|
|
|
109
121
|
tools_dict = await self._load_tools(via_server=True)
|
|
110
122
|
return list(tools_dict.values())
|
|
111
123
|
|
|
124
|
+
@property
|
|
125
|
+
def _tools_transformed(self) -> list[str]:
|
|
126
|
+
"""Get the local tools."""
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
transformation.name or tool_name
|
|
130
|
+
for tool_name, transformation in self.transformations.items()
|
|
131
|
+
]
|
|
132
|
+
|
|
112
133
|
def add_tool_from_fn(
|
|
113
134
|
self,
|
|
114
135
|
fn: Callable[..., Any],
|
|
@@ -155,6 +176,21 @@ class ToolManager:
|
|
|
155
176
|
self._tools[tool.key] = tool
|
|
156
177
|
return tool
|
|
157
178
|
|
|
179
|
+
def add_tool_transformation(
|
|
180
|
+
self, tool_name: str, transformation: ToolTransformConfig
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Add a tool transformation."""
|
|
183
|
+
self.transformations[tool_name] = transformation
|
|
184
|
+
|
|
185
|
+
def get_tool_transformation(self, tool_name: str) -> ToolTransformConfig | None:
|
|
186
|
+
"""Get a tool transformation."""
|
|
187
|
+
return self.transformations.get(tool_name)
|
|
188
|
+
|
|
189
|
+
def remove_tool_transformation(self, tool_name: str) -> None:
|
|
190
|
+
"""Remove a tool transformation."""
|
|
191
|
+
if tool_name in self.transformations:
|
|
192
|
+
del self.transformations[tool_name]
|
|
193
|
+
|
|
158
194
|
def remove_tool(self, key: str) -> None:
|
|
159
195
|
"""Remove a tool from the server.
|
|
160
196
|
|
|
@@ -175,7 +211,7 @@ class ToolManager:
|
|
|
175
211
|
filtered protocol path.
|
|
176
212
|
"""
|
|
177
213
|
# 1. Check local tools first. The server will have already applied its filter.
|
|
178
|
-
if key in self._tools:
|
|
214
|
+
if key in self._tools or key in self._tools_transformed:
|
|
179
215
|
tool = await self.get_tool(key)
|
|
180
216
|
if not tool:
|
|
181
217
|
raise NotFoundError(f"Tool {key!r} not found")
|
fastmcp/tools/tool_transform.py
CHANGED
|
@@ -4,14 +4,23 @@ import inspect
|
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from contextvars import ContextVar
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Any, Literal
|
|
7
|
+
from typing import Annotated, Any, Literal
|
|
8
8
|
|
|
9
9
|
from mcp.types import ToolAnnotations
|
|
10
10
|
from pydantic import ConfigDict
|
|
11
|
+
from pydantic.fields import Field
|
|
12
|
+
from pydantic.functional_validators import BeforeValidator
|
|
11
13
|
|
|
12
14
|
from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content
|
|
15
|
+
from fastmcp.utilities.components import _convert_set_default_none
|
|
16
|
+
from fastmcp.utilities.json_schema import compress_schema
|
|
13
17
|
from fastmcp.utilities.logging import get_logger
|
|
14
|
-
from fastmcp.utilities.types import
|
|
18
|
+
from fastmcp.utilities.types import (
|
|
19
|
+
FastMCPBaseModel,
|
|
20
|
+
NotSet,
|
|
21
|
+
NotSetT,
|
|
22
|
+
get_cached_typeadapter,
|
|
23
|
+
)
|
|
15
24
|
|
|
16
25
|
logger = get_logger(__name__)
|
|
17
26
|
|
|
@@ -193,6 +202,30 @@ class ArgTransform:
|
|
|
193
202
|
)
|
|
194
203
|
|
|
195
204
|
|
|
205
|
+
class ArgTransformConfig(FastMCPBaseModel):
|
|
206
|
+
"""A model for requesting a single argument transform."""
|
|
207
|
+
|
|
208
|
+
name: str | None = Field(default=None, description="The new name for the argument.")
|
|
209
|
+
description: str | None = Field(
|
|
210
|
+
default=None, description="The new description for the argument."
|
|
211
|
+
)
|
|
212
|
+
default: str | int | float | bool | None = Field(
|
|
213
|
+
default=None, description="The new default value for the argument."
|
|
214
|
+
)
|
|
215
|
+
hide: bool = Field(
|
|
216
|
+
default=False, description="Whether to hide the argument from the tool."
|
|
217
|
+
)
|
|
218
|
+
required: Literal[True] | None = Field(
|
|
219
|
+
default=None, description="Whether the argument is required."
|
|
220
|
+
)
|
|
221
|
+
examples: Any | None = Field(default=None, description="Examples of the argument.")
|
|
222
|
+
|
|
223
|
+
def to_arg_transform(self) -> ArgTransform:
|
|
224
|
+
"""Convert the argument transform to a FastMCP argument transform."""
|
|
225
|
+
|
|
226
|
+
return ArgTransform(**self.model_dump(exclude_unset=True)) # pyright: ignore[reportAny]
|
|
227
|
+
|
|
228
|
+
|
|
196
229
|
class TransformedTool(Tool):
|
|
197
230
|
"""A tool that is transformed from another tool.
|
|
198
231
|
|
|
@@ -325,13 +358,15 @@ class TransformedTool(Tool):
|
|
|
325
358
|
cls,
|
|
326
359
|
tool: Tool,
|
|
327
360
|
name: str | None = None,
|
|
328
|
-
|
|
361
|
+
title: str | None | NotSetT = NotSet,
|
|
362
|
+
description: str | None | NotSetT = NotSet,
|
|
329
363
|
tags: set[str] | None = None,
|
|
330
364
|
transform_fn: Callable[..., Any] | None = None,
|
|
331
365
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
332
366
|
annotations: ToolAnnotations | None = None,
|
|
333
367
|
output_schema: dict[str, Any] | None | Literal[False] = None,
|
|
334
368
|
serializer: Callable[[Any], str] | None = None,
|
|
369
|
+
meta: dict[str, Any] | None | NotSetT = NotSet,
|
|
335
370
|
enabled: bool | None = None,
|
|
336
371
|
) -> TransformedTool:
|
|
337
372
|
"""Create a transformed tool from a parent tool.
|
|
@@ -342,6 +377,7 @@ class TransformedTool(Tool):
|
|
|
342
377
|
to call the parent tool. Functions with **kwargs receive transformed
|
|
343
378
|
argument names.
|
|
344
379
|
name: New name for the tool. Defaults to parent tool's name.
|
|
380
|
+
title: New title for the tool. Defaults to parent tool's title.
|
|
345
381
|
transform_args: Optional transformations for parent tool arguments.
|
|
346
382
|
Only specified arguments are transformed, others pass through unchanged:
|
|
347
383
|
- Simple rename (str)
|
|
@@ -355,6 +391,10 @@ class TransformedTool(Tool):
|
|
|
355
391
|
- dict: Use custom output schema
|
|
356
392
|
- False: Disable output schema and structured outputs
|
|
357
393
|
serializer: New serializer. Defaults to parent's serializer.
|
|
394
|
+
meta: Control meta information:
|
|
395
|
+
- NotSet (default): Inherit from parent tool
|
|
396
|
+
- dict: Use custom meta information
|
|
397
|
+
- None: Remove meta information
|
|
358
398
|
|
|
359
399
|
Returns:
|
|
360
400
|
TransformedTool with the specified transformations.
|
|
@@ -411,7 +451,7 @@ class TransformedTool(Tool):
|
|
|
411
451
|
if unknown_args:
|
|
412
452
|
raise ValueError(
|
|
413
453
|
f"Unknown arguments in transform_args: {', '.join(sorted(unknown_args))}. "
|
|
414
|
-
f"Parent tool has: {', '.join(sorted(parent_params))}"
|
|
454
|
+
f"Parent tool `{tool.name}` has: {', '.join(sorted(parent_params))}"
|
|
415
455
|
)
|
|
416
456
|
|
|
417
457
|
# Always create the forwarding transform
|
|
@@ -506,19 +546,26 @@ class TransformedTool(Tool):
|
|
|
506
546
|
f"{', '.join(sorted(duplicates))}"
|
|
507
547
|
)
|
|
508
548
|
|
|
509
|
-
|
|
549
|
+
final_name = name or tool.name
|
|
550
|
+
final_description = (
|
|
551
|
+
description if not isinstance(description, NotSetT) else tool.description
|
|
552
|
+
)
|
|
553
|
+
final_title = title if not isinstance(title, NotSetT) else tool.title
|
|
554
|
+
final_meta = meta if not isinstance(meta, NotSetT) else tool.meta
|
|
510
555
|
|
|
511
556
|
transformed_tool = cls(
|
|
512
557
|
fn=final_fn,
|
|
513
558
|
forwarding_fn=forwarding_fn,
|
|
514
559
|
parent_tool=tool,
|
|
515
|
-
name=
|
|
560
|
+
name=final_name,
|
|
561
|
+
title=final_title,
|
|
516
562
|
description=final_description,
|
|
517
563
|
parameters=final_schema,
|
|
518
564
|
output_schema=final_output_schema,
|
|
519
565
|
tags=tags or tool.tags,
|
|
520
566
|
annotations=annotations or tool.annotations,
|
|
521
567
|
serializer=serializer or tool.serializer,
|
|
568
|
+
meta=final_meta,
|
|
522
569
|
transform_args=transform_args,
|
|
523
570
|
enabled=enabled if enabled is not None else True,
|
|
524
571
|
)
|
|
@@ -606,6 +653,7 @@ class TransformedTool(Tool):
|
|
|
606
653
|
|
|
607
654
|
if parent_defs:
|
|
608
655
|
schema["$defs"] = parent_defs
|
|
656
|
+
schema = compress_schema(schema, prune_defs=True)
|
|
609
657
|
|
|
610
658
|
# Create forwarding function that closes over everything it needs
|
|
611
659
|
async def _forward(**kwargs):
|
|
@@ -791,3 +839,71 @@ class TransformedTool(Tool):
|
|
|
791
839
|
return any(
|
|
792
840
|
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
793
841
|
)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
class ToolTransformConfig(FastMCPBaseModel):
|
|
845
|
+
"""Provides a way to transform a tool."""
|
|
846
|
+
|
|
847
|
+
name: str | None = Field(default=None, description="The new name for the tool.")
|
|
848
|
+
|
|
849
|
+
title: str | None = Field(
|
|
850
|
+
default=None,
|
|
851
|
+
description="The new title of the tool.",
|
|
852
|
+
)
|
|
853
|
+
description: str | None = Field(
|
|
854
|
+
default=None,
|
|
855
|
+
description="The new description of the tool.",
|
|
856
|
+
)
|
|
857
|
+
tags: Annotated[set[str], BeforeValidator(_convert_set_default_none)] = Field(
|
|
858
|
+
default_factory=set,
|
|
859
|
+
description="The new tags for the tool.",
|
|
860
|
+
)
|
|
861
|
+
meta: dict[str, Any] | None = Field(
|
|
862
|
+
default=None,
|
|
863
|
+
description="The new meta information for the tool.",
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
enabled: bool = Field(
|
|
867
|
+
default=True,
|
|
868
|
+
description="Whether the tool is enabled.",
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
arguments: dict[str, ArgTransformConfig] = Field(
|
|
872
|
+
default_factory=dict,
|
|
873
|
+
description="A dictionary of argument transforms to apply to the tool.",
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
def apply(self, tool: Tool) -> TransformedTool:
|
|
877
|
+
"""Create a TransformedTool from a provided tool and this transformation configuration."""
|
|
878
|
+
|
|
879
|
+
tool_changes: dict[str, Any] = self.model_dump(
|
|
880
|
+
exclude_unset=True, exclude={"arguments"}
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
return TransformedTool.from_tool(
|
|
884
|
+
tool=tool,
|
|
885
|
+
**tool_changes,
|
|
886
|
+
transform_args={k: v.to_arg_transform() for k, v in self.arguments.items()},
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def apply_transformations_to_tools(
|
|
891
|
+
tools: dict[str, Tool],
|
|
892
|
+
transformations: dict[str, ToolTransformConfig],
|
|
893
|
+
) -> dict[str, Tool]:
|
|
894
|
+
"""Apply a list of transformations to a list of tools. Tools that do not have any transforamtions
|
|
895
|
+
are left unchanged.
|
|
896
|
+
"""
|
|
897
|
+
|
|
898
|
+
transformed_tools: dict[str, Tool] = {}
|
|
899
|
+
|
|
900
|
+
for tool_name, tool in tools.items():
|
|
901
|
+
if transformation := transformations.get(tool_name):
|
|
902
|
+
transformed_tools[transformation.name or tool_name] = transformation.apply(
|
|
903
|
+
tool
|
|
904
|
+
)
|
|
905
|
+
continue
|
|
906
|
+
|
|
907
|
+
transformed_tools[tool_name] = tool
|
|
908
|
+
|
|
909
|
+
return transformed_tools
|
fastmcp/utilities/components.py
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from collections.abc import Sequence
|
|
2
|
-
from typing import Annotated, Any, TypeVar
|
|
4
|
+
from typing import Annotated, Any, TypedDict, TypeVar
|
|
3
5
|
|
|
4
6
|
from pydantic import BeforeValidator, Field, PrivateAttr
|
|
5
7
|
from typing_extensions import Self
|
|
6
8
|
|
|
9
|
+
import fastmcp
|
|
7
10
|
from fastmcp.utilities.types import FastMCPBaseModel
|
|
8
11
|
|
|
9
12
|
T = TypeVar("T")
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
class FastMCPMeta(TypedDict, total=False):
|
|
16
|
+
tags: list[str]
|
|
17
|
+
|
|
18
|
+
|
|
12
19
|
def _convert_set_default_none(maybe_set: set[T] | Sequence[T] | None) -> set[T]:
|
|
13
20
|
"""Convert a sequence to a set, defaulting to an empty set if None."""
|
|
14
21
|
if maybe_set is None:
|
|
@@ -36,7 +43,9 @@ class FastMCPComponent(FastMCPBaseModel):
|
|
|
36
43
|
default_factory=set,
|
|
37
44
|
description="Tags for the component.",
|
|
38
45
|
)
|
|
39
|
-
|
|
46
|
+
meta: dict[str, Any] | None = Field(
|
|
47
|
+
default=None, description="Meta information about the component"
|
|
48
|
+
)
|
|
40
49
|
enabled: bool = Field(
|
|
41
50
|
default=True,
|
|
42
51
|
description="Whether the component is enabled.",
|
|
@@ -58,6 +67,30 @@ class FastMCPComponent(FastMCPBaseModel):
|
|
|
58
67
|
"""
|
|
59
68
|
return self._key or self.name
|
|
60
69
|
|
|
70
|
+
def get_meta(
|
|
71
|
+
self, include_fastmcp_meta: bool | None = None
|
|
72
|
+
) -> dict[str, Any] | None:
|
|
73
|
+
"""
|
|
74
|
+
Get the meta information about the component.
|
|
75
|
+
|
|
76
|
+
If include_fastmcp_meta is True, a `_fastmcp` key will be added to the
|
|
77
|
+
meta, containing a `tags` field with the tags of the component.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
if include_fastmcp_meta is None:
|
|
81
|
+
include_fastmcp_meta = fastmcp.settings.include_fastmcp_meta
|
|
82
|
+
|
|
83
|
+
meta = self.meta or {}
|
|
84
|
+
|
|
85
|
+
if include_fastmcp_meta:
|
|
86
|
+
fastmcp_meta = FastMCPMeta(tags=sorted(self.tags))
|
|
87
|
+
# overwrite any existing _fastmcp meta with keys from the new one
|
|
88
|
+
if upstream_meta := meta.get("_fastmcp"):
|
|
89
|
+
fastmcp_meta = upstream_meta | fastmcp_meta
|
|
90
|
+
meta["_fastmcp"] = fastmcp_meta
|
|
91
|
+
|
|
92
|
+
return meta or None
|
|
93
|
+
|
|
61
94
|
def with_key(self, key: str) -> Self:
|
|
62
95
|
return self.model_copy(update={"_key": key})
|
|
63
96
|
|