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.
Files changed (65) hide show
  1. fastmcp/__init__.py +7 -2
  2. fastmcp/cli/cli.py +128 -33
  3. fastmcp/cli/install/__init__.py +2 -2
  4. fastmcp/cli/install/claude_code.py +42 -1
  5. fastmcp/cli/install/claude_desktop.py +42 -1
  6. fastmcp/cli/install/cursor.py +42 -1
  7. fastmcp/cli/install/{mcp_config.py → mcp_json.py} +51 -7
  8. fastmcp/cli/run.py +127 -1
  9. fastmcp/client/__init__.py +2 -0
  10. fastmcp/client/auth/oauth.py +68 -99
  11. fastmcp/client/oauth_callback.py +18 -0
  12. fastmcp/client/transports.py +69 -15
  13. fastmcp/contrib/component_manager/example.py +2 -2
  14. fastmcp/experimental/server/openapi/README.md +266 -0
  15. fastmcp/experimental/server/openapi/__init__.py +38 -0
  16. fastmcp/experimental/server/openapi/components.py +348 -0
  17. fastmcp/experimental/server/openapi/routing.py +132 -0
  18. fastmcp/experimental/server/openapi/server.py +466 -0
  19. fastmcp/experimental/utilities/openapi/README.md +239 -0
  20. fastmcp/experimental/utilities/openapi/__init__.py +68 -0
  21. fastmcp/experimental/utilities/openapi/director.py +208 -0
  22. fastmcp/experimental/utilities/openapi/formatters.py +355 -0
  23. fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
  24. fastmcp/experimental/utilities/openapi/models.py +85 -0
  25. fastmcp/experimental/utilities/openapi/parser.py +618 -0
  26. fastmcp/experimental/utilities/openapi/schemas.py +538 -0
  27. fastmcp/mcp_config.py +125 -88
  28. fastmcp/prompts/prompt.py +11 -1
  29. fastmcp/prompts/prompt_manager.py +1 -1
  30. fastmcp/resources/resource.py +21 -1
  31. fastmcp/resources/resource_manager.py +2 -2
  32. fastmcp/resources/template.py +20 -1
  33. fastmcp/server/auth/__init__.py +17 -2
  34. fastmcp/server/auth/auth.py +144 -7
  35. fastmcp/server/auth/providers/bearer.py +25 -473
  36. fastmcp/server/auth/providers/in_memory.py +4 -2
  37. fastmcp/server/auth/providers/jwt.py +538 -0
  38. fastmcp/server/auth/providers/workos.py +170 -0
  39. fastmcp/server/auth/registry.py +52 -0
  40. fastmcp/server/context.py +110 -26
  41. fastmcp/server/dependencies.py +9 -2
  42. fastmcp/server/http.py +62 -30
  43. fastmcp/server/middleware/middleware.py +3 -23
  44. fastmcp/server/openapi.py +26 -13
  45. fastmcp/server/proxy.py +89 -8
  46. fastmcp/server/server.py +170 -62
  47. fastmcp/settings.py +83 -18
  48. fastmcp/tools/tool.py +41 -6
  49. fastmcp/tools/tool_manager.py +39 -3
  50. fastmcp/tools/tool_transform.py +122 -6
  51. fastmcp/utilities/components.py +35 -2
  52. fastmcp/utilities/json_schema.py +136 -98
  53. fastmcp/utilities/json_schema_type.py +1 -3
  54. fastmcp/utilities/mcp_config.py +28 -0
  55. fastmcp/utilities/openapi.py +306 -30
  56. fastmcp/utilities/tests.py +54 -6
  57. fastmcp/utilities/types.py +89 -11
  58. {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
  59. fastmcp-2.11.0.dist-info/RECORD +108 -0
  60. fastmcp/server/auth/providers/bearer_env.py +0 -63
  61. fastmcp/utilities/cache.py +0 -26
  62. fastmcp-2.10.5.dist-info/RECORD +0 -93
  63. {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
  64. {fastmcp-2.10.5.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
  65. {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, model_validator
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
- default_auth_provider: Annotated[
217
- Literal["bearer_env"] | None,
262
+ server_auth: Annotated[
263
+ str | None,
218
264
  Field(
219
265
  description=inspect.cleandoc(
220
266
  """
221
- Configure the authentication provider. This setting is intended only to
222
- be used for remote confirugation of providers that fully support
223
- environment variable configuration.
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 `fasmtpc.settings` instead.",
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 TYPE_CHECKING, Annotated, Any, Generic, Literal, TypeVar
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(self, **overrides: Any) -> MCPTool:
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 = 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
- output_type = inspect.signature(fn).return_annotation
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
@@ -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 server '{mounted.prefix}': {e}"
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
- return all_tools
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")
@@ -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 NotSet, NotSetT, get_cached_typeadapter
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
- description: str | None = None,
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
- final_description = description if description is not None else tool.description
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=name or tool.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
@@ -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