fastmcp 2.11.2__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 (77) 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/logging.py +25 -1
  13. fastmcp/client/oauth_callback.py +91 -91
  14. fastmcp/client/sampling.py +12 -4
  15. fastmcp/client/transports.py +143 -67
  16. fastmcp/experimental/sampling/__init__.py +0 -0
  17. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  18. fastmcp/experimental/sampling/handlers/base.py +21 -0
  19. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  20. fastmcp/experimental/server/openapi/routing.py +1 -3
  21. fastmcp/experimental/server/openapi/server.py +10 -25
  22. fastmcp/experimental/utilities/openapi/__init__.py +2 -2
  23. fastmcp/experimental/utilities/openapi/formatters.py +34 -0
  24. fastmcp/experimental/utilities/openapi/models.py +5 -2
  25. fastmcp/experimental/utilities/openapi/parser.py +252 -70
  26. fastmcp/experimental/utilities/openapi/schemas.py +135 -106
  27. fastmcp/mcp_config.py +40 -20
  28. fastmcp/prompts/prompt_manager.py +4 -2
  29. fastmcp/resources/resource_manager.py +16 -6
  30. fastmcp/server/auth/__init__.py +11 -1
  31. fastmcp/server/auth/auth.py +19 -2
  32. fastmcp/server/auth/oauth_proxy.py +1047 -0
  33. fastmcp/server/auth/providers/azure.py +270 -0
  34. fastmcp/server/auth/providers/github.py +287 -0
  35. fastmcp/server/auth/providers/google.py +305 -0
  36. fastmcp/server/auth/providers/jwt.py +27 -16
  37. fastmcp/server/auth/providers/workos.py +256 -2
  38. fastmcp/server/auth/redirect_validation.py +65 -0
  39. fastmcp/server/auth/registry.py +1 -1
  40. fastmcp/server/context.py +91 -41
  41. fastmcp/server/dependencies.py +32 -2
  42. fastmcp/server/elicitation.py +60 -1
  43. fastmcp/server/http.py +44 -37
  44. fastmcp/server/middleware/logging.py +66 -28
  45. fastmcp/server/proxy.py +2 -0
  46. fastmcp/server/sampling/handler.py +19 -0
  47. fastmcp/server/server.py +85 -20
  48. fastmcp/settings.py +18 -3
  49. fastmcp/tools/tool.py +23 -10
  50. fastmcp/tools/tool_manager.py +5 -1
  51. fastmcp/tools/tool_transform.py +75 -32
  52. fastmcp/utilities/auth.py +34 -0
  53. fastmcp/utilities/cli.py +148 -15
  54. fastmcp/utilities/components.py +21 -5
  55. fastmcp/utilities/inspect.py +166 -37
  56. fastmcp/utilities/json_schema_type.py +4 -2
  57. fastmcp/utilities/logging.py +4 -1
  58. fastmcp/utilities/mcp_config.py +47 -18
  59. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  60. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  61. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  62. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  63. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  64. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  65. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  66. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  67. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  68. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  69. fastmcp/utilities/openapi.py +4 -4
  70. fastmcp/utilities/tests.py +7 -2
  71. fastmcp/utilities/types.py +15 -2
  72. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/METADATA +3 -2
  73. fastmcp-2.12.0rc1.dist-info/RECORD +129 -0
  74. fastmcp-2.11.2.dist-info/RECORD +0 -108
  75. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/WHEEL +0 -0
  76. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
  77. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -2,11 +2,20 @@
2
2
 
3
3
  import json
4
4
  import logging
5
+ from collections.abc import Callable
6
+ from logging import Logger
5
7
  from typing import Any
6
8
 
9
+ import pydantic_core
10
+
7
11
  from .middleware import CallNext, Middleware, MiddlewareContext
8
12
 
9
13
 
14
+ def default_serializer(data: Any) -> str:
15
+ """The default serializer for Payloads in the logging middleware."""
16
+ return pydantic_core.to_json(data, fallback=str).decode()
17
+
18
+
10
19
  class LoggingMiddleware(Middleware):
11
20
  """Middleware that provides comprehensive request and response logging.
12
21
 
@@ -33,6 +42,7 @@ class LoggingMiddleware(Middleware):
33
42
  include_payloads: bool = False,
34
43
  max_payload_length: int = 1000,
35
44
  methods: list[str] | None = None,
45
+ payload_serializer: Callable[[Any], str] | None = None,
36
46
  ):
37
47
  """Initialize logging middleware.
38
48
 
@@ -43,13 +53,14 @@ class LoggingMiddleware(Middleware):
43
53
  max_payload_length: Maximum length of payload to log (prevents huge logs)
44
54
  methods: List of methods to log. If None, logs all methods.
45
55
  """
46
- self.logger = logger or logging.getLogger("fastmcp.requests")
47
- self.log_level = log_level
48
- self.include_payloads = include_payloads
49
- self.max_payload_length = max_payload_length
50
- self.methods = methods
51
-
52
- def _format_message(self, context: MiddlewareContext) -> str:
56
+ self.logger: Logger = logger or logging.getLogger("fastmcp.requests")
57
+ self.log_level: int = log_level
58
+ self.include_payloads: bool = include_payloads
59
+ self.max_payload_length: int = max_payload_length
60
+ self.methods: list[str] | None = methods
61
+ self.payload_serializer: Callable[[Any], str] | None = payload_serializer
62
+
63
+ def _format_message(self, context: MiddlewareContext[Any]) -> str:
53
64
  """Format a message for logging."""
54
65
  parts = [
55
66
  f"source={context.source}",
@@ -57,18 +68,29 @@ class LoggingMiddleware(Middleware):
57
68
  f"method={context.method or 'unknown'}",
58
69
  ]
59
70
 
60
- if self.include_payloads and hasattr(context.message, "__dict__"):
61
- try:
62
- payload = json.dumps(context.message.__dict__, default=str)
63
- if len(payload) > self.max_payload_length:
64
- payload = payload[: self.max_payload_length] + "..."
65
- parts.append(f"payload={payload}")
66
- except (TypeError, ValueError):
67
- parts.append("payload=<non-serializable>")
71
+ if self.include_payloads:
72
+ payload: str
68
73
 
74
+ if not self.payload_serializer:
75
+ payload = default_serializer(context.message)
76
+ else:
77
+ try:
78
+ payload = self.payload_serializer(context.message)
79
+ except Exception as e:
80
+ self.logger.warning(
81
+ f"Failed {e} to serialize payload: {context.type} {context.method} {context.source}."
82
+ )
83
+ payload = default_serializer(context.message)
84
+
85
+ if len(payload) > self.max_payload_length:
86
+ payload = payload[: self.max_payload_length] + "..."
87
+
88
+ parts.append(f"payload={payload}")
69
89
  return " ".join(parts)
70
90
 
71
- async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
91
+ async def on_message(
92
+ self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
93
+ ) -> Any:
72
94
  """Log all messages."""
73
95
  message_info = self._format_message(context)
74
96
  if self.methods and context.method not in self.methods:
@@ -111,6 +133,7 @@ class StructuredLoggingMiddleware(Middleware):
111
133
  log_level: int = logging.INFO,
112
134
  include_payloads: bool = False,
113
135
  methods: list[str] | None = None,
136
+ payload_serializer: Callable[[Any], str] | None = None,
114
137
  ):
115
138
  """Initialize structured logging middleware.
116
139
 
@@ -119,15 +142,18 @@ class StructuredLoggingMiddleware(Middleware):
119
142
  log_level: Log level for messages (default: INFO)
120
143
  include_payloads: Whether to include message payloads in logs
121
144
  methods: List of methods to log. If None, logs all methods.
145
+ serializer: Callable that converts objects to a JSON string for the
146
+ payload. If not provided, uses FastMCP's default tool serializer.
122
147
  """
123
- self.logger = logger or logging.getLogger("fastmcp.structured")
124
- self.log_level = log_level
125
- self.include_payloads = include_payloads
126
- self.methods = methods
148
+ self.logger: Logger = logger or logging.getLogger("fastmcp.structured")
149
+ self.log_level: int = log_level
150
+ self.include_payloads: bool = include_payloads
151
+ self.methods: list[str] | None = methods
152
+ self.payload_serializer: Callable[[Any], str] | None = payload_serializer
127
153
 
128
154
  def _create_log_entry(
129
- self, context: MiddlewareContext, event: str, **extra_fields
130
- ) -> dict:
155
+ self, context: MiddlewareContext[Any], event: str, **extra_fields: Any
156
+ ) -> dict[str, Any]:
131
157
  """Create a structured log entry."""
132
158
  entry = {
133
159
  "event": event,
@@ -138,15 +164,27 @@ class StructuredLoggingMiddleware(Middleware):
138
164
  **extra_fields,
139
165
  }
140
166
 
141
- if self.include_payloads and hasattr(context.message, "__dict__"):
142
- try:
143
- entry["payload"] = context.message.__dict__
144
- except (TypeError, ValueError):
145
- entry["payload"] = "<non-serializable>"
167
+ if self.include_payloads:
168
+ payload: str
169
+
170
+ if not self.payload_serializer:
171
+ payload = default_serializer(context.message)
172
+ else:
173
+ try:
174
+ payload = self.payload_serializer(context.message)
175
+ except Exception as e:
176
+ self.logger.warning(
177
+ f"Failed {str(e)} to serialize payload: {context.type} {context.method} {context.source}."
178
+ )
179
+ payload = default_serializer(context.message)
180
+
181
+ entry["payload"] = payload
146
182
 
147
183
  return entry
148
184
 
149
- async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
185
+ async def on_message(
186
+ self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
187
+ ) -> Any:
150
188
  """Log structured message information."""
151
189
  start_entry = self._create_log_entry(context, "request_start")
152
190
  if self.methods and context.method not in self.methods:
fastmcp/server/proxy.py CHANGED
@@ -546,6 +546,8 @@ class ProxyClient(Client[ClientTransportT]):
546
546
  | str,
547
547
  **kwargs,
548
548
  ):
549
+ if "name" not in kwargs:
550
+ kwargs["name"] = self.generate_name()
549
551
  if "roots" not in kwargs:
550
552
  kwargs["roots"] = default_proxy_roots_handler
551
553
  if "sampling_handler" not in kwargs:
@@ -0,0 +1,19 @@
1
+ from collections.abc import Awaitable, Callable
2
+ from typing import TypeAlias
3
+
4
+ from mcp import CreateMessageResult
5
+ from mcp.server.session import ServerSession
6
+ from mcp.shared.context import LifespanContextT, RequestContext
7
+ from mcp.types import CreateMessageRequestParams as SamplingParams
8
+ from mcp.types import (
9
+ SamplingMessage,
10
+ )
11
+
12
+ ServerSamplingHandler: TypeAlias = Callable[
13
+ [
14
+ list[SamplingMessage],
15
+ SamplingParams,
16
+ RequestContext[ServerSession, LifespanContextT],
17
+ ],
18
+ str | CreateMessageResult | Awaitable[str | CreateMessageResult],
19
+ ]
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 (
@@ -49,7 +51,7 @@ from fastmcp.prompts import Prompt, PromptManager
49
51
  from fastmcp.prompts.prompt import FunctionPrompt
50
52
  from fastmcp.resources import Resource, ResourceManager
51
53
  from fastmcp.resources.template import ResourceTemplate
52
- from fastmcp.server.auth.auth import AuthProvider
54
+ from fastmcp.server.auth import AuthProvider
53
55
  from fastmcp.server.auth.registry import get_registered_provider
54
56
  from fastmcp.server.http import (
55
57
  StarletteWithLifespan,
@@ -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)
@@ -1891,7 +1941,7 @@ class FastMCP(Generic[LifespanResultT]):
1891
1941
  # Import tools from the server
1892
1942
  for key, tool in (await server.get_tools()).items():
1893
1943
  if prefix:
1894
- tool = tool.with_key(f"{prefix}_{key}")
1944
+ tool = tool.model_copy(key=f"{prefix}_{key}")
1895
1945
  self._tool_manager.add_tool(tool)
1896
1946
 
1897
1947
  # Import resources and templates from the server
@@ -1900,7 +1950,9 @@ class FastMCP(Generic[LifespanResultT]):
1900
1950
  resource_key = add_resource_prefix(
1901
1951
  key, prefix, self.resource_prefix_format
1902
1952
  )
1903
- resource = resource.with_key(resource_key)
1953
+ resource = resource.model_copy(
1954
+ update={"name": f"{prefix}_{resource.name}"}, key=resource_key
1955
+ )
1904
1956
  self._resource_manager.add_resource(resource)
1905
1957
 
1906
1958
  for key, template in (await server.get_resource_templates()).items():
@@ -1908,19 +1960,23 @@ class FastMCP(Generic[LifespanResultT]):
1908
1960
  template_key = add_resource_prefix(
1909
1961
  key, prefix, self.resource_prefix_format
1910
1962
  )
1911
- template = template.with_key(template_key)
1963
+ template = template.model_copy(
1964
+ update={"name": f"{prefix}_{template.name}"}, key=template_key
1965
+ )
1912
1966
  self._resource_manager.add_template(template)
1913
1967
 
1914
1968
  # Import prompts from the server
1915
1969
  for key, prompt in (await server.get_prompts()).items():
1916
1970
  if prefix:
1917
- prompt = prompt.with_key(f"{prefix}_{key}")
1971
+ prompt = prompt.model_copy(key=f"{prefix}_{key}")
1918
1972
  self._prompt_manager.add_prompt(prompt)
1919
1973
 
1920
1974
  if prefix:
1921
- 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
+ )
1922
1978
  else:
1923
- logger.debug(f"Imported server {server.name}")
1979
+ logger.debug(f"[{self.name}] Imported server {server.name}")
1924
1980
 
1925
1981
  @classmethod
1926
1982
  def from_openapi(
@@ -2147,6 +2203,15 @@ class FastMCP(Generic[LifespanResultT]):
2147
2203
 
2148
2204
  return True
2149
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
+
2150
2215
 
2151
2216
  @dataclass
2152
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")
@@ -222,9 +223,9 @@ class Settings(BaseSettings):
222
223
  # HTTP settings
223
224
  host: str = "127.0.0.1"
224
225
  port: int = 8000
225
- sse_path: str = "/sse/"
226
+ sse_path: str = "/sse"
226
227
  message_path: str = "/messages/"
227
- streamable_http_path: str = "/mcp/"
228
+ streamable_http_path: str = "/mcp"
228
229
  debug: bool = False
229
230
 
230
231
  # error handling
@@ -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 (
@@ -19,6 +20,7 @@ from mcp.types import ContentBlock, TextContent, ToolAnnotations
19
20
  from mcp.types import Tool as MCPTool
20
21
  from pydantic import Field, PydanticSchemaGenerationError
21
22
 
23
+ import fastmcp
22
24
  from fastmcp.server.dependencies import get_context
23
25
  from fastmcp.utilities.components import FastMCPComponent
24
26
  from fastmcp.utilities.json_schema import compress_schema
@@ -199,17 +201,18 @@ class Tool(FastMCPComponent):
199
201
  def from_tool(
200
202
  cls,
201
203
  tool: Tool,
202
- transform_fn: Callable[..., Any] | None = None,
204
+ *,
203
205
  name: str | None = None,
204
206
  title: str | None | NotSetT = NotSet,
205
- transform_args: dict[str, ArgTransform] | None = None,
206
207
  description: str | None | NotSetT = NotSet,
207
208
  tags: set[str] | None = None,
208
- annotations: ToolAnnotations | None = None,
209
- output_schema: dict[str, Any] | None | Literal[False] = None,
209
+ annotations: ToolAnnotations | None | NotSetT = NotSet,
210
+ output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
210
211
  serializer: Callable[[Any], str] | None = None,
211
212
  meta: dict[str, Any] | None | NotSetT = NotSet,
213
+ transform_args: dict[str, ArgTransform] | None = None,
212
214
  enabled: bool | None = None,
215
+ transform_fn: Callable[..., Any] | None = None,
213
216
  ) -> TransformedTool:
214
217
  from fastmcp.tools.tool_transform import TransformedTool
215
218
 
@@ -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,
@@ -75,7 +75,9 @@ class ToolManager:
75
75
  child_dict = {t.key: t for t in child_results}
76
76
  if mounted.prefix:
77
77
  for tool in child_dict.values():
78
- prefixed_tool = tool.with_key(f"{mounted.prefix}_{tool.key}")
78
+ prefixed_tool = tool.model_copy(
79
+ key=f"{mounted.prefix}_{tool.key}"
80
+ )
79
81
  all_tools[prefixed_tool.key] = prefixed_tool
80
82
  else:
81
83
  all_tools.update(child_dict)
@@ -84,6 +86,8 @@ class ToolManager:
84
86
  logger.warning(
85
87
  f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
86
88
  )
89
+ if settings.mounted_components_raise_on_load_error:
90
+ raise
87
91
  continue
88
92
 
89
93
  # Finally, add local tools, which always take precedence