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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. fastmcp/cli/cli.py +6 -6
  2. fastmcp/cli/install/claude_code.py +3 -3
  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 +81 -171
  12. fastmcp/client/transports.py +76 -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/server/auth/auth.py +40 -32
  24. fastmcp/server/auth/jwt_issuer.py +289 -0
  25. fastmcp/server/auth/oauth_proxy.py +1238 -234
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +12 -6
  28. fastmcp/server/auth/providers/aws.py +13 -2
  29. fastmcp/server/auth/providers/azure.py +137 -124
  30. fastmcp/server/auth/providers/descope.py +4 -6
  31. fastmcp/server/auth/providers/github.py +13 -7
  32. fastmcp/server/auth/providers/google.py +13 -7
  33. fastmcp/server/auth/providers/introspection.py +281 -0
  34. fastmcp/server/auth/providers/jwt.py +8 -2
  35. fastmcp/server/auth/providers/scalekit.py +179 -0
  36. fastmcp/server/auth/providers/supabase.py +172 -0
  37. fastmcp/server/auth/providers/workos.py +16 -13
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +53 -16
  40. fastmcp/server/low_level.py +121 -2
  41. fastmcp/server/middleware/caching.py +469 -0
  42. fastmcp/server/middleware/error_handling.py +6 -2
  43. fastmcp/server/middleware/logging.py +48 -37
  44. fastmcp/server/middleware/middleware.py +28 -15
  45. fastmcp/server/middleware/rate_limiting.py +3 -3
  46. fastmcp/server/proxy.py +6 -6
  47. fastmcp/server/server.py +638 -183
  48. fastmcp/settings.py +22 -9
  49. fastmcp/tools/tool.py +7 -3
  50. fastmcp/tools/tool_manager.py +22 -108
  51. fastmcp/tools/tool_transform.py +3 -3
  52. fastmcp/utilities/cli.py +2 -2
  53. fastmcp/utilities/components.py +5 -0
  54. fastmcp/utilities/inspect.py +77 -21
  55. fastmcp/utilities/logging.py +118 -8
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  59. fastmcp/utilities/tests.py +87 -4
  60. fastmcp/utilities/types.py +1 -1
  61. fastmcp/utilities/ui.py +497 -0
  62. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
  64. fastmcp/cli/claude.py +0 -135
  65. fastmcp/utilities/storage.py +0 -204
  66. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -89,6 +89,10 @@ Transport = Literal["stdio", "http", "sse", "streamable-http"]
89
89
  # Compiled URI parsing regex to split a URI into protocol and path components
90
90
  URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
91
91
 
92
+ LifespanCallable = Callable[
93
+ ["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
94
+ ]
95
+
92
96
 
93
97
  @asynccontextmanager
94
98
  async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
@@ -98,26 +102,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
98
102
  server: The server instance this lifespan is managing
99
103
 
100
104
  Returns:
101
- An empty context object
105
+ An empty dictionary as the lifespan result.
102
106
  """
103
107
  yield {}
104
108
 
105
109
 
106
- def _lifespan_wrapper(
107
- app: FastMCP[LifespanResultT],
108
- lifespan: Callable[
109
- [FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
110
- ],
110
+ def _lifespan_proxy(
111
+ fastmcp_server: FastMCP[LifespanResultT],
111
112
  ) -> Callable[
112
113
  [LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
113
114
  ]:
114
115
  @asynccontextmanager
115
116
  async def wrap(
116
- s: LowLevelServer[LifespanResultT],
117
+ low_level_server: LowLevelServer[LifespanResultT],
117
118
  ) -> AsyncIterator[LifespanResultT]:
118
- async with AsyncExitStack() as stack:
119
- context = await stack.enter_async_context(lifespan(app))
120
- yield context
119
+ if fastmcp_server._lifespan is default_lifespan:
120
+ yield {}
121
+ return
122
+
123
+ if not fastmcp_server._lifespan_result_set:
124
+ raise RuntimeError(
125
+ "FastMCP server has a lifespan defined but no lifespan result is set, which means the server's context manager was not entered. "
126
+ + " Are you running the server in a way that supports lifespans? If so, please file an issue at https://github.com/jlowin/fastmcp/issues."
127
+ )
128
+
129
+ yield fastmcp_server._lifespan_result
121
130
 
122
131
  return wrap
123
132
 
@@ -129,15 +138,11 @@ class FastMCP(Generic[LifespanResultT]):
129
138
  instructions: str | None = None,
130
139
  *,
131
140
  version: str | None = None,
141
+ website_url: str | None = None,
142
+ icons: list[mcp.types.Icon] | None = None,
132
143
  auth: AuthProvider | None | NotSetT = NotSet,
133
144
  middleware: list[Middleware] | None = None,
134
- lifespan: (
135
- Callable[
136
- [FastMCP[LifespanResultT]],
137
- AbstractAsyncContextManager[LifespanResultT],
138
- ]
139
- | None
140
- ) = None,
145
+ lifespan: LifespanCallable | None = None,
141
146
  dependencies: list[str] | None = None,
142
147
  resource_prefix_format: Literal["protocol", "path"] | None = None,
143
148
  mask_error_details: bool | None = None,
@@ -150,6 +155,7 @@ class FastMCP(Generic[LifespanResultT]):
150
155
  on_duplicate_tools: DuplicateBehavior | None = None,
151
156
  on_duplicate_resources: DuplicateBehavior | None = None,
152
157
  on_duplicate_prompts: DuplicateBehavior | None = None,
158
+ strict_input_validation: bool | None = None,
153
159
  # ---
154
160
  # ---
155
161
  # --- The following arguments are DEPRECATED ---
@@ -188,17 +194,19 @@ class FastMCP(Generic[LifespanResultT]):
188
194
  )
189
195
  self._tool_serializer = tool_serializer
190
196
 
191
- if lifespan is None:
192
- self._has_lifespan = False
193
- lifespan = default_lifespan
194
- else:
195
- self._has_lifespan = True
197
+ self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
198
+ self._lifespan_result: LifespanResultT | None = None
199
+ self._lifespan_result_set = False
200
+
196
201
  # Generate random ID if no name provided
197
202
  self._mcp_server = LowLevelServer[LifespanResultT](
203
+ fastmcp=self,
198
204
  name=name or self.generate_name(),
199
- version=version,
205
+ version=version or fastmcp.__version__,
200
206
  instructions=instructions,
201
- lifespan=_lifespan_wrapper(self, lifespan),
207
+ website_url=website_url,
208
+ icons=icons,
209
+ lifespan=_lifespan_proxy(fastmcp_server=self),
202
210
  )
203
211
 
204
212
  # if auth is `NotSet`, try to create a provider from the environment
@@ -218,6 +226,11 @@ class FastMCP(Generic[LifespanResultT]):
218
226
 
219
227
  self.include_tags = include_tags
220
228
  self.exclude_tags = exclude_tags
229
+ self.strict_input_validation = (
230
+ strict_input_validation
231
+ if strict_input_validation is not None
232
+ else fastmcp.settings.strict_input_validation
233
+ )
221
234
 
222
235
  self.middleware = middleware or []
223
236
 
@@ -333,6 +346,38 @@ class FastMCP(Generic[LifespanResultT]):
333
346
  def version(self) -> str | None:
334
347
  return self._mcp_server.version
335
348
 
349
+ @property
350
+ def website_url(self) -> str | None:
351
+ return self._mcp_server.website_url
352
+
353
+ @property
354
+ def icons(self) -> list[mcp.types.Icon]:
355
+ if self._mcp_server.icons is None:
356
+ return []
357
+ else:
358
+ return list(self._mcp_server.icons)
359
+
360
+ @asynccontextmanager
361
+ async def _lifespan_manager(self) -> AsyncIterator[None]:
362
+ if self._lifespan_result_set:
363
+ yield
364
+ return
365
+
366
+ async with self._lifespan(self) as lifespan_result:
367
+ self._lifespan_result = lifespan_result
368
+ self._lifespan_result_set = True
369
+
370
+ async with AsyncExitStack[bool | None]() as stack:
371
+ for server in self._mounted_servers:
372
+ await stack.enter_async_context(
373
+ cm=server.server._lifespan_manager()
374
+ )
375
+
376
+ yield
377
+
378
+ self._lifespan_result_set = False
379
+ self._lifespan_result = None
380
+
336
381
  async def run_async(
337
382
  self,
338
383
  transport: Transport | None = None,
@@ -386,13 +431,15 @@ class FastMCP(Generic[LifespanResultT]):
386
431
 
387
432
  def _setup_handlers(self) -> None:
388
433
  """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)
434
+ self._mcp_server.list_tools()(self._list_tools_mcp)
435
+ self._mcp_server.list_resources()(self._list_resources_mcp)
436
+ self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
437
+ self._mcp_server.list_prompts()(self._list_prompts_mcp)
438
+ self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
439
+ self._call_tool_mcp
440
+ )
441
+ self._mcp_server.read_resource()(self._read_resource_mcp)
442
+ self._mcp_server.get_prompt()(self._get_prompt_mcp)
396
443
 
397
444
  async def _apply_middleware(
398
445
  self,
@@ -409,8 +456,24 @@ class FastMCP(Generic[LifespanResultT]):
409
456
  self.middleware.append(middleware)
410
457
 
411
458
  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()
459
+ """Get all tools (unfiltered), including mounted servers, indexed by key."""
460
+ all_tools = dict(await self._tool_manager.get_tools())
461
+
462
+ for mounted in self._mounted_servers:
463
+ try:
464
+ child_tools = await mounted.server.get_tools()
465
+ for key, tool in child_tools.items():
466
+ new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
467
+ all_tools[new_key] = tool.model_copy(key=new_key)
468
+ except Exception as e:
469
+ logger.warning(
470
+ f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
471
+ )
472
+ if fastmcp.settings.mounted_components_raise_on_load_error:
473
+ raise
474
+ continue
475
+
476
+ return all_tools
414
477
 
415
478
  async def get_tool(self, key: str) -> Tool:
416
479
  tools = await self.get_tools()
@@ -419,8 +482,37 @@ class FastMCP(Generic[LifespanResultT]):
419
482
  return tools[key]
420
483
 
421
484
  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()
485
+ """Get all resources (unfiltered), including mounted servers, indexed by key."""
486
+ all_resources = dict(await self._resource_manager.get_resources())
487
+
488
+ for mounted in self._mounted_servers:
489
+ try:
490
+ child_resources = await mounted.server.get_resources()
491
+ for key, resource in child_resources.items():
492
+ new_key = (
493
+ add_resource_prefix(
494
+ key, mounted.prefix, mounted.resource_prefix_format
495
+ )
496
+ if mounted.prefix
497
+ else key
498
+ )
499
+ update = (
500
+ {"name": f"{mounted.prefix}_{resource.name}"}
501
+ if mounted.prefix and resource.name
502
+ else {}
503
+ )
504
+ all_resources[new_key] = resource.model_copy(
505
+ key=new_key, update=update
506
+ )
507
+ except Exception as e:
508
+ logger.warning(
509
+ f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
510
+ )
511
+ if fastmcp.settings.mounted_components_raise_on_load_error:
512
+ raise
513
+ continue
514
+
515
+ return all_resources
424
516
 
425
517
  async def get_resource(self, key: str) -> Resource:
426
518
  resources = await self.get_resources()
@@ -429,8 +521,37 @@ class FastMCP(Generic[LifespanResultT]):
429
521
  return resources[key]
430
522
 
431
523
  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()
524
+ """Get all resource templates (unfiltered), including mounted servers, indexed by key."""
525
+ all_templates = dict(await self._resource_manager.get_resource_templates())
526
+
527
+ for mounted in self._mounted_servers:
528
+ try:
529
+ child_templates = await mounted.server.get_resource_templates()
530
+ for key, template in child_templates.items():
531
+ new_key = (
532
+ add_resource_prefix(
533
+ key, mounted.prefix, mounted.resource_prefix_format
534
+ )
535
+ if mounted.prefix
536
+ else key
537
+ )
538
+ update = (
539
+ {"name": f"{mounted.prefix}_{template.name}"}
540
+ if mounted.prefix and template.name
541
+ else {}
542
+ )
543
+ all_templates[new_key] = template.model_copy(
544
+ key=new_key, update=update
545
+ )
546
+ except Exception as e:
547
+ logger.warning(
548
+ f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
549
+ )
550
+ if fastmcp.settings.mounted_components_raise_on_load_error:
551
+ raise
552
+ continue
553
+
554
+ return all_templates
434
555
 
435
556
  async def get_resource_template(self, key: str) -> ResourceTemplate:
436
557
  """Get a registered resource template by key."""
@@ -440,10 +561,24 @@ class FastMCP(Generic[LifespanResultT]):
440
561
  return templates[key]
441
562
 
442
563
  async def get_prompts(self) -> dict[str, Prompt]:
443
- """
444
- List all available prompts.
445
- """
446
- return await self._prompt_manager.get_prompts()
564
+ """Get all prompts (unfiltered), including mounted servers, indexed by key."""
565
+ all_prompts = dict(await self._prompt_manager.get_prompts())
566
+
567
+ for mounted in self._mounted_servers:
568
+ try:
569
+ child_prompts = await mounted.server.get_prompts()
570
+ for key, prompt in child_prompts.items():
571
+ new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
572
+ all_prompts[new_key] = prompt.model_copy(key=new_key)
573
+ except Exception as e:
574
+ logger.warning(
575
+ f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
576
+ )
577
+ if fastmcp.settings.mounted_components_raise_on_load_error:
578
+ raise
579
+ continue
580
+
581
+ return all_prompts
447
582
 
448
583
  async def get_prompt(self, key: str) -> Prompt:
449
584
  prompts = await self.get_prompts()
@@ -519,11 +654,15 @@ class FastMCP(Generic[LifespanResultT]):
519
654
 
520
655
  return routes
521
656
 
522
- async def _mcp_list_tools(self) -> list[MCPTool]:
657
+ async def _list_tools_mcp(self) -> list[MCPTool]:
658
+ """
659
+ List all available tools, in the format expected by the low-level MCP
660
+ server.
661
+ """
523
662
  logger.debug(f"[{self.name}] Handler called: list_tools")
524
663
 
525
664
  async with fastmcp.server.context.Context(fastmcp=self):
526
- tools = await self._list_tools()
665
+ tools = await self._list_tools_middleware()
527
666
  return [
528
667
  tool.to_mcp_tool(
529
668
  name=tool.key,
@@ -532,24 +671,11 @@ class FastMCP(Generic[LifespanResultT]):
532
671
  for tool in tools
533
672
  ]
534
673
 
535
- async def _list_tools(self) -> list[Tool]:
674
+ async def _list_tools_middleware(self) -> list[Tool]:
536
675
  """
537
- List all available tools, in the format expected by the low-level MCP
538
- server.
676
+ List all available tools, applying MCP middleware.
539
677
  """
540
678
 
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
679
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
554
680
  # Create the middleware context.
555
681
  mw_context = MiddlewareContext(
@@ -561,13 +687,66 @@ class FastMCP(Generic[LifespanResultT]):
561
687
  )
562
688
 
563
689
  # Apply the middleware chain.
564
- return await self._apply_middleware(mw_context, _handler)
690
+ return list(
691
+ await self._apply_middleware(
692
+ context=mw_context, call_next=self._list_tools
693
+ )
694
+ )
695
+
696
+ async def _list_tools(
697
+ self,
698
+ context: MiddlewareContext[mcp.types.ListToolsRequest],
699
+ ) -> list[Tool]:
700
+ """
701
+ List all available tools.
702
+ """
703
+ # 1. Get local tools and filter them
704
+ local_tools = await self._tool_manager.get_tools()
705
+ filtered_local = [
706
+ tool for tool in local_tools.values() if self._should_enable_component(tool)
707
+ ]
708
+
709
+ # 2. Get tools from mounted servers
710
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
711
+ # Use a dict to implement "later wins" deduplication by key
712
+ all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
713
+
714
+ for mounted in self._mounted_servers:
715
+ try:
716
+ child_tools = await mounted.server._list_tools_middleware()
717
+ for tool in child_tools:
718
+ # Apply parent server's filtering to mounted components
719
+ if not self._should_enable_component(tool):
720
+ continue
721
+
722
+ key = tool.key
723
+ if mounted.prefix:
724
+ key = f"{mounted.prefix}_{tool.key}"
725
+ tool = tool.model_copy(key=key)
726
+ # Later mounted servers override earlier ones
727
+ all_tools[key] = tool
728
+ except Exception as e:
729
+ server_name = getattr(
730
+ getattr(mounted, "server", None), "name", repr(mounted)
731
+ )
732
+ logger.warning(
733
+ f"Failed to list tools from mounted server {server_name!r}: {e}"
734
+ )
735
+ if fastmcp.settings.mounted_components_raise_on_load_error:
736
+ raise
737
+ continue
738
+
739
+ return list(all_tools.values())
565
740
 
566
- async def _mcp_list_resources(self) -> list[MCPResource]:
741
+ async def _list_resources_mcp(self) -> list[MCPResource]:
742
+ """
743
+ List all available resources, in the format expected by the low-level MCP
744
+ server.
745
+ """
567
746
  logger.debug(f"[{self.name}] Handler called: list_resources")
568
747
 
569
748
  async with fastmcp.server.context.Context(fastmcp=self):
570
- resources = await self._list_resources()
749
+ resources = await self._list_resources_middleware()
571
750
  return [
572
751
  resource.to_mcp_resource(
573
752
  uri=resource.key,
@@ -576,25 +755,11 @@ class FastMCP(Generic[LifespanResultT]):
576
755
  for resource in resources
577
756
  ]
578
757
 
579
- async def _list_resources(self) -> list[Resource]:
758
+ async def _list_resources_middleware(self) -> list[Resource]:
580
759
  """
581
- List all available resources, in the format expected by the low-level MCP
582
- server.
583
-
760
+ List all available resources, applying MCP middleware.
584
761
  """
585
762
 
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
763
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
599
764
  # Create the middleware context.
600
765
  mw_context = MiddlewareContext(
@@ -606,13 +771,75 @@ class FastMCP(Generic[LifespanResultT]):
606
771
  )
607
772
 
608
773
  # Apply the middleware chain.
609
- return await self._apply_middleware(mw_context, _handler)
774
+ return list(
775
+ await self._apply_middleware(
776
+ context=mw_context, call_next=self._list_resources
777
+ )
778
+ )
610
779
 
611
- async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
780
+ async def _list_resources(
781
+ self,
782
+ context: MiddlewareContext[dict[str, Any]],
783
+ ) -> list[Resource]:
784
+ """
785
+ List all available resources.
786
+ """
787
+ # 1. Filter local resources
788
+ local_resources = await self._resource_manager.get_resources()
789
+ filtered_local = [
790
+ resource
791
+ for resource in local_resources.values()
792
+ if self._should_enable_component(resource)
793
+ ]
794
+
795
+ # 2. Get from mounted servers with resource prefix handling
796
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
797
+ # Use a dict to implement "later wins" deduplication by key
798
+ all_resources: dict[str, Resource] = {
799
+ resource.key: resource for resource in filtered_local
800
+ }
801
+
802
+ for mounted in self._mounted_servers:
803
+ try:
804
+ child_resources = await mounted.server._list_resources_middleware()
805
+ for resource in child_resources:
806
+ # Apply parent server's filtering to mounted components
807
+ if not self._should_enable_component(resource):
808
+ continue
809
+
810
+ key = resource.key
811
+ if mounted.prefix:
812
+ key = add_resource_prefix(
813
+ resource.key,
814
+ mounted.prefix,
815
+ mounted.resource_prefix_format,
816
+ )
817
+ resource = resource.model_copy(
818
+ key=key,
819
+ update={"name": f"{mounted.prefix}_{resource.name}"},
820
+ )
821
+ # Later mounted servers override earlier ones
822
+ all_resources[key] = resource
823
+ except Exception as e:
824
+ server_name = getattr(
825
+ getattr(mounted, "server", None), "name", repr(mounted)
826
+ )
827
+ logger.warning(f"Failed to list resources from {server_name!r}: {e}")
828
+ if fastmcp.settings.mounted_components_raise_on_load_error:
829
+ raise
830
+ continue
831
+
832
+ return list(all_resources.values())
833
+
834
+ async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
835
+ """
836
+ List all available resource templates, in the format expected by the low-level MCP
837
+ server.
838
+ """
612
839
  logger.debug(f"[{self.name}] Handler called: list_resource_templates")
613
840
 
614
841
  async with fastmcp.server.context.Context(fastmcp=self):
615
- templates = await self._list_resource_templates()
842
+ templates = await self._list_resource_templates_middleware()
616
843
  return [
617
844
  template.to_mcp_template(
618
845
  uriTemplate=template.key,
@@ -621,25 +848,12 @@ class FastMCP(Generic[LifespanResultT]):
621
848
  for template in templates
622
849
  ]
623
850
 
624
- async def _list_resource_templates(self) -> list[ResourceTemplate]:
851
+ async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
625
852
  """
626
- List all available resource templates, in the format expected by the low-level MCP
627
- server.
853
+ List all available resource templates, applying MCP middleware.
628
854
 
629
855
  """
630
856
 
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
857
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
644
858
  # Create the middleware context.
645
859
  mw_context = MiddlewareContext(
@@ -651,13 +865,79 @@ class FastMCP(Generic[LifespanResultT]):
651
865
  )
652
866
 
653
867
  # Apply the middleware chain.
654
- return await self._apply_middleware(mw_context, _handler)
868
+ return list(
869
+ await self._apply_middleware(
870
+ context=mw_context, call_next=self._list_resource_templates
871
+ )
872
+ )
873
+
874
+ async def _list_resource_templates(
875
+ self,
876
+ context: MiddlewareContext[dict[str, Any]],
877
+ ) -> list[ResourceTemplate]:
878
+ """
879
+ List all available resource templates.
880
+ """
881
+ # 1. Filter local templates
882
+ local_templates = await self._resource_manager.get_resource_templates()
883
+ filtered_local = [
884
+ template
885
+ for template in local_templates.values()
886
+ if self._should_enable_component(template)
887
+ ]
888
+
889
+ # 2. Get from mounted servers with resource prefix handling
890
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
891
+ # Use a dict to implement "later wins" deduplication by key
892
+ all_templates: dict[str, ResourceTemplate] = {
893
+ template.key: template for template in filtered_local
894
+ }
655
895
 
656
- async def _mcp_list_prompts(self) -> list[MCPPrompt]:
896
+ for mounted in self._mounted_servers:
897
+ try:
898
+ child_templates = (
899
+ await mounted.server._list_resource_templates_middleware()
900
+ )
901
+ for template in child_templates:
902
+ # Apply parent server's filtering to mounted components
903
+ if not self._should_enable_component(template):
904
+ continue
905
+
906
+ key = template.key
907
+ if mounted.prefix:
908
+ key = add_resource_prefix(
909
+ template.key,
910
+ mounted.prefix,
911
+ mounted.resource_prefix_format,
912
+ )
913
+ template = template.model_copy(
914
+ key=key,
915
+ update={"name": f"{mounted.prefix}_{template.name}"},
916
+ )
917
+ # Later mounted servers override earlier ones
918
+ all_templates[key] = template
919
+ except Exception as e:
920
+ server_name = getattr(
921
+ getattr(mounted, "server", None), "name", repr(mounted)
922
+ )
923
+ logger.warning(
924
+ f"Failed to list resource templates from {server_name!r}: {e}"
925
+ )
926
+ if fastmcp.settings.mounted_components_raise_on_load_error:
927
+ raise
928
+ continue
929
+
930
+ return list(all_templates.values())
931
+
932
+ async def _list_prompts_mcp(self) -> list[MCPPrompt]:
933
+ """
934
+ List all available prompts, in the format expected by the low-level MCP
935
+ server.
936
+ """
657
937
  logger.debug(f"[{self.name}] Handler called: list_prompts")
658
938
 
659
939
  async with fastmcp.server.context.Context(fastmcp=self):
660
- prompts = await self._list_prompts()
940
+ prompts = await self._list_prompts_middleware()
661
941
  return [
662
942
  prompt.to_mcp_prompt(
663
943
  name=prompt.key,
@@ -666,25 +946,12 @@ class FastMCP(Generic[LifespanResultT]):
666
946
  for prompt in prompts
667
947
  ]
668
948
 
669
- async def _list_prompts(self) -> list[Prompt]:
949
+ async def _list_prompts_middleware(self) -> list[Prompt]:
670
950
  """
671
- List all available prompts, in the format expected by the low-level MCP
672
- server.
951
+ List all available prompts, applying MCP middleware.
673
952
 
674
953
  """
675
954
 
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
955
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
689
956
  # Create the middleware context.
690
957
  mw_context = MiddlewareContext(
@@ -696,9 +963,62 @@ class FastMCP(Generic[LifespanResultT]):
696
963
  )
697
964
 
698
965
  # Apply the middleware chain.
699
- return await self._apply_middleware(mw_context, _handler)
966
+ return list(
967
+ await self._apply_middleware(
968
+ context=mw_context, call_next=self._list_prompts
969
+ )
970
+ )
971
+
972
+ async def _list_prompts(
973
+ self,
974
+ context: MiddlewareContext[mcp.types.ListPromptsRequest],
975
+ ) -> list[Prompt]:
976
+ """
977
+ List all available prompts.
978
+ """
979
+ # 1. Filter local prompts
980
+ local_prompts = await self._prompt_manager.get_prompts()
981
+ filtered_local = [
982
+ prompt
983
+ for prompt in local_prompts.values()
984
+ if self._should_enable_component(prompt)
985
+ ]
986
+
987
+ # 2. Get from mounted servers
988
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
989
+ # Use a dict to implement "later wins" deduplication by key
990
+ all_prompts: dict[str, Prompt] = {
991
+ prompt.key: prompt for prompt in filtered_local
992
+ }
993
+
994
+ for mounted in self._mounted_servers:
995
+ try:
996
+ child_prompts = await mounted.server._list_prompts_middleware()
997
+ for prompt in child_prompts:
998
+ # Apply parent server's filtering to mounted components
999
+ if not self._should_enable_component(prompt):
1000
+ continue
1001
+
1002
+ key = prompt.key
1003
+ if mounted.prefix:
1004
+ key = f"{mounted.prefix}_{prompt.key}"
1005
+ prompt = prompt.model_copy(key=key)
1006
+ # Later mounted servers override earlier ones
1007
+ all_prompts[key] = prompt
1008
+ except Exception as e:
1009
+ server_name = getattr(
1010
+ getattr(mounted, "server", None), "name", repr(mounted)
1011
+ )
1012
+ logger.warning(
1013
+ f"Failed to list prompts from mounted server {server_name!r}: {e}"
1014
+ )
1015
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1016
+ raise
1017
+ continue
700
1018
 
701
- async def _mcp_call_tool(
1019
+ return list(all_prompts.values())
1020
+
1021
+ async def _call_tool_mcp(
702
1022
  self, key: str, arguments: dict[str, Any]
703
1023
  ) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]:
704
1024
  """
@@ -719,29 +1039,22 @@ class FastMCP(Generic[LifespanResultT]):
719
1039
 
720
1040
  async with fastmcp.server.context.Context(fastmcp=self):
721
1041
  try:
722
- result = await self._call_tool(key, arguments)
1042
+ result = await self._call_tool_middleware(key, arguments)
723
1043
  return result.to_mcp_result()
724
1044
  except DisabledError:
725
1045
  raise NotFoundError(f"Unknown tool: {key}")
726
1046
  except NotFoundError:
727
1047
  raise NotFoundError(f"Unknown tool: {key}")
728
1048
 
729
- async def _call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
1049
+ async def _call_tool_middleware(
1050
+ self,
1051
+ key: str,
1052
+ arguments: dict[str, Any],
1053
+ ) -> ToolResult:
730
1054
  """
731
1055
  Applies this server's middleware and delegates the filtered call to the manager.
732
1056
  """
733
1057
 
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
1058
  mw_context = MiddlewareContext[CallToolRequestParams](
746
1059
  message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
747
1060
  source="client",
@@ -749,9 +1062,53 @@ class FastMCP(Generic[LifespanResultT]):
749
1062
  method="tools/call",
750
1063
  fastmcp_context=fastmcp.server.dependencies.get_context(),
751
1064
  )
752
- return await self._apply_middleware(mw_context, _handler)
1065
+ return await self._apply_middleware(
1066
+ context=mw_context, call_next=self._call_tool
1067
+ )
753
1068
 
754
- async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
1069
+ async def _call_tool(
1070
+ self,
1071
+ context: MiddlewareContext[mcp.types.CallToolRequestParams],
1072
+ ) -> ToolResult:
1073
+ """
1074
+ Call a tool
1075
+ """
1076
+ tool_name = context.message.name
1077
+
1078
+ # Try mounted servers in reverse order (later wins)
1079
+ for mounted in reversed(self._mounted_servers):
1080
+ try_name = tool_name
1081
+ if mounted.prefix:
1082
+ if not tool_name.startswith(f"{mounted.prefix}_"):
1083
+ continue
1084
+ try_name = tool_name[len(mounted.prefix) + 1 :]
1085
+
1086
+ try:
1087
+ # First, get the tool to check if parent's filter allows it
1088
+ tool = await mounted.server._tool_manager.get_tool(try_name)
1089
+ if not self._should_enable_component(tool):
1090
+ # Parent filter blocks this tool, continue searching
1091
+ continue
1092
+
1093
+ return await mounted.server._call_tool_middleware(
1094
+ try_name, context.message.arguments or {}
1095
+ )
1096
+ except NotFoundError:
1097
+ continue
1098
+
1099
+ # Try local tools last (mounted servers override local)
1100
+ try:
1101
+ tool = await self._tool_manager.get_tool(tool_name)
1102
+ if self._should_enable_component(tool):
1103
+ return await self._tool_manager.call_tool(
1104
+ key=tool_name, arguments=context.message.arguments or {}
1105
+ )
1106
+ except NotFoundError:
1107
+ pass
1108
+
1109
+ raise NotFoundError(f"Unknown tool: {tool_name!r}")
1110
+
1111
+ async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
755
1112
  """
756
1113
  Handle MCP 'readResource' requests.
757
1114
 
@@ -761,7 +1118,9 @@ class FastMCP(Generic[LifespanResultT]):
761
1118
 
762
1119
  async with fastmcp.server.context.Context(fastmcp=self):
763
1120
  try:
764
- return await self._read_resource(uri)
1121
+ return list[ReadResourceContents](
1122
+ await self._read_resource_middleware(uri)
1123
+ )
765
1124
  except DisabledError:
766
1125
  # convert to NotFoundError to avoid leaking resource presence
767
1126
  raise NotFoundError(f"Unknown resource: {str(uri)!r}")
@@ -769,26 +1128,14 @@ class FastMCP(Generic[LifespanResultT]):
769
1128
  # standardize NotFound message
770
1129
  raise NotFoundError(f"Unknown resource: {str(uri)!r}")
771
1130
 
772
- async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
1131
+ async def _read_resource_middleware(
1132
+ self,
1133
+ uri: AnyUrl | str,
1134
+ ) -> list[ReadResourceContents]:
773
1135
  """
774
1136
  Applies this server's middleware and delegates the filtered call to the manager.
775
1137
  """
776
1138
 
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
1139
  # Convert string URI to AnyUrl if needed
793
1140
  if isinstance(uri, str):
794
1141
  uri_param = AnyUrl(uri)
@@ -802,9 +1149,61 @@ class FastMCP(Generic[LifespanResultT]):
802
1149
  method="resources/read",
803
1150
  fastmcp_context=fastmcp.server.dependencies.get_context(),
804
1151
  )
805
- return await self._apply_middleware(mw_context, _handler)
1152
+ return list(
1153
+ await self._apply_middleware(
1154
+ context=mw_context, call_next=self._read_resource
1155
+ )
1156
+ )
1157
+
1158
+ async def _read_resource(
1159
+ self,
1160
+ context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
1161
+ ) -> list[ReadResourceContents]:
1162
+ """
1163
+ Read a resource
1164
+ """
1165
+ uri_str = str(context.message.uri)
1166
+
1167
+ # Try mounted servers in reverse order (later wins)
1168
+ for mounted in reversed(self._mounted_servers):
1169
+ key = uri_str
1170
+ if mounted.prefix:
1171
+ if not has_resource_prefix(
1172
+ key, mounted.prefix, mounted.resource_prefix_format
1173
+ ):
1174
+ continue
1175
+ key = remove_resource_prefix(
1176
+ key, mounted.prefix, mounted.resource_prefix_format
1177
+ )
1178
+
1179
+ try:
1180
+ # First, get the resource to check if parent's filter allows it
1181
+ resource = await mounted.server._resource_manager.get_resource(key)
1182
+ if not self._should_enable_component(resource):
1183
+ # Parent filter blocks this resource, continue searching
1184
+ continue
1185
+ result = list(await mounted.server._read_resource_middleware(key))
1186
+ return result
1187
+ except NotFoundError:
1188
+ continue
806
1189
 
807
- async def _mcp_get_prompt(
1190
+ # Try local resources last (mounted servers override local)
1191
+ try:
1192
+ resource = await self._resource_manager.get_resource(uri_str)
1193
+ if self._should_enable_component(resource):
1194
+ content = await self._resource_manager.read_resource(uri_str)
1195
+ return [
1196
+ ReadResourceContents(
1197
+ content=content,
1198
+ mime_type=resource.mime_type,
1199
+ )
1200
+ ]
1201
+ except NotFoundError:
1202
+ pass
1203
+
1204
+ raise NotFoundError(f"Unknown resource: {uri_str!r}")
1205
+
1206
+ async def _get_prompt_mcp(
808
1207
  self, name: str, arguments: dict[str, Any] | None = None
809
1208
  ) -> GetPromptResult:
810
1209
  """
@@ -820,7 +1219,7 @@ class FastMCP(Generic[LifespanResultT]):
820
1219
 
821
1220
  async with fastmcp.server.context.Context(fastmcp=self):
822
1221
  try:
823
- return await self._get_prompt(name, arguments)
1222
+ return await self._get_prompt_middleware(name, arguments)
824
1223
  except DisabledError:
825
1224
  # convert to NotFoundError to avoid leaking prompt presence
826
1225
  raise NotFoundError(f"Unknown prompt: {name}")
@@ -828,24 +1227,13 @@ class FastMCP(Generic[LifespanResultT]):
828
1227
  # standardize NotFound message
829
1228
  raise NotFoundError(f"Unknown prompt: {name}")
830
1229
 
831
- async def _get_prompt(
1230
+ async def _get_prompt_middleware(
832
1231
  self, name: str, arguments: dict[str, Any] | None = None
833
1232
  ) -> GetPromptResult:
834
1233
  """
835
1234
  Applies this server's middleware and delegates the filtered call to the manager.
836
1235
  """
837
1236
 
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
1237
  mw_context = MiddlewareContext(
850
1238
  message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
851
1239
  source="client",
@@ -853,7 +1241,47 @@ class FastMCP(Generic[LifespanResultT]):
853
1241
  method="prompts/get",
854
1242
  fastmcp_context=fastmcp.server.dependencies.get_context(),
855
1243
  )
856
- return await self._apply_middleware(mw_context, _handler)
1244
+ return await self._apply_middleware(
1245
+ context=mw_context, call_next=self._get_prompt
1246
+ )
1247
+
1248
+ async def _get_prompt(
1249
+ self,
1250
+ context: MiddlewareContext[mcp.types.GetPromptRequestParams],
1251
+ ) -> GetPromptResult:
1252
+ name = context.message.name
1253
+
1254
+ # Try mounted servers in reverse order (later wins)
1255
+ for mounted in reversed(self._mounted_servers):
1256
+ try_name = name
1257
+ if mounted.prefix:
1258
+ if not name.startswith(f"{mounted.prefix}_"):
1259
+ continue
1260
+ try_name = name[len(mounted.prefix) + 1 :]
1261
+
1262
+ try:
1263
+ # First, get the prompt to check if parent's filter allows it
1264
+ prompt = await mounted.server._prompt_manager.get_prompt(try_name)
1265
+ if not self._should_enable_component(prompt):
1266
+ # Parent filter blocks this prompt, continue searching
1267
+ continue
1268
+ return await mounted.server._get_prompt_middleware(
1269
+ try_name, context.message.arguments
1270
+ )
1271
+ except NotFoundError:
1272
+ continue
1273
+
1274
+ # Try local prompts last (mounted servers override local)
1275
+ try:
1276
+ prompt = await self._prompt_manager.get_prompt(name)
1277
+ if self._should_enable_component(prompt):
1278
+ return await self._prompt_manager.render_prompt(
1279
+ name=name, arguments=context.message.arguments
1280
+ )
1281
+ except NotFoundError:
1282
+ pass
1283
+
1284
+ raise NotFoundError(f"Unknown prompt: {name!r}")
857
1285
 
858
1286
  def add_tool(self, tool: Tool) -> Tool:
859
1287
  """Add a tool to the server.
@@ -918,6 +1346,7 @@ class FastMCP(Generic[LifespanResultT]):
918
1346
  name: str | None = None,
919
1347
  title: str | None = None,
920
1348
  description: str | None = None,
1349
+ icons: list[mcp.types.Icon] | None = None,
921
1350
  tags: set[str] | None = None,
922
1351
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
923
1352
  annotations: ToolAnnotations | dict[str, Any] | None = None,
@@ -934,6 +1363,7 @@ class FastMCP(Generic[LifespanResultT]):
934
1363
  name: str | None = None,
935
1364
  title: str | None = None,
936
1365
  description: str | None = None,
1366
+ icons: list[mcp.types.Icon] | None = None,
937
1367
  tags: set[str] | None = None,
938
1368
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
939
1369
  annotations: ToolAnnotations | dict[str, Any] | None = None,
@@ -949,6 +1379,7 @@ class FastMCP(Generic[LifespanResultT]):
949
1379
  name: str | None = None,
950
1380
  title: str | None = None,
951
1381
  description: str | None = None,
1382
+ icons: list[mcp.types.Icon] | None = None,
952
1383
  tags: set[str] | None = None,
953
1384
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
954
1385
  annotations: ToolAnnotations | dict[str, Any] | None = None,
@@ -1032,6 +1463,7 @@ class FastMCP(Generic[LifespanResultT]):
1032
1463
  name=tool_name,
1033
1464
  title=title,
1034
1465
  description=description,
1466
+ icons=icons,
1035
1467
  tags=tags,
1036
1468
  output_schema=output_schema,
1037
1469
  annotations=cast(ToolAnnotations | None, annotations),
@@ -1065,6 +1497,7 @@ class FastMCP(Generic[LifespanResultT]):
1065
1497
  name=tool_name,
1066
1498
  title=title,
1067
1499
  description=description,
1500
+ icons=icons,
1068
1501
  tags=tags,
1069
1502
  output_schema=output_schema,
1070
1503
  annotations=annotations,
@@ -1162,6 +1595,7 @@ class FastMCP(Generic[LifespanResultT]):
1162
1595
  name: str | None = None,
1163
1596
  title: str | None = None,
1164
1597
  description: str | None = None,
1598
+ icons: list[mcp.types.Icon] | None = None,
1165
1599
  mime_type: str | None = None,
1166
1600
  tags: set[str] | None = None,
1167
1601
  enabled: bool | None = None,
@@ -1261,6 +1695,7 @@ class FastMCP(Generic[LifespanResultT]):
1261
1695
  name=name,
1262
1696
  title=title,
1263
1697
  description=description,
1698
+ icons=icons,
1264
1699
  mime_type=mime_type,
1265
1700
  tags=tags,
1266
1701
  enabled=enabled,
@@ -1276,6 +1711,7 @@ class FastMCP(Generic[LifespanResultT]):
1276
1711
  name=name,
1277
1712
  title=title,
1278
1713
  description=description,
1714
+ icons=icons,
1279
1715
  mime_type=mime_type,
1280
1716
  tags=tags,
1281
1717
  enabled=enabled,
@@ -1322,6 +1758,7 @@ class FastMCP(Generic[LifespanResultT]):
1322
1758
  name: str | None = None,
1323
1759
  title: str | None = None,
1324
1760
  description: str | None = None,
1761
+ icons: list[mcp.types.Icon] | None = None,
1325
1762
  tags: set[str] | None = None,
1326
1763
  enabled: bool | None = None,
1327
1764
  meta: dict[str, Any] | None = None,
@@ -1335,6 +1772,7 @@ class FastMCP(Generic[LifespanResultT]):
1335
1772
  name: str | None = None,
1336
1773
  title: str | None = None,
1337
1774
  description: str | None = None,
1775
+ icons: list[mcp.types.Icon] | None = None,
1338
1776
  tags: set[str] | None = None,
1339
1777
  enabled: bool | None = None,
1340
1778
  meta: dict[str, Any] | None = None,
@@ -1347,6 +1785,7 @@ class FastMCP(Generic[LifespanResultT]):
1347
1785
  name: str | None = None,
1348
1786
  title: str | None = None,
1349
1787
  description: str | None = None,
1788
+ icons: list[mcp.types.Icon] | None = None,
1350
1789
  tags: set[str] | None = None,
1351
1790
  enabled: bool | None = None,
1352
1791
  meta: dict[str, Any] | None = None,
@@ -1446,6 +1885,7 @@ class FastMCP(Generic[LifespanResultT]):
1446
1885
  name=prompt_name,
1447
1886
  title=title,
1448
1887
  description=description,
1888
+ icons=icons,
1449
1889
  tags=tags,
1450
1890
  enabled=enabled,
1451
1891
  meta=meta,
@@ -1476,6 +1916,7 @@ class FastMCP(Generic[LifespanResultT]):
1476
1916
  name=prompt_name,
1477
1917
  title=title,
1478
1918
  description=description,
1919
+ icons=icons,
1479
1920
  tags=tags,
1480
1921
  enabled=enabled,
1481
1922
  meta=meta,
@@ -1498,15 +1939,18 @@ class FastMCP(Generic[LifespanResultT]):
1498
1939
  )
1499
1940
 
1500
1941
  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
- )
1942
+ async with self._lifespan_manager():
1943
+ async with stdio_server() as (read_stream, write_stream):
1944
+ logger.info(
1945
+ f"Starting MCP server {self.name!r} with transport 'stdio'"
1946
+ )
1947
+ await self._mcp_server.run(
1948
+ read_stream,
1949
+ write_stream,
1950
+ self._mcp_server.create_initialization_options(
1951
+ NotificationOptions(tools_changed=True)
1952
+ ),
1953
+ )
1510
1954
 
1511
1955
  async def run_http_async(
1512
1956
  self,
@@ -1518,6 +1962,7 @@ class FastMCP(Generic[LifespanResultT]):
1518
1962
  path: str | None = None,
1519
1963
  uvicorn_config: dict[str, Any] | None = None,
1520
1964
  middleware: list[ASGIMiddleware] | None = None,
1965
+ json_response: bool | None = None,
1521
1966
  stateless_http: bool | None = None,
1522
1967
  ) -> None:
1523
1968
  """Run the server using HTTP transport.
@@ -1530,6 +1975,7 @@ class FastMCP(Generic[LifespanResultT]):
1530
1975
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1531
1976
  uvicorn_config: Additional configuration for the Uvicorn server
1532
1977
  middleware: A list of middleware to apply to the app
1978
+ json_response: Whether to use JSON response format (defaults to settings.json_response)
1533
1979
  stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
1534
1980
  """
1535
1981
  host = host or self._deprecated_settings.host
@@ -1542,6 +1988,7 @@ class FastMCP(Generic[LifespanResultT]):
1542
1988
  path=path,
1543
1989
  transport=transport,
1544
1990
  middleware=middleware,
1991
+ json_response=json_response,
1545
1992
  stateless_http=stateless_http,
1546
1993
  )
1547
1994
 
@@ -1566,6 +2013,7 @@ class FastMCP(Generic[LifespanResultT]):
1566
2013
  config_kwargs: dict[str, Any] = {
1567
2014
  "timeout_graceful_shutdown": 0,
1568
2015
  "lifespan": "on",
2016
+ "ws": "websockets-sansio",
1569
2017
  }
1570
2018
  config_kwargs.update(_uvicorn_config_from_user)
1571
2019
 
@@ -1573,14 +2021,15 @@ class FastMCP(Generic[LifespanResultT]):
1573
2021
  config_kwargs["log_level"] = default_log_level_to_use
1574
2022
 
1575
2023
  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
- )
2024
+ async with self._lifespan_manager():
2025
+ config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
2026
+ server = uvicorn.Server(config)
2027
+ path = app.state.path.lstrip("/") # type: ignore
2028
+ logger.info(
2029
+ f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
2030
+ )
1582
2031
 
1583
- await server.serve()
2032
+ await server.serve()
1584
2033
 
1585
2034
  async def run_sse_async(
1586
2035
  self,
@@ -1842,7 +2291,7 @@ class FastMCP(Generic[LifespanResultT]):
1842
2291
  # if as_proxy is not specified and the server has a custom lifespan,
1843
2292
  # we should treat it as a proxy
1844
2293
  if as_proxy is None:
1845
- as_proxy = server._has_lifespan
2294
+ as_proxy = server._lifespan != default_lifespan
1846
2295
 
1847
2296
  if as_proxy and not isinstance(server, FastMCPProxy):
1848
2297
  server = FastMCP.as_proxy(server)
@@ -1854,9 +2303,6 @@ class FastMCP(Generic[LifespanResultT]):
1854
2303
  resource_prefix_format=self.resource_prefix_format,
1855
2304
  )
1856
2305
  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
2306
 
1861
2307
  async def import_server(
1862
2308
  self,
@@ -1979,6 +2425,15 @@ class FastMCP(Generic[LifespanResultT]):
1979
2425
  prompt = prompt.model_copy(key=f"{prefix}_{key}")
1980
2426
  self._prompt_manager.add_prompt(prompt)
1981
2427
 
2428
+ if server._lifespan != default_lifespan:
2429
+ from warnings import warn
2430
+
2431
+ warn(
2432
+ message="When importing from a server with a lifespan, the lifespan from the imported server will not be used.",
2433
+ category=RuntimeWarning,
2434
+ stacklevel=2,
2435
+ )
2436
+
1982
2437
  if prefix:
1983
2438
  logger.debug(
1984
2439
  f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"