fastmcp 2.12.5__py3-none-any.whl → 2.14.0__py3-none-any.whl

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