fastmcp 2.11.3__py3-none-any.whl → 2.12.0rc1__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 (69) hide show
  1. fastmcp/__init__.py +5 -4
  2. fastmcp/cli/claude.py +22 -18
  3. fastmcp/cli/cli.py +472 -136
  4. fastmcp/cli/install/claude_code.py +37 -40
  5. fastmcp/cli/install/claude_desktop.py +37 -42
  6. fastmcp/cli/install/cursor.py +148 -38
  7. fastmcp/cli/install/mcp_json.py +38 -43
  8. fastmcp/cli/install/shared.py +64 -7
  9. fastmcp/cli/run.py +122 -215
  10. fastmcp/client/auth/oauth.py +69 -13
  11. fastmcp/client/client.py +46 -9
  12. fastmcp/client/oauth_callback.py +91 -91
  13. fastmcp/client/sampling.py +12 -4
  14. fastmcp/client/transports.py +139 -64
  15. fastmcp/experimental/sampling/__init__.py +0 -0
  16. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  17. fastmcp/experimental/sampling/handlers/base.py +21 -0
  18. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  19. fastmcp/experimental/server/openapi/routing.py +0 -2
  20. fastmcp/experimental/server/openapi/server.py +0 -2
  21. fastmcp/experimental/utilities/openapi/parser.py +5 -1
  22. fastmcp/mcp_config.py +40 -20
  23. fastmcp/prompts/prompt_manager.py +2 -0
  24. fastmcp/resources/resource_manager.py +4 -0
  25. fastmcp/server/auth/__init__.py +2 -0
  26. fastmcp/server/auth/auth.py +2 -1
  27. fastmcp/server/auth/oauth_proxy.py +1047 -0
  28. fastmcp/server/auth/providers/azure.py +270 -0
  29. fastmcp/server/auth/providers/github.py +287 -0
  30. fastmcp/server/auth/providers/google.py +305 -0
  31. fastmcp/server/auth/providers/jwt.py +24 -12
  32. fastmcp/server/auth/providers/workos.py +256 -2
  33. fastmcp/server/auth/redirect_validation.py +65 -0
  34. fastmcp/server/context.py +91 -41
  35. fastmcp/server/elicitation.py +60 -1
  36. fastmcp/server/http.py +3 -3
  37. fastmcp/server/middleware/logging.py +66 -28
  38. fastmcp/server/proxy.py +2 -0
  39. fastmcp/server/sampling/handler.py +19 -0
  40. fastmcp/server/server.py +76 -15
  41. fastmcp/settings.py +16 -1
  42. fastmcp/tools/tool.py +22 -9
  43. fastmcp/tools/tool_manager.py +2 -0
  44. fastmcp/tools/tool_transform.py +39 -10
  45. fastmcp/utilities/auth.py +34 -0
  46. fastmcp/utilities/cli.py +148 -15
  47. fastmcp/utilities/components.py +2 -1
  48. fastmcp/utilities/inspect.py +166 -37
  49. fastmcp/utilities/json_schema_type.py +4 -2
  50. fastmcp/utilities/logging.py +4 -1
  51. fastmcp/utilities/mcp_config.py +47 -18
  52. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  53. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  54. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  55. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  59. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  60. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  61. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  62. fastmcp/utilities/tests.py +7 -2
  63. fastmcp/utilities/types.py +15 -2
  64. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0rc1.dist-info}/METADATA +2 -1
  65. fastmcp-2.12.0rc1.dist-info/RECORD +129 -0
  66. fastmcp-2.11.3.dist-info/RECORD +0 -108
  67. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0rc1.dist-info}/WHEEL +0 -0
  68. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
  69. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0rc1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -3,7 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import inspect
6
+ import json
6
7
  import re
8
+ import secrets
7
9
  import warnings
8
10
  from collections.abc import AsyncIterator, Awaitable, Callable
9
11
  from contextlib import (
@@ -69,6 +71,7 @@ from fastmcp.utilities.types import NotSet, NotSetT
69
71
 
70
72
  if TYPE_CHECKING:
71
73
  from fastmcp.client import Client
74
+ from fastmcp.client.sampling import ServerSamplingHandler
72
75
  from fastmcp.client.transports import ClientTransport, ClientTransportT
73
76
  from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
74
77
  from fastmcp.experimental.server.openapi.routing import (
@@ -166,12 +169,15 @@ class FastMCP(Generic[LifespanResultT]):
166
169
  streamable_http_path: str | None = None,
167
170
  json_response: bool | None = None,
168
171
  stateless_http: bool | None = None,
172
+ sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None,
173
+ sampling_handler_behavior: Literal["always", "fallback"] | None = None,
169
174
  ):
170
175
  self.resource_prefix_format: Literal["protocol", "path"] = (
171
176
  resource_prefix_format or fastmcp.settings.resource_prefix_format
172
177
  )
173
178
 
174
179
  self._additional_http_routes: list[BaseRoute] = []
180
+ self._mounted_servers: list[MountedServer] = []
175
181
  self._tool_manager = ToolManager(
176
182
  duplicate_behavior=on_duplicate_tools,
177
183
  mask_error_details=mask_error_details,
@@ -192,8 +198,9 @@ class FastMCP(Generic[LifespanResultT]):
192
198
  lifespan = default_lifespan
193
199
  else:
194
200
  self._has_lifespan = True
201
+ # Generate random ID if no name provided
195
202
  self._mcp_server = LowLevelServer[LifespanResultT](
196
- name=name or "FastMCP",
203
+ name=name or self.generate_name(),
197
204
  version=version,
198
205
  instructions=instructions,
199
206
  lifespan=_lifespan_wrapper(self, lifespan),
@@ -221,7 +228,27 @@ class FastMCP(Generic[LifespanResultT]):
221
228
 
222
229
  # Set up MCP protocol handlers
223
230
  self._setup_handlers()
224
- self.dependencies = dependencies or fastmcp.settings.server_dependencies
231
+
232
+ # Handle dependencies with deprecation warning
233
+ # TODO: Remove dependencies parameter (deprecated in v2.11.4)
234
+ if dependencies is not None:
235
+ import warnings
236
+
237
+ warnings.warn(
238
+ "The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
239
+ "Please specify dependencies in a fastmcp.json configuration file instead:\n"
240
+ '{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
241
+ f"{json.dumps(dependencies)}\n }}\n}}\n"
242
+ "See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
243
+ DeprecationWarning,
244
+ stacklevel=2,
245
+ )
246
+ self.dependencies = (
247
+ dependencies or fastmcp.settings.server_dependencies
248
+ ) # TODO: Remove (deprecated in v2.11.4)
249
+
250
+ self.sampling_handler = sampling_handler
251
+ self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
225
252
 
226
253
  self.include_fastmcp_meta = (
227
254
  include_fastmcp_meta
@@ -444,7 +471,7 @@ class FastMCP(Generic[LifespanResultT]):
444
471
  Request and returns a Response.
445
472
 
446
473
  Args:
447
- path: URL path for the route (e.g., "/oauth/callback")
474
+ path: URL path for the route (e.g., "/auth/callback")
448
475
  methods: List of HTTP methods to support (e.g., ["GET", "POST"])
449
476
  name: Optional name for the route (to reference this route with
450
477
  Starlette's reverse URL lookup feature)
@@ -475,8 +502,26 @@ class FastMCP(Generic[LifespanResultT]):
475
502
 
476
503
  return decorator
477
504
 
505
+ def _get_additional_http_routes(self) -> list[BaseRoute]:
506
+ """Get all additional HTTP routes including from mounted servers.
507
+
508
+ Returns a list of all custom HTTP routes from this server and
509
+ recursively from all mounted servers.
510
+
511
+ Returns:
512
+ List of Starlette BaseRoute objects
513
+ """
514
+ routes = list(self._additional_http_routes)
515
+
516
+ # Recursively get routes from mounted servers
517
+ for mounted_server in self._mounted_servers:
518
+ mounted_routes = mounted_server.server._get_additional_http_routes()
519
+ routes.extend(mounted_routes)
520
+
521
+ return routes
522
+
478
523
  async def _mcp_list_tools(self) -> list[MCPTool]:
479
- logger.debug("Handler called: list_tools")
524
+ logger.debug(f"[{self.name}] Handler called: list_tools")
480
525
 
481
526
  async with fastmcp.server.context.Context(fastmcp=self):
482
527
  tools = await self._list_tools()
@@ -520,7 +565,7 @@ class FastMCP(Generic[LifespanResultT]):
520
565
  return await self._apply_middleware(mw_context, _handler)
521
566
 
522
567
  async def _mcp_list_resources(self) -> list[MCPResource]:
523
- logger.debug("Handler called: list_resources")
568
+ logger.debug(f"[{self.name}] Handler called: list_resources")
524
569
 
525
570
  async with fastmcp.server.context.Context(fastmcp=self):
526
571
  resources = await self._list_resources()
@@ -565,7 +610,7 @@ class FastMCP(Generic[LifespanResultT]):
565
610
  return await self._apply_middleware(mw_context, _handler)
566
611
 
567
612
  async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
568
- logger.debug("Handler called: list_resource_templates")
613
+ logger.debug(f"[{self.name}] Handler called: list_resource_templates")
569
614
 
570
615
  async with fastmcp.server.context.Context(fastmcp=self):
571
616
  templates = await self._list_resource_templates()
@@ -610,7 +655,7 @@ class FastMCP(Generic[LifespanResultT]):
610
655
  return await self._apply_middleware(mw_context, _handler)
611
656
 
612
657
  async def _mcp_list_prompts(self) -> list[MCPPrompt]:
613
- logger.debug("Handler called: list_prompts")
658
+ logger.debug(f"[{self.name}] Handler called: list_prompts")
614
659
 
615
660
  async with fastmcp.server.context.Context(fastmcp=self):
616
661
  prompts = await self._list_prompts()
@@ -669,7 +714,9 @@ class FastMCP(Generic[LifespanResultT]):
669
714
  Returns:
670
715
  List of MCP Content objects containing the tool results
671
716
  """
672
- logger.debug("Handler called: call_tool %s with %s", key, arguments)
717
+ logger.debug(
718
+ f"[{self.name}] Handler called: call_tool %s with %s", key, arguments
719
+ )
673
720
 
674
721
  async with fastmcp.server.context.Context(fastmcp=self):
675
722
  try:
@@ -711,7 +758,7 @@ class FastMCP(Generic[LifespanResultT]):
711
758
 
712
759
  Delegates to _read_resource, which should be overridden by FastMCP subclasses.
713
760
  """
714
- logger.debug("Handler called: read_resource %s", uri)
761
+ logger.debug(f"[{self.name}] Handler called: read_resource %s", uri)
715
762
 
716
763
  async with fastmcp.server.context.Context(fastmcp=self):
717
764
  try:
@@ -766,7 +813,9 @@ class FastMCP(Generic[LifespanResultT]):
766
813
 
767
814
  Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
768
815
  """
769
- logger.debug("Handler called: get_prompt %s with %s", name, arguments)
816
+ logger.debug(
817
+ f"[{self.name}] Handler called: get_prompt %s with %s", name, arguments
818
+ )
770
819
 
771
820
  async with fastmcp.server.context.Context(fastmcp=self):
772
821
  try:
@@ -984,7 +1033,7 @@ class FastMCP(Generic[LifespanResultT]):
984
1033
  description=description,
985
1034
  tags=tags,
986
1035
  output_schema=output_schema,
987
- annotations=annotations,
1036
+ annotations=cast(ToolAnnotations | None, annotations),
988
1037
  exclude_args=exclude_args,
989
1038
  meta=meta,
990
1039
  serializer=self._tool_serializer,
@@ -1214,7 +1263,7 @@ class FastMCP(Generic[LifespanResultT]):
1214
1263
  mime_type=mime_type,
1215
1264
  tags=tags,
1216
1265
  enabled=enabled,
1217
- annotations=annotations,
1266
+ annotations=cast(Annotations | None, annotations),
1218
1267
  meta=meta,
1219
1268
  )
1220
1269
  self.add_template(template)
@@ -1229,7 +1278,7 @@ class FastMCP(Generic[LifespanResultT]):
1229
1278
  mime_type=mime_type,
1230
1279
  tags=tags,
1231
1280
  enabled=enabled,
1232
- annotations=annotations,
1281
+ annotations=cast(Annotations | None, annotations),
1233
1282
  meta=meta,
1234
1283
  )
1235
1284
  self.add_resource(resource)
@@ -1796,6 +1845,7 @@ class FastMCP(Generic[LifespanResultT]):
1796
1845
  server=server,
1797
1846
  resource_prefix_format=self.resource_prefix_format,
1798
1847
  )
1848
+ self._mounted_servers.append(mounted_server)
1799
1849
  self._tool_manager.mount(mounted_server)
1800
1850
  self._resource_manager.mount(mounted_server)
1801
1851
  self._prompt_manager.mount(mounted_server)
@@ -1922,9 +1972,11 @@ class FastMCP(Generic[LifespanResultT]):
1922
1972
  self._prompt_manager.add_prompt(prompt)
1923
1973
 
1924
1974
  if prefix:
1925
- logger.debug(f"Imported server {server.name} with prefix '{prefix}'")
1975
+ logger.debug(
1976
+ f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
1977
+ )
1926
1978
  else:
1927
- logger.debug(f"Imported server {server.name}")
1979
+ logger.debug(f"[{self.name}] Imported server {server.name}")
1928
1980
 
1929
1981
  @classmethod
1930
1982
  def from_openapi(
@@ -2151,6 +2203,15 @@ class FastMCP(Generic[LifespanResultT]):
2151
2203
 
2152
2204
  return True
2153
2205
 
2206
+ @classmethod
2207
+ def generate_name(cls, name: str | None = None) -> str:
2208
+ class_name = cls.__name__
2209
+
2210
+ if name is None:
2211
+ return f"{class_name}-{secrets.token_hex(2)}"
2212
+ else:
2213
+ return f"{class_name}-{name}-{secrets.token_hex(2)}"
2214
+
2154
2215
 
2155
2216
  @dataclass
2156
2217
  class MountedServer:
fastmcp/settings.py CHANGED
@@ -146,6 +146,7 @@ class Settings(BaseSettings):
146
146
 
147
147
  test_mode: bool = False
148
148
 
149
+ log_enabled: bool = True
149
150
  log_level: LOG_LEVEL = "INFO"
150
151
 
151
152
  @field_validator("log_level", mode="before")
@@ -314,12 +315,26 @@ class Settings(BaseSettings):
314
315
  Whether to include FastMCP meta in the server's MCP responses.
315
316
  If True, a `_fastmcp` key will be added to the `meta` field of
316
317
  all MCP component responses. This key will contain a dict of
317
- various FastMCP-specific metadata, such as tags.
318
+ various FastMCP-specific metadata, such as tags.
318
319
  """
319
320
  ),
320
321
  ),
321
322
  ] = True
322
323
 
324
+ mounted_components_raise_on_load_error: Annotated[
325
+ bool,
326
+ Field(
327
+ default=False,
328
+ description=inspect.cleandoc(
329
+ """
330
+ If True, errors encountered when loading mounted components (tools, resources, prompts)
331
+ will be raised instead of logged as warnings. This is useful for debugging
332
+ but will interrupt normal operation.
333
+ """
334
+ ),
335
+ ),
336
+ ] = False
337
+
323
338
 
324
339
  def __getattr__(name: str):
325
340
  """
fastmcp/tools/tool.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import warnings
4
5
  from collections.abc import Callable
5
6
  from dataclasses import dataclass
6
7
  from typing import (
@@ -8,6 +9,7 @@ from typing import (
8
9
  Annotated,
9
10
  Any,
10
11
  Generic,
12
+ Literal,
11
13
  TypeVar,
12
14
  get_type_hints,
13
15
  )
@@ -18,6 +20,7 @@ from mcp.types import ContentBlock, TextContent, ToolAnnotations
18
20
  from mcp.types import Tool as MCPTool
19
21
  from pydantic import Field, PydanticSchemaGenerationError
20
22
 
23
+ import fastmcp
21
24
  from fastmcp.server.dependencies import get_context
22
25
  from fastmcp.utilities.components import FastMCPComponent
23
26
  from fastmcp.utilities.json_schema import compress_schema
@@ -162,7 +165,7 @@ class Tool(FastMCPComponent):
162
165
  tags: set[str] | None = None,
163
166
  annotations: ToolAnnotations | None = None,
164
167
  exclude_args: list[str] | None = None,
165
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
168
+ output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
166
169
  serializer: Callable[[Any], str] | None = None,
167
170
  meta: dict[str, Any] | None = None,
168
171
  enabled: bool | None = None,
@@ -204,7 +207,7 @@ class Tool(FastMCPComponent):
204
207
  description: str | None | NotSetT = NotSet,
205
208
  tags: set[str] | None = None,
206
209
  annotations: ToolAnnotations | None | NotSetT = NotSet,
207
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
210
+ output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
208
211
  serializer: Callable[[Any], str] | None = None,
209
212
  meta: dict[str, Any] | None | NotSetT = NotSet,
210
213
  transform_args: dict[str, ArgTransform] | None = None,
@@ -242,7 +245,7 @@ class FunctionTool(Tool):
242
245
  tags: set[str] | None = None,
243
246
  annotations: ToolAnnotations | None = None,
244
247
  exclude_args: list[str] | None = None,
245
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
248
+ output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
246
249
  serializer: Callable[[Any], str] | None = None,
247
250
  meta: dict[str, Any] | None = None,
248
251
  enabled: bool | None = None,
@@ -255,16 +258,26 @@ class FunctionTool(Tool):
255
258
  raise ValueError("You must provide a name for lambda functions")
256
259
 
257
260
  if isinstance(output_schema, NotSetT):
258
- output_schema = parsed_fn.output_schema
261
+ final_output_schema = parsed_fn.output_schema
259
262
  elif output_schema is False:
260
- output_schema = None
263
+ # Handle False as deprecated synonym for None (deprecated in 2.11.4)
264
+ if fastmcp.settings.deprecation_warnings:
265
+ warnings.warn(
266
+ "Passing output_schema=False is deprecated. Use output_schema=None instead.",
267
+ DeprecationWarning,
268
+ stacklevel=2,
269
+ )
270
+ final_output_schema = None
271
+ else:
272
+ # At this point output_schema is not NotSetT and not False, so it must be dict | None
273
+ final_output_schema = output_schema
261
274
  # Note: explicit schemas (dict) are used as-is without auto-wrapping
262
275
 
263
276
  # Validate that explicit schemas are object type for structured content
264
- if output_schema is not None and isinstance(output_schema, dict):
265
- if output_schema.get("type") != "object":
277
+ if final_output_schema is not None and isinstance(final_output_schema, dict):
278
+ if final_output_schema.get("type") != "object":
266
279
  raise ValueError(
267
- f'Output schemas must have "type" set to "object" due to MCP spec limitations. Received: {output_schema!r}'
280
+ f'Output schemas must have "type" set to "object" due to MCP spec limitations. Received: {final_output_schema!r}'
268
281
  )
269
282
 
270
283
  return cls(
@@ -273,7 +286,7 @@ class FunctionTool(Tool):
273
286
  title=title,
274
287
  description=description or parsed_fn.description,
275
288
  parameters=parsed_fn.input_schema,
276
- output_schema=output_schema,
289
+ output_schema=final_output_schema,
277
290
  annotations=annotations,
278
291
  tags=tags or set(),
279
292
  serializer=serializer,
@@ -86,6 +86,8 @@ class ToolManager:
86
86
  logger.warning(
87
87
  f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
88
88
  )
89
+ if settings.mounted_components_raise_on_load_error:
90
+ raise
89
91
  continue
90
92
 
91
93
  # Finally, add local tools, which always take precedence
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import warnings
4
5
  from collections.abc import Callable
5
6
  from contextvars import ContextVar
6
7
  from dataclasses import dataclass
7
- from typing import Annotated, Any, Literal
8
+ from typing import Annotated, Any, Literal, cast
8
9
 
9
10
  import pydantic_core
10
11
  from mcp.types import ToolAnnotations
@@ -12,6 +13,7 @@ from pydantic import ConfigDict
12
13
  from pydantic.fields import Field
13
14
  from pydantic.functional_validators import BeforeValidator
14
15
 
16
+ import fastmcp
15
17
  from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content
16
18
  from fastmcp.utilities.components import _convert_set_default_none
17
19
  from fastmcp.utilities.json_schema import compress_schema
@@ -27,7 +29,7 @@ logger = get_logger(__name__)
27
29
 
28
30
 
29
31
  # Context variable to store current transformed tool
30
- _current_tool: ContextVar[TransformedTool | None] = ContextVar(
32
+ _current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[assignment]
31
33
  "_current_tool", default=None
32
34
  )
33
35
 
@@ -369,7 +371,7 @@ class TransformedTool(Tool):
369
371
  transform_fn: Callable[..., Any] | None = None,
370
372
  transform_args: dict[str, ArgTransform] | None = None,
371
373
  annotations: ToolAnnotations | None | NotSetT = NotSet,
372
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
374
+ output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
373
375
  serializer: Callable[[Any], str] | None | NotSetT = NotSet,
374
376
  meta: dict[str, Any] | None | NotSetT = NotSet,
375
377
  enabled: bool | None = None,
@@ -437,7 +439,7 @@ class TransformedTool(Tool):
437
439
  })
438
440
 
439
441
  # Disable structured outputs
440
- Tool.from_tool(parent, output_schema=False)
442
+ Tool.from_tool(parent, output_schema=None)
441
443
 
442
444
  # Return ToolResult for full control
443
445
  async def custom_output(**kwargs) -> ToolResult:
@@ -471,8 +473,8 @@ class TransformedTool(Tool):
471
473
  if output_schema is NotSet:
472
474
  # Use smart fallback: try custom function, then parent
473
475
  if transform_fn is not None:
474
- assert parsed_fn is not None
475
- final_output_schema = parsed_fn.output_schema
476
+ # parsed fn is not none here
477
+ final_output_schema = cast(ParsedFunction, parsed_fn).output_schema
476
478
  if final_output_schema is None:
477
479
  # Check if function returns ToolResult - if so, don't fall back to parent
478
480
  return_annotation = inspect.signature(
@@ -484,16 +486,25 @@ class TransformedTool(Tool):
484
486
  final_output_schema = tool.output_schema
485
487
  else:
486
488
  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
487
498
  else:
488
- assert isinstance(output_schema, dict | None)
489
- final_output_schema = output_schema
499
+ final_output_schema = cast(dict | None, output_schema)
490
500
 
491
501
  if transform_fn is None:
492
502
  # User wants pure transformation - use forwarding_fn as the main function
493
503
  final_fn = forwarding_fn
494
504
  final_schema = schema
495
505
  else:
496
- assert parsed_fn is not None
506
+ # parsed fn is not none here
507
+ parsed_fn = cast(ParsedFunction, parsed_fn)
497
508
  # User provided custom function - merge schemas
498
509
  final_fn = transform_fn
499
510
 
@@ -830,12 +841,30 @@ class TransformedTool(Tool):
830
841
  if "default" in param_schema:
831
842
  final_required.discard(param_name)
832
843
 
833
- return {
844
+ # Merge $defs from both schemas, with override taking precedence
845
+ merged_defs = base_schema.get("$defs", {}).copy()
846
+ override_defs = override_schema.get("$defs", {})
847
+
848
+ for def_name, def_schema in override_defs.items():
849
+ if def_name in merged_defs:
850
+ base_def = merged_defs[def_name].copy()
851
+ base_def.update(def_schema)
852
+ merged_defs[def_name] = base_def
853
+ else:
854
+ merged_defs[def_name] = def_schema.copy()
855
+
856
+ result = {
834
857
  "type": "object",
835
858
  "properties": merged_props,
836
859
  "required": list(final_required),
837
860
  }
838
861
 
862
+ if merged_defs:
863
+ result["$defs"] = merged_defs
864
+ result = compress_schema(result, prune_defs=True)
865
+
866
+ return result
867
+
839
868
  @staticmethod
840
869
  def _function_has_kwargs(fn: Callable[..., Any]) -> bool:
841
870
  """Check if function accepts **kwargs.
@@ -0,0 +1,34 @@
1
+ """Authentication utility helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+
9
+ def parse_scopes(value: Any) -> list[str] | None:
10
+ """Parse scopes from environment variables or settings values.
11
+
12
+ Accepts either a JSON array string, a comma- or space-separated string,
13
+ a list of strings, or ``None``. Returns a list of scopes or ``None`` if
14
+ no value is provided.
15
+ """
16
+ if value is None or value == "":
17
+ return None if value is None else []
18
+ if isinstance(value, list):
19
+ return [str(v).strip() for v in value if str(v).strip()]
20
+ if isinstance(value, str):
21
+ value = value.strip()
22
+ if not value:
23
+ return []
24
+ # Try JSON array first
25
+ if value.startswith("["):
26
+ try:
27
+ data = json.loads(value)
28
+ if isinstance(data, list):
29
+ return [str(v).strip() for v in data if str(v).strip()]
30
+ except Exception:
31
+ pass
32
+ # Fallback to comma/space separated list
33
+ return [s.strip() for s in value.replace(",", " ").split() if s.strip()]
34
+ return value