fastmcp 2.12.1__py3-none-any.whl → 2.13.2__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 (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  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 +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -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 +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -7,7 +7,14 @@ import json
7
7
  import re
8
8
  import secrets
9
9
  import warnings
10
- from collections.abc import AsyncIterator, Awaitable, Callable
10
+ from collections.abc import (
11
+ AsyncIterator,
12
+ Awaitable,
13
+ Callable,
14
+ Collection,
15
+ Mapping,
16
+ Sequence,
17
+ )
11
18
  from contextlib import (
12
19
  AbstractAsyncContextManager,
13
20
  AsyncExitStack,
@@ -47,9 +54,11 @@ import fastmcp
47
54
  import fastmcp.server
48
55
  from fastmcp.exceptions import DisabledError, NotFoundError
49
56
  from fastmcp.mcp_config import MCPConfig
50
- from fastmcp.prompts import Prompt, PromptManager
57
+ from fastmcp.prompts import Prompt
51
58
  from fastmcp.prompts.prompt import FunctionPrompt
52
- from fastmcp.resources import Resource, ResourceManager
59
+ from fastmcp.prompts.prompt_manager import PromptManager
60
+ from fastmcp.resources.resource import Resource
61
+ from fastmcp.resources.resource_manager import ResourceManager
53
62
  from fastmcp.resources.template import ResourceTemplate
54
63
  from fastmcp.server.auth import AuthProvider
55
64
  from fastmcp.server.http import (
@@ -60,17 +69,17 @@ from fastmcp.server.http import (
60
69
  from fastmcp.server.low_level import LowLevelServer
61
70
  from fastmcp.server.middleware import Middleware, MiddlewareContext
62
71
  from fastmcp.settings import Settings
63
- from fastmcp.tools import ToolManager
64
72
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
73
+ from fastmcp.tools.tool_manager import ToolManager
65
74
  from fastmcp.tools.tool_transform import ToolTransformConfig
66
75
  from fastmcp.utilities.cli import log_server_banner
67
76
  from fastmcp.utilities.components import FastMCPComponent
68
- from fastmcp.utilities.logging import get_logger
77
+ from fastmcp.utilities.logging import get_logger, temporary_log_level
69
78
  from fastmcp.utilities.types import NotSet, NotSetT
70
79
 
71
80
  if TYPE_CHECKING:
72
81
  from fastmcp.client import Client
73
- from fastmcp.client.sampling import ServerSamplingHandler
82
+ from fastmcp.client.client import FastMCP1Server
74
83
  from fastmcp.client.transports import ClientTransport, ClientTransportT
75
84
  from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
76
85
  from fastmcp.experimental.server.openapi.routing import (
@@ -84,6 +93,8 @@ if TYPE_CHECKING:
84
93
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
85
94
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
86
95
  from fastmcp.server.proxy import FastMCPProxy
96
+ from fastmcp.server.sampling.handler import ServerSamplingHandler
97
+ from fastmcp.tools.tool import ToolResultSerializerType
87
98
 
88
99
  logger = get_logger(__name__)
89
100
 
@@ -93,6 +104,10 @@ Transport = Literal["stdio", "http", "sse", "streamable-http"]
93
104
  # Compiled URI parsing regex to split a URI into protocol and path components
94
105
  URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
95
106
 
107
+ LifespanCallable = Callable[
108
+ ["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
109
+ ]
110
+
96
111
 
97
112
  @asynccontextmanager
98
113
  async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
@@ -102,26 +117,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
102
117
  server: The server instance this lifespan is managing
103
118
 
104
119
  Returns:
105
- An empty context object
120
+ An empty dictionary as the lifespan result.
106
121
  """
107
122
  yield {}
108
123
 
109
124
 
110
- def _lifespan_wrapper(
111
- app: FastMCP[LifespanResultT],
112
- lifespan: Callable[
113
- [FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
114
- ],
125
+ def _lifespan_proxy(
126
+ fastmcp_server: FastMCP[LifespanResultT],
115
127
  ) -> Callable[
116
128
  [LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
117
129
  ]:
118
130
  @asynccontextmanager
119
131
  async def wrap(
120
- s: LowLevelServer[LifespanResultT],
132
+ low_level_server: LowLevelServer[LifespanResultT],
121
133
  ) -> AsyncIterator[LifespanResultT]:
122
- async with AsyncExitStack() as stack:
123
- context = await stack.enter_async_context(lifespan(app))
124
- yield context
134
+ if fastmcp_server._lifespan is default_lifespan:
135
+ yield {}
136
+ return
137
+
138
+ if not fastmcp_server._lifespan_result_set:
139
+ raise RuntimeError(
140
+ "FastMCP server has a lifespan defined but no lifespan result is set, which means the server's context manager was not entered. "
141
+ + " Are you running the server in a way that supports lifespans? If so, please file an issue at https://github.com/jlowin/fastmcp/issues."
142
+ )
143
+
144
+ yield fastmcp_server._lifespan_result
125
145
 
126
146
  return wrap
127
147
 
@@ -133,27 +153,24 @@ class FastMCP(Generic[LifespanResultT]):
133
153
  instructions: str | None = None,
134
154
  *,
135
155
  version: str | None = None,
136
- auth: AuthProvider | None | NotSetT = NotSet,
137
- middleware: list[Middleware] | None = None,
138
- lifespan: (
139
- Callable[
140
- [FastMCP[LifespanResultT]],
141
- AbstractAsyncContextManager[LifespanResultT],
142
- ]
143
- | None
144
- ) = None,
156
+ website_url: str | None = None,
157
+ icons: list[mcp.types.Icon] | None = None,
158
+ auth: AuthProvider | NotSetT | None = NotSet,
159
+ middleware: Sequence[Middleware] | None = None,
160
+ lifespan: LifespanCallable | None = None,
145
161
  dependencies: list[str] | None = None,
146
162
  resource_prefix_format: Literal["protocol", "path"] | None = None,
147
163
  mask_error_details: bool | None = None,
148
- tools: list[Tool | Callable[..., Any]] | None = None,
149
- tool_transformations: dict[str, ToolTransformConfig] | None = None,
150
- tool_serializer: Callable[[Any], str] | None = None,
151
- include_tags: set[str] | None = None,
152
- exclude_tags: set[str] | None = None,
164
+ tools: Sequence[Tool | Callable[..., Any]] | None = None,
165
+ tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
166
+ tool_serializer: ToolResultSerializerType | None = None,
167
+ include_tags: Collection[str] | None = None,
168
+ exclude_tags: Collection[str] | None = None,
153
169
  include_fastmcp_meta: bool | None = None,
154
170
  on_duplicate_tools: DuplicateBehavior | None = None,
155
171
  on_duplicate_resources: DuplicateBehavior | None = None,
156
172
  on_duplicate_prompts: DuplicateBehavior | None = None,
173
+ strict_input_validation: bool | None = None,
157
174
  # ---
158
175
  # ---
159
176
  # --- The following arguments are DEPRECATED ---
@@ -177,42 +194,46 @@ class FastMCP(Generic[LifespanResultT]):
177
194
 
178
195
  self._additional_http_routes: list[BaseRoute] = []
179
196
  self._mounted_servers: list[MountedServer] = []
180
- self._tool_manager = ToolManager(
197
+ self._tool_manager: ToolManager = ToolManager(
181
198
  duplicate_behavior=on_duplicate_tools,
182
199
  mask_error_details=mask_error_details,
183
200
  transformations=tool_transformations,
184
201
  )
185
- self._resource_manager = ResourceManager(
202
+ self._resource_manager: ResourceManager = ResourceManager(
186
203
  duplicate_behavior=on_duplicate_resources,
187
204
  mask_error_details=mask_error_details,
188
205
  )
189
- self._prompt_manager = PromptManager(
206
+ self._prompt_manager: PromptManager = PromptManager(
190
207
  duplicate_behavior=on_duplicate_prompts,
191
208
  mask_error_details=mask_error_details,
192
209
  )
193
- self._tool_serializer = tool_serializer
210
+ self._tool_serializer: Callable[[Any], str] | None = tool_serializer
211
+
212
+ self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
213
+ self._lifespan_result: LifespanResultT | None = None
214
+ self._lifespan_result_set: bool = False
194
215
 
195
- if lifespan is None:
196
- self._has_lifespan = False
197
- lifespan = default_lifespan
198
- else:
199
- self._has_lifespan = True
200
216
  # Generate random ID if no name provided
201
- self._mcp_server = LowLevelServer[LifespanResultT](
217
+ self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
218
+ LifespanResultT
219
+ ](
220
+ fastmcp=self,
202
221
  name=name or self.generate_name(),
203
- version=version,
222
+ version=version or fastmcp.__version__,
204
223
  instructions=instructions,
205
- lifespan=_lifespan_wrapper(self, lifespan),
224
+ website_url=website_url,
225
+ icons=icons,
226
+ lifespan=_lifespan_proxy(fastmcp_server=self),
206
227
  )
207
228
 
208
229
  # if auth is `NotSet`, try to create a provider from the environment
209
230
  if auth is NotSet:
210
231
  if fastmcp.settings.server_auth is not None:
211
- # ImportString returns the class itself
212
- auth = fastmcp.settings.server_auth()
232
+ # server_auth_class returns the class itself
233
+ auth = fastmcp.settings.server_auth_class()
213
234
  else:
214
235
  auth = None
215
- self.auth = cast(AuthProvider | None, auth)
236
+ self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
216
237
 
217
238
  if tools:
218
239
  for tool in tools:
@@ -220,10 +241,20 @@ class FastMCP(Generic[LifespanResultT]):
220
241
  tool = Tool.from_function(tool, serializer=self._tool_serializer)
221
242
  self.add_tool(tool)
222
243
 
223
- self.include_tags = include_tags
224
- self.exclude_tags = exclude_tags
244
+ self.include_tags: set[str] | None = (
245
+ set(include_tags) if include_tags is not None else None
246
+ )
247
+ self.exclude_tags: set[str] | None = (
248
+ set(exclude_tags) if exclude_tags is not None else None
249
+ )
250
+
251
+ self.strict_input_validation: bool = (
252
+ strict_input_validation
253
+ if strict_input_validation is not None
254
+ else fastmcp.settings.strict_input_validation
255
+ )
225
256
 
226
- self.middleware = middleware or []
257
+ self.middleware: list[Middleware] = list(middleware or [])
227
258
 
228
259
  # Set up MCP protocol handlers
229
260
  self._setup_handlers()
@@ -242,14 +273,18 @@ class FastMCP(Generic[LifespanResultT]):
242
273
  DeprecationWarning,
243
274
  stacklevel=2,
244
275
  )
245
- self.dependencies = (
276
+ self.dependencies: list[str] = (
246
277
  dependencies or fastmcp.settings.server_dependencies
247
278
  ) # TODO: Remove (deprecated in v2.11.4)
248
279
 
249
- self.sampling_handler = sampling_handler
250
- self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
280
+ self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
281
+ sampling_handler
282
+ )
283
+ self.sampling_handler_behavior: Literal["always", "fallback"] = (
284
+ sampling_handler_behavior or "fallback"
285
+ )
251
286
 
252
- self.include_fastmcp_meta = (
287
+ self.include_fastmcp_meta: bool = (
253
288
  include_fastmcp_meta
254
289
  if include_fastmcp_meta is not None
255
290
  else fastmcp.settings.include_fastmcp_meta
@@ -329,10 +364,46 @@ class FastMCP(Generic[LifespanResultT]):
329
364
  def instructions(self) -> str | None:
330
365
  return self._mcp_server.instructions
331
366
 
367
+ @instructions.setter
368
+ def instructions(self, value: str | None) -> None:
369
+ self._mcp_server.instructions = value
370
+
332
371
  @property
333
372
  def version(self) -> str | None:
334
373
  return self._mcp_server.version
335
374
 
375
+ @property
376
+ def website_url(self) -> str | None:
377
+ return self._mcp_server.website_url
378
+
379
+ @property
380
+ def icons(self) -> list[mcp.types.Icon]:
381
+ if self._mcp_server.icons is None:
382
+ return []
383
+ else:
384
+ return list(self._mcp_server.icons)
385
+
386
+ @asynccontextmanager
387
+ async def _lifespan_manager(self) -> AsyncIterator[None]:
388
+ if self._lifespan_result_set:
389
+ yield
390
+ return
391
+
392
+ async with self._lifespan(self) as lifespan_result:
393
+ self._lifespan_result = lifespan_result
394
+ self._lifespan_result_set = True
395
+
396
+ async with AsyncExitStack[bool | None]() as stack:
397
+ for server in self._mounted_servers:
398
+ await stack.enter_async_context(
399
+ cm=server.server._lifespan_manager()
400
+ )
401
+
402
+ yield
403
+
404
+ self._lifespan_result_set = False
405
+ self._lifespan_result = None
406
+
336
407
  async def run_async(
337
408
  self,
338
409
  transport: Transport | None = None,
@@ -386,13 +457,15 @@ class FastMCP(Generic[LifespanResultT]):
386
457
 
387
458
  def _setup_handlers(self) -> None:
388
459
  """Set up core MCP protocol handlers."""
389
- self._mcp_server.list_tools()(self._mcp_list_tools)
390
- self._mcp_server.list_resources()(self._mcp_list_resources)
391
- self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
392
- self._mcp_server.list_prompts()(self._mcp_list_prompts)
393
- self._mcp_server.call_tool()(self._mcp_call_tool)
394
- self._mcp_server.read_resource()(self._mcp_read_resource)
395
- self._mcp_server.get_prompt()(self._mcp_get_prompt)
460
+ self._mcp_server.list_tools()(self._list_tools_mcp)
461
+ self._mcp_server.list_resources()(self._list_resources_mcp)
462
+ self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
463
+ self._mcp_server.list_prompts()(self._list_prompts_mcp)
464
+ self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
465
+ self._call_tool_mcp
466
+ )
467
+ self._mcp_server.read_resource()(self._read_resource_mcp)
468
+ self._mcp_server.get_prompt()(self._get_prompt_mcp)
396
469
 
397
470
  async def _apply_middleware(
398
471
  self,
@@ -409,8 +482,24 @@ class FastMCP(Generic[LifespanResultT]):
409
482
  self.middleware.append(middleware)
410
483
 
411
484
  async def get_tools(self) -> dict[str, Tool]:
412
- """Get all registered tools, indexed by registered key."""
413
- return await self._tool_manager.get_tools()
485
+ """Get all tools (unfiltered), including mounted servers, indexed by key."""
486
+ all_tools = dict(await self._tool_manager.get_tools())
487
+
488
+ for mounted in self._mounted_servers:
489
+ try:
490
+ child_tools = await mounted.server.get_tools()
491
+ for key, tool in child_tools.items():
492
+ new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
493
+ all_tools[new_key] = tool.model_copy(key=new_key)
494
+ except Exception as e:
495
+ logger.warning(
496
+ f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
497
+ )
498
+ if fastmcp.settings.mounted_components_raise_on_load_error:
499
+ raise
500
+ continue
501
+
502
+ return all_tools
414
503
 
415
504
  async def get_tool(self, key: str) -> Tool:
416
505
  tools = await self.get_tools()
@@ -419,8 +508,37 @@ class FastMCP(Generic[LifespanResultT]):
419
508
  return tools[key]
420
509
 
421
510
  async def get_resources(self) -> dict[str, Resource]:
422
- """Get all registered resources, indexed by registered key."""
423
- return await self._resource_manager.get_resources()
511
+ """Get all resources (unfiltered), including mounted servers, indexed by key."""
512
+ all_resources = dict(await self._resource_manager.get_resources())
513
+
514
+ for mounted in self._mounted_servers:
515
+ try:
516
+ child_resources = await mounted.server.get_resources()
517
+ for key, resource in child_resources.items():
518
+ new_key = (
519
+ add_resource_prefix(
520
+ key, mounted.prefix, mounted.resource_prefix_format
521
+ )
522
+ if mounted.prefix
523
+ else key
524
+ )
525
+ update = (
526
+ {"name": f"{mounted.prefix}_{resource.name}"}
527
+ if mounted.prefix and resource.name
528
+ else {}
529
+ )
530
+ all_resources[new_key] = resource.model_copy(
531
+ key=new_key, update=update
532
+ )
533
+ except Exception as e:
534
+ logger.warning(
535
+ f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
536
+ )
537
+ if fastmcp.settings.mounted_components_raise_on_load_error:
538
+ raise
539
+ continue
540
+
541
+ return all_resources
424
542
 
425
543
  async def get_resource(self, key: str) -> Resource:
426
544
  resources = await self.get_resources()
@@ -429,8 +547,37 @@ class FastMCP(Generic[LifespanResultT]):
429
547
  return resources[key]
430
548
 
431
549
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
432
- """Get all registered resource templates, indexed by registered key."""
433
- return await self._resource_manager.get_resource_templates()
550
+ """Get all resource templates (unfiltered), including mounted servers, indexed by key."""
551
+ all_templates = dict(await self._resource_manager.get_resource_templates())
552
+
553
+ for mounted in self._mounted_servers:
554
+ try:
555
+ child_templates = await mounted.server.get_resource_templates()
556
+ for key, template in child_templates.items():
557
+ new_key = (
558
+ add_resource_prefix(
559
+ key, mounted.prefix, mounted.resource_prefix_format
560
+ )
561
+ if mounted.prefix
562
+ else key
563
+ )
564
+ update = (
565
+ {"name": f"{mounted.prefix}_{template.name}"}
566
+ if mounted.prefix and template.name
567
+ else {}
568
+ )
569
+ all_templates[new_key] = template.model_copy(
570
+ key=new_key, update=update
571
+ )
572
+ except Exception as e:
573
+ logger.warning(
574
+ f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
575
+ )
576
+ if fastmcp.settings.mounted_components_raise_on_load_error:
577
+ raise
578
+ continue
579
+
580
+ return all_templates
434
581
 
435
582
  async def get_resource_template(self, key: str) -> ResourceTemplate:
436
583
  """Get a registered resource template by key."""
@@ -440,10 +587,24 @@ class FastMCP(Generic[LifespanResultT]):
440
587
  return templates[key]
441
588
 
442
589
  async def get_prompts(self) -> dict[str, Prompt]:
443
- """
444
- List all available prompts.
445
- """
446
- return await self._prompt_manager.get_prompts()
590
+ """Get all prompts (unfiltered), including mounted servers, indexed by key."""
591
+ all_prompts = dict(await self._prompt_manager.get_prompts())
592
+
593
+ for mounted in self._mounted_servers:
594
+ try:
595
+ child_prompts = await mounted.server.get_prompts()
596
+ for key, prompt in child_prompts.items():
597
+ new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
598
+ all_prompts[new_key] = prompt.model_copy(key=new_key)
599
+ except Exception as e:
600
+ logger.warning(
601
+ f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
602
+ )
603
+ if fastmcp.settings.mounted_components_raise_on_load_error:
604
+ raise
605
+ continue
606
+
607
+ return all_prompts
447
608
 
448
609
  async def get_prompt(self, key: str) -> Prompt:
449
610
  prompts = await self.get_prompts()
@@ -519,11 +680,15 @@ class FastMCP(Generic[LifespanResultT]):
519
680
 
520
681
  return routes
521
682
 
522
- async def _mcp_list_tools(self) -> list[MCPTool]:
683
+ async def _list_tools_mcp(self) -> list[MCPTool]:
684
+ """
685
+ List all available tools, in the format expected by the low-level MCP
686
+ server.
687
+ """
523
688
  logger.debug(f"[{self.name}] Handler called: list_tools")
524
689
 
525
690
  async with fastmcp.server.context.Context(fastmcp=self):
526
- tools = await self._list_tools()
691
+ tools = await self._list_tools_middleware()
527
692
  return [
528
693
  tool.to_mcp_tool(
529
694
  name=tool.key,
@@ -532,24 +697,11 @@ class FastMCP(Generic[LifespanResultT]):
532
697
  for tool in tools
533
698
  ]
534
699
 
535
- async def _list_tools(self) -> list[Tool]:
700
+ async def _list_tools_middleware(self) -> list[Tool]:
536
701
  """
537
- List all available tools, in the format expected by the low-level MCP
538
- server.
702
+ List all available tools, applying MCP middleware.
539
703
  """
540
704
 
541
- async def _handler(
542
- context: MiddlewareContext[mcp.types.ListToolsRequest],
543
- ) -> list[Tool]:
544
- tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
545
-
546
- mcp_tools: list[Tool] = []
547
- for tool in tools:
548
- if self._should_enable_component(tool):
549
- mcp_tools.append(tool)
550
-
551
- return mcp_tools
552
-
553
705
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
554
706
  # Create the middleware context.
555
707
  mw_context = MiddlewareContext(
@@ -561,13 +713,66 @@ class FastMCP(Generic[LifespanResultT]):
561
713
  )
562
714
 
563
715
  # Apply the middleware chain.
564
- return await self._apply_middleware(mw_context, _handler)
716
+ return list(
717
+ await self._apply_middleware(
718
+ context=mw_context, call_next=self._list_tools
719
+ )
720
+ )
721
+
722
+ async def _list_tools(
723
+ self,
724
+ context: MiddlewareContext[mcp.types.ListToolsRequest],
725
+ ) -> list[Tool]:
726
+ """
727
+ List all available tools.
728
+ """
729
+ # 1. Get local tools and filter them
730
+ local_tools = await self._tool_manager.get_tools()
731
+ filtered_local = [
732
+ tool for tool in local_tools.values() if self._should_enable_component(tool)
733
+ ]
734
+
735
+ # 2. Get tools from mounted servers
736
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
737
+ # Use a dict to implement "later wins" deduplication by key
738
+ all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
739
+
740
+ for mounted in self._mounted_servers:
741
+ try:
742
+ child_tools = await mounted.server._list_tools_middleware()
743
+ for tool in child_tools:
744
+ # Apply parent server's filtering to mounted components
745
+ if not self._should_enable_component(tool):
746
+ continue
747
+
748
+ key = tool.key
749
+ if mounted.prefix:
750
+ key = f"{mounted.prefix}_{tool.key}"
751
+ tool = tool.model_copy(key=key)
752
+ # Later mounted servers override earlier ones
753
+ all_tools[key] = tool
754
+ except Exception as e:
755
+ server_name = getattr(
756
+ getattr(mounted, "server", None), "name", repr(mounted)
757
+ )
758
+ logger.warning(
759
+ f"Failed to list tools from mounted server {server_name!r}: {e}"
760
+ )
761
+ if fastmcp.settings.mounted_components_raise_on_load_error:
762
+ raise
763
+ continue
764
+
765
+ return list(all_tools.values())
565
766
 
566
- async def _mcp_list_resources(self) -> list[MCPResource]:
767
+ async def _list_resources_mcp(self) -> list[MCPResource]:
768
+ """
769
+ List all available resources, in the format expected by the low-level MCP
770
+ server.
771
+ """
567
772
  logger.debug(f"[{self.name}] Handler called: list_resources")
568
773
 
569
774
  async with fastmcp.server.context.Context(fastmcp=self):
570
- resources = await self._list_resources()
775
+ resources = await self._list_resources_middleware()
571
776
  return [
572
777
  resource.to_mcp_resource(
573
778
  uri=resource.key,
@@ -576,25 +781,11 @@ class FastMCP(Generic[LifespanResultT]):
576
781
  for resource in resources
577
782
  ]
578
783
 
579
- async def _list_resources(self) -> list[Resource]:
784
+ async def _list_resources_middleware(self) -> list[Resource]:
580
785
  """
581
- List all available resources, in the format expected by the low-level MCP
582
- server.
583
-
786
+ List all available resources, applying MCP middleware.
584
787
  """
585
788
 
586
- async def _handler(
587
- context: MiddlewareContext[dict[str, Any]],
588
- ) -> list[Resource]:
589
- resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
590
-
591
- mcp_resources: list[Resource] = []
592
- for resource in resources:
593
- if self._should_enable_component(resource):
594
- mcp_resources.append(resource)
595
-
596
- return mcp_resources
597
-
598
789
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
599
790
  # Create the middleware context.
600
791
  mw_context = MiddlewareContext(
@@ -606,13 +797,75 @@ class FastMCP(Generic[LifespanResultT]):
606
797
  )
607
798
 
608
799
  # Apply the middleware chain.
609
- return await self._apply_middleware(mw_context, _handler)
800
+ return list(
801
+ await self._apply_middleware(
802
+ context=mw_context, call_next=self._list_resources
803
+ )
804
+ )
610
805
 
611
- async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
806
+ async def _list_resources(
807
+ self,
808
+ context: MiddlewareContext[dict[str, Any]],
809
+ ) -> list[Resource]:
810
+ """
811
+ List all available resources.
812
+ """
813
+ # 1. Filter local resources
814
+ local_resources = await self._resource_manager.get_resources()
815
+ filtered_local = [
816
+ resource
817
+ for resource in local_resources.values()
818
+ if self._should_enable_component(resource)
819
+ ]
820
+
821
+ # 2. Get from mounted servers with resource prefix handling
822
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
823
+ # Use a dict to implement "later wins" deduplication by key
824
+ all_resources: dict[str, Resource] = {
825
+ resource.key: resource for resource in filtered_local
826
+ }
827
+
828
+ for mounted in self._mounted_servers:
829
+ try:
830
+ child_resources = await mounted.server._list_resources_middleware()
831
+ for resource in child_resources:
832
+ # Apply parent server's filtering to mounted components
833
+ if not self._should_enable_component(resource):
834
+ continue
835
+
836
+ key = resource.key
837
+ if mounted.prefix:
838
+ key = add_resource_prefix(
839
+ resource.key,
840
+ mounted.prefix,
841
+ mounted.resource_prefix_format,
842
+ )
843
+ resource = resource.model_copy(
844
+ key=key,
845
+ update={"name": f"{mounted.prefix}_{resource.name}"},
846
+ )
847
+ # Later mounted servers override earlier ones
848
+ all_resources[key] = resource
849
+ except Exception as e:
850
+ server_name = getattr(
851
+ getattr(mounted, "server", None), "name", repr(mounted)
852
+ )
853
+ logger.warning(f"Failed to list resources from {server_name!r}: {e}")
854
+ if fastmcp.settings.mounted_components_raise_on_load_error:
855
+ raise
856
+ continue
857
+
858
+ return list(all_resources.values())
859
+
860
+ async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
861
+ """
862
+ List all available resource templates, in the format expected by the low-level MCP
863
+ server.
864
+ """
612
865
  logger.debug(f"[{self.name}] Handler called: list_resource_templates")
613
866
 
614
867
  async with fastmcp.server.context.Context(fastmcp=self):
615
- templates = await self._list_resource_templates()
868
+ templates = await self._list_resource_templates_middleware()
616
869
  return [
617
870
  template.to_mcp_template(
618
871
  uriTemplate=template.key,
@@ -621,25 +874,12 @@ class FastMCP(Generic[LifespanResultT]):
621
874
  for template in templates
622
875
  ]
623
876
 
624
- async def _list_resource_templates(self) -> list[ResourceTemplate]:
877
+ async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
625
878
  """
626
- List all available resource templates, in the format expected by the low-level MCP
627
- server.
879
+ List all available resource templates, applying MCP middleware.
628
880
 
629
881
  """
630
882
 
631
- async def _handler(
632
- context: MiddlewareContext[dict[str, Any]],
633
- ) -> list[ResourceTemplate]:
634
- templates = await self._resource_manager.list_resource_templates()
635
-
636
- mcp_templates: list[ResourceTemplate] = []
637
- for template in templates:
638
- if self._should_enable_component(template):
639
- mcp_templates.append(template)
640
-
641
- return mcp_templates
642
-
643
883
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
644
884
  # Create the middleware context.
645
885
  mw_context = MiddlewareContext(
@@ -651,13 +891,79 @@ class FastMCP(Generic[LifespanResultT]):
651
891
  )
652
892
 
653
893
  # Apply the middleware chain.
654
- return await self._apply_middleware(mw_context, _handler)
894
+ return list(
895
+ await self._apply_middleware(
896
+ context=mw_context, call_next=self._list_resource_templates
897
+ )
898
+ )
655
899
 
656
- async def _mcp_list_prompts(self) -> list[MCPPrompt]:
900
+ async def _list_resource_templates(
901
+ self,
902
+ context: MiddlewareContext[dict[str, Any]],
903
+ ) -> list[ResourceTemplate]:
904
+ """
905
+ List all available resource templates.
906
+ """
907
+ # 1. Filter local templates
908
+ local_templates = await self._resource_manager.get_resource_templates()
909
+ filtered_local = [
910
+ template
911
+ for template in local_templates.values()
912
+ if self._should_enable_component(template)
913
+ ]
914
+
915
+ # 2. Get from mounted servers with resource prefix handling
916
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
917
+ # Use a dict to implement "later wins" deduplication by key
918
+ all_templates: dict[str, ResourceTemplate] = {
919
+ template.key: template for template in filtered_local
920
+ }
921
+
922
+ for mounted in self._mounted_servers:
923
+ try:
924
+ child_templates = (
925
+ await mounted.server._list_resource_templates_middleware()
926
+ )
927
+ for template in child_templates:
928
+ # Apply parent server's filtering to mounted components
929
+ if not self._should_enable_component(template):
930
+ continue
931
+
932
+ key = template.key
933
+ if mounted.prefix:
934
+ key = add_resource_prefix(
935
+ template.key,
936
+ mounted.prefix,
937
+ mounted.resource_prefix_format,
938
+ )
939
+ template = template.model_copy(
940
+ key=key,
941
+ update={"name": f"{mounted.prefix}_{template.name}"},
942
+ )
943
+ # Later mounted servers override earlier ones
944
+ all_templates[key] = template
945
+ except Exception as e:
946
+ server_name = getattr(
947
+ getattr(mounted, "server", None), "name", repr(mounted)
948
+ )
949
+ logger.warning(
950
+ f"Failed to list resource templates from {server_name!r}: {e}"
951
+ )
952
+ if fastmcp.settings.mounted_components_raise_on_load_error:
953
+ raise
954
+ continue
955
+
956
+ return list(all_templates.values())
957
+
958
+ async def _list_prompts_mcp(self) -> list[MCPPrompt]:
959
+ """
960
+ List all available prompts, in the format expected by the low-level MCP
961
+ server.
962
+ """
657
963
  logger.debug(f"[{self.name}] Handler called: list_prompts")
658
964
 
659
965
  async with fastmcp.server.context.Context(fastmcp=self):
660
- prompts = await self._list_prompts()
966
+ prompts = await self._list_prompts_middleware()
661
967
  return [
662
968
  prompt.to_mcp_prompt(
663
969
  name=prompt.key,
@@ -666,25 +972,12 @@ class FastMCP(Generic[LifespanResultT]):
666
972
  for prompt in prompts
667
973
  ]
668
974
 
669
- async def _list_prompts(self) -> list[Prompt]:
975
+ async def _list_prompts_middleware(self) -> list[Prompt]:
670
976
  """
671
- List all available prompts, in the format expected by the low-level MCP
672
- server.
977
+ List all available prompts, applying MCP middleware.
673
978
 
674
979
  """
675
980
 
676
- async def _handler(
677
- context: MiddlewareContext[mcp.types.ListPromptsRequest],
678
- ) -> list[Prompt]:
679
- prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
680
-
681
- mcp_prompts: list[Prompt] = []
682
- for prompt in prompts:
683
- if self._should_enable_component(prompt):
684
- mcp_prompts.append(prompt)
685
-
686
- return mcp_prompts
687
-
688
981
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
689
982
  # Create the middleware context.
690
983
  mw_context = MiddlewareContext(
@@ -696,11 +989,68 @@ class FastMCP(Generic[LifespanResultT]):
696
989
  )
697
990
 
698
991
  # Apply the middleware chain.
699
- return await self._apply_middleware(mw_context, _handler)
992
+ return list(
993
+ await self._apply_middleware(
994
+ context=mw_context, call_next=self._list_prompts
995
+ )
996
+ )
997
+
998
+ async def _list_prompts(
999
+ self,
1000
+ context: MiddlewareContext[mcp.types.ListPromptsRequest],
1001
+ ) -> list[Prompt]:
1002
+ """
1003
+ List all available prompts.
1004
+ """
1005
+ # 1. Filter local prompts
1006
+ local_prompts = await self._prompt_manager.get_prompts()
1007
+ filtered_local = [
1008
+ prompt
1009
+ for prompt in local_prompts.values()
1010
+ if self._should_enable_component(prompt)
1011
+ ]
1012
+
1013
+ # 2. Get from mounted servers
1014
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
1015
+ # Use a dict to implement "later wins" deduplication by key
1016
+ all_prompts: dict[str, Prompt] = {
1017
+ prompt.key: prompt for prompt in filtered_local
1018
+ }
1019
+
1020
+ for mounted in self._mounted_servers:
1021
+ try:
1022
+ child_prompts = await mounted.server._list_prompts_middleware()
1023
+ for prompt in child_prompts:
1024
+ # Apply parent server's filtering to mounted components
1025
+ if not self._should_enable_component(prompt):
1026
+ continue
1027
+
1028
+ key = prompt.key
1029
+ if mounted.prefix:
1030
+ key = f"{mounted.prefix}_{prompt.key}"
1031
+ prompt = prompt.model_copy(key=key)
1032
+ # Later mounted servers override earlier ones
1033
+ all_prompts[key] = prompt
1034
+ except Exception as e:
1035
+ server_name = getattr(
1036
+ getattr(mounted, "server", None), "name", repr(mounted)
1037
+ )
1038
+ logger.warning(
1039
+ f"Failed to list prompts from mounted server {server_name!r}: {e}"
1040
+ )
1041
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1042
+ raise
1043
+ continue
700
1044
 
701
- async def _mcp_call_tool(
1045
+ return list(all_prompts.values())
1046
+
1047
+ async def _call_tool_mcp(
702
1048
  self, key: str, arguments: dict[str, Any]
703
- ) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]:
1049
+ ) -> (
1050
+ list[ContentBlock]
1051
+ | tuple[list[ContentBlock], dict[str, Any]]
1052
+ | mcp.types.CallToolResult
1053
+ ):
704
1054
  """
705
1055
  Handle MCP 'callTool' requests.
706
1056
 
@@ -719,29 +1069,22 @@ class FastMCP(Generic[LifespanResultT]):
719
1069
 
720
1070
  async with fastmcp.server.context.Context(fastmcp=self):
721
1071
  try:
722
- result = await self._call_tool(key, arguments)
1072
+ result = await self._call_tool_middleware(key, arguments)
723
1073
  return result.to_mcp_result()
724
- except DisabledError:
725
- raise NotFoundError(f"Unknown tool: {key}")
726
- except NotFoundError:
727
- raise NotFoundError(f"Unknown tool: {key}")
1074
+ except DisabledError as e:
1075
+ raise NotFoundError(f"Unknown tool: {key}") from e
1076
+ except NotFoundError as e:
1077
+ raise NotFoundError(f"Unknown tool: {key}") from e
728
1078
 
729
- async def _call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
1079
+ async def _call_tool_middleware(
1080
+ self,
1081
+ key: str,
1082
+ arguments: dict[str, Any],
1083
+ ) -> ToolResult:
730
1084
  """
731
1085
  Applies this server's middleware and delegates the filtered call to the manager.
732
1086
  """
733
1087
 
734
- async def _handler(
735
- context: MiddlewareContext[mcp.types.CallToolRequestParams],
736
- ) -> ToolResult:
737
- tool = await self._tool_manager.get_tool(context.message.name)
738
- if not self._should_enable_component(tool):
739
- raise NotFoundError(f"Unknown tool: {context.message.name!r}")
740
-
741
- return await self._tool_manager.call_tool(
742
- key=context.message.name, arguments=context.message.arguments or {}
743
- )
744
-
745
1088
  mw_context = MiddlewareContext[CallToolRequestParams](
746
1089
  message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
747
1090
  source="client",
@@ -749,9 +1092,53 @@ class FastMCP(Generic[LifespanResultT]):
749
1092
  method="tools/call",
750
1093
  fastmcp_context=fastmcp.server.dependencies.get_context(),
751
1094
  )
752
- return await self._apply_middleware(mw_context, _handler)
1095
+ return await self._apply_middleware(
1096
+ context=mw_context, call_next=self._call_tool
1097
+ )
753
1098
 
754
- async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
1099
+ async def _call_tool(
1100
+ self,
1101
+ context: MiddlewareContext[mcp.types.CallToolRequestParams],
1102
+ ) -> ToolResult:
1103
+ """
1104
+ Call a tool
1105
+ """
1106
+ tool_name = context.message.name
1107
+
1108
+ # Try mounted servers in reverse order (later wins)
1109
+ for mounted in reversed(self._mounted_servers):
1110
+ try_name = tool_name
1111
+ if mounted.prefix:
1112
+ if not tool_name.startswith(f"{mounted.prefix}_"):
1113
+ continue
1114
+ try_name = tool_name[len(mounted.prefix) + 1 :]
1115
+
1116
+ try:
1117
+ # First, get the tool to check if parent's filter allows it
1118
+ tool = await mounted.server._tool_manager.get_tool(try_name)
1119
+ if not self._should_enable_component(tool):
1120
+ # Parent filter blocks this tool, continue searching
1121
+ continue
1122
+
1123
+ return await mounted.server._call_tool_middleware(
1124
+ try_name, context.message.arguments or {}
1125
+ )
1126
+ except NotFoundError:
1127
+ continue
1128
+
1129
+ # Try local tools last (mounted servers override local)
1130
+ try:
1131
+ tool = await self._tool_manager.get_tool(tool_name)
1132
+ if self._should_enable_component(tool):
1133
+ return await self._tool_manager.call_tool(
1134
+ key=tool_name, arguments=context.message.arguments or {}
1135
+ )
1136
+ except NotFoundError:
1137
+ pass
1138
+
1139
+ raise NotFoundError(f"Unknown tool: {tool_name!r}")
1140
+
1141
+ async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
755
1142
  """
756
1143
  Handle MCP 'readResource' requests.
757
1144
 
@@ -761,39 +1148,26 @@ class FastMCP(Generic[LifespanResultT]):
761
1148
 
762
1149
  async with fastmcp.server.context.Context(fastmcp=self):
763
1150
  try:
764
- return await self._read_resource(uri)
765
- except DisabledError:
1151
+ return list[ReadResourceContents](
1152
+ await self._read_resource_middleware(uri)
1153
+ )
1154
+ except DisabledError as e:
766
1155
  # convert to NotFoundError to avoid leaking resource presence
767
- raise NotFoundError(f"Unknown resource: {str(uri)!r}")
768
- except NotFoundError:
1156
+ raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
1157
+ except NotFoundError as e:
769
1158
  # standardize NotFound message
770
- raise NotFoundError(f"Unknown resource: {str(uri)!r}")
1159
+ raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
771
1160
 
772
- async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
1161
+ async def _read_resource_middleware(
1162
+ self,
1163
+ uri: AnyUrl | str,
1164
+ ) -> list[ReadResourceContents]:
773
1165
  """
774
1166
  Applies this server's middleware and delegates the filtered call to the manager.
775
1167
  """
776
1168
 
777
- async def _handler(
778
- context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
779
- ) -> list[ReadResourceContents]:
780
- resource = await self._resource_manager.get_resource(context.message.uri)
781
- if not self._should_enable_component(resource):
782
- raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
783
-
784
- content = await self._resource_manager.read_resource(context.message.uri)
785
- return [
786
- ReadResourceContents(
787
- content=content,
788
- mime_type=resource.mime_type,
789
- )
790
- ]
791
-
792
1169
  # Convert string URI to AnyUrl if needed
793
- if isinstance(uri, str):
794
- uri_param = AnyUrl(uri)
795
- else:
796
- uri_param = uri
1170
+ uri_param = AnyUrl(uri) if isinstance(uri, str) else uri
797
1171
 
798
1172
  mw_context = MiddlewareContext(
799
1173
  message=mcp.types.ReadResourceRequestParams(uri=uri_param),
@@ -802,9 +1176,61 @@ class FastMCP(Generic[LifespanResultT]):
802
1176
  method="resources/read",
803
1177
  fastmcp_context=fastmcp.server.dependencies.get_context(),
804
1178
  )
805
- return await self._apply_middleware(mw_context, _handler)
1179
+ return list(
1180
+ await self._apply_middleware(
1181
+ context=mw_context, call_next=self._read_resource
1182
+ )
1183
+ )
806
1184
 
807
- async def _mcp_get_prompt(
1185
+ async def _read_resource(
1186
+ self,
1187
+ context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
1188
+ ) -> list[ReadResourceContents]:
1189
+ """
1190
+ Read a resource
1191
+ """
1192
+ uri_str = str(context.message.uri)
1193
+
1194
+ # Try mounted servers in reverse order (later wins)
1195
+ for mounted in reversed(self._mounted_servers):
1196
+ key = uri_str
1197
+ if mounted.prefix:
1198
+ if not has_resource_prefix(
1199
+ key, mounted.prefix, mounted.resource_prefix_format
1200
+ ):
1201
+ continue
1202
+ key = remove_resource_prefix(
1203
+ key, mounted.prefix, mounted.resource_prefix_format
1204
+ )
1205
+
1206
+ try:
1207
+ # First, get the resource to check if parent's filter allows it
1208
+ resource = await mounted.server._resource_manager.get_resource(key)
1209
+ if not self._should_enable_component(resource):
1210
+ # Parent filter blocks this resource, continue searching
1211
+ continue
1212
+ result = list(await mounted.server._read_resource_middleware(key))
1213
+ return result
1214
+ except NotFoundError:
1215
+ continue
1216
+
1217
+ # Try local resources last (mounted servers override local)
1218
+ try:
1219
+ resource = await self._resource_manager.get_resource(uri_str)
1220
+ if self._should_enable_component(resource):
1221
+ content = await self._resource_manager.read_resource(uri_str)
1222
+ return [
1223
+ ReadResourceContents(
1224
+ content=content,
1225
+ mime_type=resource.mime_type,
1226
+ )
1227
+ ]
1228
+ except NotFoundError:
1229
+ pass
1230
+
1231
+ raise NotFoundError(f"Unknown resource: {uri_str!r}")
1232
+
1233
+ async def _get_prompt_mcp(
808
1234
  self, name: str, arguments: dict[str, Any] | None = None
809
1235
  ) -> GetPromptResult:
810
1236
  """
@@ -812,38 +1238,29 @@ class FastMCP(Generic[LifespanResultT]):
812
1238
 
813
1239
  Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
814
1240
  """
1241
+ import fastmcp.server.context
1242
+
815
1243
  logger.debug(
816
1244
  f"[{self.name}] Handler called: get_prompt %s with %s", name, arguments
817
1245
  )
818
1246
 
819
1247
  async with fastmcp.server.context.Context(fastmcp=self):
820
1248
  try:
821
- return await self._get_prompt(name, arguments)
822
- except DisabledError:
1249
+ return await self._get_prompt_middleware(name, arguments)
1250
+ except DisabledError as e:
823
1251
  # convert to NotFoundError to avoid leaking prompt presence
824
- raise NotFoundError(f"Unknown prompt: {name}")
825
- except NotFoundError:
1252
+ raise NotFoundError(f"Unknown prompt: {name}") from e
1253
+ except NotFoundError as e:
826
1254
  # standardize NotFound message
827
- raise NotFoundError(f"Unknown prompt: {name}")
1255
+ raise NotFoundError(f"Unknown prompt: {name}") from e
828
1256
 
829
- async def _get_prompt(
1257
+ async def _get_prompt_middleware(
830
1258
  self, name: str, arguments: dict[str, Any] | None = None
831
1259
  ) -> GetPromptResult:
832
1260
  """
833
1261
  Applies this server's middleware and delegates the filtered call to the manager.
834
1262
  """
835
1263
 
836
- async def _handler(
837
- context: MiddlewareContext[mcp.types.GetPromptRequestParams],
838
- ) -> GetPromptResult:
839
- prompt = await self._prompt_manager.get_prompt(context.message.name)
840
- if not self._should_enable_component(prompt):
841
- raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
842
-
843
- return await self._prompt_manager.render_prompt(
844
- name=context.message.name, arguments=context.message.arguments
845
- )
846
-
847
1264
  mw_context = MiddlewareContext(
848
1265
  message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
849
1266
  source="client",
@@ -851,7 +1268,47 @@ class FastMCP(Generic[LifespanResultT]):
851
1268
  method="prompts/get",
852
1269
  fastmcp_context=fastmcp.server.dependencies.get_context(),
853
1270
  )
854
- return await self._apply_middleware(mw_context, _handler)
1271
+ return await self._apply_middleware(
1272
+ context=mw_context, call_next=self._get_prompt
1273
+ )
1274
+
1275
+ async def _get_prompt(
1276
+ self,
1277
+ context: MiddlewareContext[mcp.types.GetPromptRequestParams],
1278
+ ) -> GetPromptResult:
1279
+ name = context.message.name
1280
+
1281
+ # Try mounted servers in reverse order (later wins)
1282
+ for mounted in reversed(self._mounted_servers):
1283
+ try_name = name
1284
+ if mounted.prefix:
1285
+ if not name.startswith(f"{mounted.prefix}_"):
1286
+ continue
1287
+ try_name = name[len(mounted.prefix) + 1 :]
1288
+
1289
+ try:
1290
+ # First, get the prompt to check if parent's filter allows it
1291
+ prompt = await mounted.server._prompt_manager.get_prompt(try_name)
1292
+ if not self._should_enable_component(prompt):
1293
+ # Parent filter blocks this prompt, continue searching
1294
+ continue
1295
+ return await mounted.server._get_prompt_middleware(
1296
+ try_name, context.message.arguments
1297
+ )
1298
+ except NotFoundError:
1299
+ continue
1300
+
1301
+ # Try local prompts last (mounted servers override local)
1302
+ try:
1303
+ prompt = await self._prompt_manager.get_prompt(name)
1304
+ if self._should_enable_component(prompt):
1305
+ return await self._prompt_manager.render_prompt(
1306
+ name=name, arguments=context.message.arguments
1307
+ )
1308
+ except NotFoundError:
1309
+ pass
1310
+
1311
+ raise NotFoundError(f"Unknown prompt: {name!r}")
855
1312
 
856
1313
  def add_tool(self, tool: Tool) -> Tool:
857
1314
  """Add a tool to the server.
@@ -916,8 +1373,9 @@ class FastMCP(Generic[LifespanResultT]):
916
1373
  name: str | None = None,
917
1374
  title: str | None = None,
918
1375
  description: str | None = None,
1376
+ icons: list[mcp.types.Icon] | None = None,
919
1377
  tags: set[str] | None = None,
920
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1378
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
921
1379
  annotations: ToolAnnotations | dict[str, Any] | None = None,
922
1380
  exclude_args: list[str] | None = None,
923
1381
  meta: dict[str, Any] | None = None,
@@ -932,8 +1390,9 @@ class FastMCP(Generic[LifespanResultT]):
932
1390
  name: str | None = None,
933
1391
  title: str | None = None,
934
1392
  description: str | None = None,
1393
+ icons: list[mcp.types.Icon] | None = None,
935
1394
  tags: set[str] | None = None,
936
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1395
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
937
1396
  annotations: ToolAnnotations | dict[str, Any] | None = None,
938
1397
  exclude_args: list[str] | None = None,
939
1398
  meta: dict[str, Any] | None = None,
@@ -947,8 +1406,9 @@ class FastMCP(Generic[LifespanResultT]):
947
1406
  name: str | None = None,
948
1407
  title: str | None = None,
949
1408
  description: str | None = None,
1409
+ icons: list[mcp.types.Icon] | None = None,
950
1410
  tags: set[str] | None = None,
951
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1411
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
952
1412
  annotations: ToolAnnotations | dict[str, Any] | None = None,
953
1413
  exclude_args: list[str] | None = None,
954
1414
  meta: dict[str, Any] | None = None,
@@ -974,7 +1434,9 @@ class FastMCP(Generic[LifespanResultT]):
974
1434
  tags: Optional set of tags for categorizing the tool
975
1435
  output_schema: Optional JSON schema for the tool's output
976
1436
  annotations: Optional annotations about the tool's behavior
977
- exclude_args: Optional list of argument names to exclude from the tool schema
1437
+ exclude_args: Optional list of argument names to exclude from the tool schema.
1438
+ Note: `exclude_args` will be deprecated in FastMCP 2.14 in favor of dependency
1439
+ injection with `Depends()` for better lifecycle management.
978
1440
  meta: Optional meta information about the tool
979
1441
  enabled: Optional boolean to enable or disable the tool
980
1442
 
@@ -1025,14 +1487,16 @@ class FastMCP(Generic[LifespanResultT]):
1025
1487
  tool_name = name # Use keyword name if provided, otherwise None
1026
1488
 
1027
1489
  # Register the tool immediately and return the tool object
1490
+ # Note: Deprecation warning for exclude_args is handled in Tool.from_function
1028
1491
  tool = Tool.from_function(
1029
1492
  fn,
1030
1493
  name=tool_name,
1031
1494
  title=title,
1032
1495
  description=description,
1496
+ icons=icons,
1033
1497
  tags=tags,
1034
1498
  output_schema=output_schema,
1035
- annotations=cast(ToolAnnotations | None, annotations),
1499
+ annotations=annotations,
1036
1500
  exclude_args=exclude_args,
1037
1501
  meta=meta,
1038
1502
  serializer=self._tool_serializer,
@@ -1063,6 +1527,7 @@ class FastMCP(Generic[LifespanResultT]):
1063
1527
  name=tool_name,
1064
1528
  title=title,
1065
1529
  description=description,
1530
+ icons=icons,
1066
1531
  tags=tags,
1067
1532
  output_schema=output_schema,
1068
1533
  annotations=annotations,
@@ -1160,6 +1625,7 @@ class FastMCP(Generic[LifespanResultT]):
1160
1625
  name: str | None = None,
1161
1626
  title: str | None = None,
1162
1627
  description: str | None = None,
1628
+ icons: list[mcp.types.Icon] | None = None,
1163
1629
  mime_type: str | None = None,
1164
1630
  tags: set[str] | None = None,
1165
1631
  enabled: bool | None = None,
@@ -1208,8 +1674,8 @@ class FastMCP(Generic[LifespanResultT]):
1208
1674
  return f"Weather for {city}"
1209
1675
 
1210
1676
  @server.resource("resource://{city}/weather")
1211
- def get_weather_with_context(city: str, ctx: Context) -> str:
1212
- ctx.info(f"Fetching weather for {city}")
1677
+ async def get_weather_with_context(city: str, ctx: Context) -> str:
1678
+ await ctx.info(f"Fetching weather for {city}")
1213
1679
  return f"Weather for {city}"
1214
1680
 
1215
1681
  @server.resource("resource://{city}/weather")
@@ -1259,10 +1725,11 @@ class FastMCP(Generic[LifespanResultT]):
1259
1725
  name=name,
1260
1726
  title=title,
1261
1727
  description=description,
1728
+ icons=icons,
1262
1729
  mime_type=mime_type,
1263
1730
  tags=tags,
1264
1731
  enabled=enabled,
1265
- annotations=cast(Annotations | None, annotations),
1732
+ annotations=annotations,
1266
1733
  meta=meta,
1267
1734
  )
1268
1735
  self.add_template(template)
@@ -1274,10 +1741,11 @@ class FastMCP(Generic[LifespanResultT]):
1274
1741
  name=name,
1275
1742
  title=title,
1276
1743
  description=description,
1744
+ icons=icons,
1277
1745
  mime_type=mime_type,
1278
1746
  tags=tags,
1279
1747
  enabled=enabled,
1280
- annotations=cast(Annotations | None, annotations),
1748
+ annotations=annotations,
1281
1749
  meta=meta,
1282
1750
  )
1283
1751
  self.add_resource(resource)
@@ -1320,6 +1788,7 @@ class FastMCP(Generic[LifespanResultT]):
1320
1788
  name: str | None = None,
1321
1789
  title: str | None = None,
1322
1790
  description: str | None = None,
1791
+ icons: list[mcp.types.Icon] | None = None,
1323
1792
  tags: set[str] | None = None,
1324
1793
  enabled: bool | None = None,
1325
1794
  meta: dict[str, Any] | None = None,
@@ -1333,6 +1802,7 @@ class FastMCP(Generic[LifespanResultT]):
1333
1802
  name: str | None = None,
1334
1803
  title: str | None = None,
1335
1804
  description: str | None = None,
1805
+ icons: list[mcp.types.Icon] | None = None,
1336
1806
  tags: set[str] | None = None,
1337
1807
  enabled: bool | None = None,
1338
1808
  meta: dict[str, Any] | None = None,
@@ -1345,6 +1815,7 @@ class FastMCP(Generic[LifespanResultT]):
1345
1815
  name: str | None = None,
1346
1816
  title: str | None = None,
1347
1817
  description: str | None = None,
1818
+ icons: list[mcp.types.Icon] | None = None,
1348
1819
  tags: set[str] | None = None,
1349
1820
  enabled: bool | None = None,
1350
1821
  meta: dict[str, Any] | None = None,
@@ -1384,8 +1855,8 @@ class FastMCP(Generic[LifespanResultT]):
1384
1855
  ]
1385
1856
 
1386
1857
  @server.prompt()
1387
- def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
1388
- ctx.info(f"Analyzing table {table_name}")
1858
+ async def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
1859
+ await ctx.info(f"Analyzing table {table_name}")
1389
1860
  schema = read_table_schema(table_name)
1390
1861
  return [
1391
1862
  {
@@ -1395,7 +1866,7 @@ class FastMCP(Generic[LifespanResultT]):
1395
1866
  ]
1396
1867
 
1397
1868
  @server.prompt("custom_name")
1398
- def analyze_file(path: str) -> list[Message]:
1869
+ async def analyze_file(path: str) -> list[Message]:
1399
1870
  content = await read_file(path)
1400
1871
  return [
1401
1872
  {
@@ -1444,6 +1915,7 @@ class FastMCP(Generic[LifespanResultT]):
1444
1915
  name=prompt_name,
1445
1916
  title=title,
1446
1917
  description=description,
1918
+ icons=icons,
1447
1919
  tags=tags,
1448
1920
  enabled=enabled,
1449
1921
  meta=meta,
@@ -1474,14 +1946,21 @@ class FastMCP(Generic[LifespanResultT]):
1474
1946
  name=prompt_name,
1475
1947
  title=title,
1476
1948
  description=description,
1949
+ icons=icons,
1477
1950
  tags=tags,
1478
1951
  enabled=enabled,
1479
1952
  meta=meta,
1480
1953
  )
1481
1954
 
1482
- async def run_stdio_async(self, show_banner: bool = True) -> None:
1483
- """Run the server using stdio transport."""
1955
+ async def run_stdio_async(
1956
+ self, show_banner: bool = True, log_level: str | None = None
1957
+ ) -> None:
1958
+ """Run the server using stdio transport.
1484
1959
 
1960
+ Args:
1961
+ show_banner: Whether to display the server banner
1962
+ log_level: Log level for the server
1963
+ """
1485
1964
  # Display server banner
1486
1965
  if show_banner:
1487
1966
  log_server_banner(
@@ -1489,15 +1968,19 @@ class FastMCP(Generic[LifespanResultT]):
1489
1968
  transport="stdio",
1490
1969
  )
1491
1970
 
1492
- async with stdio_server() as (read_stream, write_stream):
1493
- logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
1494
- await self._mcp_server.run(
1495
- read_stream,
1496
- write_stream,
1497
- self._mcp_server.create_initialization_options(
1498
- NotificationOptions(tools_changed=True)
1499
- ),
1500
- )
1971
+ with temporary_log_level(log_level):
1972
+ async with self._lifespan_manager():
1973
+ async with stdio_server() as (read_stream, write_stream):
1974
+ logger.info(
1975
+ f"Starting MCP server {self.name!r} with transport 'stdio'"
1976
+ )
1977
+ await self._mcp_server.run(
1978
+ read_stream,
1979
+ write_stream,
1980
+ self._mcp_server.create_initialization_options(
1981
+ NotificationOptions(tools_changed=True)
1982
+ ),
1983
+ )
1501
1984
 
1502
1985
  async def run_http_async(
1503
1986
  self,
@@ -1509,6 +1992,7 @@ class FastMCP(Generic[LifespanResultT]):
1509
1992
  path: str | None = None,
1510
1993
  uvicorn_config: dict[str, Any] | None = None,
1511
1994
  middleware: list[ASGIMiddleware] | None = None,
1995
+ json_response: bool | None = None,
1512
1996
  stateless_http: bool | None = None,
1513
1997
  ) -> None:
1514
1998
  """Run the server using HTTP transport.
@@ -1521,9 +2005,9 @@ class FastMCP(Generic[LifespanResultT]):
1521
2005
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1522
2006
  uvicorn_config: Additional configuration for the Uvicorn server
1523
2007
  middleware: A list of middleware to apply to the app
2008
+ json_response: Whether to use JSON response format (defaults to settings.json_response)
1524
2009
  stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
1525
2010
  """
1526
-
1527
2011
  host = host or self._deprecated_settings.host
1528
2012
  port = port or self._deprecated_settings.port
1529
2013
  default_log_level_to_use = (
@@ -1534,6 +2018,7 @@ class FastMCP(Generic[LifespanResultT]):
1534
2018
  path=path,
1535
2019
  transport=transport,
1536
2020
  middleware=middleware,
2021
+ json_response=json_response,
1537
2022
  stateless_http=stateless_http,
1538
2023
  )
1539
2024
 
@@ -1553,25 +2038,28 @@ class FastMCP(Generic[LifespanResultT]):
1553
2038
  port=port,
1554
2039
  path=server_path,
1555
2040
  )
1556
- _uvicorn_config_from_user = uvicorn_config or {}
2041
+ uvicorn_config_from_user = uvicorn_config or {}
1557
2042
 
1558
2043
  config_kwargs: dict[str, Any] = {
1559
2044
  "timeout_graceful_shutdown": 0,
1560
2045
  "lifespan": "on",
2046
+ "ws": "websockets-sansio",
1561
2047
  }
1562
- config_kwargs.update(_uvicorn_config_from_user)
2048
+ config_kwargs.update(uvicorn_config_from_user)
1563
2049
 
1564
2050
  if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
1565
2051
  config_kwargs["log_level"] = default_log_level_to_use
1566
2052
 
1567
- config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
1568
- server = uvicorn.Server(config)
1569
- path = app.state.path.lstrip("/") # type: ignore
1570
- logger.info(
1571
- f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
1572
- )
2053
+ with temporary_log_level(log_level):
2054
+ async with self._lifespan_manager():
2055
+ config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
2056
+ server = uvicorn.Server(config)
2057
+ path = app.state.path.lstrip("/") # type: ignore
2058
+ logger.info(
2059
+ f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
2060
+ )
1573
2061
 
1574
- await server.serve()
2062
+ await server.serve()
1575
2063
 
1576
2064
  async def run_sse_async(
1577
2065
  self,
@@ -1833,7 +2321,7 @@ class FastMCP(Generic[LifespanResultT]):
1833
2321
  # if as_proxy is not specified and the server has a custom lifespan,
1834
2322
  # we should treat it as a proxy
1835
2323
  if as_proxy is None:
1836
- as_proxy = server._has_lifespan
2324
+ as_proxy = server._lifespan != default_lifespan
1837
2325
 
1838
2326
  if as_proxy and not isinstance(server, FastMCPProxy):
1839
2327
  server = FastMCP.as_proxy(server)
@@ -1845,9 +2333,6 @@ class FastMCP(Generic[LifespanResultT]):
1845
2333
  resource_prefix_format=self.resource_prefix_format,
1846
2334
  )
1847
2335
  self._mounted_servers.append(mounted_server)
1848
- self._tool_manager.mount(mounted_server)
1849
- self._resource_manager.mount(mounted_server)
1850
- self._prompt_manager.mount(mounted_server)
1851
2336
 
1852
2337
  async def import_server(
1853
2338
  self,
@@ -1970,6 +2455,15 @@ class FastMCP(Generic[LifespanResultT]):
1970
2455
  prompt = prompt.model_copy(key=f"{prefix}_{key}")
1971
2456
  self._prompt_manager.add_prompt(prompt)
1972
2457
 
2458
+ if server._lifespan != default_lifespan:
2459
+ from warnings import warn
2460
+
2461
+ warn(
2462
+ message="When importing from a server with a lifespan, the lifespan from the imported server will not be used.",
2463
+ category=RuntimeWarning,
2464
+ stacklevel=2,
2465
+ )
2466
+
1973
2467
  if prefix:
1974
2468
  logger.debug(
1975
2469
  f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
@@ -2096,6 +2590,7 @@ class FastMCP(Generic[LifespanResultT]):
2096
2590
  Client[ClientTransportT]
2097
2591
  | ClientTransport
2098
2592
  | FastMCP[Any]
2593
+ | FastMCP1Server
2099
2594
  | AnyUrl
2100
2595
  | Path
2101
2596
  | MCPConfig
@@ -2120,10 +2615,8 @@ class FastMCP(Generic[LifespanResultT]):
2120
2615
  # - Connected clients: reuse existing session for all requests
2121
2616
  # - Disconnected clients: create fresh sessions per request for isolation
2122
2617
  if client.is_connected():
2123
- from fastmcp.utilities.logging import get_logger
2124
-
2125
- logger = get_logger(__name__)
2126
- logger.info(
2618
+ proxy_logger = get_logger(__name__)
2619
+ proxy_logger.info(
2127
2620
  "Proxy detected connected client - reusing existing session for all requests. "
2128
2621
  "This may cause context mixing in concurrent scenarios."
2129
2622
  )
@@ -2140,7 +2633,7 @@ class FastMCP(Generic[LifespanResultT]):
2140
2633
 
2141
2634
  client_factory = fresh_client_factory
2142
2635
  else:
2143
- base_client = ProxyClient(backend)
2636
+ base_client = ProxyClient(backend) # type: ignore
2144
2637
 
2145
2638
  # Fresh client created from transport - use fresh sessions per request
2146
2639
  def proxy_client_factory():
@@ -2195,10 +2688,7 @@ class FastMCP(Generic[LifespanResultT]):
2195
2688
  return False
2196
2689
 
2197
2690
  if self.include_tags is not None:
2198
- if any(itag in component.tags for itag in self.include_tags):
2199
- return True
2200
- else:
2201
- return False
2691
+ return bool(any(itag in component.tags for itag in self.include_tags))
2202
2692
 
2203
2693
  return True
2204
2694