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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. fastmcp/cli/cli.py +7 -6
  2. fastmcp/cli/install/claude_code.py +6 -6
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +85 -171
  12. fastmcp/client/transports.py +77 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/resources/types.py +30 -24
  24. fastmcp/server/auth/auth.py +40 -32
  25. fastmcp/server/auth/handlers/authorize.py +324 -0
  26. fastmcp/server/auth/jwt_issuer.py +236 -0
  27. fastmcp/server/auth/middleware.py +96 -0
  28. fastmcp/server/auth/oauth_proxy.py +1256 -242
  29. fastmcp/server/auth/oidc_proxy.py +23 -6
  30. fastmcp/server/auth/providers/auth0.py +40 -21
  31. fastmcp/server/auth/providers/aws.py +29 -3
  32. fastmcp/server/auth/providers/azure.py +178 -127
  33. fastmcp/server/auth/providers/descope.py +4 -6
  34. fastmcp/server/auth/providers/github.py +29 -8
  35. fastmcp/server/auth/providers/google.py +30 -9
  36. fastmcp/server/auth/providers/introspection.py +281 -0
  37. fastmcp/server/auth/providers/jwt.py +8 -2
  38. fastmcp/server/auth/providers/scalekit.py +179 -0
  39. fastmcp/server/auth/providers/supabase.py +172 -0
  40. fastmcp/server/auth/providers/workos.py +32 -14
  41. fastmcp/server/context.py +122 -36
  42. fastmcp/server/http.py +58 -18
  43. fastmcp/server/low_level.py +121 -2
  44. fastmcp/server/middleware/caching.py +469 -0
  45. fastmcp/server/middleware/error_handling.py +6 -2
  46. fastmcp/server/middleware/logging.py +48 -37
  47. fastmcp/server/middleware/middleware.py +28 -15
  48. fastmcp/server/middleware/rate_limiting.py +3 -3
  49. fastmcp/server/middleware/tool_injection.py +116 -0
  50. fastmcp/server/proxy.py +6 -6
  51. fastmcp/server/server.py +683 -207
  52. fastmcp/settings.py +24 -10
  53. fastmcp/tools/tool.py +7 -3
  54. fastmcp/tools/tool_manager.py +30 -112
  55. fastmcp/tools/tool_transform.py +3 -3
  56. fastmcp/utilities/cli.py +62 -22
  57. fastmcp/utilities/components.py +5 -0
  58. fastmcp/utilities/inspect.py +77 -21
  59. fastmcp/utilities/logging.py +118 -8
  60. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  61. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  62. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  63. fastmcp/utilities/tests.py +87 -4
  64. fastmcp/utilities/types.py +1 -1
  65. fastmcp/utilities/ui.py +617 -0
  66. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/METADATA +10 -6
  67. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/RECORD +70 -63
  68. fastmcp/cli/claude.py +0 -135
  69. fastmcp/utilities/storage.py +0 -204
  70. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/WHEEL +0 -0
  71. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/entry_points.txt +0 -0
  72. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.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 AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
12
19
  from dataclasses import dataclass
13
20
  from functools import partial
@@ -43,9 +50,11 @@ import fastmcp
43
50
  import fastmcp.server
44
51
  from fastmcp.exceptions import DisabledError, NotFoundError
45
52
  from fastmcp.mcp_config import MCPConfig
46
- from fastmcp.prompts import Prompt, PromptManager
53
+ from fastmcp.prompts import Prompt
47
54
  from fastmcp.prompts.prompt import FunctionPrompt
48
- from fastmcp.resources import Resource, ResourceManager
55
+ from fastmcp.prompts.prompt_manager import PromptManager
56
+ from fastmcp.resources.resource import Resource
57
+ from fastmcp.resources.resource_manager import ResourceManager
49
58
  from fastmcp.resources.template import ResourceTemplate
50
59
  from fastmcp.server.auth import AuthProvider
51
60
  from fastmcp.server.http import (
@@ -56,8 +65,8 @@ from fastmcp.server.http import (
56
65
  from fastmcp.server.low_level import LowLevelServer
57
66
  from fastmcp.server.middleware import Middleware, MiddlewareContext
58
67
  from fastmcp.settings import Settings
59
- from fastmcp.tools import ToolManager
60
68
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
69
+ from fastmcp.tools.tool_manager import ToolManager
61
70
  from fastmcp.tools.tool_transform import ToolTransformConfig
62
71
  from fastmcp.utilities.cli import log_server_banner
63
72
  from fastmcp.utilities.components import FastMCPComponent
@@ -66,7 +75,6 @@ from fastmcp.utilities.types import NotSet, NotSetT
66
75
 
67
76
  if TYPE_CHECKING:
68
77
  from fastmcp.client import Client
69
- from fastmcp.client.sampling import ServerSamplingHandler
70
78
  from fastmcp.client.transports import ClientTransport, ClientTransportT
71
79
  from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
72
80
  from fastmcp.experimental.server.openapi.routing import (
@@ -80,6 +88,8 @@ if TYPE_CHECKING:
80
88
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
81
89
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
82
90
  from fastmcp.server.proxy import FastMCPProxy
91
+ from fastmcp.server.sampling.handler import ServerSamplingHandler
92
+ from fastmcp.tools.tool import ToolResultSerializerType
83
93
 
84
94
  logger = get_logger(__name__)
85
95
 
@@ -89,6 +99,10 @@ Transport = Literal["stdio", "http", "sse", "streamable-http"]
89
99
  # Compiled URI parsing regex to split a URI into protocol and path components
90
100
  URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
91
101
 
102
+ LifespanCallable = Callable[
103
+ ["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
104
+ ]
105
+
92
106
 
93
107
  @asynccontextmanager
94
108
  async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
@@ -98,26 +112,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
98
112
  server: The server instance this lifespan is managing
99
113
 
100
114
  Returns:
101
- An empty context object
115
+ An empty dictionary as the lifespan result.
102
116
  """
103
117
  yield {}
104
118
 
105
119
 
106
- def _lifespan_wrapper(
107
- app: FastMCP[LifespanResultT],
108
- lifespan: Callable[
109
- [FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
110
- ],
120
+ def _lifespan_proxy(
121
+ fastmcp_server: FastMCP[LifespanResultT],
111
122
  ) -> Callable[
112
123
  [LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
113
124
  ]:
114
125
  @asynccontextmanager
115
126
  async def wrap(
116
- s: LowLevelServer[LifespanResultT],
127
+ low_level_server: LowLevelServer[LifespanResultT],
117
128
  ) -> AsyncIterator[LifespanResultT]:
118
- async with AsyncExitStack() as stack:
119
- context = await stack.enter_async_context(lifespan(app))
120
- yield context
129
+ if fastmcp_server._lifespan is default_lifespan:
130
+ yield {}
131
+ return
132
+
133
+ if not fastmcp_server._lifespan_result_set:
134
+ raise RuntimeError(
135
+ "FastMCP server has a lifespan defined but no lifespan result is set, which means the server's context manager was not entered. "
136
+ + " Are you running the server in a way that supports lifespans? If so, please file an issue at https://github.com/jlowin/fastmcp/issues."
137
+ )
138
+
139
+ yield fastmcp_server._lifespan_result
121
140
 
122
141
  return wrap
123
142
 
@@ -129,27 +148,24 @@ class FastMCP(Generic[LifespanResultT]):
129
148
  instructions: str | None = None,
130
149
  *,
131
150
  version: str | None = None,
151
+ website_url: str | None = None,
152
+ icons: list[mcp.types.Icon] | None = None,
132
153
  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,
154
+ middleware: Sequence[Middleware] | None = None,
155
+ lifespan: LifespanCallable | None = None,
141
156
  dependencies: list[str] | None = None,
142
157
  resource_prefix_format: Literal["protocol", "path"] | None = None,
143
158
  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,
159
+ tools: Sequence[Tool | Callable[..., Any]] | None = None,
160
+ tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
161
+ tool_serializer: ToolResultSerializerType | None = None,
162
+ include_tags: Collection[str] | None = None,
163
+ exclude_tags: Collection[str] | None = None,
149
164
  include_fastmcp_meta: bool | None = None,
150
165
  on_duplicate_tools: DuplicateBehavior | None = None,
151
166
  on_duplicate_resources: DuplicateBehavior | None = None,
152
167
  on_duplicate_prompts: DuplicateBehavior | None = None,
168
+ strict_input_validation: bool | None = None,
153
169
  # ---
154
170
  # ---
155
171
  # --- The following arguments are DEPRECATED ---
@@ -173,32 +189,36 @@ class FastMCP(Generic[LifespanResultT]):
173
189
 
174
190
  self._additional_http_routes: list[BaseRoute] = []
175
191
  self._mounted_servers: list[MountedServer] = []
176
- self._tool_manager = ToolManager(
192
+ self._tool_manager: ToolManager = ToolManager(
177
193
  duplicate_behavior=on_duplicate_tools,
178
194
  mask_error_details=mask_error_details,
179
195
  transformations=tool_transformations,
180
196
  )
181
- self._resource_manager = ResourceManager(
197
+ self._resource_manager: ResourceManager = ResourceManager(
182
198
  duplicate_behavior=on_duplicate_resources,
183
199
  mask_error_details=mask_error_details,
184
200
  )
185
- self._prompt_manager = PromptManager(
201
+ self._prompt_manager: PromptManager = PromptManager(
186
202
  duplicate_behavior=on_duplicate_prompts,
187
203
  mask_error_details=mask_error_details,
188
204
  )
189
- self._tool_serializer = tool_serializer
205
+ self._tool_serializer: Callable[[Any], str] | None = tool_serializer
206
+
207
+ self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
208
+ self._lifespan_result: LifespanResultT | None = None
209
+ self._lifespan_result_set: bool = False
190
210
 
191
- if lifespan is None:
192
- self._has_lifespan = False
193
- lifespan = default_lifespan
194
- else:
195
- self._has_lifespan = True
196
211
  # Generate random ID if no name provided
197
- self._mcp_server = LowLevelServer[LifespanResultT](
212
+ self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
213
+ LifespanResultT
214
+ ](
215
+ fastmcp=self,
198
216
  name=name or self.generate_name(),
199
- version=version,
217
+ version=version or fastmcp.__version__,
200
218
  instructions=instructions,
201
- lifespan=_lifespan_wrapper(self, lifespan),
219
+ website_url=website_url,
220
+ icons=icons,
221
+ lifespan=_lifespan_proxy(fastmcp_server=self),
202
222
  )
203
223
 
204
224
  # if auth is `NotSet`, try to create a provider from the environment
@@ -208,7 +228,7 @@ class FastMCP(Generic[LifespanResultT]):
208
228
  auth = fastmcp.settings.server_auth_class()
209
229
  else:
210
230
  auth = None
211
- self.auth = cast(AuthProvider | None, auth)
231
+ self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
212
232
 
213
233
  if tools:
214
234
  for tool in tools:
@@ -216,10 +236,20 @@ class FastMCP(Generic[LifespanResultT]):
216
236
  tool = Tool.from_function(tool, serializer=self._tool_serializer)
217
237
  self.add_tool(tool)
218
238
 
219
- self.include_tags = include_tags
220
- self.exclude_tags = exclude_tags
239
+ self.include_tags: set[str] | None = (
240
+ set(include_tags) if include_tags is not None else None
241
+ )
242
+ self.exclude_tags: set[str] | None = (
243
+ set(exclude_tags) if exclude_tags is not None else None
244
+ )
245
+
246
+ self.strict_input_validation: bool = (
247
+ strict_input_validation
248
+ if strict_input_validation is not None
249
+ else fastmcp.settings.strict_input_validation
250
+ )
221
251
 
222
- self.middleware = middleware or []
252
+ self.middleware: list[Middleware] = list(middleware or [])
223
253
 
224
254
  # Set up MCP protocol handlers
225
255
  self._setup_handlers()
@@ -238,14 +268,18 @@ class FastMCP(Generic[LifespanResultT]):
238
268
  DeprecationWarning,
239
269
  stacklevel=2,
240
270
  )
241
- self.dependencies = (
271
+ self.dependencies: list[str] = (
242
272
  dependencies or fastmcp.settings.server_dependencies
243
273
  ) # TODO: Remove (deprecated in v2.11.4)
244
274
 
245
- self.sampling_handler = sampling_handler
246
- self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
275
+ self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
276
+ sampling_handler
277
+ )
278
+ self.sampling_handler_behavior: Literal["always", "fallback"] = (
279
+ sampling_handler_behavior or "fallback"
280
+ )
247
281
 
248
- self.include_fastmcp_meta = (
282
+ self.include_fastmcp_meta: bool = (
249
283
  include_fastmcp_meta
250
284
  if include_fastmcp_meta is not None
251
285
  else fastmcp.settings.include_fastmcp_meta
@@ -333,6 +367,38 @@ class FastMCP(Generic[LifespanResultT]):
333
367
  def version(self) -> str | None:
334
368
  return self._mcp_server.version
335
369
 
370
+ @property
371
+ def website_url(self) -> str | None:
372
+ return self._mcp_server.website_url
373
+
374
+ @property
375
+ def icons(self) -> list[mcp.types.Icon]:
376
+ if self._mcp_server.icons is None:
377
+ return []
378
+ else:
379
+ return list(self._mcp_server.icons)
380
+
381
+ @asynccontextmanager
382
+ async def _lifespan_manager(self) -> AsyncIterator[None]:
383
+ if self._lifespan_result_set:
384
+ yield
385
+ return
386
+
387
+ async with self._lifespan(self) as lifespan_result:
388
+ self._lifespan_result = lifespan_result
389
+ self._lifespan_result_set = True
390
+
391
+ async with AsyncExitStack[bool | None]() as stack:
392
+ for server in self._mounted_servers:
393
+ await stack.enter_async_context(
394
+ cm=server.server._lifespan_manager()
395
+ )
396
+
397
+ yield
398
+
399
+ self._lifespan_result_set = False
400
+ self._lifespan_result = None
401
+
336
402
  async def run_async(
337
403
  self,
338
404
  transport: Transport | None = None,
@@ -386,13 +452,15 @@ class FastMCP(Generic[LifespanResultT]):
386
452
 
387
453
  def _setup_handlers(self) -> None:
388
454
  """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)
455
+ self._mcp_server.list_tools()(self._list_tools_mcp)
456
+ self._mcp_server.list_resources()(self._list_resources_mcp)
457
+ self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
458
+ self._mcp_server.list_prompts()(self._list_prompts_mcp)
459
+ self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
460
+ self._call_tool_mcp
461
+ )
462
+ self._mcp_server.read_resource()(self._read_resource_mcp)
463
+ self._mcp_server.get_prompt()(self._get_prompt_mcp)
396
464
 
397
465
  async def _apply_middleware(
398
466
  self,
@@ -409,8 +477,24 @@ class FastMCP(Generic[LifespanResultT]):
409
477
  self.middleware.append(middleware)
410
478
 
411
479
  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()
480
+ """Get all tools (unfiltered), including mounted servers, indexed by key."""
481
+ all_tools = dict(await self._tool_manager.get_tools())
482
+
483
+ for mounted in self._mounted_servers:
484
+ try:
485
+ child_tools = await mounted.server.get_tools()
486
+ for key, tool in child_tools.items():
487
+ new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
488
+ all_tools[new_key] = tool.model_copy(key=new_key)
489
+ except Exception as e:
490
+ logger.warning(
491
+ f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
492
+ )
493
+ if fastmcp.settings.mounted_components_raise_on_load_error:
494
+ raise
495
+ continue
496
+
497
+ return all_tools
414
498
 
415
499
  async def get_tool(self, key: str) -> Tool:
416
500
  tools = await self.get_tools()
@@ -419,8 +503,37 @@ class FastMCP(Generic[LifespanResultT]):
419
503
  return tools[key]
420
504
 
421
505
  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()
506
+ """Get all resources (unfiltered), including mounted servers, indexed by key."""
507
+ all_resources = dict(await self._resource_manager.get_resources())
508
+
509
+ for mounted in self._mounted_servers:
510
+ try:
511
+ child_resources = await mounted.server.get_resources()
512
+ for key, resource in child_resources.items():
513
+ new_key = (
514
+ add_resource_prefix(
515
+ key, mounted.prefix, mounted.resource_prefix_format
516
+ )
517
+ if mounted.prefix
518
+ else key
519
+ )
520
+ update = (
521
+ {"name": f"{mounted.prefix}_{resource.name}"}
522
+ if mounted.prefix and resource.name
523
+ else {}
524
+ )
525
+ all_resources[new_key] = resource.model_copy(
526
+ key=new_key, update=update
527
+ )
528
+ except Exception as e:
529
+ logger.warning(
530
+ f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
531
+ )
532
+ if fastmcp.settings.mounted_components_raise_on_load_error:
533
+ raise
534
+ continue
535
+
536
+ return all_resources
424
537
 
425
538
  async def get_resource(self, key: str) -> Resource:
426
539
  resources = await self.get_resources()
@@ -429,8 +542,37 @@ class FastMCP(Generic[LifespanResultT]):
429
542
  return resources[key]
430
543
 
431
544
  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()
545
+ """Get all resource templates (unfiltered), including mounted servers, indexed by key."""
546
+ all_templates = dict(await self._resource_manager.get_resource_templates())
547
+
548
+ for mounted in self._mounted_servers:
549
+ try:
550
+ child_templates = await mounted.server.get_resource_templates()
551
+ for key, template in child_templates.items():
552
+ new_key = (
553
+ add_resource_prefix(
554
+ key, mounted.prefix, mounted.resource_prefix_format
555
+ )
556
+ if mounted.prefix
557
+ else key
558
+ )
559
+ update = (
560
+ {"name": f"{mounted.prefix}_{template.name}"}
561
+ if mounted.prefix and template.name
562
+ else {}
563
+ )
564
+ all_templates[new_key] = template.model_copy(
565
+ key=new_key, update=update
566
+ )
567
+ except Exception as e:
568
+ logger.warning(
569
+ f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
570
+ )
571
+ if fastmcp.settings.mounted_components_raise_on_load_error:
572
+ raise
573
+ continue
574
+
575
+ return all_templates
434
576
 
435
577
  async def get_resource_template(self, key: str) -> ResourceTemplate:
436
578
  """Get a registered resource template by key."""
@@ -440,10 +582,24 @@ class FastMCP(Generic[LifespanResultT]):
440
582
  return templates[key]
441
583
 
442
584
  async def get_prompts(self) -> dict[str, Prompt]:
443
- """
444
- List all available prompts.
445
- """
446
- return await self._prompt_manager.get_prompts()
585
+ """Get all prompts (unfiltered), including mounted servers, indexed by key."""
586
+ all_prompts = dict(await self._prompt_manager.get_prompts())
587
+
588
+ for mounted in self._mounted_servers:
589
+ try:
590
+ child_prompts = await mounted.server.get_prompts()
591
+ for key, prompt in child_prompts.items():
592
+ new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
593
+ all_prompts[new_key] = prompt.model_copy(key=new_key)
594
+ except Exception as e:
595
+ logger.warning(
596
+ f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
597
+ )
598
+ if fastmcp.settings.mounted_components_raise_on_load_error:
599
+ raise
600
+ continue
601
+
602
+ return all_prompts
447
603
 
448
604
  async def get_prompt(self, key: str) -> Prompt:
449
605
  prompts = await self.get_prompts()
@@ -519,11 +675,15 @@ class FastMCP(Generic[LifespanResultT]):
519
675
 
520
676
  return routes
521
677
 
522
- async def _mcp_list_tools(self) -> list[MCPTool]:
678
+ async def _list_tools_mcp(self) -> list[MCPTool]:
679
+ """
680
+ List all available tools, in the format expected by the low-level MCP
681
+ server.
682
+ """
523
683
  logger.debug(f"[{self.name}] Handler called: list_tools")
524
684
 
525
685
  async with fastmcp.server.context.Context(fastmcp=self):
526
- tools = await self._list_tools()
686
+ tools = await self._list_tools_middleware()
527
687
  return [
528
688
  tool.to_mcp_tool(
529
689
  name=tool.key,
@@ -532,24 +692,11 @@ class FastMCP(Generic[LifespanResultT]):
532
692
  for tool in tools
533
693
  ]
534
694
 
535
- async def _list_tools(self) -> list[Tool]:
695
+ async def _list_tools_middleware(self) -> list[Tool]:
536
696
  """
537
- List all available tools, in the format expected by the low-level MCP
538
- server.
697
+ List all available tools, applying MCP middleware.
539
698
  """
540
699
 
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
700
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
554
701
  # Create the middleware context.
555
702
  mw_context = MiddlewareContext(
@@ -561,13 +708,66 @@ class FastMCP(Generic[LifespanResultT]):
561
708
  )
562
709
 
563
710
  # Apply the middleware chain.
564
- return await self._apply_middleware(mw_context, _handler)
711
+ return list(
712
+ await self._apply_middleware(
713
+ context=mw_context, call_next=self._list_tools
714
+ )
715
+ )
716
+
717
+ async def _list_tools(
718
+ self,
719
+ context: MiddlewareContext[mcp.types.ListToolsRequest],
720
+ ) -> list[Tool]:
721
+ """
722
+ List all available tools.
723
+ """
724
+ # 1. Get local tools and filter them
725
+ local_tools = await self._tool_manager.get_tools()
726
+ filtered_local = [
727
+ tool for tool in local_tools.values() if self._should_enable_component(tool)
728
+ ]
729
+
730
+ # 2. Get tools from mounted servers
731
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
732
+ # Use a dict to implement "later wins" deduplication by key
733
+ all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
734
+
735
+ for mounted in self._mounted_servers:
736
+ try:
737
+ child_tools = await mounted.server._list_tools_middleware()
738
+ for tool in child_tools:
739
+ # Apply parent server's filtering to mounted components
740
+ if not self._should_enable_component(tool):
741
+ continue
742
+
743
+ key = tool.key
744
+ if mounted.prefix:
745
+ key = f"{mounted.prefix}_{tool.key}"
746
+ tool = tool.model_copy(key=key)
747
+ # Later mounted servers override earlier ones
748
+ all_tools[key] = tool
749
+ except Exception as e:
750
+ server_name = getattr(
751
+ getattr(mounted, "server", None), "name", repr(mounted)
752
+ )
753
+ logger.warning(
754
+ f"Failed to list tools from mounted server {server_name!r}: {e}"
755
+ )
756
+ if fastmcp.settings.mounted_components_raise_on_load_error:
757
+ raise
758
+ continue
759
+
760
+ return list(all_tools.values())
565
761
 
566
- async def _mcp_list_resources(self) -> list[MCPResource]:
762
+ async def _list_resources_mcp(self) -> list[MCPResource]:
763
+ """
764
+ List all available resources, in the format expected by the low-level MCP
765
+ server.
766
+ """
567
767
  logger.debug(f"[{self.name}] Handler called: list_resources")
568
768
 
569
769
  async with fastmcp.server.context.Context(fastmcp=self):
570
- resources = await self._list_resources()
770
+ resources = await self._list_resources_middleware()
571
771
  return [
572
772
  resource.to_mcp_resource(
573
773
  uri=resource.key,
@@ -576,25 +776,11 @@ class FastMCP(Generic[LifespanResultT]):
576
776
  for resource in resources
577
777
  ]
578
778
 
579
- async def _list_resources(self) -> list[Resource]:
779
+ async def _list_resources_middleware(self) -> list[Resource]:
580
780
  """
581
- List all available resources, in the format expected by the low-level MCP
582
- server.
583
-
781
+ List all available resources, applying MCP middleware.
584
782
  """
585
783
 
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
784
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
599
785
  # Create the middleware context.
600
786
  mw_context = MiddlewareContext(
@@ -606,13 +792,75 @@ class FastMCP(Generic[LifespanResultT]):
606
792
  )
607
793
 
608
794
  # Apply the middleware chain.
609
- return await self._apply_middleware(mw_context, _handler)
795
+ return list(
796
+ await self._apply_middleware(
797
+ context=mw_context, call_next=self._list_resources
798
+ )
799
+ )
610
800
 
611
- async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
801
+ async def _list_resources(
802
+ self,
803
+ context: MiddlewareContext[dict[str, Any]],
804
+ ) -> list[Resource]:
805
+ """
806
+ List all available resources.
807
+ """
808
+ # 1. Filter local resources
809
+ local_resources = await self._resource_manager.get_resources()
810
+ filtered_local = [
811
+ resource
812
+ for resource in local_resources.values()
813
+ if self._should_enable_component(resource)
814
+ ]
815
+
816
+ # 2. Get from mounted servers with resource prefix handling
817
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
818
+ # Use a dict to implement "later wins" deduplication by key
819
+ all_resources: dict[str, Resource] = {
820
+ resource.key: resource for resource in filtered_local
821
+ }
822
+
823
+ for mounted in self._mounted_servers:
824
+ try:
825
+ child_resources = await mounted.server._list_resources_middleware()
826
+ for resource in child_resources:
827
+ # Apply parent server's filtering to mounted components
828
+ if not self._should_enable_component(resource):
829
+ continue
830
+
831
+ key = resource.key
832
+ if mounted.prefix:
833
+ key = add_resource_prefix(
834
+ resource.key,
835
+ mounted.prefix,
836
+ mounted.resource_prefix_format,
837
+ )
838
+ resource = resource.model_copy(
839
+ key=key,
840
+ update={"name": f"{mounted.prefix}_{resource.name}"},
841
+ )
842
+ # Later mounted servers override earlier ones
843
+ all_resources[key] = resource
844
+ except Exception as e:
845
+ server_name = getattr(
846
+ getattr(mounted, "server", None), "name", repr(mounted)
847
+ )
848
+ logger.warning(f"Failed to list resources from {server_name!r}: {e}")
849
+ if fastmcp.settings.mounted_components_raise_on_load_error:
850
+ raise
851
+ continue
852
+
853
+ return list(all_resources.values())
854
+
855
+ async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
856
+ """
857
+ List all available resource templates, in the format expected by the low-level MCP
858
+ server.
859
+ """
612
860
  logger.debug(f"[{self.name}] Handler called: list_resource_templates")
613
861
 
614
862
  async with fastmcp.server.context.Context(fastmcp=self):
615
- templates = await self._list_resource_templates()
863
+ templates = await self._list_resource_templates_middleware()
616
864
  return [
617
865
  template.to_mcp_template(
618
866
  uriTemplate=template.key,
@@ -621,25 +869,12 @@ class FastMCP(Generic[LifespanResultT]):
621
869
  for template in templates
622
870
  ]
623
871
 
624
- async def _list_resource_templates(self) -> list[ResourceTemplate]:
872
+ async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
625
873
  """
626
- List all available resource templates, in the format expected by the low-level MCP
627
- server.
874
+ List all available resource templates, applying MCP middleware.
628
875
 
629
876
  """
630
877
 
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
878
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
644
879
  # Create the middleware context.
645
880
  mw_context = MiddlewareContext(
@@ -651,13 +886,79 @@ class FastMCP(Generic[LifespanResultT]):
651
886
  )
652
887
 
653
888
  # Apply the middleware chain.
654
- return await self._apply_middleware(mw_context, _handler)
889
+ return list(
890
+ await self._apply_middleware(
891
+ context=mw_context, call_next=self._list_resource_templates
892
+ )
893
+ )
655
894
 
656
- async def _mcp_list_prompts(self) -> list[MCPPrompt]:
895
+ async def _list_resource_templates(
896
+ self,
897
+ context: MiddlewareContext[dict[str, Any]],
898
+ ) -> list[ResourceTemplate]:
899
+ """
900
+ List all available resource templates.
901
+ """
902
+ # 1. Filter local templates
903
+ local_templates = await self._resource_manager.get_resource_templates()
904
+ filtered_local = [
905
+ template
906
+ for template in local_templates.values()
907
+ if self._should_enable_component(template)
908
+ ]
909
+
910
+ # 2. Get from mounted servers with resource prefix handling
911
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
912
+ # Use a dict to implement "later wins" deduplication by key
913
+ all_templates: dict[str, ResourceTemplate] = {
914
+ template.key: template for template in filtered_local
915
+ }
916
+
917
+ for mounted in self._mounted_servers:
918
+ try:
919
+ child_templates = (
920
+ await mounted.server._list_resource_templates_middleware()
921
+ )
922
+ for template in child_templates:
923
+ # Apply parent server's filtering to mounted components
924
+ if not self._should_enable_component(template):
925
+ continue
926
+
927
+ key = template.key
928
+ if mounted.prefix:
929
+ key = add_resource_prefix(
930
+ template.key,
931
+ mounted.prefix,
932
+ mounted.resource_prefix_format,
933
+ )
934
+ template = template.model_copy(
935
+ key=key,
936
+ update={"name": f"{mounted.prefix}_{template.name}"},
937
+ )
938
+ # Later mounted servers override earlier ones
939
+ all_templates[key] = template
940
+ except Exception as e:
941
+ server_name = getattr(
942
+ getattr(mounted, "server", None), "name", repr(mounted)
943
+ )
944
+ logger.warning(
945
+ f"Failed to list resource templates from {server_name!r}: {e}"
946
+ )
947
+ if fastmcp.settings.mounted_components_raise_on_load_error:
948
+ raise
949
+ continue
950
+
951
+ return list(all_templates.values())
952
+
953
+ async def _list_prompts_mcp(self) -> list[MCPPrompt]:
954
+ """
955
+ List all available prompts, in the format expected by the low-level MCP
956
+ server.
957
+ """
657
958
  logger.debug(f"[{self.name}] Handler called: list_prompts")
658
959
 
659
960
  async with fastmcp.server.context.Context(fastmcp=self):
660
- prompts = await self._list_prompts()
961
+ prompts = await self._list_prompts_middleware()
661
962
  return [
662
963
  prompt.to_mcp_prompt(
663
964
  name=prompt.key,
@@ -666,25 +967,12 @@ class FastMCP(Generic[LifespanResultT]):
666
967
  for prompt in prompts
667
968
  ]
668
969
 
669
- async def _list_prompts(self) -> list[Prompt]:
970
+ async def _list_prompts_middleware(self) -> list[Prompt]:
670
971
  """
671
- List all available prompts, in the format expected by the low-level MCP
672
- server.
972
+ List all available prompts, applying MCP middleware.
673
973
 
674
974
  """
675
975
 
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
976
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
689
977
  # Create the middleware context.
690
978
  mw_context = MiddlewareContext(
@@ -696,9 +984,62 @@ class FastMCP(Generic[LifespanResultT]):
696
984
  )
697
985
 
698
986
  # Apply the middleware chain.
699
- return await self._apply_middleware(mw_context, _handler)
987
+ return list(
988
+ await self._apply_middleware(
989
+ context=mw_context, call_next=self._list_prompts
990
+ )
991
+ )
992
+
993
+ async def _list_prompts(
994
+ self,
995
+ context: MiddlewareContext[mcp.types.ListPromptsRequest],
996
+ ) -> list[Prompt]:
997
+ """
998
+ List all available prompts.
999
+ """
1000
+ # 1. Filter local prompts
1001
+ local_prompts = await self._prompt_manager.get_prompts()
1002
+ filtered_local = [
1003
+ prompt
1004
+ for prompt in local_prompts.values()
1005
+ if self._should_enable_component(prompt)
1006
+ ]
1007
+
1008
+ # 2. Get from mounted servers
1009
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
1010
+ # Use a dict to implement "later wins" deduplication by key
1011
+ all_prompts: dict[str, Prompt] = {
1012
+ prompt.key: prompt for prompt in filtered_local
1013
+ }
700
1014
 
701
- async def _mcp_call_tool(
1015
+ for mounted in self._mounted_servers:
1016
+ try:
1017
+ child_prompts = await mounted.server._list_prompts_middleware()
1018
+ for prompt in child_prompts:
1019
+ # Apply parent server's filtering to mounted components
1020
+ if not self._should_enable_component(prompt):
1021
+ continue
1022
+
1023
+ key = prompt.key
1024
+ if mounted.prefix:
1025
+ key = f"{mounted.prefix}_{prompt.key}"
1026
+ prompt = prompt.model_copy(key=key)
1027
+ # Later mounted servers override earlier ones
1028
+ all_prompts[key] = prompt
1029
+ except Exception as e:
1030
+ server_name = getattr(
1031
+ getattr(mounted, "server", None), "name", repr(mounted)
1032
+ )
1033
+ logger.warning(
1034
+ f"Failed to list prompts from mounted server {server_name!r}: {e}"
1035
+ )
1036
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1037
+ raise
1038
+ continue
1039
+
1040
+ return list(all_prompts.values())
1041
+
1042
+ async def _call_tool_mcp(
702
1043
  self, key: str, arguments: dict[str, Any]
703
1044
  ) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]:
704
1045
  """
@@ -719,29 +1060,22 @@ class FastMCP(Generic[LifespanResultT]):
719
1060
 
720
1061
  async with fastmcp.server.context.Context(fastmcp=self):
721
1062
  try:
722
- result = await self._call_tool(key, arguments)
1063
+ result = await self._call_tool_middleware(key, arguments)
723
1064
  return result.to_mcp_result()
724
1065
  except DisabledError:
725
1066
  raise NotFoundError(f"Unknown tool: {key}")
726
1067
  except NotFoundError:
727
1068
  raise NotFoundError(f"Unknown tool: {key}")
728
1069
 
729
- async def _call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
1070
+ async def _call_tool_middleware(
1071
+ self,
1072
+ key: str,
1073
+ arguments: dict[str, Any],
1074
+ ) -> ToolResult:
730
1075
  """
731
1076
  Applies this server's middleware and delegates the filtered call to the manager.
732
1077
  """
733
1078
 
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
1079
  mw_context = MiddlewareContext[CallToolRequestParams](
746
1080
  message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
747
1081
  source="client",
@@ -749,9 +1083,53 @@ class FastMCP(Generic[LifespanResultT]):
749
1083
  method="tools/call",
750
1084
  fastmcp_context=fastmcp.server.dependencies.get_context(),
751
1085
  )
752
- return await self._apply_middleware(mw_context, _handler)
1086
+ return await self._apply_middleware(
1087
+ context=mw_context, call_next=self._call_tool
1088
+ )
753
1089
 
754
- async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
1090
+ async def _call_tool(
1091
+ self,
1092
+ context: MiddlewareContext[mcp.types.CallToolRequestParams],
1093
+ ) -> ToolResult:
1094
+ """
1095
+ Call a tool
1096
+ """
1097
+ tool_name = context.message.name
1098
+
1099
+ # Try mounted servers in reverse order (later wins)
1100
+ for mounted in reversed(self._mounted_servers):
1101
+ try_name = tool_name
1102
+ if mounted.prefix:
1103
+ if not tool_name.startswith(f"{mounted.prefix}_"):
1104
+ continue
1105
+ try_name = tool_name[len(mounted.prefix) + 1 :]
1106
+
1107
+ try:
1108
+ # First, get the tool to check if parent's filter allows it
1109
+ tool = await mounted.server._tool_manager.get_tool(try_name)
1110
+ if not self._should_enable_component(tool):
1111
+ # Parent filter blocks this tool, continue searching
1112
+ continue
1113
+
1114
+ return await mounted.server._call_tool_middleware(
1115
+ try_name, context.message.arguments or {}
1116
+ )
1117
+ except NotFoundError:
1118
+ continue
1119
+
1120
+ # Try local tools last (mounted servers override local)
1121
+ try:
1122
+ tool = await self._tool_manager.get_tool(tool_name)
1123
+ if self._should_enable_component(tool):
1124
+ return await self._tool_manager.call_tool(
1125
+ key=tool_name, arguments=context.message.arguments or {}
1126
+ )
1127
+ except NotFoundError:
1128
+ pass
1129
+
1130
+ raise NotFoundError(f"Unknown tool: {tool_name!r}")
1131
+
1132
+ async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
755
1133
  """
756
1134
  Handle MCP 'readResource' requests.
757
1135
 
@@ -761,7 +1139,9 @@ class FastMCP(Generic[LifespanResultT]):
761
1139
 
762
1140
  async with fastmcp.server.context.Context(fastmcp=self):
763
1141
  try:
764
- return await self._read_resource(uri)
1142
+ return list[ReadResourceContents](
1143
+ await self._read_resource_middleware(uri)
1144
+ )
765
1145
  except DisabledError:
766
1146
  # convert to NotFoundError to avoid leaking resource presence
767
1147
  raise NotFoundError(f"Unknown resource: {str(uri)!r}")
@@ -769,26 +1149,14 @@ class FastMCP(Generic[LifespanResultT]):
769
1149
  # standardize NotFound message
770
1150
  raise NotFoundError(f"Unknown resource: {str(uri)!r}")
771
1151
 
772
- async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
1152
+ async def _read_resource_middleware(
1153
+ self,
1154
+ uri: AnyUrl | str,
1155
+ ) -> list[ReadResourceContents]:
773
1156
  """
774
1157
  Applies this server's middleware and delegates the filtered call to the manager.
775
1158
  """
776
1159
 
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
1160
  # Convert string URI to AnyUrl if needed
793
1161
  if isinstance(uri, str):
794
1162
  uri_param = AnyUrl(uri)
@@ -802,9 +1170,61 @@ class FastMCP(Generic[LifespanResultT]):
802
1170
  method="resources/read",
803
1171
  fastmcp_context=fastmcp.server.dependencies.get_context(),
804
1172
  )
805
- return await self._apply_middleware(mw_context, _handler)
1173
+ return list(
1174
+ await self._apply_middleware(
1175
+ context=mw_context, call_next=self._read_resource
1176
+ )
1177
+ )
1178
+
1179
+ async def _read_resource(
1180
+ self,
1181
+ context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
1182
+ ) -> list[ReadResourceContents]:
1183
+ """
1184
+ Read a resource
1185
+ """
1186
+ uri_str = str(context.message.uri)
1187
+
1188
+ # Try mounted servers in reverse order (later wins)
1189
+ for mounted in reversed(self._mounted_servers):
1190
+ key = uri_str
1191
+ if mounted.prefix:
1192
+ if not has_resource_prefix(
1193
+ key, mounted.prefix, mounted.resource_prefix_format
1194
+ ):
1195
+ continue
1196
+ key = remove_resource_prefix(
1197
+ key, mounted.prefix, mounted.resource_prefix_format
1198
+ )
1199
+
1200
+ try:
1201
+ # First, get the resource to check if parent's filter allows it
1202
+ resource = await mounted.server._resource_manager.get_resource(key)
1203
+ if not self._should_enable_component(resource):
1204
+ # Parent filter blocks this resource, continue searching
1205
+ continue
1206
+ result = list(await mounted.server._read_resource_middleware(key))
1207
+ return result
1208
+ except NotFoundError:
1209
+ continue
1210
+
1211
+ # Try local resources last (mounted servers override local)
1212
+ try:
1213
+ resource = await self._resource_manager.get_resource(uri_str)
1214
+ if self._should_enable_component(resource):
1215
+ content = await self._resource_manager.read_resource(uri_str)
1216
+ return [
1217
+ ReadResourceContents(
1218
+ content=content,
1219
+ mime_type=resource.mime_type,
1220
+ )
1221
+ ]
1222
+ except NotFoundError:
1223
+ pass
806
1224
 
807
- async def _mcp_get_prompt(
1225
+ raise NotFoundError(f"Unknown resource: {uri_str!r}")
1226
+
1227
+ async def _get_prompt_mcp(
808
1228
  self, name: str, arguments: dict[str, Any] | None = None
809
1229
  ) -> GetPromptResult:
810
1230
  """
@@ -820,7 +1240,7 @@ class FastMCP(Generic[LifespanResultT]):
820
1240
 
821
1241
  async with fastmcp.server.context.Context(fastmcp=self):
822
1242
  try:
823
- return await self._get_prompt(name, arguments)
1243
+ return await self._get_prompt_middleware(name, arguments)
824
1244
  except DisabledError:
825
1245
  # convert to NotFoundError to avoid leaking prompt presence
826
1246
  raise NotFoundError(f"Unknown prompt: {name}")
@@ -828,24 +1248,13 @@ class FastMCP(Generic[LifespanResultT]):
828
1248
  # standardize NotFound message
829
1249
  raise NotFoundError(f"Unknown prompt: {name}")
830
1250
 
831
- async def _get_prompt(
1251
+ async def _get_prompt_middleware(
832
1252
  self, name: str, arguments: dict[str, Any] | None = None
833
1253
  ) -> GetPromptResult:
834
1254
  """
835
1255
  Applies this server's middleware and delegates the filtered call to the manager.
836
1256
  """
837
1257
 
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
1258
  mw_context = MiddlewareContext(
850
1259
  message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
851
1260
  source="client",
@@ -853,7 +1262,47 @@ class FastMCP(Generic[LifespanResultT]):
853
1262
  method="prompts/get",
854
1263
  fastmcp_context=fastmcp.server.dependencies.get_context(),
855
1264
  )
856
- return await self._apply_middleware(mw_context, _handler)
1265
+ return await self._apply_middleware(
1266
+ context=mw_context, call_next=self._get_prompt
1267
+ )
1268
+
1269
+ async def _get_prompt(
1270
+ self,
1271
+ context: MiddlewareContext[mcp.types.GetPromptRequestParams],
1272
+ ) -> GetPromptResult:
1273
+ name = context.message.name
1274
+
1275
+ # Try mounted servers in reverse order (later wins)
1276
+ for mounted in reversed(self._mounted_servers):
1277
+ try_name = name
1278
+ if mounted.prefix:
1279
+ if not name.startswith(f"{mounted.prefix}_"):
1280
+ continue
1281
+ try_name = name[len(mounted.prefix) + 1 :]
1282
+
1283
+ try:
1284
+ # First, get the prompt to check if parent's filter allows it
1285
+ prompt = await mounted.server._prompt_manager.get_prompt(try_name)
1286
+ if not self._should_enable_component(prompt):
1287
+ # Parent filter blocks this prompt, continue searching
1288
+ continue
1289
+ return await mounted.server._get_prompt_middleware(
1290
+ try_name, context.message.arguments
1291
+ )
1292
+ except NotFoundError:
1293
+ continue
1294
+
1295
+ # Try local prompts last (mounted servers override local)
1296
+ try:
1297
+ prompt = await self._prompt_manager.get_prompt(name)
1298
+ if self._should_enable_component(prompt):
1299
+ return await self._prompt_manager.render_prompt(
1300
+ name=name, arguments=context.message.arguments
1301
+ )
1302
+ except NotFoundError:
1303
+ pass
1304
+
1305
+ raise NotFoundError(f"Unknown prompt: {name!r}")
857
1306
 
858
1307
  def add_tool(self, tool: Tool) -> Tool:
859
1308
  """Add a tool to the server.
@@ -918,6 +1367,7 @@ class FastMCP(Generic[LifespanResultT]):
918
1367
  name: str | None = None,
919
1368
  title: str | None = None,
920
1369
  description: str | None = None,
1370
+ icons: list[mcp.types.Icon] | None = None,
921
1371
  tags: set[str] | None = None,
922
1372
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
923
1373
  annotations: ToolAnnotations | dict[str, Any] | None = None,
@@ -934,6 +1384,7 @@ class FastMCP(Generic[LifespanResultT]):
934
1384
  name: str | None = None,
935
1385
  title: str | None = None,
936
1386
  description: str | None = None,
1387
+ icons: list[mcp.types.Icon] | None = None,
937
1388
  tags: set[str] | None = None,
938
1389
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
939
1390
  annotations: ToolAnnotations | dict[str, Any] | None = None,
@@ -949,6 +1400,7 @@ class FastMCP(Generic[LifespanResultT]):
949
1400
  name: str | None = None,
950
1401
  title: str | None = None,
951
1402
  description: str | None = None,
1403
+ icons: list[mcp.types.Icon] | None = None,
952
1404
  tags: set[str] | None = None,
953
1405
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
954
1406
  annotations: ToolAnnotations | dict[str, Any] | None = None,
@@ -1032,6 +1484,7 @@ class FastMCP(Generic[LifespanResultT]):
1032
1484
  name=tool_name,
1033
1485
  title=title,
1034
1486
  description=description,
1487
+ icons=icons,
1035
1488
  tags=tags,
1036
1489
  output_schema=output_schema,
1037
1490
  annotations=cast(ToolAnnotations | None, annotations),
@@ -1065,6 +1518,7 @@ class FastMCP(Generic[LifespanResultT]):
1065
1518
  name=tool_name,
1066
1519
  title=title,
1067
1520
  description=description,
1521
+ icons=icons,
1068
1522
  tags=tags,
1069
1523
  output_schema=output_schema,
1070
1524
  annotations=annotations,
@@ -1162,6 +1616,7 @@ class FastMCP(Generic[LifespanResultT]):
1162
1616
  name: str | None = None,
1163
1617
  title: str | None = None,
1164
1618
  description: str | None = None,
1619
+ icons: list[mcp.types.Icon] | None = None,
1165
1620
  mime_type: str | None = None,
1166
1621
  tags: set[str] | None = None,
1167
1622
  enabled: bool | None = None,
@@ -1261,6 +1716,7 @@ class FastMCP(Generic[LifespanResultT]):
1261
1716
  name=name,
1262
1717
  title=title,
1263
1718
  description=description,
1719
+ icons=icons,
1264
1720
  mime_type=mime_type,
1265
1721
  tags=tags,
1266
1722
  enabled=enabled,
@@ -1276,6 +1732,7 @@ class FastMCP(Generic[LifespanResultT]):
1276
1732
  name=name,
1277
1733
  title=title,
1278
1734
  description=description,
1735
+ icons=icons,
1279
1736
  mime_type=mime_type,
1280
1737
  tags=tags,
1281
1738
  enabled=enabled,
@@ -1322,6 +1779,7 @@ class FastMCP(Generic[LifespanResultT]):
1322
1779
  name: str | None = None,
1323
1780
  title: str | None = None,
1324
1781
  description: str | None = None,
1782
+ icons: list[mcp.types.Icon] | None = None,
1325
1783
  tags: set[str] | None = None,
1326
1784
  enabled: bool | None = None,
1327
1785
  meta: dict[str, Any] | None = None,
@@ -1335,6 +1793,7 @@ class FastMCP(Generic[LifespanResultT]):
1335
1793
  name: str | None = None,
1336
1794
  title: str | None = None,
1337
1795
  description: str | None = None,
1796
+ icons: list[mcp.types.Icon] | None = None,
1338
1797
  tags: set[str] | None = None,
1339
1798
  enabled: bool | None = None,
1340
1799
  meta: dict[str, Any] | None = None,
@@ -1347,6 +1806,7 @@ class FastMCP(Generic[LifespanResultT]):
1347
1806
  name: str | None = None,
1348
1807
  title: str | None = None,
1349
1808
  description: str | None = None,
1809
+ icons: list[mcp.types.Icon] | None = None,
1350
1810
  tags: set[str] | None = None,
1351
1811
  enabled: bool | None = None,
1352
1812
  meta: dict[str, Any] | None = None,
@@ -1446,6 +1906,7 @@ class FastMCP(Generic[LifespanResultT]):
1446
1906
  name=prompt_name,
1447
1907
  title=title,
1448
1908
  description=description,
1909
+ icons=icons,
1449
1910
  tags=tags,
1450
1911
  enabled=enabled,
1451
1912
  meta=meta,
@@ -1476,6 +1937,7 @@ class FastMCP(Generic[LifespanResultT]):
1476
1937
  name=prompt_name,
1477
1938
  title=title,
1478
1939
  description=description,
1940
+ icons=icons,
1479
1941
  tags=tags,
1480
1942
  enabled=enabled,
1481
1943
  meta=meta,
@@ -1498,15 +1960,18 @@ class FastMCP(Generic[LifespanResultT]):
1498
1960
  )
1499
1961
 
1500
1962
  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
- )
1963
+ async with self._lifespan_manager():
1964
+ async with stdio_server() as (read_stream, write_stream):
1965
+ logger.info(
1966
+ f"Starting MCP server {self.name!r} with transport 'stdio'"
1967
+ )
1968
+ await self._mcp_server.run(
1969
+ read_stream,
1970
+ write_stream,
1971
+ self._mcp_server.create_initialization_options(
1972
+ NotificationOptions(tools_changed=True)
1973
+ ),
1974
+ )
1510
1975
 
1511
1976
  async def run_http_async(
1512
1977
  self,
@@ -1518,6 +1983,7 @@ class FastMCP(Generic[LifespanResultT]):
1518
1983
  path: str | None = None,
1519
1984
  uvicorn_config: dict[str, Any] | None = None,
1520
1985
  middleware: list[ASGIMiddleware] | None = None,
1986
+ json_response: bool | None = None,
1521
1987
  stateless_http: bool | None = None,
1522
1988
  ) -> None:
1523
1989
  """Run the server using HTTP transport.
@@ -1530,6 +1996,7 @@ class FastMCP(Generic[LifespanResultT]):
1530
1996
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1531
1997
  uvicorn_config: Additional configuration for the Uvicorn server
1532
1998
  middleware: A list of middleware to apply to the app
1999
+ json_response: Whether to use JSON response format (defaults to settings.json_response)
1533
2000
  stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
1534
2001
  """
1535
2002
  host = host or self._deprecated_settings.host
@@ -1542,6 +2009,7 @@ class FastMCP(Generic[LifespanResultT]):
1542
2009
  path=path,
1543
2010
  transport=transport,
1544
2011
  middleware=middleware,
2012
+ json_response=json_response,
1545
2013
  stateless_http=stateless_http,
1546
2014
  )
1547
2015
 
@@ -1566,6 +2034,7 @@ class FastMCP(Generic[LifespanResultT]):
1566
2034
  config_kwargs: dict[str, Any] = {
1567
2035
  "timeout_graceful_shutdown": 0,
1568
2036
  "lifespan": "on",
2037
+ "ws": "websockets-sansio",
1569
2038
  }
1570
2039
  config_kwargs.update(_uvicorn_config_from_user)
1571
2040
 
@@ -1573,14 +2042,15 @@ class FastMCP(Generic[LifespanResultT]):
1573
2042
  config_kwargs["log_level"] = default_log_level_to_use
1574
2043
 
1575
2044
  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
- )
2045
+ async with self._lifespan_manager():
2046
+ config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
2047
+ server = uvicorn.Server(config)
2048
+ path = app.state.path.lstrip("/") # type: ignore
2049
+ logger.info(
2050
+ f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
2051
+ )
1582
2052
 
1583
- await server.serve()
2053
+ await server.serve()
1584
2054
 
1585
2055
  async def run_sse_async(
1586
2056
  self,
@@ -1842,7 +2312,7 @@ class FastMCP(Generic[LifespanResultT]):
1842
2312
  # if as_proxy is not specified and the server has a custom lifespan,
1843
2313
  # we should treat it as a proxy
1844
2314
  if as_proxy is None:
1845
- as_proxy = server._has_lifespan
2315
+ as_proxy = server._lifespan != default_lifespan
1846
2316
 
1847
2317
  if as_proxy and not isinstance(server, FastMCPProxy):
1848
2318
  server = FastMCP.as_proxy(server)
@@ -1854,9 +2324,6 @@ class FastMCP(Generic[LifespanResultT]):
1854
2324
  resource_prefix_format=self.resource_prefix_format,
1855
2325
  )
1856
2326
  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
2327
 
1861
2328
  async def import_server(
1862
2329
  self,
@@ -1979,6 +2446,15 @@ class FastMCP(Generic[LifespanResultT]):
1979
2446
  prompt = prompt.model_copy(key=f"{prefix}_{key}")
1980
2447
  self._prompt_manager.add_prompt(prompt)
1981
2448
 
2449
+ if server._lifespan != default_lifespan:
2450
+ from warnings import warn
2451
+
2452
+ warn(
2453
+ message="When importing from a server with a lifespan, the lifespan from the imported server will not be used.",
2454
+ category=RuntimeWarning,
2455
+ stacklevel=2,
2456
+ )
2457
+
1982
2458
  if prefix:
1983
2459
  logger.debug(
1984
2460
  f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"