fastmcp 2.8.1__py3-none-any.whl → 2.9.1__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 (43) 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 +23 -7
  5. fastmcp/client/logging.py +1 -2
  6. fastmcp/client/messages.py +126 -0
  7. fastmcp/client/transports.py +17 -2
  8. fastmcp/contrib/mcp_mixin/README.md +79 -2
  9. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -0
  10. fastmcp/prompts/prompt.py +109 -13
  11. fastmcp/prompts/prompt_manager.py +119 -43
  12. fastmcp/resources/resource.py +27 -1
  13. fastmcp/resources/resource_manager.py +249 -76
  14. fastmcp/resources/template.py +44 -2
  15. fastmcp/server/auth/providers/bearer.py +62 -13
  16. fastmcp/server/context.py +113 -10
  17. fastmcp/server/http.py +8 -0
  18. fastmcp/server/low_level.py +35 -0
  19. fastmcp/server/middleware/__init__.py +6 -0
  20. fastmcp/server/middleware/error_handling.py +206 -0
  21. fastmcp/server/middleware/logging.py +165 -0
  22. fastmcp/server/middleware/middleware.py +236 -0
  23. fastmcp/server/middleware/rate_limiting.py +231 -0
  24. fastmcp/server/middleware/timing.py +156 -0
  25. fastmcp/server/proxy.py +250 -140
  26. fastmcp/server/server.py +446 -280
  27. fastmcp/settings.py +2 -2
  28. fastmcp/tools/tool.py +22 -2
  29. fastmcp/tools/tool_manager.py +114 -45
  30. fastmcp/tools/tool_transform.py +42 -16
  31. fastmcp/utilities/components.py +22 -2
  32. fastmcp/utilities/inspect.py +326 -0
  33. fastmcp/utilities/json_schema.py +67 -23
  34. fastmcp/utilities/mcp_config.py +13 -7
  35. fastmcp/utilities/openapi.py +75 -5
  36. fastmcp/utilities/tests.py +1 -1
  37. fastmcp/utilities/types.py +90 -1
  38. {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/METADATA +2 -2
  39. fastmcp-2.9.1.dist-info/RECORD +78 -0
  40. fastmcp-2.8.1.dist-info/RECORD +0 -69
  41. {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/WHEEL +0 -0
  42. {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/entry_points.txt +0 -0
  43. {fastmcp-2.8.1.dist-info → fastmcp-2.9.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -12,16 +12,17 @@ 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
24
- from mcp.server.lowlevel.server import Server as MCPServer
25
26
  from mcp.server.stdio import stdio_server
26
27
  from mcp.types import (
27
28
  AnyFunction,
@@ -33,7 +34,7 @@ from mcp.types import Resource as MCPResource
33
34
  from mcp.types import ResourceTemplate as MCPResourceTemplate
34
35
  from mcp.types import Tool as MCPTool
35
36
  from pydantic import AnyUrl
36
- from starlette.middleware import Middleware
37
+ from starlette.middleware import Middleware as ASGIMiddleware
37
38
  from starlette.requests import Request
38
39
  from starlette.responses import Response
39
40
  from starlette.routing import BaseRoute, Route
@@ -52,6 +53,8 @@ from fastmcp.server.http import (
52
53
  create_sse_app,
53
54
  create_streamable_http_app,
54
55
  )
56
+ from fastmcp.server.low_level import LowLevelServer
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"^([^:]+://)(.*?)$")
@@ -95,10 +99,12 @@ def _lifespan_wrapper(
95
99
  [FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
96
100
  ],
97
101
  ) -> Callable[
98
- [MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
102
+ [LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
99
103
  ]:
100
104
  @asynccontextmanager
101
- async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
105
+ async def wrap(
106
+ s: LowLevelServer[LifespanResultT],
107
+ ) -> AsyncIterator[LifespanResultT]:
102
108
  async with AsyncExitStack() as stack:
103
109
  context = await stack.enter_async_context(lifespan(app))
104
110
  yield context
@@ -111,7 +117,10 @@ class FastMCP(Generic[LifespanResultT]):
111
117
  self,
112
118
  name: str | None = None,
113
119
  instructions: str | None = None,
120
+ *,
121
+ version: str | None = None,
114
122
  auth: OAuthProvider | None = None,
123
+ middleware: list[Middleware] | None = None,
115
124
  lifespan: (
116
125
  Callable[
117
126
  [FastMCP[LifespanResultT]],
@@ -152,7 +161,6 @@ class FastMCP(Generic[LifespanResultT]):
152
161
  self._cache = TimedCache(
153
162
  expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
154
163
  )
155
- self._mounted_servers: dict[str, MountedServer] = {}
156
164
  self._additional_http_routes: list[BaseRoute] = []
157
165
  self._tool_manager = ToolManager(
158
166
  duplicate_behavior=on_duplicate_tools,
@@ -173,8 +181,9 @@ class FastMCP(Generic[LifespanResultT]):
173
181
  lifespan = default_lifespan
174
182
  else:
175
183
  self._has_lifespan = True
176
- self._mcp_server = MCPServer[LifespanResultT](
184
+ self._mcp_server = LowLevelServer[LifespanResultT](
177
185
  name=name or "FastMCP",
186
+ version=version,
178
187
  instructions=instructions,
179
188
  lifespan=_lifespan_wrapper(self, lifespan),
180
189
  )
@@ -192,6 +201,8 @@ class FastMCP(Generic[LifespanResultT]):
192
201
  self.include_tags = include_tags
193
202
  self.exclude_tags = exclude_tags
194
203
 
204
+ self.middleware = middleware or []
205
+
195
206
  # Set up MCP protocol handlers
196
207
  self._setup_handlers()
197
208
  self.dependencies = dependencies or fastmcp.settings.server_dependencies
@@ -272,7 +283,7 @@ class FastMCP(Generic[LifespanResultT]):
272
283
 
273
284
  async def run_async(
274
285
  self,
275
- transport: Literal["stdio", "streamable-http", "sse"] | None = None,
286
+ transport: Transport | None = None,
276
287
  **transport_kwargs: Any,
277
288
  ) -> None:
278
289
  """Run the FastMCP server asynchronously.
@@ -282,19 +293,19 @@ class FastMCP(Generic[LifespanResultT]):
282
293
  """
283
294
  if transport is None:
284
295
  transport = "stdio"
285
- if transport not in {"stdio", "streamable-http", "sse"}:
296
+ if transport not in {"stdio", "http", "sse", "streamable-http"}:
286
297
  raise ValueError(f"Unknown transport: {transport}")
287
298
 
288
299
  if transport == "stdio":
289
300
  await self.run_stdio_async(**transport_kwargs)
290
- elif transport in {"streamable-http", "sse"}:
301
+ elif transport in {"http", "sse", "streamable-http"}:
291
302
  await self.run_http_async(transport=transport, **transport_kwargs)
292
303
  else:
293
304
  raise ValueError(f"Unknown transport: {transport}")
294
305
 
295
306
  def run(
296
307
  self,
297
- transport: Literal["stdio", "streamable-http", "sse"] | None = None,
308
+ transport: Transport | None = None,
298
309
  **transport_kwargs: Any,
299
310
  ) -> None:
300
311
  """Run the FastMCP server. Note this is a synchronous function.
@@ -315,22 +326,23 @@ class FastMCP(Generic[LifespanResultT]):
315
326
  self._mcp_server.read_resource()(self._mcp_read_resource)
316
327
  self._mcp_server.get_prompt()(self._mcp_get_prompt)
317
328
 
329
+ async def _apply_middleware(
330
+ self,
331
+ context: MiddlewareContext[Any],
332
+ call_next: Callable[[MiddlewareContext[Any]], Awaitable[Any]],
333
+ ) -> Any:
334
+ """Builds and executes the middleware chain."""
335
+ chain = call_next
336
+ for mw in reversed(self.middleware):
337
+ chain = partial(mw, call_next=chain)
338
+ return await chain(context)
339
+
340
+ def add_middleware(self, middleware: Middleware) -> None:
341
+ self.middleware.append(middleware)
342
+
318
343
  async def get_tools(self) -> dict[str, Tool]:
319
344
  """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
345
+ return await self._tool_manager.get_tools()
334
346
 
335
347
  async def get_tool(self, key: str) -> Tool:
336
348
  tools = await self.get_tools()
@@ -340,20 +352,7 @@ class FastMCP(Generic[LifespanResultT]):
340
352
 
341
353
  async def get_resources(self) -> dict[str, Resource]:
342
354
  """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
355
+ return await self._resource_manager.get_resources()
357
356
 
358
357
  async def get_resource(self, key: str) -> Resource:
359
358
  resources = await self.get_resources()
@@ -363,25 +362,10 @@ class FastMCP(Generic[LifespanResultT]):
363
362
 
364
363
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
365
364
  """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
365
+ return await self._resource_manager.get_resource_templates()
383
366
 
384
367
  async def get_resource_template(self, key: str) -> ResourceTemplate:
368
+ """Get a registered resource template by key."""
385
369
  templates = await self.get_resource_templates()
386
370
  if key not in templates:
387
371
  raise NotFoundError(f"Unknown resource template: {key}")
@@ -391,20 +375,7 @@ class FastMCP(Generic[LifespanResultT]):
391
375
  """
392
376
  List all available prompts.
393
377
  """
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
378
+ return await self._prompt_manager.get_prompts()
408
379
 
409
380
  async def get_prompt(self, key: str) -> Prompt:
410
381
  prompts = await self.get_prompts()
@@ -435,9 +406,12 @@ class FastMCP(Generic[LifespanResultT]):
435
406
  include_in_schema: Whether to include in OpenAPI schema, defaults to True
436
407
 
437
408
  Example:
409
+ Register a custom HTTP route for a health check endpoint:
410
+ ```python
438
411
  @server.custom_route("/health", methods=["GET"])
439
412
  async def health_check(request: Request) -> Response:
440
413
  return JSONResponse({"status": "ok"})
414
+ ```
441
415
  """
442
416
 
443
417
  def decorator(
@@ -457,58 +431,165 @@ class FastMCP(Generic[LifespanResultT]):
457
431
  return decorator
458
432
 
459
433
  async def _mcp_list_tools(self) -> list[MCPTool]:
434
+ logger.debug("Handler called: list_tools")
435
+
436
+ async with fastmcp.server.context.Context(fastmcp=self):
437
+ tools = await self._list_tools()
438
+ return [tool.to_mcp_tool(name=tool.key) for tool in tools]
439
+
440
+ async def _list_tools(self) -> list[Tool]:
460
441
  """
461
442
  List all available tools, in the format expected by the low-level MCP
462
443
  server.
463
444
 
464
445
  """
465
- tools = await self.get_tools()
466
446
 
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))
447
+ async def _handler(
448
+ context: MiddlewareContext[mcp.types.ListToolsRequest],
449
+ ) -> list[Tool]:
450
+ tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
451
+
452
+ mcp_tools: list[Tool] = []
453
+ for tool in tools:
454
+ if self._should_enable_component(tool):
455
+ mcp_tools.append(tool)
456
+
457
+ return mcp_tools
458
+
459
+ async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
460
+ # Create the middleware context.
461
+ mw_context = MiddlewareContext(
462
+ message=mcp.types.ListToolsRequest(method="tools/list"),
463
+ source="client",
464
+ type="request",
465
+ method="tools/list",
466
+ fastmcp_context=fastmcp_ctx,
467
+ )
471
468
 
472
- return mcp_tools
469
+ # Apply the middleware chain.
470
+ return await self._apply_middleware(mw_context, _handler)
473
471
 
474
472
  async def _mcp_list_resources(self) -> list[MCPResource]:
473
+ logger.debug("Handler called: list_resources")
474
+
475
+ async with fastmcp.server.context.Context(fastmcp=self):
476
+ resources = await self._list_resources()
477
+ return [
478
+ resource.to_mcp_resource(uri=resource.key) for resource in resources
479
+ ]
480
+
481
+ async def _list_resources(self) -> list[Resource]:
475
482
  """
476
483
  List all available resources, in the format expected by the low-level MCP
477
484
  server.
478
485
 
479
486
  """
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
487
+
488
+ async def _handler(
489
+ context: MiddlewareContext[dict[str, Any]],
490
+ ) -> list[Resource]:
491
+ resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
492
+
493
+ mcp_resources: list[Resource] = []
494
+ for resource in resources:
495
+ if self._should_enable_component(resource):
496
+ mcp_resources.append(resource)
497
+
498
+ return mcp_resources
499
+
500
+ async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
501
+ # Create the middleware context.
502
+ mw_context = MiddlewareContext(
503
+ message={}, # List resources doesn't have parameters
504
+ source="client",
505
+ type="request",
506
+ method="resources/list",
507
+ fastmcp_context=fastmcp_ctx,
508
+ )
509
+
510
+ # Apply the middleware chain.
511
+ return await self._apply_middleware(mw_context, _handler)
486
512
 
487
513
  async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
514
+ logger.debug("Handler called: list_resource_templates")
515
+
516
+ async with fastmcp.server.context.Context(fastmcp=self):
517
+ templates = await self._list_resource_templates()
518
+ return [
519
+ template.to_mcp_template(uriTemplate=template.key)
520
+ for template in templates
521
+ ]
522
+
523
+ async def _list_resource_templates(self) -> list[ResourceTemplate]:
488
524
  """
489
- List all available resource templates, in the format expected by the low-level
490
- MCP server.
525
+ List all available resource templates, in the format expected by the low-level MCP
526
+ server.
491
527
 
492
528
  """
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
529
+
530
+ async def _handler(
531
+ context: MiddlewareContext[dict[str, Any]],
532
+ ) -> list[ResourceTemplate]:
533
+ templates = await self._resource_manager.list_resource_templates()
534
+
535
+ mcp_templates: list[ResourceTemplate] = []
536
+ for template in templates:
537
+ if self._should_enable_component(template):
538
+ mcp_templates.append(template)
539
+
540
+ return mcp_templates
541
+
542
+ async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
543
+ # Create the middleware context.
544
+ mw_context = MiddlewareContext(
545
+ message={}, # List resource templates doesn't have parameters
546
+ source="client",
547
+ type="request",
548
+ method="resources/templates/list",
549
+ fastmcp_context=fastmcp_ctx,
550
+ )
551
+
552
+ # Apply the middleware chain.
553
+ return await self._apply_middleware(mw_context, _handler)
499
554
 
500
555
  async def _mcp_list_prompts(self) -> list[MCPPrompt]:
556
+ logger.debug("Handler called: list_prompts")
557
+
558
+ async with fastmcp.server.context.Context(fastmcp=self):
559
+ prompts = await self._list_prompts()
560
+ return [prompt.to_mcp_prompt(name=prompt.key) for prompt in prompts]
561
+
562
+ async def _list_prompts(self) -> list[Prompt]:
501
563
  """
502
564
  List all available prompts, in the format expected by the low-level MCP
503
565
  server.
504
566
 
505
567
  """
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
568
+
569
+ async def _handler(
570
+ context: MiddlewareContext[mcp.types.ListPromptsRequest],
571
+ ) -> list[Prompt]:
572
+ prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
573
+
574
+ mcp_prompts: list[Prompt] = []
575
+ for prompt in prompts:
576
+ if self._should_enable_component(prompt):
577
+ mcp_prompts.append(prompt)
578
+
579
+ return mcp_prompts
580
+
581
+ async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
582
+ # Create the middleware context.
583
+ mw_context = MiddlewareContext(
584
+ message=mcp.types.ListPromptsRequest(method="prompts/list"),
585
+ source="client",
586
+ type="request",
587
+ method="prompts/list",
588
+ fastmcp_context=fastmcp_ctx,
589
+ )
590
+
591
+ # Apply the middleware chain.
592
+ return await self._apply_middleware(mw_context, _handler)
512
593
 
513
594
  async def _mcp_call_tool(
514
595
  self, key: str, arguments: dict[str, Any]
@@ -525,46 +606,40 @@ class FastMCP(Generic[LifespanResultT]):
525
606
  Returns:
526
607
  List of MCP Content objects containing the tool results
527
608
  """
528
- logger.debug("Call tool: %s with %s", key, arguments)
609
+ logger.debug("Handler called: call_tool %s with %s", key, arguments)
529
610
 
530
- # Create and use context for the entire call
531
- with fastmcp.server.context.Context(fastmcp=self):
611
+ async with fastmcp.server.context.Context(fastmcp=self):
532
612
  try:
533
613
  return await self._call_tool(key, arguments)
534
614
  except DisabledError:
535
- # convert to NotFoundError to avoid leaking tool presence
536
615
  raise NotFoundError(f"Unknown tool: {key}")
537
616
  except NotFoundError:
538
- # standardize NotFound message
539
617
  raise NotFoundError(f"Unknown tool: {key}")
540
618
 
541
619
  async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
542
620
  """
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
621
+ Applies this server's middleware and delegates the filtered call to the manager.
552
622
  """
553
623
 
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)
624
+ async def _handler(
625
+ context: MiddlewareContext[mcp.types.CallToolRequestParams],
626
+ ) -> list[MCPContent]:
627
+ tool = await self._tool_manager.get_tool(context.message.name)
557
628
  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)
629
+ raise NotFoundError(f"Unknown tool: {context.message.name!r}")
560
630
 
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)
631
+ return await self._tool_manager.call_tool(
632
+ key=context.message.name, arguments=context.message.arguments or {}
633
+ )
566
634
 
567
- raise NotFoundError(f"Unknown tool: {key!r}")
635
+ mw_context = MiddlewareContext(
636
+ message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
637
+ source="client",
638
+ type="request",
639
+ method="tools/call",
640
+ fastmcp_context=fastmcp.server.dependencies.get_context(),
641
+ )
642
+ return await self._apply_middleware(mw_context, _handler)
568
643
 
569
644
  async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
570
645
  """
@@ -572,9 +647,9 @@ class FastMCP(Generic[LifespanResultT]):
572
647
 
573
648
  Delegates to _read_resource, which should be overridden by FastMCP subclasses.
574
649
  """
575
- logger.debug("Read resource: %s", uri)
650
+ logger.debug("Handler called: read_resource %s", uri)
576
651
 
577
- with fastmcp.server.context.Context(fastmcp=self):
652
+ async with fastmcp.server.context.Context(fastmcp=self):
578
653
  try:
579
654
  return await self._read_resource(uri)
580
655
  except DisabledError:
@@ -586,27 +661,38 @@ class FastMCP(Generic[LifespanResultT]):
586
661
 
587
662
  async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
588
663
  """
589
- Read a resource by URI, in the format expected by the low-level MCP
590
- server.
664
+ Applies this server's middleware and delegates the filtered call to the manager.
591
665
  """
592
- if self._resource_manager.has_resource(uri):
593
- resource = await self._resource_manager.get_resource(uri)
666
+
667
+ async def _handler(
668
+ context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
669
+ ) -> list[ReadResourceContents]:
670
+ resource = await self._resource_manager.get_resource(context.message.uri)
594
671
  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)
672
+ raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
673
+
674
+ content = await self._resource_manager.read_resource(context.message.uri)
597
675
  return [
598
676
  ReadResourceContents(
599
677
  content=content,
600
678
  mime_type=resource.mime_type,
601
679
  )
602
680
  ]
681
+
682
+ # Convert string URI to AnyUrl if needed
683
+ if isinstance(uri, str):
684
+ uri_param = AnyUrl(uri)
603
685
  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}")
686
+ uri_param = uri
687
+
688
+ mw_context = MiddlewareContext(
689
+ message=mcp.types.ReadResourceRequestParams(uri=uri_param),
690
+ source="client",
691
+ type="request",
692
+ method="resources/read",
693
+ fastmcp_context=fastmcp.server.dependencies.get_context(),
694
+ )
695
+ return await self._apply_middleware(mw_context, _handler)
610
696
 
611
697
  async def _mcp_get_prompt(
612
698
  self, name: str, arguments: dict[str, Any] | None = None
@@ -616,9 +702,9 @@ class FastMCP(Generic[LifespanResultT]):
616
702
 
617
703
  Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
618
704
  """
619
- logger.debug("Get prompt: %s with %s", name, arguments)
705
+ logger.debug("Handler called: get_prompt %s with %s", name, arguments)
620
706
 
621
- with fastmcp.server.context.Context(fastmcp=self):
707
+ async with fastmcp.server.context.Context(fastmcp=self):
622
708
  try:
623
709
  return await self._get_prompt(name, arguments)
624
710
  except DisabledError:
@@ -631,31 +717,29 @@ class FastMCP(Generic[LifespanResultT]):
631
717
  async def _get_prompt(
632
718
  self, name: str, arguments: dict[str, Any] | None = None
633
719
  ) -> 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
720
  """
643
- logger.debug("Get prompt: %s with %s", name, arguments)
721
+ Applies this server's middleware and delegates the filtered call to the manager.
722
+ """
644
723
 
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)
724
+ async def _handler(
725
+ context: MiddlewareContext[mcp.types.GetPromptRequestParams],
726
+ ) -> GetPromptResult:
727
+ prompt = await self._prompt_manager.get_prompt(context.message.name)
648
728
  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)
729
+ raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
651
730
 
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)
731
+ return await self._prompt_manager.render_prompt(
732
+ name=context.message.name, arguments=context.message.arguments
733
+ )
657
734
 
658
- raise NotFoundError(f"Unknown prompt: {name}")
735
+ mw_context = MiddlewareContext(
736
+ message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
737
+ source="client",
738
+ type="request",
739
+ method="prompts/get",
740
+ fastmcp_context=fastmcp.server.dependencies.get_context(),
741
+ )
742
+ return await self._apply_middleware(mw_context, _handler)
659
743
 
660
744
  def add_tool(self, tool: Tool) -> None:
661
745
  """Add a tool to the server.
@@ -669,6 +753,15 @@ class FastMCP(Generic[LifespanResultT]):
669
753
  self._tool_manager.add_tool(tool)
670
754
  self._cache.clear()
671
755
 
756
+ # Send notification if we're in a request context
757
+ try:
758
+ from fastmcp.server.dependencies import get_context
759
+
760
+ context = get_context()
761
+ context._queue_tool_list_changed() # type: ignore[private-use]
762
+ except RuntimeError:
763
+ pass # No context available
764
+
672
765
  def remove_tool(self, name: str) -> None:
673
766
  """Remove a tool from the server.
674
767
 
@@ -681,6 +774,15 @@ class FastMCP(Generic[LifespanResultT]):
681
774
  self._tool_manager.remove_tool(name)
682
775
  self._cache.clear()
683
776
 
777
+ # Send notification if we're in a request context
778
+ try:
779
+ from fastmcp.server.dependencies import get_context
780
+
781
+ context = get_context()
782
+ context._queue_tool_list_changed() # type: ignore[private-use]
783
+ except RuntimeError:
784
+ pass # No context available
785
+
684
786
  @overload
685
787
  def tool(
686
788
  self,
@@ -736,15 +838,18 @@ class FastMCP(Generic[LifespanResultT]):
736
838
  name: Optional name for the tool (keyword-only, alternative to name_or_fn)
737
839
  description: Optional description of what the tool does
738
840
  tags: Optional set of tags for categorizing the tool
739
- annotations: Optional annotations about the tool's behavior (e.g. {"is_async": True})
841
+ annotations: Optional annotations about the tool's behavior
740
842
  exclude_args: Optional list of argument names to exclude from the tool schema
741
843
  enabled: Optional boolean to enable or disable the tool
742
844
 
743
- Example:
845
+ Examples:
846
+ Register a tool with a custom name:
847
+ ```python
744
848
  @server.tool
745
849
  def my_tool(x: int) -> str:
746
850
  return str(x)
747
851
 
852
+ # Register a tool with a custom name
748
853
  @server.tool
749
854
  def my_tool(x: int) -> str:
750
855
  return str(x)
@@ -759,6 +864,7 @@ class FastMCP(Generic[LifespanResultT]):
759
864
 
760
865
  # Direct function call
761
866
  server.tool(my_function, name="custom_name")
867
+ ```
762
868
  """
763
869
  if isinstance(annotations, dict):
764
870
  annotations = ToolAnnotations(**annotations)
@@ -823,23 +929,41 @@ class FastMCP(Generic[LifespanResultT]):
823
929
  enabled=enabled,
824
930
  )
825
931
 
826
- def add_resource(self, resource: Resource, key: str | None = None) -> None:
932
+ def add_resource(self, resource: Resource) -> None:
827
933
  """Add a resource to the server.
828
934
 
829
935
  Args:
830
936
  resource: A Resource instance to add
831
937
  """
832
938
 
833
- self._resource_manager.add_resource(resource, key=key)
939
+ self._resource_manager.add_resource(resource)
834
940
  self._cache.clear()
835
941
 
836
- def add_template(self, template: ResourceTemplate, key: str | None = None) -> None:
942
+ # Send notification if we're in a request context
943
+ try:
944
+ from fastmcp.server.dependencies import get_context
945
+
946
+ context = get_context()
947
+ context._queue_resource_list_changed() # type: ignore[private-use]
948
+ except RuntimeError:
949
+ pass # No context available
950
+
951
+ def add_template(self, template: ResourceTemplate) -> None:
837
952
  """Add a resource template to the server.
838
953
 
839
954
  Args:
840
955
  template: A ResourceTemplate instance to add
841
956
  """
842
- self._resource_manager.add_template(template, key=key)
957
+ self._resource_manager.add_template(template)
958
+
959
+ # Send notification if we're in a request context
960
+ try:
961
+ from fastmcp.server.dependencies import get_context
962
+
963
+ context = get_context()
964
+ context._queue_resource_list_changed() # type: ignore[private-use]
965
+ except RuntimeError:
966
+ pass # No context available
843
967
 
844
968
  def add_resource_fn(
845
969
  self,
@@ -913,7 +1037,9 @@ class FastMCP(Generic[LifespanResultT]):
913
1037
  tags: Optional set of tags for categorizing the resource
914
1038
  enabled: Optional boolean to enable or disable the resource
915
1039
 
916
- Example:
1040
+ Examples:
1041
+ Register a resource with a custom name:
1042
+ ```python
917
1043
  @server.resource("resource://my-resource")
918
1044
  def get_data() -> str:
919
1045
  return "Hello, world!"
@@ -936,6 +1062,7 @@ class FastMCP(Generic[LifespanResultT]):
936
1062
  async def get_weather(city: str) -> str:
937
1063
  data = await fetch_weather(city)
938
1064
  return f"Weather for {city}: {data}"
1065
+ ```
939
1066
  """
940
1067
  # Check if user passed function directly instead of calling decorator
941
1068
  if inspect.isroutine(uri):
@@ -1009,6 +1136,15 @@ class FastMCP(Generic[LifespanResultT]):
1009
1136
  self._prompt_manager.add_prompt(prompt)
1010
1137
  self._cache.clear()
1011
1138
 
1139
+ # Send notification if we're in a request context
1140
+ try:
1141
+ from fastmcp.server.dependencies import get_context
1142
+
1143
+ context = get_context()
1144
+ context._queue_prompt_list_changed() # type: ignore[private-use]
1145
+ except RuntimeError:
1146
+ pass # No context available
1147
+
1012
1148
  @overload
1013
1149
  def prompt(
1014
1150
  self,
@@ -1060,7 +1196,9 @@ class FastMCP(Generic[LifespanResultT]):
1060
1196
  tags: Optional set of tags for categorizing the prompt
1061
1197
  enabled: Optional boolean to enable or disable the prompt
1062
1198
 
1063
- Example:
1199
+ Examples:
1200
+
1201
+ ```python
1064
1202
  @server.prompt
1065
1203
  def analyze_table(table_name: str) -> list[Message]:
1066
1204
  schema = read_table_schema(table_name)
@@ -1104,6 +1242,7 @@ class FastMCP(Generic[LifespanResultT]):
1104
1242
 
1105
1243
  # Direct function call
1106
1244
  server.prompt(my_function, name="custom_name")
1245
+ ```
1107
1246
  """
1108
1247
 
1109
1248
  if isinstance(name_or_fn, classmethod):
@@ -1176,13 +1315,13 @@ class FastMCP(Generic[LifespanResultT]):
1176
1315
 
1177
1316
  async def run_http_async(
1178
1317
  self,
1179
- transport: Literal["streamable-http", "sse"] = "streamable-http",
1318
+ transport: Literal["http", "streamable-http", "sse"] = "http",
1180
1319
  host: str | None = None,
1181
1320
  port: int | None = None,
1182
1321
  log_level: str | None = None,
1183
1322
  path: str | None = None,
1184
1323
  uvicorn_config: dict[str, Any] | None = None,
1185
- middleware: list[Middleware] | None = None,
1324
+ middleware: list[ASGIMiddleware] | None = None,
1186
1325
  ) -> None:
1187
1326
  """Run the server using HTTP transport.
1188
1327
 
@@ -1253,7 +1392,7 @@ class FastMCP(Generic[LifespanResultT]):
1253
1392
  self,
1254
1393
  path: str | None = None,
1255
1394
  message_path: str | None = None,
1256
- middleware: list[Middleware] | None = None,
1395
+ middleware: list[ASGIMiddleware] | None = None,
1257
1396
  ) -> StarletteWithLifespan:
1258
1397
  """
1259
1398
  Create a Starlette app for the SSE server.
@@ -1283,7 +1422,7 @@ class FastMCP(Generic[LifespanResultT]):
1283
1422
  def streamable_http_app(
1284
1423
  self,
1285
1424
  path: str | None = None,
1286
- middleware: list[Middleware] | None = None,
1425
+ middleware: list[ASGIMiddleware] | None = None,
1287
1426
  ) -> StarletteWithLifespan:
1288
1427
  """
1289
1428
  Create a Starlette app for the StreamableHTTP server.
@@ -1304,10 +1443,10 @@ class FastMCP(Generic[LifespanResultT]):
1304
1443
  def http_app(
1305
1444
  self,
1306
1445
  path: str | None = None,
1307
- middleware: list[Middleware] | None = None,
1446
+ middleware: list[ASGIMiddleware] | None = None,
1308
1447
  json_response: bool | None = None,
1309
1448
  stateless_http: bool | None = None,
1310
- transport: Literal["streamable-http", "sse"] = "streamable-http",
1449
+ transport: Literal["http", "streamable-http", "sse"] = "http",
1311
1450
  ) -> StarletteWithLifespan:
1312
1451
  """Create a Starlette app using the specified HTTP transport.
1313
1452
 
@@ -1320,7 +1459,7 @@ class FastMCP(Generic[LifespanResultT]):
1320
1459
  A Starlette application configured with the specified transport
1321
1460
  """
1322
1461
 
1323
- if transport == "streamable-http":
1462
+ if transport in ("streamable-http", "http"):
1324
1463
  return create_streamable_http_app(
1325
1464
  server=self,
1326
1465
  streamable_http_path=path
@@ -1367,7 +1506,7 @@ class FastMCP(Generic[LifespanResultT]):
1367
1506
  stacklevel=2,
1368
1507
  )
1369
1508
  await self.run_http_async(
1370
- transport="streamable-http",
1509
+ transport="http",
1371
1510
  host=host,
1372
1511
  port=port,
1373
1512
  log_level=log_level,
@@ -1377,15 +1516,15 @@ class FastMCP(Generic[LifespanResultT]):
1377
1516
 
1378
1517
  def mount(
1379
1518
  self,
1380
- prefix: str,
1381
1519
  server: FastMCP[LifespanResultT],
1520
+ prefix: str | None = None,
1382
1521
  as_proxy: bool | None = None,
1383
1522
  *,
1384
1523
  tool_separator: str | None = None,
1385
1524
  resource_separator: str | None = None,
1386
1525
  prompt_separator: str | None = None,
1387
1526
  ) -> None:
1388
- """Mount another FastMCP server on this server with the given prefix.
1527
+ """Mount another FastMCP server on this server with an optional prefix.
1389
1528
 
1390
1529
  Unlike importing (with import_server), mounting establishes a dynamic connection
1391
1530
  between servers. When a client interacts with a mounted server's objects through
@@ -1393,7 +1532,7 @@ class FastMCP(Generic[LifespanResultT]):
1393
1532
  This means changes to the mounted server are immediately reflected when accessed
1394
1533
  through the parent.
1395
1534
 
1396
- When a server is mounted:
1535
+ When a server is mounted with a prefix:
1397
1536
  - Tools from the mounted server are accessible with prefixed names.
1398
1537
  Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
1399
1538
  - Resources are accessible with prefixed URIs.
@@ -1406,6 +1545,10 @@ class FastMCP(Generic[LifespanResultT]):
1406
1545
  Example: If server has a prompt named "weather_prompt", it will be available as
1407
1546
  "prefix_weather_prompt".
1408
1547
 
1548
+ When a server is mounted without a prefix (prefix=None), its tools, resources, templates,
1549
+ and prompts are accessible with their original names. Multiple servers can be mounted
1550
+ without prefixes, and they will be tried in order until a match is found.
1551
+
1409
1552
  There are two modes for mounting servers:
1410
1553
  1. Direct mounting (default when server has no custom lifespan): The parent server
1411
1554
  directly accesses the mounted server's objects in-memory for better performance.
@@ -1418,8 +1561,9 @@ class FastMCP(Generic[LifespanResultT]):
1418
1561
  execution, but with slightly higher overhead.
1419
1562
 
1420
1563
  Args:
1421
- prefix: Prefix to use for the mounted server's objects.
1422
1564
  server: The FastMCP server to mount.
1565
+ prefix: Optional prefix to use for the mounted server's objects. If None,
1566
+ the server's objects are accessible with their original names.
1423
1567
  as_proxy: Whether to treat the mounted server as a proxy. If None (default),
1424
1568
  automatically determined based on whether the server has a custom lifespan
1425
1569
  (True if it has a custom lifespan, False otherwise).
@@ -1431,6 +1575,20 @@ class FastMCP(Generic[LifespanResultT]):
1431
1575
  from fastmcp.client.transports import FastMCPTransport
1432
1576
  from fastmcp.server.proxy import FastMCPProxy
1433
1577
 
1578
+ # Deprecated since 2.9.0
1579
+ # Prior to 2.9.0, the first positional argument was the prefix and the
1580
+ # second was the server. Here we swap them if needed now that the prefix
1581
+ # is optional.
1582
+ if isinstance(server, str):
1583
+ if fastmcp.settings.deprecation_warnings:
1584
+ warnings.warn(
1585
+ "Mount prefixes are now optional and the first positional argument "
1586
+ "should be the server you want to mount.",
1587
+ DeprecationWarning,
1588
+ stacklevel=2,
1589
+ )
1590
+ server, prefix = cast(FastMCP[Any], prefix), server
1591
+
1434
1592
  if tool_separator is not None:
1435
1593
  # Deprecated since 2.4.0
1436
1594
  if fastmcp.settings.deprecation_warnings:
@@ -1469,21 +1627,22 @@ class FastMCP(Generic[LifespanResultT]):
1469
1627
  if as_proxy and not isinstance(server, FastMCPProxy):
1470
1628
  server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
1471
1629
 
1630
+ # Delegate mounting to all three managers
1472
1631
  mounted_server = MountedServer(
1473
- server=server,
1474
1632
  prefix=prefix,
1633
+ server=server,
1634
+ resource_prefix_format=self.resource_prefix_format,
1475
1635
  )
1476
- self._mounted_servers[prefix] = mounted_server
1477
- self._cache.clear()
1636
+ self._tool_manager.mount(mounted_server)
1637
+ self._resource_manager.mount(mounted_server)
1638
+ self._prompt_manager.mount(mounted_server)
1478
1639
 
1479
- def unmount(self, prefix: str) -> None:
1480
- self._mounted_servers.pop(prefix)
1481
1640
  self._cache.clear()
1482
1641
 
1483
1642
  async def import_server(
1484
1643
  self,
1485
- prefix: str,
1486
1644
  server: FastMCP[LifespanResultT],
1645
+ prefix: str | None = None,
1487
1646
  tool_separator: str | None = None,
1488
1647
  resource_separator: str | None = None,
1489
1648
  prompt_separator: str | None = None,
@@ -1497,7 +1656,7 @@ class FastMCP(Generic[LifespanResultT]):
1497
1656
  future changes to the imported server will not be reflected in the
1498
1657
  importing server. Server-level configurations and lifespans are not imported.
1499
1658
 
1500
- When a server is imported:
1659
+ When a server is imported with a prefix:
1501
1660
  - The tools are imported with prefixed names
1502
1661
  Example: If server has a tool named "get_weather", it will be
1503
1662
  available as "prefix_get_weather"
@@ -1511,14 +1670,33 @@ class FastMCP(Generic[LifespanResultT]):
1511
1670
  Example: If server has a prompt named "weather_prompt", it will be available as
1512
1671
  "prefix_weather_prompt"
1513
1672
 
1673
+ When a server is imported without a prefix (prefix=None), its tools, resources,
1674
+ templates, and prompts are imported with their original names.
1675
+
1514
1676
  Args:
1515
- prefix: The prefix to use for the imported server
1516
1677
  server: The FastMCP server to import
1678
+ prefix: Optional prefix to use for the imported server's objects. If None,
1679
+ objects are imported with their original names.
1517
1680
  tool_separator: Deprecated. Separator for tool names.
1518
1681
  resource_separator: Deprecated and ignored. Prefix is now
1519
1682
  applied using the protocol://prefix/path format
1520
1683
  prompt_separator: Deprecated. Separator for prompt names.
1521
1684
  """
1685
+
1686
+ # Deprecated since 2.9.0
1687
+ # Prior to 2.9.0, the first positional argument was the prefix and the
1688
+ # second was the server. Here we swap them if needed now that the prefix
1689
+ # is optional.
1690
+ if isinstance(server, str):
1691
+ if fastmcp.settings.deprecation_warnings:
1692
+ warnings.warn(
1693
+ "Import prefixes are now optional and the first positional argument "
1694
+ "should be the server you want to import.",
1695
+ DeprecationWarning,
1696
+ stacklevel=2,
1697
+ )
1698
+ server, prefix = cast(FastMCP[Any], prefix), server
1699
+
1522
1700
  if tool_separator is not None:
1523
1701
  # Deprecated since 2.4.0
1524
1702
  if fastmcp.settings.deprecation_warnings:
@@ -1549,29 +1727,39 @@ class FastMCP(Generic[LifespanResultT]):
1549
1727
  stacklevel=2,
1550
1728
  )
1551
1729
 
1552
- # Import tools from the mounted server
1553
- tool_prefix = f"{prefix}_"
1730
+ # Import tools from the server
1554
1731
  for key, tool in (await server.get_tools()).items():
1555
- self._tool_manager.add_tool(tool, key=f"{tool_prefix}{key}")
1732
+ if prefix:
1733
+ tool = tool.with_key(f"{prefix}_{key}")
1734
+ self._tool_manager.add_tool(tool)
1556
1735
 
1557
- # Import resources and templates from the mounted server
1736
+ # Import resources and templates from the server
1558
1737
  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)
1738
+ if prefix:
1739
+ resource_key = add_resource_prefix(
1740
+ key, prefix, self.resource_prefix_format
1741
+ )
1742
+ resource = resource.with_key(resource_key)
1743
+ self._resource_manager.add_resource(resource)
1561
1744
 
1562
1745
  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)
1746
+ if prefix:
1747
+ template_key = add_resource_prefix(
1748
+ key, prefix, self.resource_prefix_format
1749
+ )
1750
+ template = template.with_key(template_key)
1751
+ self._resource_manager.add_template(template)
1565
1752
 
1566
- # Import prompts from the mounted server
1567
- prompt_prefix = f"{prefix}_"
1753
+ # Import prompts from the server
1568
1754
  for key, prompt in (await server.get_prompts()).items():
1569
- self._prompt_manager.add_prompt(prompt, key=f"{prompt_prefix}{key}")
1755
+ if prefix:
1756
+ prompt = prompt.with_key(f"{prefix}_{key}")
1757
+ self._prompt_manager.add_prompt(prompt)
1570
1758
 
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}'")
1759
+ if prefix:
1760
+ logger.debug(f"Imported server {server.name} with prefix '{prefix}'")
1761
+ else:
1762
+ logger.debug(f"Imported server {server.name}")
1575
1763
 
1576
1764
  self._cache.clear()
1577
1765
 
@@ -1660,10 +1848,10 @@ class FastMCP(Generic[LifespanResultT]):
1660
1848
  ) -> FastMCPProxy:
1661
1849
  """Create a FastMCP proxy server for the given backend.
1662
1850
 
1663
- The ``backend`` argument can be either an existing :class:`~fastmcp.client.Client`
1664
- instance or any value accepted as the ``transport`` argument of
1665
- :class:`~fastmcp.client.Client`. This mirrors the convenience of the
1666
- ``Client`` constructor.
1851
+ The `backend` argument can be either an existing `fastmcp.client.Client`
1852
+ instance or any value accepted as the `transport` argument of
1853
+ `fastmcp.client.Client`. This mirrors the convenience of the
1854
+ `fastmcp.client.Client` constructor.
1667
1855
  """
1668
1856
  from fastmcp.client.client import Client
1669
1857
  from fastmcp.server.proxy import FastMCPProxy
@@ -1700,14 +1888,14 @@ class FastMCP(Generic[LifespanResultT]):
1700
1888
  Given a component, determine if it should be enabled. Returns True if it should be enabled; False if it should not.
1701
1889
 
1702
1890
  Rules:
1703
- If the component's enabled property is False, always return False.
1704
- If both include_tags and exclude_tags are None, return True.
1705
- If exclude_tags is provided, check each exclude tag:
1891
+ - If the component's enabled property is False, always return False.
1892
+ - If both include_tags and exclude_tags are None, return True.
1893
+ - If exclude_tags is provided, check each exclude tag:
1706
1894
  - If the exclude tag is a string, it must be present in the input tags to exclude.
1707
- If include_tags is provided, check each include tag:
1895
+ - If include_tags is provided, check each include tag:
1708
1896
  - If the include tag is a string, it must be present in the input tags to include.
1709
- If include_tags is provided and none of the include tags match, return False.
1710
- If include_tags is not provided, return True.
1897
+ - If include_tags is provided and none of the include tags match, return False.
1898
+ - If include_tags is not provided, return True.
1711
1899
  """
1712
1900
  if not component.enabled:
1713
1901
  return False
@@ -1728,60 +1916,11 @@ class FastMCP(Generic[LifespanResultT]):
1728
1916
  return True
1729
1917
 
1730
1918
 
1919
+ @dataclass
1731
1920
  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}_")
1921
+ prefix: str | None
1922
+ server: FastMCP[Any]
1923
+ resource_prefix_format: Literal["protocol", "path"] | None = None
1785
1924
 
1786
1925
 
1787
1926
  def add_resource_prefix(
@@ -1797,12 +1936,21 @@ def add_resource_prefix(
1797
1936
  The resource URI with the prefix added
1798
1937
 
1799
1938
  Examples:
1800
- >>> add_resource_prefix("resource://path/to/resource", "prefix")
1801
- "resource://prefix/path/to/resource" # with new style
1802
- >>> add_resource_prefix("resource://path/to/resource", "prefix")
1803
- "prefix+resource://path/to/resource" # with legacy style
1804
- >>> add_resource_prefix("resource:///absolute/path", "prefix")
1805
- "resource://prefix//absolute/path" # with new style
1939
+ With new style:
1940
+ ```python
1941
+ add_resource_prefix("resource://path/to/resource", "prefix")
1942
+ "resource://prefix/path/to/resource"
1943
+ ```
1944
+ With legacy style:
1945
+ ```python
1946
+ add_resource_prefix("resource://path/to/resource", "prefix")
1947
+ "prefix+resource://path/to/resource"
1948
+ ```
1949
+ With absolute path:
1950
+ ```python
1951
+ add_resource_prefix("resource:///absolute/path", "prefix")
1952
+ "resource://prefix//absolute/path"
1953
+ ```
1806
1954
 
1807
1955
  Raises:
1808
1956
  ValueError: If the URI doesn't match the expected protocol://path format
@@ -1848,12 +1996,21 @@ def remove_resource_prefix(
1848
1996
  The resource URI with the prefix removed
1849
1997
 
1850
1998
  Examples:
1851
- >>> remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
1852
- "resource://path/to/resource" # with new style
1853
- >>> remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
1854
- "resource://path/to/resource" # with legacy style
1855
- >>> remove_resource_prefix("resource://prefix//absolute/path", "prefix")
1856
- "resource:///absolute/path" # with new style
1999
+ With new style:
2000
+ ```python
2001
+ remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
2002
+ "resource://path/to/resource"
2003
+ ```
2004
+ With legacy style:
2005
+ ```python
2006
+ remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
2007
+ "resource://path/to/resource"
2008
+ ```
2009
+ With absolute path:
2010
+ ```python
2011
+ remove_resource_prefix("resource://prefix//absolute/path", "prefix")
2012
+ "resource:///absolute/path"
2013
+ ```
1857
2014
 
1858
2015
  Raises:
1859
2016
  ValueError: If the URI doesn't match the expected protocol://path format
@@ -1906,12 +2063,21 @@ def has_resource_prefix(
1906
2063
  True if the URI has the specified prefix, False otherwise
1907
2064
 
1908
2065
  Examples:
1909
- >>> has_resource_prefix("resource://prefix/path/to/resource", "prefix")
1910
- True # with new style
1911
- >>> has_resource_prefix("prefix+resource://path/to/resource", "prefix")
1912
- True # with legacy style
1913
- >>> has_resource_prefix("resource://other/path/to/resource", "prefix")
2066
+ With new style:
2067
+ ```python
2068
+ has_resource_prefix("resource://prefix/path/to/resource", "prefix")
2069
+ True
2070
+ ```
2071
+ With legacy style:
2072
+ ```python
2073
+ has_resource_prefix("prefix+resource://path/to/resource", "prefix")
2074
+ True
2075
+ ```
2076
+ With other path:
2077
+ ```python
2078
+ has_resource_prefix("resource://other/path/to/resource", "prefix")
1914
2079
  False
2080
+ ```
1915
2081
 
1916
2082
  Raises:
1917
2083
  ValueError: If the URI doesn't match the expected protocol://path format