fastmcp 2.8.1__py3-none-any.whl → 2.9.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 (38) hide show
  1. fastmcp/cli/cli.py +99 -1
  2. fastmcp/cli/run.py +1 -3
  3. fastmcp/client/auth/oauth.py +1 -2
  4. fastmcp/client/client.py +21 -5
  5. fastmcp/client/transports.py +17 -2
  6. fastmcp/contrib/mcp_mixin/README.md +79 -2
  7. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -0
  8. fastmcp/prompts/prompt.py +91 -11
  9. fastmcp/prompts/prompt_manager.py +119 -43
  10. fastmcp/resources/resource.py +11 -1
  11. fastmcp/resources/resource_manager.py +249 -76
  12. fastmcp/resources/template.py +27 -1
  13. fastmcp/server/auth/providers/bearer.py +32 -10
  14. fastmcp/server/context.py +41 -2
  15. fastmcp/server/http.py +8 -0
  16. fastmcp/server/middleware/__init__.py +6 -0
  17. fastmcp/server/middleware/error_handling.py +206 -0
  18. fastmcp/server/middleware/logging.py +165 -0
  19. fastmcp/server/middleware/middleware.py +236 -0
  20. fastmcp/server/middleware/rate_limiting.py +231 -0
  21. fastmcp/server/middleware/timing.py +156 -0
  22. fastmcp/server/proxy.py +250 -140
  23. fastmcp/server/server.py +320 -242
  24. fastmcp/settings.py +2 -2
  25. fastmcp/tools/tool.py +6 -2
  26. fastmcp/tools/tool_manager.py +114 -45
  27. fastmcp/utilities/components.py +22 -2
  28. fastmcp/utilities/inspect.py +326 -0
  29. fastmcp/utilities/json_schema.py +67 -23
  30. fastmcp/utilities/mcp_config.py +13 -7
  31. fastmcp/utilities/openapi.py +5 -3
  32. fastmcp/utilities/tests.py +1 -1
  33. fastmcp/utilities/types.py +90 -1
  34. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/METADATA +2 -2
  35. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/RECORD +38 -31
  36. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/WHEEL +0 -0
  37. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/entry_points.txt +0 -0
  38. {fastmcp-2.8.1.dist-info → fastmcp-2.9.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -12,12 +12,14 @@ from contextlib import (
12
12
  AsyncExitStack,
13
13
  asynccontextmanager,
14
14
  )
15
+ from dataclasses import dataclass
15
16
  from functools import partial
16
17
  from pathlib import Path
17
- from typing import TYPE_CHECKING, Any, Generic, Literal, overload
18
+ from typing import TYPE_CHECKING, Any, Generic, Literal, cast, overload
18
19
 
19
20
  import anyio
20
21
  import httpx
22
+ import mcp.types
21
23
  import uvicorn
22
24
  from mcp.server.lowlevel.helper_types import ReadResourceContents
23
25
  from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
@@ -33,7 +35,7 @@ from mcp.types import Resource as MCPResource
33
35
  from mcp.types import ResourceTemplate as MCPResourceTemplate
34
36
  from mcp.types import Tool as MCPTool
35
37
  from pydantic import AnyUrl
36
- from starlette.middleware import Middleware
38
+ from starlette.middleware import Middleware as ASGIMiddleware
37
39
  from starlette.requests import Request
38
40
  from starlette.responses import Response
39
41
  from starlette.routing import BaseRoute, Route
@@ -52,6 +54,7 @@ from fastmcp.server.http import (
52
54
  create_sse_app,
53
55
  create_streamable_http_app,
54
56
  )
57
+ from fastmcp.server.middleware import Middleware, MiddlewareContext
55
58
  from fastmcp.settings import Settings
56
59
  from fastmcp.tools import ToolManager
57
60
  from fastmcp.tools.tool import FunctionTool, Tool
@@ -71,6 +74,7 @@ if TYPE_CHECKING:
71
74
  logger = get_logger(__name__)
72
75
 
73
76
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
77
+ Transport = Literal["stdio", "http", "sse", "streamable-http"]
74
78
 
75
79
  # Compiled URI parsing regex to split a URI into protocol and path components
76
80
  URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
@@ -111,7 +115,10 @@ class FastMCP(Generic[LifespanResultT]):
111
115
  self,
112
116
  name: str | None = None,
113
117
  instructions: str | None = None,
118
+ *,
119
+ version: str | None = None,
114
120
  auth: OAuthProvider | None = None,
121
+ middleware: list[Middleware] | None = None,
115
122
  lifespan: (
116
123
  Callable[
117
124
  [FastMCP[LifespanResultT]],
@@ -152,7 +159,6 @@ class FastMCP(Generic[LifespanResultT]):
152
159
  self._cache = TimedCache(
153
160
  expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
154
161
  )
155
- self._mounted_servers: dict[str, MountedServer] = {}
156
162
  self._additional_http_routes: list[BaseRoute] = []
157
163
  self._tool_manager = ToolManager(
158
164
  duplicate_behavior=on_duplicate_tools,
@@ -175,6 +181,7 @@ class FastMCP(Generic[LifespanResultT]):
175
181
  self._has_lifespan = True
176
182
  self._mcp_server = MCPServer[LifespanResultT](
177
183
  name=name or "FastMCP",
184
+ version=version,
178
185
  instructions=instructions,
179
186
  lifespan=_lifespan_wrapper(self, lifespan),
180
187
  )
@@ -192,6 +199,8 @@ class FastMCP(Generic[LifespanResultT]):
192
199
  self.include_tags = include_tags
193
200
  self.exclude_tags = exclude_tags
194
201
 
202
+ self.middleware = middleware or []
203
+
195
204
  # Set up MCP protocol handlers
196
205
  self._setup_handlers()
197
206
  self.dependencies = dependencies or fastmcp.settings.server_dependencies
@@ -272,7 +281,7 @@ class FastMCP(Generic[LifespanResultT]):
272
281
 
273
282
  async def run_async(
274
283
  self,
275
- transport: Literal["stdio", "streamable-http", "sse"] | None = None,
284
+ transport: Transport | None = None,
276
285
  **transport_kwargs: Any,
277
286
  ) -> None:
278
287
  """Run the FastMCP server asynchronously.
@@ -282,19 +291,19 @@ class FastMCP(Generic[LifespanResultT]):
282
291
  """
283
292
  if transport is None:
284
293
  transport = "stdio"
285
- if transport not in {"stdio", "streamable-http", "sse"}:
294
+ if transport not in {"stdio", "http", "sse", "streamable-http"}:
286
295
  raise ValueError(f"Unknown transport: {transport}")
287
296
 
288
297
  if transport == "stdio":
289
298
  await self.run_stdio_async(**transport_kwargs)
290
- elif transport in {"streamable-http", "sse"}:
299
+ elif transport in {"http", "sse", "streamable-http"}:
291
300
  await self.run_http_async(transport=transport, **transport_kwargs)
292
301
  else:
293
302
  raise ValueError(f"Unknown transport: {transport}")
294
303
 
295
304
  def run(
296
305
  self,
297
- transport: Literal["stdio", "streamable-http", "sse"] | None = None,
306
+ transport: Transport | None = None,
298
307
  **transport_kwargs: Any,
299
308
  ) -> None:
300
309
  """Run the FastMCP server. Note this is a synchronous function.
@@ -315,22 +324,23 @@ class FastMCP(Generic[LifespanResultT]):
315
324
  self._mcp_server.read_resource()(self._mcp_read_resource)
316
325
  self._mcp_server.get_prompt()(self._mcp_get_prompt)
317
326
 
327
+ async def _apply_middleware(
328
+ self,
329
+ context: MiddlewareContext[Any],
330
+ call_next: Callable[[MiddlewareContext[Any]], Awaitable[Any]],
331
+ ) -> Any:
332
+ """Builds and executes the middleware chain."""
333
+ chain = call_next
334
+ for mw in reversed(self.middleware):
335
+ chain = partial(mw, call_next=chain)
336
+ return await chain(context)
337
+
338
+ def add_middleware(self, middleware: Middleware) -> None:
339
+ self.middleware.append(middleware)
340
+
318
341
  async def get_tools(self) -> dict[str, Tool]:
319
342
  """Get all registered tools, indexed by registered key."""
320
- if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
321
- tools: dict[str, Tool] = {}
322
- for prefix, server in self._mounted_servers.items():
323
- try:
324
- server_tools = await server.get_tools()
325
- tools.update(server_tools)
326
- except Exception as e:
327
- logger.warning(
328
- f"Failed to get tools from mounted server '{prefix}': {e}"
329
- )
330
- continue
331
- tools.update(self._tool_manager.get_tools())
332
- self._cache.set("tools", tools)
333
- return tools
343
+ return await self._tool_manager.get_tools()
334
344
 
335
345
  async def get_tool(self, key: str) -> Tool:
336
346
  tools = await self.get_tools()
@@ -340,20 +350,7 @@ class FastMCP(Generic[LifespanResultT]):
340
350
 
341
351
  async def get_resources(self) -> dict[str, Resource]:
342
352
  """Get all registered resources, indexed by registered key."""
343
- if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
344
- resources: dict[str, Resource] = {}
345
- for prefix, server in self._mounted_servers.items():
346
- try:
347
- server_resources = await server.get_resources()
348
- resources.update(server_resources)
349
- except Exception as e:
350
- logger.warning(
351
- f"Failed to get resources from mounted server '{prefix}': {e}"
352
- )
353
- continue
354
- resources.update(self._resource_manager.get_resources())
355
- self._cache.set("resources", resources)
356
- return resources
353
+ return await self._resource_manager.get_resources()
357
354
 
358
355
  async def get_resource(self, key: str) -> Resource:
359
356
  resources = await self.get_resources()
@@ -363,23 +360,7 @@ class FastMCP(Generic[LifespanResultT]):
363
360
 
364
361
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
365
362
  """Get all registered resource templates, indexed by registered key."""
366
- if (
367
- templates := self._cache.get("resource_templates")
368
- ) is self._cache.NOT_FOUND:
369
- templates: dict[str, ResourceTemplate] = {}
370
- for prefix, server in self._mounted_servers.items():
371
- try:
372
- server_templates = await server.get_resource_templates()
373
- templates.update(server_templates)
374
- except Exception as e:
375
- logger.warning(
376
- "Failed to get resource templates from mounted server "
377
- f"'{prefix}': {e}"
378
- )
379
- continue
380
- templates.update(self._resource_manager.get_templates())
381
- self._cache.set("resource_templates", templates)
382
- return templates
363
+ return await self._resource_manager.get_resource_templates()
383
364
 
384
365
  async def get_resource_template(self, key: str) -> ResourceTemplate:
385
366
  templates = await self.get_resource_templates()
@@ -391,20 +372,7 @@ class FastMCP(Generic[LifespanResultT]):
391
372
  """
392
373
  List all available prompts.
393
374
  """
394
- if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
395
- prompts: dict[str, Prompt] = {}
396
- for prefix, server in self._mounted_servers.items():
397
- try:
398
- server_prompts = await server.get_prompts()
399
- prompts.update(server_prompts)
400
- except Exception as e:
401
- logger.warning(
402
- f"Failed to get prompts from mounted server '{prefix}': {e}"
403
- )
404
- continue
405
- prompts.update(self._prompt_manager.get_prompts())
406
- self._cache.set("prompts", prompts)
407
- return prompts
375
+ return await self._prompt_manager.get_prompts()
408
376
 
409
377
  async def get_prompt(self, key: str) -> Prompt:
410
378
  prompts = await self.get_prompts()
@@ -457,58 +425,165 @@ class FastMCP(Generic[LifespanResultT]):
457
425
  return decorator
458
426
 
459
427
  async def _mcp_list_tools(self) -> list[MCPTool]:
428
+ logger.debug("Handler called: list_tools")
429
+
430
+ with fastmcp.server.context.Context(fastmcp=self):
431
+ tools = await self._list_tools()
432
+ return [tool.to_mcp_tool(name=tool.key) for tool in tools]
433
+
434
+ async def _list_tools(self) -> list[Tool]:
460
435
  """
461
436
  List all available tools, in the format expected by the low-level MCP
462
437
  server.
463
438
 
464
439
  """
465
- tools = await self.get_tools()
466
440
 
467
- mcp_tools: list[MCPTool] = []
468
- for key, tool in tools.items():
469
- if self._should_enable_component(tool):
470
- mcp_tools.append(tool.to_mcp_tool(name=key))
441
+ async def _handler(
442
+ context: MiddlewareContext[mcp.types.ListToolsRequest],
443
+ ) -> list[Tool]:
444
+ tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
445
+
446
+ mcp_tools: list[Tool] = []
447
+ for tool in tools:
448
+ if self._should_enable_component(tool):
449
+ mcp_tools.append(tool)
450
+
451
+ return mcp_tools
452
+
453
+ with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
454
+ # Create the middleware context.
455
+ mw_context = MiddlewareContext(
456
+ message=mcp.types.ListToolsRequest(method="tools/list"),
457
+ source="client",
458
+ type="request",
459
+ method="tools/list",
460
+ fastmcp_context=fastmcp_ctx,
461
+ )
471
462
 
472
- return mcp_tools
463
+ # Apply the middleware chain.
464
+ return await self._apply_middleware(mw_context, _handler)
473
465
 
474
466
  async def _mcp_list_resources(self) -> list[MCPResource]:
467
+ logger.debug("Handler called: list_resources")
468
+
469
+ with fastmcp.server.context.Context(fastmcp=self):
470
+ resources = await self._list_resources()
471
+ return [
472
+ resource.to_mcp_resource(uri=resource.key) for resource in resources
473
+ ]
474
+
475
+ async def _list_resources(self) -> list[Resource]:
475
476
  """
476
477
  List all available resources, in the format expected by the low-level MCP
477
478
  server.
478
479
 
479
480
  """
480
- resources = await self.get_resources()
481
- mcp_resources: list[MCPResource] = []
482
- for key, resource in resources.items():
483
- if self._should_enable_component(resource):
484
- mcp_resources.append(resource.to_mcp_resource(uri=key))
485
- return mcp_resources
481
+
482
+ async def _handler(
483
+ context: MiddlewareContext[dict[str, Any]],
484
+ ) -> list[Resource]:
485
+ resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
486
+
487
+ mcp_resources: list[Resource] = []
488
+ for resource in resources:
489
+ if self._should_enable_component(resource):
490
+ mcp_resources.append(resource)
491
+
492
+ return mcp_resources
493
+
494
+ with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
495
+ # Create the middleware context.
496
+ mw_context = MiddlewareContext(
497
+ message={}, # List resources doesn't have parameters
498
+ source="client",
499
+ type="request",
500
+ method="resources/list",
501
+ fastmcp_context=fastmcp_ctx,
502
+ )
503
+
504
+ # Apply the middleware chain.
505
+ return await self._apply_middleware(mw_context, _handler)
486
506
 
487
507
  async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
508
+ logger.debug("Handler called: list_resource_templates")
509
+
510
+ with fastmcp.server.context.Context(fastmcp=self):
511
+ templates = await self._list_resource_templates()
512
+ return [
513
+ template.to_mcp_template(uriTemplate=template.key)
514
+ for template in templates
515
+ ]
516
+
517
+ async def _list_resource_templates(self) -> list[ResourceTemplate]:
488
518
  """
489
- List all available resource templates, in the format expected by the low-level
490
- MCP server.
519
+ List all available resource templates, in the format expected by the low-level MCP
520
+ server.
491
521
 
492
522
  """
493
- templates = await self.get_resource_templates()
494
- mcp_templates: list[MCPResourceTemplate] = []
495
- for key, template in templates.items():
496
- if self._should_enable_component(template):
497
- mcp_templates.append(template.to_mcp_template(uriTemplate=key))
498
- return mcp_templates
523
+
524
+ async def _handler(
525
+ context: MiddlewareContext[dict[str, Any]],
526
+ ) -> list[ResourceTemplate]:
527
+ templates = await self._resource_manager.list_resource_templates()
528
+
529
+ mcp_templates: list[ResourceTemplate] = []
530
+ for template in templates:
531
+ if self._should_enable_component(template):
532
+ mcp_templates.append(template)
533
+
534
+ return mcp_templates
535
+
536
+ with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
537
+ # Create the middleware context.
538
+ mw_context = MiddlewareContext(
539
+ message={}, # List resource templates doesn't have parameters
540
+ source="client",
541
+ type="request",
542
+ method="resources/templates/list",
543
+ fastmcp_context=fastmcp_ctx,
544
+ )
545
+
546
+ # Apply the middleware chain.
547
+ return await self._apply_middleware(mw_context, _handler)
499
548
 
500
549
  async def _mcp_list_prompts(self) -> list[MCPPrompt]:
550
+ logger.debug("Handler called: list_prompts")
551
+
552
+ with fastmcp.server.context.Context(fastmcp=self):
553
+ prompts = await self._list_prompts()
554
+ return [prompt.to_mcp_prompt(name=prompt.key) for prompt in prompts]
555
+
556
+ async def _list_prompts(self) -> list[Prompt]:
501
557
  """
502
558
  List all available prompts, in the format expected by the low-level MCP
503
559
  server.
504
560
 
505
561
  """
506
- prompts = await self.get_prompts()
507
- mcp_prompts: list[MCPPrompt] = []
508
- for key, prompt in prompts.items():
509
- if self._should_enable_component(prompt):
510
- mcp_prompts.append(prompt.to_mcp_prompt(name=key))
511
- return mcp_prompts
562
+
563
+ async def _handler(
564
+ context: MiddlewareContext[mcp.types.ListPromptsRequest],
565
+ ) -> list[Prompt]:
566
+ prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
567
+
568
+ mcp_prompts: list[Prompt] = []
569
+ for prompt in prompts:
570
+ if self._should_enable_component(prompt):
571
+ mcp_prompts.append(prompt)
572
+
573
+ return mcp_prompts
574
+
575
+ with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
576
+ # Create the middleware context.
577
+ mw_context = MiddlewareContext(
578
+ message=mcp.types.ListPromptsRequest(method="prompts/list"),
579
+ source="client",
580
+ type="request",
581
+ method="prompts/list",
582
+ fastmcp_context=fastmcp_ctx,
583
+ )
584
+
585
+ # Apply the middleware chain.
586
+ return await self._apply_middleware(mw_context, _handler)
512
587
 
513
588
  async def _mcp_call_tool(
514
589
  self, key: str, arguments: dict[str, Any]
@@ -525,46 +600,40 @@ class FastMCP(Generic[LifespanResultT]):
525
600
  Returns:
526
601
  List of MCP Content objects containing the tool results
527
602
  """
528
- logger.debug("Call tool: %s with %s", key, arguments)
603
+ logger.debug("Handler called: call_tool %s with %s", key, arguments)
529
604
 
530
- # Create and use context for the entire call
531
605
  with fastmcp.server.context.Context(fastmcp=self):
532
606
  try:
533
607
  return await self._call_tool(key, arguments)
534
608
  except DisabledError:
535
- # convert to NotFoundError to avoid leaking tool presence
536
609
  raise NotFoundError(f"Unknown tool: {key}")
537
610
  except NotFoundError:
538
- # standardize NotFound message
539
611
  raise NotFoundError(f"Unknown tool: {key}")
540
612
 
541
613
  async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
542
614
  """
543
- Call a tool with raw MCP arguments. FastMCP subclasses should override
544
- this method, not _mcp_call_tool.
545
-
546
- Args:
547
- key: The name of the tool to call arguments: Arguments to pass to
548
- the tool
549
-
550
- Returns:
551
- List of MCP Content objects containing the tool results
615
+ Applies this server's middleware and delegates the filtered call to the manager.
552
616
  """
553
617
 
554
- # Get tool, checking first from our tools, then from the mounted servers
555
- if self._tool_manager.has_tool(key):
556
- tool = self._tool_manager.get_tool(key)
618
+ async def _handler(
619
+ context: MiddlewareContext[mcp.types.CallToolRequestParams],
620
+ ) -> list[MCPContent]:
621
+ tool = await self._tool_manager.get_tool(context.message.name)
557
622
  if not self._should_enable_component(tool):
558
- raise DisabledError(f"Tool {key!r} is disabled")
559
- return await self._tool_manager.call_tool(key, arguments)
623
+ raise NotFoundError(f"Unknown tool: {context.message.name!r}")
560
624
 
561
- # Check mounted servers to see if they have the tool
562
- for server in self._mounted_servers.values():
563
- if server.match_tool(key):
564
- tool_key = server.strip_tool_prefix(key)
565
- return await server.server._call_tool(tool_key, arguments)
625
+ return await self._tool_manager.call_tool(
626
+ key=context.message.name, arguments=context.message.arguments or {}
627
+ )
566
628
 
567
- raise NotFoundError(f"Unknown tool: {key!r}")
629
+ mw_context = MiddlewareContext(
630
+ message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
631
+ source="client",
632
+ type="request",
633
+ method="tools/call",
634
+ fastmcp_context=fastmcp.server.dependencies.get_context(),
635
+ )
636
+ return await self._apply_middleware(mw_context, _handler)
568
637
 
569
638
  async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
570
639
  """
@@ -572,7 +641,7 @@ class FastMCP(Generic[LifespanResultT]):
572
641
 
573
642
  Delegates to _read_resource, which should be overridden by FastMCP subclasses.
574
643
  """
575
- logger.debug("Read resource: %s", uri)
644
+ logger.debug("Handler called: read_resource %s", uri)
576
645
 
577
646
  with fastmcp.server.context.Context(fastmcp=self):
578
647
  try:
@@ -586,27 +655,38 @@ class FastMCP(Generic[LifespanResultT]):
586
655
 
587
656
  async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
588
657
  """
589
- Read a resource by URI, in the format expected by the low-level MCP
590
- server.
658
+ Applies this server's middleware and delegates the filtered call to the manager.
591
659
  """
592
- if self._resource_manager.has_resource(uri):
593
- resource = await self._resource_manager.get_resource(uri)
660
+
661
+ async def _handler(
662
+ context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
663
+ ) -> list[ReadResourceContents]:
664
+ resource = await self._resource_manager.get_resource(context.message.uri)
594
665
  if not self._should_enable_component(resource):
595
- raise DisabledError(f"Resource {str(uri)!r} is disabled")
596
- content = await self._resource_manager.read_resource(uri)
666
+ raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
667
+
668
+ content = await self._resource_manager.read_resource(context.message.uri)
597
669
  return [
598
670
  ReadResourceContents(
599
671
  content=content,
600
672
  mime_type=resource.mime_type,
601
673
  )
602
674
  ]
675
+
676
+ # Convert string URI to AnyUrl if needed
677
+ if isinstance(uri, str):
678
+ uri_param = AnyUrl(uri)
603
679
  else:
604
- for server in self._mounted_servers.values():
605
- if server.match_resource(str(uri)):
606
- new_uri = server.strip_resource_prefix(str(uri))
607
- return await server.server._mcp_read_resource(new_uri)
608
- else:
609
- raise NotFoundError(f"Unknown resource: {uri}")
680
+ uri_param = uri
681
+
682
+ mw_context = MiddlewareContext(
683
+ message=mcp.types.ReadResourceRequestParams(uri=uri_param),
684
+ source="client",
685
+ type="request",
686
+ method="resources/read",
687
+ fastmcp_context=fastmcp.server.dependencies.get_context(),
688
+ )
689
+ return await self._apply_middleware(mw_context, _handler)
610
690
 
611
691
  async def _mcp_get_prompt(
612
692
  self, name: str, arguments: dict[str, Any] | None = None
@@ -616,7 +696,7 @@ class FastMCP(Generic[LifespanResultT]):
616
696
 
617
697
  Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
618
698
  """
619
- logger.debug("Get prompt: %s with %s", name, arguments)
699
+ logger.debug("Handler called: get_prompt %s with %s", name, arguments)
620
700
 
621
701
  with fastmcp.server.context.Context(fastmcp=self):
622
702
  try:
@@ -631,31 +711,29 @@ class FastMCP(Generic[LifespanResultT]):
631
711
  async def _get_prompt(
632
712
  self, name: str, arguments: dict[str, Any] | None = None
633
713
  ) -> GetPromptResult:
634
- """Handle MCP 'getPrompt' requests.
635
-
636
- Args:
637
- name: The name of the prompt to render
638
- arguments: Arguments to pass to the prompt
639
-
640
- Returns:
641
- GetPromptResult containing the rendered prompt messages
642
714
  """
643
- logger.debug("Get prompt: %s with %s", name, arguments)
715
+ Applies this server's middleware and delegates the filtered call to the manager.
716
+ """
644
717
 
645
- # Get prompt, checking first from our prompts, then from the mounted servers
646
- if self._prompt_manager.has_prompt(name):
647
- prompt = self._prompt_manager.get_prompt(name)
718
+ async def _handler(
719
+ context: MiddlewareContext[mcp.types.GetPromptRequestParams],
720
+ ) -> GetPromptResult:
721
+ prompt = await self._prompt_manager.get_prompt(context.message.name)
648
722
  if not self._should_enable_component(prompt):
649
- raise DisabledError(f"Prompt {name!r} is disabled")
650
- return await self._prompt_manager.render_prompt(name, arguments)
723
+ raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
651
724
 
652
- # Check mounted servers to see if they have the prompt
653
- for server in self._mounted_servers.values():
654
- if server.match_prompt(name):
655
- prompt_name = server.strip_prompt_prefix(name)
656
- return await server.server._mcp_get_prompt(prompt_name, arguments)
725
+ return await self._prompt_manager.render_prompt(
726
+ name=context.message.name, arguments=context.message.arguments
727
+ )
657
728
 
658
- raise NotFoundError(f"Unknown prompt: {name}")
729
+ mw_context = MiddlewareContext(
730
+ message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
731
+ source="client",
732
+ type="request",
733
+ method="prompts/get",
734
+ fastmcp_context=fastmcp.server.dependencies.get_context(),
735
+ )
736
+ return await self._apply_middleware(mw_context, _handler)
659
737
 
660
738
  def add_tool(self, tool: Tool) -> None:
661
739
  """Add a tool to the server.
@@ -823,23 +901,23 @@ class FastMCP(Generic[LifespanResultT]):
823
901
  enabled=enabled,
824
902
  )
825
903
 
826
- def add_resource(self, resource: Resource, key: str | None = None) -> None:
904
+ def add_resource(self, resource: Resource) -> None:
827
905
  """Add a resource to the server.
828
906
 
829
907
  Args:
830
908
  resource: A Resource instance to add
831
909
  """
832
910
 
833
- self._resource_manager.add_resource(resource, key=key)
911
+ self._resource_manager.add_resource(resource)
834
912
  self._cache.clear()
835
913
 
836
- def add_template(self, template: ResourceTemplate, key: str | None = None) -> None:
914
+ def add_template(self, template: ResourceTemplate) -> None:
837
915
  """Add a resource template to the server.
838
916
 
839
917
  Args:
840
918
  template: A ResourceTemplate instance to add
841
919
  """
842
- self._resource_manager.add_template(template, key=key)
920
+ self._resource_manager.add_template(template)
843
921
 
844
922
  def add_resource_fn(
845
923
  self,
@@ -1176,13 +1254,13 @@ class FastMCP(Generic[LifespanResultT]):
1176
1254
 
1177
1255
  async def run_http_async(
1178
1256
  self,
1179
- transport: Literal["streamable-http", "sse"] = "streamable-http",
1257
+ transport: Literal["http", "streamable-http", "sse"] = "http",
1180
1258
  host: str | None = None,
1181
1259
  port: int | None = None,
1182
1260
  log_level: str | None = None,
1183
1261
  path: str | None = None,
1184
1262
  uvicorn_config: dict[str, Any] | None = None,
1185
- middleware: list[Middleware] | None = None,
1263
+ middleware: list[ASGIMiddleware] | None = None,
1186
1264
  ) -> None:
1187
1265
  """Run the server using HTTP transport.
1188
1266
 
@@ -1253,7 +1331,7 @@ class FastMCP(Generic[LifespanResultT]):
1253
1331
  self,
1254
1332
  path: str | None = None,
1255
1333
  message_path: str | None = None,
1256
- middleware: list[Middleware] | None = None,
1334
+ middleware: list[ASGIMiddleware] | None = None,
1257
1335
  ) -> StarletteWithLifespan:
1258
1336
  """
1259
1337
  Create a Starlette app for the SSE server.
@@ -1283,7 +1361,7 @@ class FastMCP(Generic[LifespanResultT]):
1283
1361
  def streamable_http_app(
1284
1362
  self,
1285
1363
  path: str | None = None,
1286
- middleware: list[Middleware] | None = None,
1364
+ middleware: list[ASGIMiddleware] | None = None,
1287
1365
  ) -> StarletteWithLifespan:
1288
1366
  """
1289
1367
  Create a Starlette app for the StreamableHTTP server.
@@ -1304,10 +1382,10 @@ class FastMCP(Generic[LifespanResultT]):
1304
1382
  def http_app(
1305
1383
  self,
1306
1384
  path: str | None = None,
1307
- middleware: list[Middleware] | None = None,
1385
+ middleware: list[ASGIMiddleware] | None = None,
1308
1386
  json_response: bool | None = None,
1309
1387
  stateless_http: bool | None = None,
1310
- transport: Literal["streamable-http", "sse"] = "streamable-http",
1388
+ transport: Literal["http", "streamable-http", "sse"] = "http",
1311
1389
  ) -> StarletteWithLifespan:
1312
1390
  """Create a Starlette app using the specified HTTP transport.
1313
1391
 
@@ -1320,7 +1398,7 @@ class FastMCP(Generic[LifespanResultT]):
1320
1398
  A Starlette application configured with the specified transport
1321
1399
  """
1322
1400
 
1323
- if transport == "streamable-http":
1401
+ if transport in ("streamable-http", "http"):
1324
1402
  return create_streamable_http_app(
1325
1403
  server=self,
1326
1404
  streamable_http_path=path
@@ -1367,7 +1445,7 @@ class FastMCP(Generic[LifespanResultT]):
1367
1445
  stacklevel=2,
1368
1446
  )
1369
1447
  await self.run_http_async(
1370
- transport="streamable-http",
1448
+ transport="http",
1371
1449
  host=host,
1372
1450
  port=port,
1373
1451
  log_level=log_level,
@@ -1377,15 +1455,15 @@ class FastMCP(Generic[LifespanResultT]):
1377
1455
 
1378
1456
  def mount(
1379
1457
  self,
1380
- prefix: str,
1381
1458
  server: FastMCP[LifespanResultT],
1459
+ prefix: str | None = None,
1382
1460
  as_proxy: bool | None = None,
1383
1461
  *,
1384
1462
  tool_separator: str | None = None,
1385
1463
  resource_separator: str | None = None,
1386
1464
  prompt_separator: str | None = None,
1387
1465
  ) -> None:
1388
- """Mount another FastMCP server on this server with the given prefix.
1466
+ """Mount another FastMCP server on this server with an optional prefix.
1389
1467
 
1390
1468
  Unlike importing (with import_server), mounting establishes a dynamic connection
1391
1469
  between servers. When a client interacts with a mounted server's objects through
@@ -1393,7 +1471,7 @@ class FastMCP(Generic[LifespanResultT]):
1393
1471
  This means changes to the mounted server are immediately reflected when accessed
1394
1472
  through the parent.
1395
1473
 
1396
- When a server is mounted:
1474
+ When a server is mounted with a prefix:
1397
1475
  - Tools from the mounted server are accessible with prefixed names.
1398
1476
  Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
1399
1477
  - Resources are accessible with prefixed URIs.
@@ -1406,6 +1484,10 @@ class FastMCP(Generic[LifespanResultT]):
1406
1484
  Example: If server has a prompt named "weather_prompt", it will be available as
1407
1485
  "prefix_weather_prompt".
1408
1486
 
1487
+ When a server is mounted without a prefix (prefix=None), its tools, resources, templates,
1488
+ and prompts are accessible with their original names. Multiple servers can be mounted
1489
+ without prefixes, and they will be tried in order until a match is found.
1490
+
1409
1491
  There are two modes for mounting servers:
1410
1492
  1. Direct mounting (default when server has no custom lifespan): The parent server
1411
1493
  directly accesses the mounted server's objects in-memory for better performance.
@@ -1418,8 +1500,9 @@ class FastMCP(Generic[LifespanResultT]):
1418
1500
  execution, but with slightly higher overhead.
1419
1501
 
1420
1502
  Args:
1421
- prefix: Prefix to use for the mounted server's objects.
1422
1503
  server: The FastMCP server to mount.
1504
+ prefix: Optional prefix to use for the mounted server's objects. If None,
1505
+ the server's objects are accessible with their original names.
1423
1506
  as_proxy: Whether to treat the mounted server as a proxy. If None (default),
1424
1507
  automatically determined based on whether the server has a custom lifespan
1425
1508
  (True if it has a custom lifespan, False otherwise).
@@ -1431,6 +1514,20 @@ class FastMCP(Generic[LifespanResultT]):
1431
1514
  from fastmcp.client.transports import FastMCPTransport
1432
1515
  from fastmcp.server.proxy import FastMCPProxy
1433
1516
 
1517
+ # Deprecated since 2.9.0
1518
+ # Prior to 2.9.0, the first positional argument was the prefix and the
1519
+ # second was the server. Here we swap them if needed now that the prefix
1520
+ # is optional.
1521
+ if isinstance(server, str):
1522
+ if fastmcp.settings.deprecation_warnings:
1523
+ warnings.warn(
1524
+ "Mount prefixes are now optional and the first positional argument "
1525
+ "should be the server you want to mount.",
1526
+ DeprecationWarning,
1527
+ stacklevel=2,
1528
+ )
1529
+ server, prefix = cast(FastMCP[Any], prefix), server
1530
+
1434
1531
  if tool_separator is not None:
1435
1532
  # Deprecated since 2.4.0
1436
1533
  if fastmcp.settings.deprecation_warnings:
@@ -1469,21 +1566,22 @@ class FastMCP(Generic[LifespanResultT]):
1469
1566
  if as_proxy and not isinstance(server, FastMCPProxy):
1470
1567
  server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
1471
1568
 
1569
+ # Delegate mounting to all three managers
1472
1570
  mounted_server = MountedServer(
1473
- server=server,
1474
1571
  prefix=prefix,
1572
+ server=server,
1573
+ resource_prefix_format=self.resource_prefix_format,
1475
1574
  )
1476
- self._mounted_servers[prefix] = mounted_server
1477
- self._cache.clear()
1575
+ self._tool_manager.mount(mounted_server)
1576
+ self._resource_manager.mount(mounted_server)
1577
+ self._prompt_manager.mount(mounted_server)
1478
1578
 
1479
- def unmount(self, prefix: str) -> None:
1480
- self._mounted_servers.pop(prefix)
1481
1579
  self._cache.clear()
1482
1580
 
1483
1581
  async def import_server(
1484
1582
  self,
1485
- prefix: str,
1486
1583
  server: FastMCP[LifespanResultT],
1584
+ prefix: str | None = None,
1487
1585
  tool_separator: str | None = None,
1488
1586
  resource_separator: str | None = None,
1489
1587
  prompt_separator: str | None = None,
@@ -1497,7 +1595,7 @@ class FastMCP(Generic[LifespanResultT]):
1497
1595
  future changes to the imported server will not be reflected in the
1498
1596
  importing server. Server-level configurations and lifespans are not imported.
1499
1597
 
1500
- When a server is imported:
1598
+ When a server is imported with a prefix:
1501
1599
  - The tools are imported with prefixed names
1502
1600
  Example: If server has a tool named "get_weather", it will be
1503
1601
  available as "prefix_get_weather"
@@ -1511,14 +1609,33 @@ class FastMCP(Generic[LifespanResultT]):
1511
1609
  Example: If server has a prompt named "weather_prompt", it will be available as
1512
1610
  "prefix_weather_prompt"
1513
1611
 
1612
+ When a server is imported without a prefix (prefix=None), its tools, resources,
1613
+ templates, and prompts are imported with their original names.
1614
+
1514
1615
  Args:
1515
- prefix: The prefix to use for the imported server
1516
1616
  server: The FastMCP server to import
1617
+ prefix: Optional prefix to use for the imported server's objects. If None,
1618
+ objects are imported with their original names.
1517
1619
  tool_separator: Deprecated. Separator for tool names.
1518
1620
  resource_separator: Deprecated and ignored. Prefix is now
1519
1621
  applied using the protocol://prefix/path format
1520
1622
  prompt_separator: Deprecated. Separator for prompt names.
1521
1623
  """
1624
+
1625
+ # Deprecated since 2.9.0
1626
+ # Prior to 2.9.0, the first positional argument was the prefix and the
1627
+ # second was the server. Here we swap them if needed now that the prefix
1628
+ # is optional.
1629
+ if isinstance(server, str):
1630
+ if fastmcp.settings.deprecation_warnings:
1631
+ warnings.warn(
1632
+ "Import prefixes are now optional and the first positional argument "
1633
+ "should be the server you want to import.",
1634
+ DeprecationWarning,
1635
+ stacklevel=2,
1636
+ )
1637
+ server, prefix = cast(FastMCP[Any], prefix), server
1638
+
1522
1639
  if tool_separator is not None:
1523
1640
  # Deprecated since 2.4.0
1524
1641
  if fastmcp.settings.deprecation_warnings:
@@ -1549,29 +1666,39 @@ class FastMCP(Generic[LifespanResultT]):
1549
1666
  stacklevel=2,
1550
1667
  )
1551
1668
 
1552
- # Import tools from the mounted server
1553
- tool_prefix = f"{prefix}_"
1669
+ # Import tools from the server
1554
1670
  for key, tool in (await server.get_tools()).items():
1555
- self._tool_manager.add_tool(tool, key=f"{tool_prefix}{key}")
1671
+ if prefix:
1672
+ tool = tool.with_key(f"{prefix}_{key}")
1673
+ self._tool_manager.add_tool(tool)
1556
1674
 
1557
- # Import resources and templates from the mounted server
1675
+ # Import resources and templates from the server
1558
1676
  for key, resource in (await server.get_resources()).items():
1559
- prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
1560
- self._resource_manager.add_resource(resource, key=prefixed_key)
1677
+ if prefix:
1678
+ resource_key = add_resource_prefix(
1679
+ key, prefix, self.resource_prefix_format
1680
+ )
1681
+ resource = resource.with_key(resource_key)
1682
+ self._resource_manager.add_resource(resource)
1561
1683
 
1562
1684
  for key, template in (await server.get_resource_templates()).items():
1563
- prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
1564
- self._resource_manager.add_template(template, key=prefixed_key)
1685
+ if prefix:
1686
+ template_key = add_resource_prefix(
1687
+ key, prefix, self.resource_prefix_format
1688
+ )
1689
+ template = template.with_key(template_key)
1690
+ self._resource_manager.add_template(template)
1565
1691
 
1566
- # Import prompts from the mounted server
1567
- prompt_prefix = f"{prefix}_"
1692
+ # Import prompts from the server
1568
1693
  for key, prompt in (await server.get_prompts()).items():
1569
- self._prompt_manager.add_prompt(prompt, key=f"{prompt_prefix}{key}")
1694
+ if prefix:
1695
+ prompt = prompt.with_key(f"{prefix}_{key}")
1696
+ self._prompt_manager.add_prompt(prompt)
1570
1697
 
1571
- logger.info(f"Imported server {server.name} with prefix '{prefix}'")
1572
- logger.debug(f"Imported tools with prefix '{tool_prefix}'")
1573
- logger.debug(f"Imported resources and templates with prefix '{prefix}/'")
1574
- logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
1698
+ if prefix:
1699
+ logger.debug(f"Imported server {server.name} with prefix '{prefix}'")
1700
+ else:
1701
+ logger.debug(f"Imported server {server.name}")
1575
1702
 
1576
1703
  self._cache.clear()
1577
1704
 
@@ -1728,60 +1855,11 @@ class FastMCP(Generic[LifespanResultT]):
1728
1855
  return True
1729
1856
 
1730
1857
 
1858
+ @dataclass
1731
1859
  class MountedServer:
1732
- def __init__(
1733
- self,
1734
- prefix: str,
1735
- server: FastMCP[LifespanResultT],
1736
- ):
1737
- self.server = server
1738
- self.prefix = prefix
1739
-
1740
- async def get_tools(self) -> dict[str, Tool]:
1741
- tools = await self.server.get_tools()
1742
- return {f"{self.prefix}_{key}": tool for key, tool in tools.items()}
1743
-
1744
- async def get_resources(self) -> dict[str, Resource]:
1745
- resources = await self.server.get_resources()
1746
- return {
1747
- add_resource_prefix(
1748
- key, self.prefix, self.server.resource_prefix_format
1749
- ): resource
1750
- for key, resource in resources.items()
1751
- }
1752
-
1753
- async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
1754
- templates = await self.server.get_resource_templates()
1755
- return {
1756
- add_resource_prefix(
1757
- key, self.prefix, self.server.resource_prefix_format
1758
- ): template
1759
- for key, template in templates.items()
1760
- }
1761
-
1762
- async def get_prompts(self) -> dict[str, Prompt]:
1763
- prompts = await self.server.get_prompts()
1764
- return {f"{self.prefix}_{key}": prompt for key, prompt in prompts.items()}
1765
-
1766
- def match_tool(self, key: str) -> bool:
1767
- return key.startswith(f"{self.prefix}_")
1768
-
1769
- def strip_tool_prefix(self, key: str) -> str:
1770
- return key.removeprefix(f"{self.prefix}_")
1771
-
1772
- def match_resource(self, key: str) -> bool:
1773
- return has_resource_prefix(key, self.prefix, self.server.resource_prefix_format)
1774
-
1775
- def strip_resource_prefix(self, key: str) -> str:
1776
- return remove_resource_prefix(
1777
- key, self.prefix, self.server.resource_prefix_format
1778
- )
1779
-
1780
- def match_prompt(self, key: str) -> bool:
1781
- return key.startswith(f"{self.prefix}_")
1782
-
1783
- def strip_prompt_prefix(self, key: str) -> str:
1784
- return key.removeprefix(f"{self.prefix}_")
1860
+ prefix: str | None
1861
+ server: FastMCP[Any]
1862
+ resource_prefix_format: Literal["protocol", "path"] | None = None
1785
1863
 
1786
1864
 
1787
1865
  def add_resource_prefix(