fastmcp 2.12.5__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 (108) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +11 -11
  3. fastmcp/cli/install/claude_code.py +6 -6
  4. fastmcp/cli/install/claude_desktop.py +3 -3
  5. fastmcp/cli/install/cursor.py +18 -12
  6. fastmcp/cli/install/gemini_cli.py +3 -3
  7. fastmcp/cli/install/mcp_json.py +3 -3
  8. fastmcp/cli/run.py +13 -8
  9. fastmcp/client/__init__.py +9 -9
  10. fastmcp/client/auth/oauth.py +115 -217
  11. fastmcp/client/client.py +105 -39
  12. fastmcp/client/logging.py +18 -14
  13. fastmcp/client/oauth_callback.py +85 -171
  14. fastmcp/client/sampling.py +1 -1
  15. fastmcp/client/transports.py +80 -25
  16. fastmcp/contrib/component_manager/__init__.py +1 -1
  17. fastmcp/contrib/component_manager/component_manager.py +2 -2
  18. fastmcp/contrib/component_manager/component_service.py +6 -6
  19. fastmcp/contrib/mcp_mixin/README.md +32 -1
  20. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  21. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  22. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  23. fastmcp/experimental/server/openapi/__init__.py +5 -8
  24. fastmcp/experimental/server/openapi/components.py +11 -7
  25. fastmcp/experimental/server/openapi/routing.py +2 -2
  26. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  27. fastmcp/experimental/utilities/openapi/director.py +14 -15
  28. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  29. fastmcp/experimental/utilities/openapi/models.py +3 -3
  30. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  31. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  32. fastmcp/mcp_config.py +3 -4
  33. fastmcp/prompts/__init__.py +1 -1
  34. fastmcp/prompts/prompt.py +22 -19
  35. fastmcp/prompts/prompt_manager.py +16 -101
  36. fastmcp/resources/__init__.py +5 -5
  37. fastmcp/resources/resource.py +14 -9
  38. fastmcp/resources/resource_manager.py +9 -168
  39. fastmcp/resources/template.py +107 -17
  40. fastmcp/resources/types.py +30 -24
  41. fastmcp/server/__init__.py +1 -1
  42. fastmcp/server/auth/__init__.py +9 -5
  43. fastmcp/server/auth/auth.py +70 -43
  44. fastmcp/server/auth/handlers/authorize.py +326 -0
  45. fastmcp/server/auth/jwt_issuer.py +236 -0
  46. fastmcp/server/auth/middleware.py +96 -0
  47. fastmcp/server/auth/oauth_proxy.py +1510 -289
  48. fastmcp/server/auth/oidc_proxy.py +84 -20
  49. fastmcp/server/auth/providers/auth0.py +40 -21
  50. fastmcp/server/auth/providers/aws.py +29 -3
  51. fastmcp/server/auth/providers/azure.py +312 -131
  52. fastmcp/server/auth/providers/bearer.py +1 -1
  53. fastmcp/server/auth/providers/debug.py +114 -0
  54. fastmcp/server/auth/providers/descope.py +86 -29
  55. fastmcp/server/auth/providers/discord.py +308 -0
  56. fastmcp/server/auth/providers/github.py +29 -8
  57. fastmcp/server/auth/providers/google.py +48 -9
  58. fastmcp/server/auth/providers/in_memory.py +27 -3
  59. fastmcp/server/auth/providers/introspection.py +281 -0
  60. fastmcp/server/auth/providers/jwt.py +48 -31
  61. fastmcp/server/auth/providers/oci.py +233 -0
  62. fastmcp/server/auth/providers/scalekit.py +238 -0
  63. fastmcp/server/auth/providers/supabase.py +188 -0
  64. fastmcp/server/auth/providers/workos.py +35 -17
  65. fastmcp/server/context.py +177 -51
  66. fastmcp/server/dependencies.py +39 -12
  67. fastmcp/server/elicitation.py +1 -1
  68. fastmcp/server/http.py +56 -17
  69. fastmcp/server/low_level.py +121 -2
  70. fastmcp/server/middleware/__init__.py +1 -1
  71. fastmcp/server/middleware/caching.py +476 -0
  72. fastmcp/server/middleware/error_handling.py +14 -10
  73. fastmcp/server/middleware/logging.py +50 -39
  74. fastmcp/server/middleware/middleware.py +29 -16
  75. fastmcp/server/middleware/rate_limiting.py +3 -3
  76. fastmcp/server/middleware/tool_injection.py +116 -0
  77. fastmcp/server/openapi.py +10 -6
  78. fastmcp/server/proxy.py +22 -11
  79. fastmcp/server/server.py +725 -242
  80. fastmcp/settings.py +24 -10
  81. fastmcp/tools/__init__.py +1 -1
  82. fastmcp/tools/tool.py +70 -23
  83. fastmcp/tools/tool_manager.py +30 -112
  84. fastmcp/tools/tool_transform.py +12 -10
  85. fastmcp/utilities/cli.py +67 -28
  86. fastmcp/utilities/components.py +7 -2
  87. fastmcp/utilities/inspect.py +79 -23
  88. fastmcp/utilities/json_schema.py +4 -4
  89. fastmcp/utilities/json_schema_type.py +4 -4
  90. fastmcp/utilities/logging.py +118 -8
  91. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  92. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  93. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  94. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
  95. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  96. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  97. fastmcp/utilities/openapi.py +11 -11
  98. fastmcp/utilities/tests.py +85 -4
  99. fastmcp/utilities/types.py +78 -16
  100. fastmcp/utilities/ui.py +626 -0
  101. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
  102. fastmcp-2.13.2.dist-info/RECORD +144 -0
  103. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  104. fastmcp/cli/claude.py +0 -135
  105. fastmcp/utilities/storage.py +0 -204
  106. fastmcp-2.12.5.dist-info/RECORD +0 -134
  107. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  108. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -7,8 +7,19 @@ import json
7
7
  import re
8
8
  import secrets
9
9
  import warnings
10
- from collections.abc import AsyncIterator, Awaitable, Callable
11
- from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
10
+ from collections.abc import (
11
+ AsyncIterator,
12
+ Awaitable,
13
+ Callable,
14
+ Collection,
15
+ Mapping,
16
+ Sequence,
17
+ )
18
+ from contextlib import (
19
+ AbstractAsyncContextManager,
20
+ AsyncExitStack,
21
+ asynccontextmanager,
22
+ )
12
23
  from dataclasses import dataclass
13
24
  from functools import partial
14
25
  from pathlib import Path
@@ -43,9 +54,11 @@ import fastmcp
43
54
  import fastmcp.server
44
55
  from fastmcp.exceptions import DisabledError, NotFoundError
45
56
  from fastmcp.mcp_config import MCPConfig
46
- from fastmcp.prompts import Prompt, PromptManager
57
+ from fastmcp.prompts import Prompt
47
58
  from fastmcp.prompts.prompt import FunctionPrompt
48
- 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
49
62
  from fastmcp.resources.template import ResourceTemplate
50
63
  from fastmcp.server.auth import AuthProvider
51
64
  from fastmcp.server.http import (
@@ -56,8 +69,8 @@ from fastmcp.server.http import (
56
69
  from fastmcp.server.low_level import LowLevelServer
57
70
  from fastmcp.server.middleware import Middleware, MiddlewareContext
58
71
  from fastmcp.settings import Settings
59
- from fastmcp.tools import ToolManager
60
72
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
73
+ from fastmcp.tools.tool_manager import ToolManager
61
74
  from fastmcp.tools.tool_transform import ToolTransformConfig
62
75
  from fastmcp.utilities.cli import log_server_banner
63
76
  from fastmcp.utilities.components import FastMCPComponent
@@ -66,7 +79,7 @@ from fastmcp.utilities.types import NotSet, NotSetT
66
79
 
67
80
  if TYPE_CHECKING:
68
81
  from fastmcp.client import Client
69
- from fastmcp.client.sampling import ServerSamplingHandler
82
+ from fastmcp.client.client import FastMCP1Server
70
83
  from fastmcp.client.transports import ClientTransport, ClientTransportT
71
84
  from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
72
85
  from fastmcp.experimental.server.openapi.routing import (
@@ -80,6 +93,8 @@ if TYPE_CHECKING:
80
93
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
81
94
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
82
95
  from fastmcp.server.proxy import FastMCPProxy
96
+ from fastmcp.server.sampling.handler import ServerSamplingHandler
97
+ from fastmcp.tools.tool import ToolResultSerializerType
83
98
 
84
99
  logger = get_logger(__name__)
85
100
 
@@ -89,6 +104,10 @@ Transport = Literal["stdio", "http", "sse", "streamable-http"]
89
104
  # Compiled URI parsing regex to split a URI into protocol and path components
90
105
  URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
91
106
 
107
+ LifespanCallable = Callable[
108
+ ["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
109
+ ]
110
+
92
111
 
93
112
  @asynccontextmanager
94
113
  async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
@@ -98,26 +117,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
98
117
  server: The server instance this lifespan is managing
99
118
 
100
119
  Returns:
101
- An empty context object
120
+ An empty dictionary as the lifespan result.
102
121
  """
103
122
  yield {}
104
123
 
105
124
 
106
- def _lifespan_wrapper(
107
- app: FastMCP[LifespanResultT],
108
- lifespan: Callable[
109
- [FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
110
- ],
125
+ def _lifespan_proxy(
126
+ fastmcp_server: FastMCP[LifespanResultT],
111
127
  ) -> Callable[
112
128
  [LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
113
129
  ]:
114
130
  @asynccontextmanager
115
131
  async def wrap(
116
- s: LowLevelServer[LifespanResultT],
132
+ low_level_server: LowLevelServer[LifespanResultT],
117
133
  ) -> AsyncIterator[LifespanResultT]:
118
- async with AsyncExitStack() as stack:
119
- context = await stack.enter_async_context(lifespan(app))
120
- 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
121
145
 
122
146
  return wrap
123
147
 
@@ -129,27 +153,24 @@ class FastMCP(Generic[LifespanResultT]):
129
153
  instructions: str | None = None,
130
154
  *,
131
155
  version: str | None = None,
132
- auth: AuthProvider | None | NotSetT = NotSet,
133
- middleware: list[Middleware] | None = None,
134
- lifespan: (
135
- Callable[
136
- [FastMCP[LifespanResultT]],
137
- AbstractAsyncContextManager[LifespanResultT],
138
- ]
139
- | None
140
- ) = 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,
141
161
  dependencies: list[str] | None = None,
142
162
  resource_prefix_format: Literal["protocol", "path"] | None = None,
143
163
  mask_error_details: bool | None = None,
144
- tools: list[Tool | Callable[..., Any]] | None = None,
145
- tool_transformations: dict[str, ToolTransformConfig] | None = None,
146
- tool_serializer: Callable[[Any], str] | None = None,
147
- include_tags: set[str] | None = None,
148
- 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,
149
169
  include_fastmcp_meta: bool | None = None,
150
170
  on_duplicate_tools: DuplicateBehavior | None = None,
151
171
  on_duplicate_resources: DuplicateBehavior | None = None,
152
172
  on_duplicate_prompts: DuplicateBehavior | None = None,
173
+ strict_input_validation: bool | None = None,
153
174
  # ---
154
175
  # ---
155
176
  # --- The following arguments are DEPRECATED ---
@@ -173,32 +194,36 @@ class FastMCP(Generic[LifespanResultT]):
173
194
 
174
195
  self._additional_http_routes: list[BaseRoute] = []
175
196
  self._mounted_servers: list[MountedServer] = []
176
- self._tool_manager = ToolManager(
197
+ self._tool_manager: ToolManager = ToolManager(
177
198
  duplicate_behavior=on_duplicate_tools,
178
199
  mask_error_details=mask_error_details,
179
200
  transformations=tool_transformations,
180
201
  )
181
- self._resource_manager = ResourceManager(
202
+ self._resource_manager: ResourceManager = ResourceManager(
182
203
  duplicate_behavior=on_duplicate_resources,
183
204
  mask_error_details=mask_error_details,
184
205
  )
185
- self._prompt_manager = PromptManager(
206
+ self._prompt_manager: PromptManager = PromptManager(
186
207
  duplicate_behavior=on_duplicate_prompts,
187
208
  mask_error_details=mask_error_details,
188
209
  )
189
- 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
190
215
 
191
- if lifespan is None:
192
- self._has_lifespan = False
193
- lifespan = default_lifespan
194
- else:
195
- self._has_lifespan = True
196
216
  # Generate random ID if no name provided
197
- self._mcp_server = LowLevelServer[LifespanResultT](
217
+ self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
218
+ LifespanResultT
219
+ ](
220
+ fastmcp=self,
198
221
  name=name or self.generate_name(),
199
- version=version,
222
+ version=version or fastmcp.__version__,
200
223
  instructions=instructions,
201
- lifespan=_lifespan_wrapper(self, lifespan),
224
+ website_url=website_url,
225
+ icons=icons,
226
+ lifespan=_lifespan_proxy(fastmcp_server=self),
202
227
  )
203
228
 
204
229
  # if auth is `NotSet`, try to create a provider from the environment
@@ -208,7 +233,7 @@ class FastMCP(Generic[LifespanResultT]):
208
233
  auth = fastmcp.settings.server_auth_class()
209
234
  else:
210
235
  auth = None
211
- self.auth = cast(AuthProvider | None, auth)
236
+ self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
212
237
 
213
238
  if tools:
214
239
  for tool in tools:
@@ -216,10 +241,20 @@ class FastMCP(Generic[LifespanResultT]):
216
241
  tool = Tool.from_function(tool, serializer=self._tool_serializer)
217
242
  self.add_tool(tool)
218
243
 
219
- self.include_tags = include_tags
220
- 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
+ )
221
250
 
222
- self.middleware = middleware or []
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
+ )
256
+
257
+ self.middleware: list[Middleware] = list(middleware or [])
223
258
 
224
259
  # Set up MCP protocol handlers
225
260
  self._setup_handlers()
@@ -238,14 +273,18 @@ class FastMCP(Generic[LifespanResultT]):
238
273
  DeprecationWarning,
239
274
  stacklevel=2,
240
275
  )
241
- self.dependencies = (
276
+ self.dependencies: list[str] = (
242
277
  dependencies or fastmcp.settings.server_dependencies
243
278
  ) # TODO: Remove (deprecated in v2.11.4)
244
279
 
245
- self.sampling_handler = sampling_handler
246
- 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
+ )
247
286
 
248
- self.include_fastmcp_meta = (
287
+ self.include_fastmcp_meta: bool = (
249
288
  include_fastmcp_meta
250
289
  if include_fastmcp_meta is not None
251
290
  else fastmcp.settings.include_fastmcp_meta
@@ -333,6 +372,38 @@ class FastMCP(Generic[LifespanResultT]):
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
+ )
565
721
 
566
- async def _mcp_list_resources(self) -> list[MCPResource]:
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())
766
+
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
+ )
899
+
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
+ }
655
921
 
656
- async def _mcp_list_prompts(self) -> list[MCPPrompt]:
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
+ )
1098
+
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 :]
753
1115
 
754
- async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
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
+ )
1184
+
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
806
1216
 
807
- async def _mcp_get_prompt(
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
  """
@@ -820,32 +1246,21 @@ class FastMCP(Generic[LifespanResultT]):
820
1246
 
821
1247
  async with fastmcp.server.context.Context(fastmcp=self):
822
1248
  try:
823
- return await self._get_prompt(name, arguments)
824
- except DisabledError:
1249
+ return await self._get_prompt_middleware(name, arguments)
1250
+ except DisabledError as e:
825
1251
  # convert to NotFoundError to avoid leaking prompt presence
826
- raise NotFoundError(f"Unknown prompt: {name}")
827
- except NotFoundError:
1252
+ raise NotFoundError(f"Unknown prompt: {name}") from e
1253
+ except NotFoundError as e:
828
1254
  # standardize NotFound message
829
- raise NotFoundError(f"Unknown prompt: {name}")
1255
+ raise NotFoundError(f"Unknown prompt: {name}") from e
830
1256
 
831
- async def _get_prompt(
1257
+ async def _get_prompt_middleware(
832
1258
  self, name: str, arguments: dict[str, Any] | None = None
833
1259
  ) -> GetPromptResult:
834
1260
  """
835
1261
  Applies this server's middleware and delegates the filtered call to the manager.
836
1262
  """
837
1263
 
838
- async def _handler(
839
- context: MiddlewareContext[mcp.types.GetPromptRequestParams],
840
- ) -> GetPromptResult:
841
- prompt = await self._prompt_manager.get_prompt(context.message.name)
842
- if not self._should_enable_component(prompt):
843
- raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
844
-
845
- return await self._prompt_manager.render_prompt(
846
- name=context.message.name, arguments=context.message.arguments
847
- )
848
-
849
1264
  mw_context = MiddlewareContext(
850
1265
  message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
851
1266
  source="client",
@@ -853,7 +1268,47 @@ class FastMCP(Generic[LifespanResultT]):
853
1268
  method="prompts/get",
854
1269
  fastmcp_context=fastmcp.server.dependencies.get_context(),
855
1270
  )
856
- 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}")
857
1312
 
858
1313
  def add_tool(self, tool: Tool) -> Tool:
859
1314
  """Add a tool to the server.
@@ -918,8 +1373,9 @@ class FastMCP(Generic[LifespanResultT]):
918
1373
  name: str | None = None,
919
1374
  title: str | None = None,
920
1375
  description: str | None = None,
1376
+ icons: list[mcp.types.Icon] | None = None,
921
1377
  tags: set[str] | None = None,
922
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1378
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
923
1379
  annotations: ToolAnnotations | dict[str, Any] | None = None,
924
1380
  exclude_args: list[str] | None = None,
925
1381
  meta: dict[str, Any] | None = None,
@@ -934,8 +1390,9 @@ class FastMCP(Generic[LifespanResultT]):
934
1390
  name: str | None = None,
935
1391
  title: str | None = None,
936
1392
  description: str | None = None,
1393
+ icons: list[mcp.types.Icon] | None = None,
937
1394
  tags: set[str] | None = None,
938
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1395
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
939
1396
  annotations: ToolAnnotations | dict[str, Any] | None = None,
940
1397
  exclude_args: list[str] | None = None,
941
1398
  meta: dict[str, Any] | None = None,
@@ -949,8 +1406,9 @@ class FastMCP(Generic[LifespanResultT]):
949
1406
  name: str | None = None,
950
1407
  title: str | None = None,
951
1408
  description: str | None = None,
1409
+ icons: list[mcp.types.Icon] | None = None,
952
1410
  tags: set[str] | None = None,
953
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1411
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
954
1412
  annotations: ToolAnnotations | dict[str, Any] | None = None,
955
1413
  exclude_args: list[str] | None = None,
956
1414
  meta: dict[str, Any] | None = None,
@@ -976,7 +1434,9 @@ class FastMCP(Generic[LifespanResultT]):
976
1434
  tags: Optional set of tags for categorizing the tool
977
1435
  output_schema: Optional JSON schema for the tool's output
978
1436
  annotations: Optional annotations about the tool's behavior
979
- 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.
980
1440
  meta: Optional meta information about the tool
981
1441
  enabled: Optional boolean to enable or disable the tool
982
1442
 
@@ -1027,14 +1487,16 @@ class FastMCP(Generic[LifespanResultT]):
1027
1487
  tool_name = name # Use keyword name if provided, otherwise None
1028
1488
 
1029
1489
  # Register the tool immediately and return the tool object
1490
+ # Note: Deprecation warning for exclude_args is handled in Tool.from_function
1030
1491
  tool = Tool.from_function(
1031
1492
  fn,
1032
1493
  name=tool_name,
1033
1494
  title=title,
1034
1495
  description=description,
1496
+ icons=icons,
1035
1497
  tags=tags,
1036
1498
  output_schema=output_schema,
1037
- annotations=cast(ToolAnnotations | None, annotations),
1499
+ annotations=annotations,
1038
1500
  exclude_args=exclude_args,
1039
1501
  meta=meta,
1040
1502
  serializer=self._tool_serializer,
@@ -1065,6 +1527,7 @@ class FastMCP(Generic[LifespanResultT]):
1065
1527
  name=tool_name,
1066
1528
  title=title,
1067
1529
  description=description,
1530
+ icons=icons,
1068
1531
  tags=tags,
1069
1532
  output_schema=output_schema,
1070
1533
  annotations=annotations,
@@ -1162,6 +1625,7 @@ class FastMCP(Generic[LifespanResultT]):
1162
1625
  name: str | None = None,
1163
1626
  title: str | None = None,
1164
1627
  description: str | None = None,
1628
+ icons: list[mcp.types.Icon] | None = None,
1165
1629
  mime_type: str | None = None,
1166
1630
  tags: set[str] | None = None,
1167
1631
  enabled: bool | None = None,
@@ -1261,10 +1725,11 @@ class FastMCP(Generic[LifespanResultT]):
1261
1725
  name=name,
1262
1726
  title=title,
1263
1727
  description=description,
1728
+ icons=icons,
1264
1729
  mime_type=mime_type,
1265
1730
  tags=tags,
1266
1731
  enabled=enabled,
1267
- annotations=cast(Annotations | None, annotations),
1732
+ annotations=annotations,
1268
1733
  meta=meta,
1269
1734
  )
1270
1735
  self.add_template(template)
@@ -1276,10 +1741,11 @@ class FastMCP(Generic[LifespanResultT]):
1276
1741
  name=name,
1277
1742
  title=title,
1278
1743
  description=description,
1744
+ icons=icons,
1279
1745
  mime_type=mime_type,
1280
1746
  tags=tags,
1281
1747
  enabled=enabled,
1282
- annotations=cast(Annotations | None, annotations),
1748
+ annotations=annotations,
1283
1749
  meta=meta,
1284
1750
  )
1285
1751
  self.add_resource(resource)
@@ -1322,6 +1788,7 @@ class FastMCP(Generic[LifespanResultT]):
1322
1788
  name: str | None = None,
1323
1789
  title: str | None = None,
1324
1790
  description: str | None = None,
1791
+ icons: list[mcp.types.Icon] | None = None,
1325
1792
  tags: set[str] | None = None,
1326
1793
  enabled: bool | None = None,
1327
1794
  meta: dict[str, Any] | None = None,
@@ -1335,6 +1802,7 @@ class FastMCP(Generic[LifespanResultT]):
1335
1802
  name: str | None = None,
1336
1803
  title: str | None = None,
1337
1804
  description: str | None = None,
1805
+ icons: list[mcp.types.Icon] | None = None,
1338
1806
  tags: set[str] | None = None,
1339
1807
  enabled: bool | None = None,
1340
1808
  meta: dict[str, Any] | None = None,
@@ -1347,6 +1815,7 @@ class FastMCP(Generic[LifespanResultT]):
1347
1815
  name: str | None = None,
1348
1816
  title: str | None = None,
1349
1817
  description: str | None = None,
1818
+ icons: list[mcp.types.Icon] | None = None,
1350
1819
  tags: set[str] | None = None,
1351
1820
  enabled: bool | None = None,
1352
1821
  meta: dict[str, Any] | None = None,
@@ -1446,6 +1915,7 @@ class FastMCP(Generic[LifespanResultT]):
1446
1915
  name=prompt_name,
1447
1916
  title=title,
1448
1917
  description=description,
1918
+ icons=icons,
1449
1919
  tags=tags,
1450
1920
  enabled=enabled,
1451
1921
  meta=meta,
@@ -1476,6 +1946,7 @@ class FastMCP(Generic[LifespanResultT]):
1476
1946
  name=prompt_name,
1477
1947
  title=title,
1478
1948
  description=description,
1949
+ icons=icons,
1479
1950
  tags=tags,
1480
1951
  enabled=enabled,
1481
1952
  meta=meta,
@@ -1498,15 +1969,18 @@ class FastMCP(Generic[LifespanResultT]):
1498
1969
  )
1499
1970
 
1500
1971
  with temporary_log_level(log_level):
1501
- async with stdio_server() as (read_stream, write_stream):
1502
- logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
1503
- await self._mcp_server.run(
1504
- read_stream,
1505
- write_stream,
1506
- self._mcp_server.create_initialization_options(
1507
- NotificationOptions(tools_changed=True)
1508
- ),
1509
- )
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
+ )
1510
1984
 
1511
1985
  async def run_http_async(
1512
1986
  self,
@@ -1518,6 +1992,7 @@ class FastMCP(Generic[LifespanResultT]):
1518
1992
  path: str | None = None,
1519
1993
  uvicorn_config: dict[str, Any] | None = None,
1520
1994
  middleware: list[ASGIMiddleware] | None = None,
1995
+ json_response: bool | None = None,
1521
1996
  stateless_http: bool | None = None,
1522
1997
  ) -> None:
1523
1998
  """Run the server using HTTP transport.
@@ -1530,6 +2005,7 @@ class FastMCP(Generic[LifespanResultT]):
1530
2005
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1531
2006
  uvicorn_config: Additional configuration for the Uvicorn server
1532
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)
1533
2009
  stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
1534
2010
  """
1535
2011
  host = host or self._deprecated_settings.host
@@ -1542,6 +2018,7 @@ class FastMCP(Generic[LifespanResultT]):
1542
2018
  path=path,
1543
2019
  transport=transport,
1544
2020
  middleware=middleware,
2021
+ json_response=json_response,
1545
2022
  stateless_http=stateless_http,
1546
2023
  )
1547
2024
 
@@ -1561,26 +2038,28 @@ class FastMCP(Generic[LifespanResultT]):
1561
2038
  port=port,
1562
2039
  path=server_path,
1563
2040
  )
1564
- _uvicorn_config_from_user = uvicorn_config or {}
2041
+ uvicorn_config_from_user = uvicorn_config or {}
1565
2042
 
1566
2043
  config_kwargs: dict[str, Any] = {
1567
2044
  "timeout_graceful_shutdown": 0,
1568
2045
  "lifespan": "on",
2046
+ "ws": "websockets-sansio",
1569
2047
  }
1570
- config_kwargs.update(_uvicorn_config_from_user)
2048
+ config_kwargs.update(uvicorn_config_from_user)
1571
2049
 
1572
2050
  if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
1573
2051
  config_kwargs["log_level"] = default_log_level_to_use
1574
2052
 
1575
2053
  with temporary_log_level(log_level):
1576
- config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
1577
- server = uvicorn.Server(config)
1578
- path = app.state.path.lstrip("/") # type: ignore
1579
- logger.info(
1580
- f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
1581
- )
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
+ )
1582
2061
 
1583
- await server.serve()
2062
+ await server.serve()
1584
2063
 
1585
2064
  async def run_sse_async(
1586
2065
  self,
@@ -1842,7 +2321,7 @@ class FastMCP(Generic[LifespanResultT]):
1842
2321
  # if as_proxy is not specified and the server has a custom lifespan,
1843
2322
  # we should treat it as a proxy
1844
2323
  if as_proxy is None:
1845
- as_proxy = server._has_lifespan
2324
+ as_proxy = server._lifespan != default_lifespan
1846
2325
 
1847
2326
  if as_proxy and not isinstance(server, FastMCPProxy):
1848
2327
  server = FastMCP.as_proxy(server)
@@ -1854,9 +2333,6 @@ class FastMCP(Generic[LifespanResultT]):
1854
2333
  resource_prefix_format=self.resource_prefix_format,
1855
2334
  )
1856
2335
  self._mounted_servers.append(mounted_server)
1857
- self._tool_manager.mount(mounted_server)
1858
- self._resource_manager.mount(mounted_server)
1859
- self._prompt_manager.mount(mounted_server)
1860
2336
 
1861
2337
  async def import_server(
1862
2338
  self,
@@ -1979,6 +2455,15 @@ class FastMCP(Generic[LifespanResultT]):
1979
2455
  prompt = prompt.model_copy(key=f"{prefix}_{key}")
1980
2456
  self._prompt_manager.add_prompt(prompt)
1981
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
+
1982
2467
  if prefix:
1983
2468
  logger.debug(
1984
2469
  f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
@@ -2105,6 +2590,7 @@ class FastMCP(Generic[LifespanResultT]):
2105
2590
  Client[ClientTransportT]
2106
2591
  | ClientTransport
2107
2592
  | FastMCP[Any]
2593
+ | FastMCP1Server
2108
2594
  | AnyUrl
2109
2595
  | Path
2110
2596
  | MCPConfig
@@ -2129,8 +2615,8 @@ class FastMCP(Generic[LifespanResultT]):
2129
2615
  # - Connected clients: reuse existing session for all requests
2130
2616
  # - Disconnected clients: create fresh sessions per request for isolation
2131
2617
  if client.is_connected():
2132
- _proxy_logger = get_logger(__name__)
2133
- _proxy_logger.info(
2618
+ proxy_logger = get_logger(__name__)
2619
+ proxy_logger.info(
2134
2620
  "Proxy detected connected client - reusing existing session for all requests. "
2135
2621
  "This may cause context mixing in concurrent scenarios."
2136
2622
  )
@@ -2147,7 +2633,7 @@ class FastMCP(Generic[LifespanResultT]):
2147
2633
 
2148
2634
  client_factory = fresh_client_factory
2149
2635
  else:
2150
- base_client = ProxyClient(backend)
2636
+ base_client = ProxyClient(backend) # type: ignore
2151
2637
 
2152
2638
  # Fresh client created from transport - use fresh sessions per request
2153
2639
  def proxy_client_factory():
@@ -2202,10 +2688,7 @@ class FastMCP(Generic[LifespanResultT]):
2202
2688
  return False
2203
2689
 
2204
2690
  if self.include_tags is not None:
2205
- if any(itag in component.tags for itag in self.include_tags):
2206
- return True
2207
- else:
2208
- return False
2691
+ return bool(any(itag in component.tags for itag in self.include_tags))
2209
2692
 
2210
2693
  return True
2211
2694