fastmcp 2.12.5__py3-none-any.whl → 2.14.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 (133) hide show
  1. fastmcp/__init__.py +2 -23
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +19 -33
  5. fastmcp/cli/install/claude_code.py +6 -6
  6. fastmcp/cli/install/claude_desktop.py +3 -3
  7. fastmcp/cli/install/cursor.py +18 -12
  8. fastmcp/cli/install/gemini_cli.py +3 -3
  9. fastmcp/cli/install/mcp_json.py +3 -3
  10. fastmcp/cli/install/shared.py +0 -15
  11. fastmcp/cli/run.py +13 -8
  12. fastmcp/cli/tasks.py +110 -0
  13. fastmcp/client/__init__.py +9 -9
  14. fastmcp/client/auth/oauth.py +123 -225
  15. fastmcp/client/client.py +697 -95
  16. fastmcp/client/elicitation.py +11 -5
  17. fastmcp/client/logging.py +18 -14
  18. fastmcp/client/messages.py +7 -5
  19. fastmcp/client/oauth_callback.py +85 -171
  20. fastmcp/client/roots.py +2 -1
  21. fastmcp/client/sampling.py +1 -1
  22. fastmcp/client/tasks.py +614 -0
  23. fastmcp/client/transports.py +117 -30
  24. fastmcp/contrib/component_manager/__init__.py +1 -1
  25. fastmcp/contrib/component_manager/component_manager.py +2 -2
  26. fastmcp/contrib/component_manager/component_service.py +10 -26
  27. fastmcp/contrib/mcp_mixin/README.md +32 -1
  28. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  29. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  30. fastmcp/dependencies.py +25 -0
  31. fastmcp/experimental/sampling/handlers/openai.py +3 -3
  32. fastmcp/experimental/server/openapi/__init__.py +20 -21
  33. fastmcp/experimental/utilities/openapi/__init__.py +16 -47
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +54 -51
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +43 -21
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +161 -61
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -14
  45. fastmcp/server/auth/auth.py +197 -46
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1469 -298
  50. fastmcp/server/auth/oidc_proxy.py +91 -20
  51. fastmcp/server/auth/providers/auth0.py +40 -21
  52. fastmcp/server/auth/providers/aws.py +29 -3
  53. fastmcp/server/auth/providers/azure.py +312 -131
  54. fastmcp/server/auth/providers/debug.py +114 -0
  55. fastmcp/server/auth/providers/descope.py +86 -29
  56. fastmcp/server/auth/providers/discord.py +308 -0
  57. fastmcp/server/auth/providers/github.py +29 -8
  58. fastmcp/server/auth/providers/google.py +48 -9
  59. fastmcp/server/auth/providers/in_memory.py +29 -5
  60. fastmcp/server/auth/providers/introspection.py +281 -0
  61. fastmcp/server/auth/providers/jwt.py +48 -31
  62. fastmcp/server/auth/providers/oci.py +233 -0
  63. fastmcp/server/auth/providers/scalekit.py +238 -0
  64. fastmcp/server/auth/providers/supabase.py +188 -0
  65. fastmcp/server/auth/providers/workos.py +35 -17
  66. fastmcp/server/context.py +236 -116
  67. fastmcp/server/dependencies.py +503 -18
  68. fastmcp/server/elicitation.py +286 -48
  69. fastmcp/server/event_store.py +177 -0
  70. fastmcp/server/http.py +71 -20
  71. fastmcp/server/low_level.py +165 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +50 -39
  76. fastmcp/server/middleware/middleware.py +29 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi/__init__.py +35 -0
  80. fastmcp/{experimental/server → server}/openapi/components.py +15 -10
  81. fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
  82. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  83. fastmcp/server/proxy.py +72 -48
  84. fastmcp/server/server.py +1415 -733
  85. fastmcp/server/tasks/__init__.py +21 -0
  86. fastmcp/server/tasks/capabilities.py +22 -0
  87. fastmcp/server/tasks/config.py +89 -0
  88. fastmcp/server/tasks/converters.py +205 -0
  89. fastmcp/server/tasks/handlers.py +356 -0
  90. fastmcp/server/tasks/keys.py +93 -0
  91. fastmcp/server/tasks/protocol.py +355 -0
  92. fastmcp/server/tasks/subscriptions.py +205 -0
  93. fastmcp/settings.py +125 -113
  94. fastmcp/tools/__init__.py +1 -1
  95. fastmcp/tools/tool.py +138 -55
  96. fastmcp/tools/tool_manager.py +30 -112
  97. fastmcp/tools/tool_transform.py +12 -21
  98. fastmcp/utilities/cli.py +67 -28
  99. fastmcp/utilities/components.py +10 -5
  100. fastmcp/utilities/inspect.py +79 -23
  101. fastmcp/utilities/json_schema.py +4 -4
  102. fastmcp/utilities/json_schema_type.py +8 -8
  103. fastmcp/utilities/logging.py +118 -8
  104. fastmcp/utilities/mcp_config.py +1 -2
  105. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  106. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  107. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  108. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
  109. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  110. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  111. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  112. fastmcp/utilities/openapi/__init__.py +63 -0
  113. fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
  114. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  115. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
  116. fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
  117. fastmcp/utilities/tests.py +92 -5
  118. fastmcp/utilities/types.py +86 -16
  119. fastmcp/utilities/ui.py +626 -0
  120. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
  121. fastmcp-2.14.0.dist-info/RECORD +156 -0
  122. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
  123. fastmcp/cli/claude.py +0 -135
  124. fastmcp/server/auth/providers/bearer.py +0 -25
  125. fastmcp/server/openapi.py +0 -1083
  126. fastmcp/utilities/openapi.py +0 -1568
  127. fastmcp/utilities/storage.py +0 -204
  128. fastmcp-2.12.5.dist-info/RECORD +0 -134
  129. fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  130. fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
  131. fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
  132. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  133. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -2,13 +2,26 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import inspect
6
- import json
7
7
  import re
8
8
  import secrets
9
9
  import warnings
10
- from collections.abc import AsyncIterator, Awaitable, Callable
11
- from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
10
+ import weakref
11
+ from collections.abc import (
12
+ AsyncIterator,
13
+ Awaitable,
14
+ Callable,
15
+ Collection,
16
+ Mapping,
17
+ Sequence,
18
+ )
19
+ from contextlib import (
20
+ AbstractAsyncContextManager,
21
+ AsyncExitStack,
22
+ asynccontextmanager,
23
+ suppress,
24
+ )
12
25
  from dataclasses import dataclass
13
26
  from functools import partial
14
27
  from pathlib import Path
@@ -18,14 +31,18 @@ import anyio
18
31
  import httpx
19
32
  import mcp.types
20
33
  import uvicorn
34
+ from docket import Docket, Worker
21
35
  from mcp.server.lowlevel.helper_types import ReadResourceContents
22
36
  from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
23
37
  from mcp.server.stdio import stdio_server
38
+ from mcp.shared.exceptions import McpError
24
39
  from mcp.types import (
40
+ METHOD_NOT_FOUND,
25
41
  Annotations,
26
42
  AnyFunction,
27
43
  CallToolRequestParams,
28
44
  ContentBlock,
45
+ ErrorData,
29
46
  GetPromptResult,
30
47
  ToolAnnotations,
31
48
  )
@@ -43,11 +60,14 @@ import fastmcp
43
60
  import fastmcp.server
44
61
  from fastmcp.exceptions import DisabledError, NotFoundError
45
62
  from fastmcp.mcp_config import MCPConfig
46
- from fastmcp.prompts import Prompt, PromptManager
63
+ from fastmcp.prompts import Prompt
47
64
  from fastmcp.prompts.prompt import FunctionPrompt
48
- from fastmcp.resources import Resource, ResourceManager
49
- from fastmcp.resources.template import ResourceTemplate
65
+ from fastmcp.prompts.prompt_manager import PromptManager
66
+ from fastmcp.resources.resource import FunctionResource, Resource
67
+ from fastmcp.resources.resource_manager import ResourceManager
68
+ from fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate
50
69
  from fastmcp.server.auth import AuthProvider
70
+ from fastmcp.server.event_store import EventStore
51
71
  from fastmcp.server.http import (
52
72
  StarletteWithLifespan,
53
73
  create_sse_app,
@@ -55,9 +75,16 @@ from fastmcp.server.http import (
55
75
  )
56
76
  from fastmcp.server.low_level import LowLevelServer
57
77
  from fastmcp.server.middleware import Middleware, MiddlewareContext
78
+ from fastmcp.server.tasks.capabilities import get_task_capabilities
79
+ from fastmcp.server.tasks.config import TaskConfig
80
+ from fastmcp.server.tasks.handlers import (
81
+ handle_prompt_as_task,
82
+ handle_resource_as_task,
83
+ handle_tool_as_task,
84
+ )
58
85
  from fastmcp.settings import Settings
59
- from fastmcp.tools import ToolManager
60
86
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
87
+ from fastmcp.tools.tool_manager import ToolManager
61
88
  from fastmcp.tools.tool_transform import ToolTransformConfig
62
89
  from fastmcp.utilities.cli import log_server_banner
63
90
  from fastmcp.utilities.components import FastMCPComponent
@@ -66,29 +93,45 @@ from fastmcp.utilities.types import NotSet, NotSetT
66
93
 
67
94
  if TYPE_CHECKING:
68
95
  from fastmcp.client import Client
69
- from fastmcp.client.sampling import ServerSamplingHandler
96
+ from fastmcp.client.client import FastMCP1Server
70
97
  from fastmcp.client.transports import ClientTransport, ClientTransportT
71
- from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
72
- from fastmcp.experimental.server.openapi.routing import (
73
- ComponentFn as OpenAPIComponentFnNew,
74
- )
75
- from fastmcp.experimental.server.openapi.routing import RouteMap as RouteMapNew
76
- from fastmcp.experimental.server.openapi.routing import (
77
- RouteMapFn as OpenAPIRouteMapFnNew,
78
- )
79
98
  from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
80
99
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
81
100
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
82
101
  from fastmcp.server.proxy import FastMCPProxy
102
+ from fastmcp.server.sampling.handler import ServerSamplingHandler
103
+ from fastmcp.tools.tool import ToolResultSerializerType
83
104
 
84
105
  logger = get_logger(__name__)
85
106
 
107
+
108
+ def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
109
+ """Create a wrapper function with a custom __name__ for Docket registration.
110
+
111
+ Docket uses fn.__name__ as the key for function registration and lookup.
112
+ When mounting servers, we need unique names to avoid collisions between
113
+ mounted servers that have identically-named functions.
114
+ """
115
+ import functools
116
+
117
+ @functools.wraps(fn)
118
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
119
+ return await fn(*args, **kwargs)
120
+
121
+ wrapper.__name__ = name
122
+ return wrapper
123
+
124
+
86
125
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
87
126
  Transport = Literal["stdio", "http", "sse", "streamable-http"]
88
127
 
89
128
  # Compiled URI parsing regex to split a URI into protocol and path components
90
129
  URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
91
130
 
131
+ LifespanCallable = Callable[
132
+ ["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
133
+ ]
134
+
92
135
 
93
136
  @asynccontextmanager
94
137
  async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
@@ -98,26 +141,31 @@ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[An
98
141
  server: The server instance this lifespan is managing
99
142
 
100
143
  Returns:
101
- An empty context object
144
+ An empty dictionary as the lifespan result.
102
145
  """
103
146
  yield {}
104
147
 
105
148
 
106
- def _lifespan_wrapper(
107
- app: FastMCP[LifespanResultT],
108
- lifespan: Callable[
109
- [FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
110
- ],
149
+ def _lifespan_proxy(
150
+ fastmcp_server: FastMCP[LifespanResultT],
111
151
  ) -> Callable[
112
152
  [LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
113
153
  ]:
114
154
  @asynccontextmanager
115
155
  async def wrap(
116
- s: LowLevelServer[LifespanResultT],
156
+ low_level_server: LowLevelServer[LifespanResultT],
117
157
  ) -> AsyncIterator[LifespanResultT]:
118
- async with AsyncExitStack() as stack:
119
- context = await stack.enter_async_context(lifespan(app))
120
- yield context
158
+ if fastmcp_server._lifespan is default_lifespan:
159
+ yield {}
160
+ return
161
+
162
+ if not fastmcp_server._lifespan_result_set:
163
+ raise RuntimeError(
164
+ "FastMCP server has a lifespan defined but no lifespan result is set, which means the server's context manager was not entered. "
165
+ + " Are you running the server in a way that supports lifespans? If so, please file an issue at https://github.com/jlowin/fastmcp/issues."
166
+ )
167
+
168
+ yield fastmcp_server._lifespan_result
121
169
 
122
170
  return wrap
123
171
 
@@ -129,27 +177,23 @@ class FastMCP(Generic[LifespanResultT]):
129
177
  instructions: str | None = None,
130
178
  *,
131
179
  version: str | None = None,
132
- auth: AuthProvider | None | NotSetT = NotSet,
133
- middleware: list[Middleware] | None = None,
134
- lifespan: (
135
- Callable[
136
- [FastMCP[LifespanResultT]],
137
- AbstractAsyncContextManager[LifespanResultT],
138
- ]
139
- | None
140
- ) = None,
141
- dependencies: list[str] | None = None,
142
- resource_prefix_format: Literal["protocol", "path"] | None = None,
180
+ website_url: str | None = None,
181
+ icons: list[mcp.types.Icon] | None = None,
182
+ auth: AuthProvider | NotSetT | None = NotSet,
183
+ middleware: Sequence[Middleware] | None = None,
184
+ lifespan: LifespanCallable | None = None,
143
185
  mask_error_details: bool | None = None,
144
- tools: list[Tool | Callable[..., Any]] | None = None,
145
- tool_transformations: dict[str, ToolTransformConfig] | None = None,
146
- tool_serializer: Callable[[Any], str] | None = None,
147
- include_tags: set[str] | None = None,
148
- exclude_tags: set[str] | None = None,
186
+ tools: Sequence[Tool | Callable[..., Any]] | None = None,
187
+ tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
188
+ tool_serializer: ToolResultSerializerType | None = None,
189
+ include_tags: Collection[str] | None = None,
190
+ exclude_tags: Collection[str] | None = None,
149
191
  include_fastmcp_meta: bool | None = None,
150
192
  on_duplicate_tools: DuplicateBehavior | None = None,
151
193
  on_duplicate_resources: DuplicateBehavior | None = None,
152
194
  on_duplicate_prompts: DuplicateBehavior | None = None,
195
+ strict_input_validation: bool | None = None,
196
+ tasks: bool | None = None,
153
197
  # ---
154
198
  # ---
155
199
  # --- The following arguments are DEPRECATED ---
@@ -167,38 +211,48 @@ class FastMCP(Generic[LifespanResultT]):
167
211
  sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None,
168
212
  sampling_handler_behavior: Literal["always", "fallback"] | None = None,
169
213
  ):
170
- self.resource_prefix_format: Literal["protocol", "path"] = (
171
- resource_prefix_format or fastmcp.settings.resource_prefix_format
172
- )
214
+ # Resolve server default for background task support
215
+ self._support_tasks_by_default: bool = tasks if tasks is not None else False
216
+
217
+ # Docket instance (set during lifespan for cross-task access)
218
+ self._docket = None
173
219
 
174
220
  self._additional_http_routes: list[BaseRoute] = []
175
221
  self._mounted_servers: list[MountedServer] = []
176
- self._tool_manager = ToolManager(
222
+ self._is_mounted: bool = (
223
+ False # Set to True when this server is mounted on another
224
+ )
225
+ self._tool_manager: ToolManager = ToolManager(
177
226
  duplicate_behavior=on_duplicate_tools,
178
227
  mask_error_details=mask_error_details,
179
228
  transformations=tool_transformations,
180
229
  )
181
- self._resource_manager = ResourceManager(
230
+ self._resource_manager: ResourceManager = ResourceManager(
182
231
  duplicate_behavior=on_duplicate_resources,
183
232
  mask_error_details=mask_error_details,
184
233
  )
185
- self._prompt_manager = PromptManager(
234
+ self._prompt_manager: PromptManager = PromptManager(
186
235
  duplicate_behavior=on_duplicate_prompts,
187
236
  mask_error_details=mask_error_details,
188
237
  )
189
- self._tool_serializer = tool_serializer
238
+ self._tool_serializer: Callable[[Any], str] | None = tool_serializer
239
+
240
+ self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
241
+ self._lifespan_result: LifespanResultT | None = None
242
+ self._lifespan_result_set: bool = False
243
+ self._started: asyncio.Event = asyncio.Event()
190
244
 
191
- if lifespan is None:
192
- self._has_lifespan = False
193
- lifespan = default_lifespan
194
- else:
195
- self._has_lifespan = True
196
245
  # Generate random ID if no name provided
197
- self._mcp_server = LowLevelServer[LifespanResultT](
246
+ self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
247
+ LifespanResultT
248
+ ](
249
+ fastmcp=self,
198
250
  name=name or self.generate_name(),
199
- version=version,
251
+ version=version or fastmcp.__version__,
200
252
  instructions=instructions,
201
- lifespan=_lifespan_wrapper(self, lifespan),
253
+ website_url=website_url,
254
+ icons=icons,
255
+ lifespan=_lifespan_proxy(fastmcp_server=self),
202
256
  )
203
257
 
204
258
  # if auth is `NotSet`, try to create a provider from the environment
@@ -208,7 +262,7 @@ class FastMCP(Generic[LifespanResultT]):
208
262
  auth = fastmcp.settings.server_auth_class()
209
263
  else:
210
264
  auth = None
211
- self.auth = cast(AuthProvider | None, auth)
265
+ self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
212
266
 
213
267
  if tools:
214
268
  for tool in tools:
@@ -216,42 +270,37 @@ class FastMCP(Generic[LifespanResultT]):
216
270
  tool = Tool.from_function(tool, serializer=self._tool_serializer)
217
271
  self.add_tool(tool)
218
272
 
219
- self.include_tags = include_tags
220
- self.exclude_tags = exclude_tags
273
+ self.include_tags: set[str] | None = (
274
+ set(include_tags) if include_tags is not None else None
275
+ )
276
+ self.exclude_tags: set[str] | None = (
277
+ set(exclude_tags) if exclude_tags is not None else None
278
+ )
279
+
280
+ self.strict_input_validation: bool = (
281
+ strict_input_validation
282
+ if strict_input_validation is not None
283
+ else fastmcp.settings.strict_input_validation
284
+ )
221
285
 
222
- self.middleware = middleware or []
286
+ self.middleware: list[Middleware] = list(middleware or [])
223
287
 
224
288
  # Set up MCP protocol handlers
225
289
  self._setup_handlers()
226
290
 
227
- # Handle dependencies with deprecation warning
228
- # TODO: Remove dependencies parameter (deprecated in v2.11.4)
229
- if dependencies is not None:
230
- import warnings
231
-
232
- warnings.warn(
233
- "The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
234
- "Please specify dependencies in a fastmcp.json configuration file instead:\n"
235
- '{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
236
- f"{json.dumps(dependencies)}\n }}\n}}\n"
237
- "See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
238
- DeprecationWarning,
239
- stacklevel=2,
240
- )
241
- self.dependencies = (
242
- dependencies or fastmcp.settings.server_dependencies
243
- ) # TODO: Remove (deprecated in v2.11.4)
244
-
245
- self.sampling_handler = sampling_handler
246
- self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
291
+ self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
292
+ sampling_handler
293
+ )
294
+ self.sampling_handler_behavior: Literal["always", "fallback"] = (
295
+ sampling_handler_behavior or "fallback"
296
+ )
247
297
 
248
- self.include_fastmcp_meta = (
298
+ self.include_fastmcp_meta: bool = (
249
299
  include_fastmcp_meta
250
300
  if include_fastmcp_meta is not None
251
301
  else fastmcp.settings.include_fastmcp_meta
252
302
  )
253
303
 
254
- # handle deprecated settings
255
304
  self._handle_deprecated_settings(
256
305
  log_level=log_level,
257
306
  debug=debug,
@@ -333,6 +382,225 @@ class FastMCP(Generic[LifespanResultT]):
333
382
  def version(self) -> str | None:
334
383
  return self._mcp_server.version
335
384
 
385
+ @property
386
+ def website_url(self) -> str | None:
387
+ return self._mcp_server.website_url
388
+
389
+ @property
390
+ def icons(self) -> list[mcp.types.Icon]:
391
+ if self._mcp_server.icons is None:
392
+ return []
393
+ else:
394
+ return list(self._mcp_server.icons)
395
+
396
+ @property
397
+ def docket(self) -> Docket | None:
398
+ """Get the Docket instance if Docket support is enabled.
399
+
400
+ Returns None if Docket is not enabled or server hasn't been started yet.
401
+ """
402
+ return self._docket
403
+
404
+ @asynccontextmanager
405
+ async def _docket_lifespan(self) -> AsyncIterator[None]:
406
+ """Manage Docket instance and Worker for background task execution."""
407
+ from fastmcp import settings
408
+
409
+ # Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
410
+ from fastmcp.server.dependencies import (
411
+ _current_docket,
412
+ _current_server,
413
+ _current_worker,
414
+ )
415
+
416
+ server_token = _current_server.set(weakref.ref(self))
417
+
418
+ try:
419
+ # For directly mounted servers, the parent's Docket/Worker handles all
420
+ # task execution. Skip creating our own to avoid race conditions with
421
+ # multiple workers competing for tasks from the same queue.
422
+ if self._is_mounted:
423
+ yield
424
+ return
425
+
426
+ # Create Docket instance using configured name and URL
427
+ async with Docket(
428
+ name=settings.docket.name,
429
+ url=settings.docket.url,
430
+ ) as docket:
431
+ # Store on server instance for cross-task access (FastMCPTransport)
432
+ self._docket = docket
433
+
434
+ # Register local task-enabled tools/prompts/resources with Docket
435
+ # Only function-based variants support background tasks
436
+ # Register components where task execution is not "forbidden"
437
+ for tool in self._tool_manager._tools.values():
438
+ if (
439
+ isinstance(tool, FunctionTool)
440
+ and tool.task_config.mode != "forbidden"
441
+ ):
442
+ docket.register(tool.fn)
443
+
444
+ for prompt in self._prompt_manager._prompts.values():
445
+ if (
446
+ isinstance(prompt, FunctionPrompt)
447
+ and prompt.task_config.mode != "forbidden"
448
+ ):
449
+ # task execution requires async fn (validated at creation time)
450
+ docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
451
+
452
+ for resource in self._resource_manager._resources.values():
453
+ if (
454
+ isinstance(resource, FunctionResource)
455
+ and resource.task_config.mode != "forbidden"
456
+ ):
457
+ docket.register(resource.fn)
458
+
459
+ for template in self._resource_manager._templates.values():
460
+ if (
461
+ isinstance(template, FunctionResourceTemplate)
462
+ and template.task_config.mode != "forbidden"
463
+ ):
464
+ docket.register(template.fn)
465
+
466
+ # Also register functions from mounted servers so tasks can
467
+ # execute in the parent's Docket context
468
+ for mounted in self._mounted_servers:
469
+ await self._register_mounted_server_functions(
470
+ mounted.server, docket, mounted.prefix
471
+ )
472
+
473
+ # Set Docket in ContextVar so CurrentDocket can access it
474
+ docket_token = _current_docket.set(docket)
475
+ try:
476
+ # Build worker kwargs from settings
477
+ worker_kwargs: dict[str, Any] = {
478
+ "concurrency": settings.docket.concurrency,
479
+ "redelivery_timeout": settings.docket.redelivery_timeout,
480
+ "reconnection_delay": settings.docket.reconnection_delay,
481
+ }
482
+ if settings.docket.worker_name:
483
+ worker_kwargs["name"] = settings.docket.worker_name
484
+
485
+ # Create and start Worker
486
+ async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
487
+ # Set Worker in ContextVar so CurrentWorker can access it
488
+ worker_token = _current_worker.set(worker)
489
+ try:
490
+ worker_task = asyncio.create_task(worker.run_forever())
491
+ try:
492
+ yield
493
+ finally:
494
+ # Cancel worker task on exit with timeout to prevent hanging
495
+ worker_task.cancel()
496
+ with suppress(
497
+ asyncio.CancelledError, asyncio.TimeoutError
498
+ ):
499
+ await asyncio.wait_for(worker_task, timeout=2.0)
500
+ finally:
501
+ _current_worker.reset(worker_token)
502
+ finally:
503
+ # Reset ContextVar
504
+ _current_docket.reset(docket_token)
505
+ # Clear instance attribute
506
+ self._docket = None
507
+ finally:
508
+ # Reset server ContextVar
509
+ _current_server.reset(server_token)
510
+
511
+ async def _register_mounted_server_functions(
512
+ self, server: FastMCP, docket: Docket, prefix: str | None
513
+ ) -> None:
514
+ """Register task-enabled functions from a mounted server with Docket.
515
+
516
+ This enables background task execution for mounted server components
517
+ through the parent server's Docket context.
518
+
519
+ Args:
520
+ server: The mounted server whose functions to register
521
+ docket: The Docket instance to register with
522
+ prefix: The mount prefix to prepend to function names (matches
523
+ client-facing tool/prompt names)
524
+ """
525
+ # Register tools with prefixed names to avoid collisions
526
+ for tool in server._tool_manager._tools.values():
527
+ if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
528
+ # Use same naming as client-facing tool keys
529
+ fn_name = f"{prefix}_{tool.key}" if prefix else tool.key
530
+ named_fn = _create_named_fn_wrapper(tool.fn, fn_name)
531
+ docket.register(named_fn)
532
+
533
+ # Register prompts with prefixed names
534
+ for prompt in server._prompt_manager._prompts.values():
535
+ if (
536
+ isinstance(prompt, FunctionPrompt)
537
+ and prompt.task_config.mode != "forbidden"
538
+ ):
539
+ fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
540
+ named_fn = _create_named_fn_wrapper(
541
+ cast(Callable[..., Awaitable[Any]], prompt.fn), fn_name
542
+ )
543
+ docket.register(named_fn)
544
+
545
+ # Register resources with prefixed names (use name, not key/URI)
546
+ for resource in server._resource_manager._resources.values():
547
+ if (
548
+ isinstance(resource, FunctionResource)
549
+ and resource.task_config.mode != "forbidden"
550
+ ):
551
+ fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
552
+ named_fn = _create_named_fn_wrapper(resource.fn, fn_name)
553
+ docket.register(named_fn)
554
+
555
+ # Register resource templates with prefixed names (use name, not key/URI)
556
+ for template in server._resource_manager._templates.values():
557
+ if (
558
+ isinstance(template, FunctionResourceTemplate)
559
+ and template.task_config.mode != "forbidden"
560
+ ):
561
+ fn_name = f"{prefix}_{template.name}" if prefix else template.name
562
+ named_fn = _create_named_fn_wrapper(template.fn, fn_name)
563
+ docket.register(named_fn)
564
+
565
+ # Recursively register from nested mounted servers with accumulated prefix
566
+ for nested in server._mounted_servers:
567
+ nested_prefix = (
568
+ f"{prefix}_{nested.prefix}"
569
+ if prefix and nested.prefix
570
+ else (prefix or nested.prefix)
571
+ )
572
+ await self._register_mounted_server_functions(
573
+ nested.server, docket, nested_prefix
574
+ )
575
+
576
+ @asynccontextmanager
577
+ async def _lifespan_manager(self) -> AsyncIterator[None]:
578
+ if self._lifespan_result_set:
579
+ yield
580
+ return
581
+
582
+ async with (
583
+ self._lifespan(self) as user_lifespan_result,
584
+ self._docket_lifespan(),
585
+ ):
586
+ self._lifespan_result = user_lifespan_result
587
+ self._lifespan_result_set = True
588
+
589
+ async with AsyncExitStack[bool | None]() as stack:
590
+ for server in self._mounted_servers:
591
+ await stack.enter_async_context(
592
+ cm=server.server._lifespan_manager()
593
+ )
594
+
595
+ self._started.set()
596
+ try:
597
+ yield
598
+ finally:
599
+ self._started.clear()
600
+
601
+ self._lifespan_result_set = False
602
+ self._lifespan_result = None
603
+
336
604
  async def run_async(
337
605
  self,
338
606
  transport: Transport | None = None,
@@ -386,13 +654,267 @@ class FastMCP(Generic[LifespanResultT]):
386
654
 
387
655
  def _setup_handlers(self) -> None:
388
656
  """Set up core MCP protocol handlers."""
389
- self._mcp_server.list_tools()(self._mcp_list_tools)
390
- self._mcp_server.list_resources()(self._mcp_list_resources)
391
- self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
392
- self._mcp_server.list_prompts()(self._mcp_list_prompts)
393
- self._mcp_server.call_tool()(self._mcp_call_tool)
394
- self._mcp_server.read_resource()(self._mcp_read_resource)
395
- self._mcp_server.get_prompt()(self._mcp_get_prompt)
657
+ self._mcp_server.list_tools()(self._list_tools_mcp)
658
+ self._mcp_server.list_resources()(self._list_resources_mcp)
659
+ self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
660
+ self._mcp_server.list_prompts()(self._list_prompts_mcp)
661
+ self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
662
+ self._call_tool_mcp
663
+ )
664
+ # Register custom read_resource handler (SDK decorator doesn't support CreateTaskResult)
665
+ self._setup_read_resource_handler()
666
+ # Register custom get_prompt handler (SDK decorator doesn't support CreateTaskResult)
667
+ self._setup_get_prompt_handler()
668
+ # Register custom SEP-1686 task protocol handlers
669
+ self._setup_task_protocol_handlers()
670
+
671
+ def _setup_read_resource_handler(self) -> None:
672
+ """
673
+ Set up custom read_resource handler that supports task-augmented responses.
674
+
675
+ The SDK's read_resource decorator doesn't support CreateTaskResult returns,
676
+ so we register a custom handler that checks request_context.experimental.is_task.
677
+ """
678
+
679
+ async def handler(req: mcp.types.ReadResourceRequest) -> mcp.types.ServerResult:
680
+ uri = req.params.uri
681
+
682
+ # Check for task metadata via SDK's request context
683
+ task_meta = None
684
+ try:
685
+ ctx = self._mcp_server.request_context
686
+ if ctx.experimental.is_task:
687
+ task_meta = ctx.experimental.task_metadata
688
+ except (AttributeError, LookupError):
689
+ pass
690
+
691
+ # Check for task metadata and route appropriately
692
+ async with fastmcp.server.context.Context(fastmcp=self):
693
+ # Get resource including from mounted servers
694
+ resource = await self._get_resource_or_template_or_none(str(uri))
695
+ if (
696
+ resource
697
+ and self._should_enable_component(resource)
698
+ and hasattr(resource, "task_config")
699
+ ):
700
+ task_mode = resource.task_config.mode # type: ignore[union-attr]
701
+
702
+ # Enforce mode="required" - must have task metadata
703
+ if task_mode == "required" and not task_meta:
704
+ raise McpError(
705
+ ErrorData(
706
+ code=METHOD_NOT_FOUND,
707
+ message=f"Resource '{uri}' requires task-augmented execution",
708
+ )
709
+ )
710
+
711
+ # Route to background if task metadata present and mode allows
712
+ if task_meta and task_mode != "forbidden":
713
+ # For FunctionResource/FunctionResourceTemplate, use Docket
714
+ if isinstance(
715
+ resource,
716
+ FunctionResource | FunctionResourceTemplate,
717
+ ):
718
+ task_meta_dict = task_meta.model_dump(exclude_none=True)
719
+ return await handle_resource_as_task(
720
+ self, str(uri), resource, task_meta_dict
721
+ )
722
+
723
+ # Forbidden mode: task requested but mode="forbidden"
724
+ # Raise error since resources don't have isError field
725
+ if task_meta and task_mode == "forbidden":
726
+ raise McpError(
727
+ ErrorData(
728
+ code=METHOD_NOT_FOUND,
729
+ message=f"Resource '{uri}' does not support task-augmented execution",
730
+ )
731
+ )
732
+
733
+ # Synchronous execution
734
+ result = await self._read_resource_mcp(uri)
735
+
736
+ # Graceful degradation: if we got here with task_meta, something went wrong
737
+ # (This should be unreachable now that forbidden raises)
738
+ if task_meta:
739
+ mcp_contents = []
740
+ for item in result:
741
+ if isinstance(item.content, str):
742
+ mcp_contents.append(
743
+ mcp.types.TextResourceContents(
744
+ uri=uri,
745
+ text=item.content,
746
+ mimeType=item.mime_type or "text/plain",
747
+ )
748
+ )
749
+ elif isinstance(item.content, bytes):
750
+ import base64
751
+
752
+ mcp_contents.append(
753
+ mcp.types.BlobResourceContents(
754
+ uri=uri,
755
+ blob=base64.b64encode(item.content).decode(),
756
+ mimeType=item.mime_type or "application/octet-stream",
757
+ )
758
+ )
759
+ return mcp.types.ServerResult(
760
+ mcp.types.ReadResourceResult(
761
+ contents=mcp_contents,
762
+ _meta={
763
+ "modelcontextprotocol.io/task": {
764
+ "returned_immediately": True
765
+ }
766
+ },
767
+ )
768
+ )
769
+
770
+ # Convert to proper ServerResult
771
+ if isinstance(result, mcp.types.ServerResult):
772
+ return result
773
+
774
+ mcp_contents = []
775
+ for item in result:
776
+ if isinstance(item.content, str):
777
+ mcp_contents.append(
778
+ mcp.types.TextResourceContents(
779
+ uri=uri,
780
+ text=item.content,
781
+ mimeType=item.mime_type or "text/plain",
782
+ )
783
+ )
784
+ elif isinstance(item.content, bytes):
785
+ import base64
786
+
787
+ mcp_contents.append(
788
+ mcp.types.BlobResourceContents(
789
+ uri=uri,
790
+ blob=base64.b64encode(item.content).decode(),
791
+ mimeType=item.mime_type or "application/octet-stream",
792
+ )
793
+ )
794
+
795
+ return mcp.types.ServerResult(
796
+ mcp.types.ReadResourceResult(contents=mcp_contents)
797
+ )
798
+
799
+ self._mcp_server.request_handlers[mcp.types.ReadResourceRequest] = handler
800
+
801
+ def _setup_get_prompt_handler(self) -> None:
802
+ """
803
+ Set up custom get_prompt handler that supports task-augmented responses.
804
+
805
+ The SDK's get_prompt decorator doesn't support CreateTaskResult returns,
806
+ so we register a custom handler that checks request_context.experimental.is_task.
807
+ """
808
+
809
+ async def handler(req: mcp.types.GetPromptRequest) -> mcp.types.ServerResult:
810
+ name = req.params.name
811
+ arguments = req.params.arguments
812
+
813
+ # Check for task metadata via SDK's request context
814
+ task_meta = None
815
+ try:
816
+ ctx = self._mcp_server.request_context
817
+ if ctx.experimental.is_task:
818
+ task_meta = ctx.experimental.task_metadata
819
+ except (AttributeError, LookupError):
820
+ pass
821
+
822
+ # Check for task metadata and route appropriately
823
+ async with fastmcp.server.context.Context(fastmcp=self):
824
+ prompts = await self.get_prompts()
825
+ prompt = prompts.get(name)
826
+ if (
827
+ prompt
828
+ and self._should_enable_component(prompt)
829
+ and hasattr(prompt, "task_config")
830
+ and prompt.task_config
831
+ ):
832
+ task_mode = prompt.task_config.mode # type: ignore[union-attr]
833
+
834
+ # Enforce mode="required" - must have task metadata
835
+ if task_mode == "required" and not task_meta:
836
+ raise McpError(
837
+ ErrorData(
838
+ code=METHOD_NOT_FOUND,
839
+ message=f"Prompt '{name}' requires task-augmented execution",
840
+ )
841
+ )
842
+
843
+ # Route to background if task metadata present and mode allows
844
+ if task_meta and task_mode != "forbidden":
845
+ task_meta_dict = task_meta.model_dump(exclude_none=True)
846
+ result = await handle_prompt_as_task(
847
+ self, name, arguments, task_meta_dict
848
+ )
849
+ return mcp.types.ServerResult(result)
850
+
851
+ # Forbidden mode: task requested but mode="forbidden"
852
+ # Raise error since prompts don't have isError field
853
+ if task_meta and task_mode == "forbidden":
854
+ raise McpError(
855
+ ErrorData(
856
+ code=METHOD_NOT_FOUND,
857
+ message=f"Prompt '{name}' does not support task-augmented execution",
858
+ )
859
+ )
860
+
861
+ # Synchronous execution
862
+ result = await self._get_prompt_mcp(name, arguments)
863
+ return mcp.types.ServerResult(result)
864
+
865
+ self._mcp_server.request_handlers[mcp.types.GetPromptRequest] = handler
866
+
867
+ def _setup_task_protocol_handlers(self) -> None:
868
+ """Register SEP-1686 task protocol handlers with SDK."""
869
+ from mcp.types import (
870
+ CancelTaskRequest,
871
+ GetTaskPayloadRequest,
872
+ GetTaskRequest,
873
+ ListTasksRequest,
874
+ ServerResult,
875
+ )
876
+
877
+ from fastmcp.server.tasks.protocol import (
878
+ tasks_cancel_handler,
879
+ tasks_get_handler,
880
+ tasks_list_handler,
881
+ tasks_result_handler,
882
+ )
883
+
884
+ # Manually register handlers (SDK decorators fail with locally-defined functions)
885
+ # SDK expects handlers that receive Request objects and return ServerResult
886
+
887
+ async def handle_get_task(req: GetTaskRequest) -> ServerResult:
888
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
889
+ result = await tasks_get_handler(self, params)
890
+ return ServerResult(result)
891
+
892
+ async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:
893
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
894
+ result = await tasks_result_handler(self, params)
895
+ return ServerResult(result)
896
+
897
+ async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:
898
+ params = (
899
+ req.params.model_dump(by_alias=True, exclude_none=True)
900
+ if req.params
901
+ else {}
902
+ )
903
+ result = await tasks_list_handler(self, params)
904
+ return ServerResult(result)
905
+
906
+ async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:
907
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
908
+ result = await tasks_cancel_handler(self, params)
909
+ return ServerResult(result)
910
+
911
+ # Register directly with SDK (same as what decorators do internally)
912
+ self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task
913
+ self._mcp_server.request_handlers[GetTaskPayloadRequest] = (
914
+ handle_get_task_result
915
+ )
916
+ self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks
917
+ self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task
396
918
 
397
919
  async def _apply_middleware(
398
920
  self,
@@ -409,8 +931,24 @@ class FastMCP(Generic[LifespanResultT]):
409
931
  self.middleware.append(middleware)
410
932
 
411
933
  async def get_tools(self) -> dict[str, Tool]:
412
- """Get all registered tools, indexed by registered key."""
413
- return await self._tool_manager.get_tools()
934
+ """Get all tools (unfiltered), including mounted servers, indexed by key."""
935
+ all_tools = dict(await self._tool_manager.get_tools())
936
+
937
+ for mounted in self._mounted_servers:
938
+ try:
939
+ child_tools = await mounted.server.get_tools()
940
+ for key, tool in child_tools.items():
941
+ new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
942
+ all_tools[new_key] = tool.model_copy(key=new_key)
943
+ except Exception as e:
944
+ logger.warning(
945
+ f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
946
+ )
947
+ if fastmcp.settings.mounted_components_raise_on_load_error:
948
+ raise
949
+ continue
950
+
951
+ return all_tools
414
952
 
415
953
  async def get_tool(self, key: str) -> Tool:
416
954
  tools = await self.get_tools()
@@ -418,9 +956,63 @@ class FastMCP(Generic[LifespanResultT]):
418
956
  raise NotFoundError(f"Unknown tool: {key}")
419
957
  return tools[key]
420
958
 
959
+ async def _get_tool_with_task_config(self, key: str) -> Tool | None:
960
+ """Get a tool by key, returning None if not found.
961
+
962
+ Used for task config checking where we need the actual tool object
963
+ (including from mounted servers and proxies) but don't want to raise.
964
+ """
965
+ try:
966
+ return await self.get_tool(key)
967
+ except NotFoundError:
968
+ return None
969
+
970
+ async def _get_resource_or_template_or_none(
971
+ self, uri: str
972
+ ) -> Resource | ResourceTemplate | None:
973
+ """Get a resource or template by URI, searching recursively. Returns None if not found."""
974
+ try:
975
+ return await self.get_resource(uri)
976
+ except NotFoundError:
977
+ pass
978
+
979
+ templates = await self.get_resource_templates()
980
+ for template in templates.values():
981
+ if template.matches(uri):
982
+ return template
983
+
984
+ return None
985
+
421
986
  async def get_resources(self) -> dict[str, Resource]:
422
- """Get all registered resources, indexed by registered key."""
423
- return await self._resource_manager.get_resources()
987
+ """Get all resources (unfiltered), including mounted servers, indexed by key."""
988
+ all_resources = dict(await self._resource_manager.get_resources())
989
+
990
+ for mounted in self._mounted_servers:
991
+ try:
992
+ child_resources = await mounted.server.get_resources()
993
+ for key, resource in child_resources.items():
994
+ new_key = (
995
+ add_resource_prefix(key, mounted.prefix)
996
+ if mounted.prefix
997
+ else key
998
+ )
999
+ update = (
1000
+ {"name": f"{mounted.prefix}_{resource.name}"}
1001
+ if mounted.prefix and resource.name
1002
+ else {}
1003
+ )
1004
+ all_resources[new_key] = resource.model_copy(
1005
+ key=new_key, update=update
1006
+ )
1007
+ except Exception as e:
1008
+ logger.warning(
1009
+ f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
1010
+ )
1011
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1012
+ raise
1013
+ continue
1014
+
1015
+ return all_resources
424
1016
 
425
1017
  async def get_resource(self, key: str) -> Resource:
426
1018
  resources = await self.get_resources()
@@ -429,8 +1021,36 @@ class FastMCP(Generic[LifespanResultT]):
429
1021
  return resources[key]
430
1022
 
431
1023
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
432
- """Get all registered resource templates, indexed by registered key."""
433
- return await self._resource_manager.get_resource_templates()
1024
+ """Get all resource templates (unfiltered), including mounted servers, indexed by key."""
1025
+ all_templates = dict(await self._resource_manager.get_resource_templates())
1026
+
1027
+ for mounted in self._mounted_servers:
1028
+ try:
1029
+ child_templates = await mounted.server.get_resource_templates()
1030
+ for key, template in child_templates.items():
1031
+ new_key = (
1032
+ add_resource_prefix(key, mounted.prefix)
1033
+ if mounted.prefix
1034
+ else key
1035
+ )
1036
+ update: dict[str, Any] = {}
1037
+ if mounted.prefix:
1038
+ if template.name:
1039
+ update["name"] = f"{mounted.prefix}_{template.name}"
1040
+ # Update uri_template so matches() works with prefixed URIs
1041
+ update["uri_template"] = new_key
1042
+ all_templates[new_key] = template.model_copy(
1043
+ key=new_key, update=update
1044
+ )
1045
+ except Exception as e:
1046
+ logger.warning(
1047
+ f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
1048
+ )
1049
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1050
+ raise
1051
+ continue
1052
+
1053
+ return all_templates
434
1054
 
435
1055
  async def get_resource_template(self, key: str) -> ResourceTemplate:
436
1056
  """Get a registered resource template by key."""
@@ -440,10 +1060,24 @@ class FastMCP(Generic[LifespanResultT]):
440
1060
  return templates[key]
441
1061
 
442
1062
  async def get_prompts(self) -> dict[str, Prompt]:
443
- """
444
- List all available prompts.
445
- """
446
- return await self._prompt_manager.get_prompts()
1063
+ """Get all prompts (unfiltered), including mounted servers, indexed by key."""
1064
+ all_prompts = dict(await self._prompt_manager.get_prompts())
1065
+
1066
+ for mounted in self._mounted_servers:
1067
+ try:
1068
+ child_prompts = await mounted.server.get_prompts()
1069
+ for key, prompt in child_prompts.items():
1070
+ new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
1071
+ all_prompts[new_key] = prompt.model_copy(key=new_key)
1072
+ except Exception as e:
1073
+ logger.warning(
1074
+ f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
1075
+ )
1076
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1077
+ raise
1078
+ continue
1079
+
1080
+ return all_prompts
447
1081
 
448
1082
  async def get_prompt(self, key: str) -> Prompt:
449
1083
  prompts = await self.get_prompts()
@@ -519,11 +1153,15 @@ class FastMCP(Generic[LifespanResultT]):
519
1153
 
520
1154
  return routes
521
1155
 
522
- async def _mcp_list_tools(self) -> list[MCPTool]:
1156
+ async def _list_tools_mcp(self) -> list[MCPTool]:
1157
+ """
1158
+ List all available tools, in the format expected by the low-level MCP
1159
+ server.
1160
+ """
523
1161
  logger.debug(f"[{self.name}] Handler called: list_tools")
524
1162
 
525
1163
  async with fastmcp.server.context.Context(fastmcp=self):
526
- tools = await self._list_tools()
1164
+ tools = await self._list_tools_middleware()
527
1165
  return [
528
1166
  tool.to_mcp_tool(
529
1167
  name=tool.key,
@@ -532,24 +1170,11 @@ class FastMCP(Generic[LifespanResultT]):
532
1170
  for tool in tools
533
1171
  ]
534
1172
 
535
- async def _list_tools(self) -> list[Tool]:
1173
+ async def _list_tools_middleware(self) -> list[Tool]:
536
1174
  """
537
- List all available tools, in the format expected by the low-level MCP
538
- server.
1175
+ List all available tools, applying MCP middleware.
539
1176
  """
540
1177
 
541
- async def _handler(
542
- context: MiddlewareContext[mcp.types.ListToolsRequest],
543
- ) -> list[Tool]:
544
- tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
545
-
546
- mcp_tools: list[Tool] = []
547
- for tool in tools:
548
- if self._should_enable_component(tool):
549
- mcp_tools.append(tool)
550
-
551
- return mcp_tools
552
-
553
1178
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
554
1179
  # Create the middleware context.
555
1180
  mw_context = MiddlewareContext(
@@ -561,13 +1186,66 @@ class FastMCP(Generic[LifespanResultT]):
561
1186
  )
562
1187
 
563
1188
  # Apply the middleware chain.
564
- return await self._apply_middleware(mw_context, _handler)
1189
+ return list(
1190
+ await self._apply_middleware(
1191
+ context=mw_context, call_next=self._list_tools
1192
+ )
1193
+ )
565
1194
 
566
- async def _mcp_list_resources(self) -> list[MCPResource]:
1195
+ async def _list_tools(
1196
+ self,
1197
+ context: MiddlewareContext[mcp.types.ListToolsRequest],
1198
+ ) -> list[Tool]:
1199
+ """
1200
+ List all available tools.
1201
+ """
1202
+ # 1. Get local tools and filter them
1203
+ local_tools = await self._tool_manager.get_tools()
1204
+ filtered_local = [
1205
+ tool for tool in local_tools.values() if self._should_enable_component(tool)
1206
+ ]
1207
+
1208
+ # 2. Get tools from mounted servers
1209
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
1210
+ # Use a dict to implement "later wins" deduplication by key
1211
+ all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
1212
+
1213
+ for mounted in self._mounted_servers:
1214
+ try:
1215
+ child_tools = await mounted.server._list_tools_middleware()
1216
+ for tool in child_tools:
1217
+ # Apply parent server's filtering to mounted components
1218
+ if not self._should_enable_component(tool):
1219
+ continue
1220
+
1221
+ key = tool.key
1222
+ if mounted.prefix:
1223
+ key = f"{mounted.prefix}_{tool.key}"
1224
+ tool = tool.model_copy(key=key)
1225
+ # Later mounted servers override earlier ones
1226
+ all_tools[key] = tool
1227
+ except Exception as e:
1228
+ server_name = getattr(
1229
+ getattr(mounted, "server", None), "name", repr(mounted)
1230
+ )
1231
+ logger.warning(
1232
+ f"Failed to list tools from mounted server {server_name!r}: {e}"
1233
+ )
1234
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1235
+ raise
1236
+ continue
1237
+
1238
+ return list(all_tools.values())
1239
+
1240
+ async def _list_resources_mcp(self) -> list[MCPResource]:
1241
+ """
1242
+ List all available resources, in the format expected by the low-level MCP
1243
+ server.
1244
+ """
567
1245
  logger.debug(f"[{self.name}] Handler called: list_resources")
568
1246
 
569
1247
  async with fastmcp.server.context.Context(fastmcp=self):
570
- resources = await self._list_resources()
1248
+ resources = await self._list_resources_middleware()
571
1249
  return [
572
1250
  resource.to_mcp_resource(
573
1251
  uri=resource.key,
@@ -576,25 +1254,11 @@ class FastMCP(Generic[LifespanResultT]):
576
1254
  for resource in resources
577
1255
  ]
578
1256
 
579
- async def _list_resources(self) -> list[Resource]:
1257
+ async def _list_resources_middleware(self) -> list[Resource]:
580
1258
  """
581
- List all available resources, in the format expected by the low-level MCP
582
- server.
583
-
1259
+ List all available resources, applying MCP middleware.
584
1260
  """
585
1261
 
586
- async def _handler(
587
- context: MiddlewareContext[dict[str, Any]],
588
- ) -> list[Resource]:
589
- resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
590
-
591
- mcp_resources: list[Resource] = []
592
- for resource in resources:
593
- if self._should_enable_component(resource):
594
- mcp_resources.append(resource)
595
-
596
- return mcp_resources
597
-
598
1262
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
599
1263
  # Create the middleware context.
600
1264
  mw_context = MiddlewareContext(
@@ -606,13 +1270,71 @@ class FastMCP(Generic[LifespanResultT]):
606
1270
  )
607
1271
 
608
1272
  # Apply the middleware chain.
609
- return await self._apply_middleware(mw_context, _handler)
1273
+ return list(
1274
+ await self._apply_middleware(
1275
+ context=mw_context, call_next=self._list_resources
1276
+ )
1277
+ )
610
1278
 
611
- async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
1279
+ async def _list_resources(
1280
+ self,
1281
+ context: MiddlewareContext[dict[str, Any]],
1282
+ ) -> list[Resource]:
1283
+ """
1284
+ List all available resources.
1285
+ """
1286
+ # 1. Filter local resources
1287
+ local_resources = await self._resource_manager.get_resources()
1288
+ filtered_local = [
1289
+ resource
1290
+ for resource in local_resources.values()
1291
+ if self._should_enable_component(resource)
1292
+ ]
1293
+
1294
+ # 2. Get from mounted servers with resource prefix handling
1295
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
1296
+ # Use a dict to implement "later wins" deduplication by key
1297
+ all_resources: dict[str, Resource] = {
1298
+ resource.key: resource for resource in filtered_local
1299
+ }
1300
+
1301
+ for mounted in self._mounted_servers:
1302
+ try:
1303
+ child_resources = await mounted.server._list_resources_middleware()
1304
+ for resource in child_resources:
1305
+ # Apply parent server's filtering to mounted components
1306
+ if not self._should_enable_component(resource):
1307
+ continue
1308
+
1309
+ key = resource.key
1310
+ if mounted.prefix:
1311
+ key = add_resource_prefix(resource.key, mounted.prefix)
1312
+ resource = resource.model_copy(
1313
+ key=key,
1314
+ update={"name": f"{mounted.prefix}_{resource.name}"},
1315
+ )
1316
+ # Later mounted servers override earlier ones
1317
+ all_resources[key] = resource
1318
+ except Exception as e:
1319
+ server_name = getattr(
1320
+ getattr(mounted, "server", None), "name", repr(mounted)
1321
+ )
1322
+ logger.warning(f"Failed to list resources from {server_name!r}: {e}")
1323
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1324
+ raise
1325
+ continue
1326
+
1327
+ return list(all_resources.values())
1328
+
1329
+ async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
1330
+ """
1331
+ List all available resource templates, in the format expected by the low-level MCP
1332
+ server.
1333
+ """
612
1334
  logger.debug(f"[{self.name}] Handler called: list_resource_templates")
613
1335
 
614
1336
  async with fastmcp.server.context.Context(fastmcp=self):
615
- templates = await self._list_resource_templates()
1337
+ templates = await self._list_resource_templates_middleware()
616
1338
  return [
617
1339
  template.to_mcp_template(
618
1340
  uriTemplate=template.key,
@@ -621,25 +1343,12 @@ class FastMCP(Generic[LifespanResultT]):
621
1343
  for template in templates
622
1344
  ]
623
1345
 
624
- async def _list_resource_templates(self) -> list[ResourceTemplate]:
1346
+ async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
625
1347
  """
626
- List all available resource templates, in the format expected by the low-level MCP
627
- server.
1348
+ List all available resource templates, applying MCP middleware.
628
1349
 
629
1350
  """
630
1351
 
631
- async def _handler(
632
- context: MiddlewareContext[dict[str, Any]],
633
- ) -> list[ResourceTemplate]:
634
- templates = await self._resource_manager.list_resource_templates()
635
-
636
- mcp_templates: list[ResourceTemplate] = []
637
- for template in templates:
638
- if self._should_enable_component(template):
639
- mcp_templates.append(template)
640
-
641
- return mcp_templates
642
-
643
1352
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
644
1353
  # Create the middleware context.
645
1354
  mw_context = MiddlewareContext(
@@ -651,13 +1360,75 @@ class FastMCP(Generic[LifespanResultT]):
651
1360
  )
652
1361
 
653
1362
  # Apply the middleware chain.
654
- return await self._apply_middleware(mw_context, _handler)
1363
+ return list(
1364
+ await self._apply_middleware(
1365
+ context=mw_context, call_next=self._list_resource_templates
1366
+ )
1367
+ )
1368
+
1369
+ async def _list_resource_templates(
1370
+ self,
1371
+ context: MiddlewareContext[dict[str, Any]],
1372
+ ) -> list[ResourceTemplate]:
1373
+ """
1374
+ List all available resource templates.
1375
+ """
1376
+ # 1. Filter local templates
1377
+ local_templates = await self._resource_manager.get_resource_templates()
1378
+ filtered_local = [
1379
+ template
1380
+ for template in local_templates.values()
1381
+ if self._should_enable_component(template)
1382
+ ]
1383
+
1384
+ # 2. Get from mounted servers with resource prefix handling
1385
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
1386
+ # Use a dict to implement "later wins" deduplication by key
1387
+ all_templates: dict[str, ResourceTemplate] = {
1388
+ template.key: template for template in filtered_local
1389
+ }
655
1390
 
656
- async def _mcp_list_prompts(self) -> list[MCPPrompt]:
1391
+ for mounted in self._mounted_servers:
1392
+ try:
1393
+ child_templates = (
1394
+ await mounted.server._list_resource_templates_middleware()
1395
+ )
1396
+ for template in child_templates:
1397
+ # Apply parent server's filtering to mounted components
1398
+ if not self._should_enable_component(template):
1399
+ continue
1400
+
1401
+ key = template.key
1402
+ if mounted.prefix:
1403
+ key = add_resource_prefix(template.key, mounted.prefix)
1404
+ template = template.model_copy(
1405
+ key=key,
1406
+ update={"name": f"{mounted.prefix}_{template.name}"},
1407
+ )
1408
+ # Later mounted servers override earlier ones
1409
+ all_templates[key] = template
1410
+ except Exception as e:
1411
+ server_name = getattr(
1412
+ getattr(mounted, "server", None), "name", repr(mounted)
1413
+ )
1414
+ logger.warning(
1415
+ f"Failed to list resource templates from {server_name!r}: {e}"
1416
+ )
1417
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1418
+ raise
1419
+ continue
1420
+
1421
+ return list(all_templates.values())
1422
+
1423
+ async def _list_prompts_mcp(self) -> list[MCPPrompt]:
1424
+ """
1425
+ List all available prompts, in the format expected by the low-level MCP
1426
+ server.
1427
+ """
657
1428
  logger.debug(f"[{self.name}] Handler called: list_prompts")
658
1429
 
659
1430
  async with fastmcp.server.context.Context(fastmcp=self):
660
- prompts = await self._list_prompts()
1431
+ prompts = await self._list_prompts_middleware()
661
1432
  return [
662
1433
  prompt.to_mcp_prompt(
663
1434
  name=prompt.key,
@@ -666,25 +1437,12 @@ class FastMCP(Generic[LifespanResultT]):
666
1437
  for prompt in prompts
667
1438
  ]
668
1439
 
669
- async def _list_prompts(self) -> list[Prompt]:
1440
+ async def _list_prompts_middleware(self) -> list[Prompt]:
670
1441
  """
671
- List all available prompts, in the format expected by the low-level MCP
672
- server.
1442
+ List all available prompts, applying MCP middleware.
673
1443
 
674
1444
  """
675
1445
 
676
- async def _handler(
677
- context: MiddlewareContext[mcp.types.ListPromptsRequest],
678
- ) -> list[Prompt]:
679
- prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
680
-
681
- mcp_prompts: list[Prompt] = []
682
- for prompt in prompts:
683
- if self._should_enable_component(prompt):
684
- mcp_prompts.append(prompt)
685
-
686
- return mcp_prompts
687
-
688
1446
  async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
689
1447
  # Create the middleware context.
690
1448
  mw_context = MiddlewareContext(
@@ -696,15 +1454,72 @@ class FastMCP(Generic[LifespanResultT]):
696
1454
  )
697
1455
 
698
1456
  # Apply the middleware chain.
699
- return await self._apply_middleware(mw_context, _handler)
1457
+ return list(
1458
+ await self._apply_middleware(
1459
+ context=mw_context, call_next=self._list_prompts
1460
+ )
1461
+ )
1462
+
1463
+ async def _list_prompts(
1464
+ self,
1465
+ context: MiddlewareContext[mcp.types.ListPromptsRequest],
1466
+ ) -> list[Prompt]:
1467
+ """
1468
+ List all available prompts.
1469
+ """
1470
+ # 1. Filter local prompts
1471
+ local_prompts = await self._prompt_manager.get_prompts()
1472
+ filtered_local = [
1473
+ prompt
1474
+ for prompt in local_prompts.values()
1475
+ if self._should_enable_component(prompt)
1476
+ ]
1477
+
1478
+ # 2. Get from mounted servers
1479
+ # Mounted servers apply their own filtering, but we also apply parent's filtering
1480
+ # Use a dict to implement "later wins" deduplication by key
1481
+ all_prompts: dict[str, Prompt] = {
1482
+ prompt.key: prompt for prompt in filtered_local
1483
+ }
1484
+
1485
+ for mounted in self._mounted_servers:
1486
+ try:
1487
+ child_prompts = await mounted.server._list_prompts_middleware()
1488
+ for prompt in child_prompts:
1489
+ # Apply parent server's filtering to mounted components
1490
+ if not self._should_enable_component(prompt):
1491
+ continue
1492
+
1493
+ key = prompt.key
1494
+ if mounted.prefix:
1495
+ key = f"{mounted.prefix}_{prompt.key}"
1496
+ prompt = prompt.model_copy(key=key)
1497
+ # Later mounted servers override earlier ones
1498
+ all_prompts[key] = prompt
1499
+ except Exception as e:
1500
+ server_name = getattr(
1501
+ getattr(mounted, "server", None), "name", repr(mounted)
1502
+ )
1503
+ logger.warning(
1504
+ f"Failed to list prompts from mounted server {server_name!r}: {e}"
1505
+ )
1506
+ if fastmcp.settings.mounted_components_raise_on_load_error:
1507
+ raise
1508
+ continue
1509
+
1510
+ return list(all_prompts.values())
700
1511
 
701
- async def _mcp_call_tool(
1512
+ async def _call_tool_mcp(
702
1513
  self, key: str, arguments: dict[str, Any]
703
- ) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]:
1514
+ ) -> (
1515
+ list[ContentBlock]
1516
+ | tuple[list[ContentBlock], dict[str, Any]]
1517
+ | mcp.types.CallToolResult
1518
+ ):
704
1519
  """
705
1520
  Handle MCP 'callTool' requests.
706
1521
 
707
- Delegates to _call_tool, which should be overridden by FastMCP subclasses.
1522
+ Detects SEP-1686 task metadata and routes to background execution if supported.
708
1523
 
709
1524
  Args:
710
1525
  key: The name of the tool to call
@@ -719,29 +1534,81 @@ class FastMCP(Generic[LifespanResultT]):
719
1534
 
720
1535
  async with fastmcp.server.context.Context(fastmcp=self):
721
1536
  try:
722
- result = await self._call_tool(key, arguments)
1537
+ # Check for SEP-1686 task metadata via request context
1538
+ task_meta = None
1539
+ try:
1540
+ # Access task metadata from SDK's request context
1541
+ ctx = self._mcp_server.request_context
1542
+ if ctx.experimental.is_task:
1543
+ task_meta = ctx.experimental.task_metadata
1544
+ except (AttributeError, LookupError):
1545
+ # No request context available - proceed without task metadata
1546
+ pass
1547
+
1548
+ # Get tool from local manager, mounted servers, or proxy
1549
+ tool = await self._get_tool_with_task_config(key)
1550
+ if (
1551
+ tool
1552
+ and self._should_enable_component(tool)
1553
+ and hasattr(tool, "task_config")
1554
+ ):
1555
+ task_mode = tool.task_config.mode # type: ignore[union-attr]
1556
+
1557
+ # Enforce mode="required" - must have task metadata
1558
+ if task_mode == "required" and not task_meta:
1559
+ raise McpError(
1560
+ ErrorData(
1561
+ code=METHOD_NOT_FOUND,
1562
+ message=f"Tool '{key}' requires task-augmented execution",
1563
+ )
1564
+ )
1565
+
1566
+ # Route to background if task metadata present and mode allows
1567
+ if task_meta and task_mode != "forbidden":
1568
+ # For FunctionTool, use Docket for background execution
1569
+ if isinstance(tool, FunctionTool):
1570
+ task_meta_dict = task_meta.model_dump(exclude_none=True)
1571
+ return await handle_tool_as_task(
1572
+ self, key, arguments, task_meta_dict
1573
+ )
1574
+ # For ProxyTool/mounted tools, proceed with normal execution
1575
+ # They will forward task metadata to their backend
1576
+
1577
+ # Forbidden mode: task requested but mode="forbidden"
1578
+ # Return error result with returned_immediately=True
1579
+ if task_meta and task_mode == "forbidden":
1580
+ return mcp.types.CallToolResult(
1581
+ content=[
1582
+ mcp.types.TextContent(
1583
+ type="text",
1584
+ text=f"Tool '{key}' does not support task-augmented execution",
1585
+ )
1586
+ ],
1587
+ isError=True,
1588
+ _meta={
1589
+ "modelcontextprotocol.io/task": {
1590
+ "returned_immediately": True
1591
+ }
1592
+ },
1593
+ )
1594
+
1595
+ # Synchronous execution (normal path)
1596
+ result = await self._call_tool_middleware(key, arguments)
723
1597
  return result.to_mcp_result()
724
- except DisabledError:
725
- raise NotFoundError(f"Unknown tool: {key}")
726
- except NotFoundError:
727
- raise NotFoundError(f"Unknown tool: {key}")
1598
+ except DisabledError as e:
1599
+ raise NotFoundError(f"Unknown tool: {key}") from e
1600
+ except NotFoundError as e:
1601
+ raise NotFoundError(f"Unknown tool: {key}") from e
728
1602
 
729
- async def _call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
1603
+ async def _call_tool_middleware(
1604
+ self,
1605
+ key: str,
1606
+ arguments: dict[str, Any],
1607
+ ) -> ToolResult:
730
1608
  """
731
1609
  Applies this server's middleware and delegates the filtered call to the manager.
732
1610
  """
733
1611
 
734
- async def _handler(
735
- context: MiddlewareContext[mcp.types.CallToolRequestParams],
736
- ) -> ToolResult:
737
- tool = await self._tool_manager.get_tool(context.message.name)
738
- if not self._should_enable_component(tool):
739
- raise NotFoundError(f"Unknown tool: {context.message.name!r}")
740
-
741
- return await self._tool_manager.call_tool(
742
- key=context.message.name, arguments=context.message.arguments or {}
743
- )
744
-
745
1612
  mw_context = MiddlewareContext[CallToolRequestParams](
746
1613
  message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
747
1614
  source="client",
@@ -749,9 +1616,55 @@ class FastMCP(Generic[LifespanResultT]):
749
1616
  method="tools/call",
750
1617
  fastmcp_context=fastmcp.server.dependencies.get_context(),
751
1618
  )
752
- return await self._apply_middleware(mw_context, _handler)
1619
+ return await self._apply_middleware(
1620
+ context=mw_context, call_next=self._call_tool
1621
+ )
753
1622
 
754
- async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
1623
+ async def _call_tool(
1624
+ self,
1625
+ context: MiddlewareContext[mcp.types.CallToolRequestParams],
1626
+ ) -> ToolResult:
1627
+ """
1628
+ Call a tool
1629
+ """
1630
+ tool_name = context.message.name
1631
+
1632
+ # Try mounted servers in reverse order (later wins)
1633
+ for mounted in reversed(self._mounted_servers):
1634
+ try_name = tool_name
1635
+ if mounted.prefix:
1636
+ if not tool_name.startswith(f"{mounted.prefix}_"):
1637
+ continue
1638
+ try_name = tool_name[len(mounted.prefix) + 1 :]
1639
+
1640
+ try:
1641
+ # First, get the tool to check if parent's filter allows it
1642
+ # Use get_tool() instead of _tool_manager.get_tool() to support
1643
+ # nested mounted servers (tools mounted more than 2 levels deep)
1644
+ tool = await mounted.server.get_tool(try_name)
1645
+ if not self._should_enable_component(tool):
1646
+ # Parent filter blocks this tool, continue searching
1647
+ continue
1648
+
1649
+ return await mounted.server._call_tool_middleware(
1650
+ try_name, context.message.arguments or {}
1651
+ )
1652
+ except NotFoundError:
1653
+ continue
1654
+
1655
+ # Try local tools last (mounted servers override local)
1656
+ try:
1657
+ tool = await self._tool_manager.get_tool(tool_name)
1658
+ if self._should_enable_component(tool):
1659
+ return await self._tool_manager.call_tool(
1660
+ key=tool_name, arguments=context.message.arguments or {}
1661
+ )
1662
+ except NotFoundError:
1663
+ pass
1664
+
1665
+ raise NotFoundError(f"Unknown tool: {tool_name!r}")
1666
+
1667
+ async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
755
1668
  """
756
1669
  Handle MCP 'readResource' requests.
757
1670
 
@@ -761,39 +1674,27 @@ class FastMCP(Generic[LifespanResultT]):
761
1674
 
762
1675
  async with fastmcp.server.context.Context(fastmcp=self):
763
1676
  try:
764
- return await self._read_resource(uri)
765
- except DisabledError:
1677
+ # Task routing handled by custom handler
1678
+ return list[ReadResourceContents](
1679
+ await self._read_resource_middleware(uri)
1680
+ )
1681
+ except DisabledError as e:
766
1682
  # convert to NotFoundError to avoid leaking resource presence
767
- raise NotFoundError(f"Unknown resource: {str(uri)!r}")
768
- except NotFoundError:
1683
+ raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
1684
+ except NotFoundError as e:
769
1685
  # standardize NotFound message
770
- raise NotFoundError(f"Unknown resource: {str(uri)!r}")
1686
+ raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
771
1687
 
772
- async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
1688
+ async def _read_resource_middleware(
1689
+ self,
1690
+ uri: AnyUrl | str,
1691
+ ) -> list[ReadResourceContents]:
773
1692
  """
774
1693
  Applies this server's middleware and delegates the filtered call to the manager.
775
1694
  """
776
1695
 
777
- async def _handler(
778
- context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
779
- ) -> list[ReadResourceContents]:
780
- resource = await self._resource_manager.get_resource(context.message.uri)
781
- if not self._should_enable_component(resource):
782
- raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
783
-
784
- content = await self._resource_manager.read_resource(context.message.uri)
785
- return [
786
- ReadResourceContents(
787
- content=content,
788
- mime_type=resource.mime_type,
789
- )
790
- ]
791
-
792
1696
  # Convert string URI to AnyUrl if needed
793
- if isinstance(uri, str):
794
- uri_param = AnyUrl(uri)
795
- else:
796
- uri_param = uri
1697
+ uri_param = AnyUrl(uri) if isinstance(uri, str) else uri
797
1698
 
798
1699
  mw_context = MiddlewareContext(
799
1700
  message=mcp.types.ReadResourceRequestParams(uri=uri_param),
@@ -802,9 +1703,61 @@ class FastMCP(Generic[LifespanResultT]):
802
1703
  method="resources/read",
803
1704
  fastmcp_context=fastmcp.server.dependencies.get_context(),
804
1705
  )
805
- return await self._apply_middleware(mw_context, _handler)
1706
+ return list(
1707
+ await self._apply_middleware(
1708
+ context=mw_context, call_next=self._read_resource
1709
+ )
1710
+ )
1711
+
1712
+ async def _read_resource(
1713
+ self,
1714
+ context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
1715
+ ) -> list[ReadResourceContents]:
1716
+ """
1717
+ Read a resource
1718
+ """
1719
+ uri_str = str(context.message.uri)
1720
+
1721
+ # Try mounted servers in reverse order (later wins)
1722
+ for mounted in reversed(self._mounted_servers):
1723
+ key = uri_str
1724
+ if mounted.prefix:
1725
+ if not has_resource_prefix(key, mounted.prefix):
1726
+ continue
1727
+ key = remove_resource_prefix(key, mounted.prefix)
1728
+
1729
+ # First, get the resource/template to check if parent's filter allows it
1730
+ # Use get_resource_or_template to support nested mounted servers
1731
+ # (resources/templates mounted more than 2 levels deep)
1732
+ resource = await mounted.server._get_resource_or_template_or_none(key)
1733
+ if resource is None:
1734
+ continue
1735
+ if not self._should_enable_component(resource):
1736
+ # Parent filter blocks this resource, continue searching
1737
+ continue
1738
+ try:
1739
+ result = list(await mounted.server._read_resource_middleware(key))
1740
+ return result
1741
+ except NotFoundError:
1742
+ continue
1743
+
1744
+ # Try local resources last (mounted servers override local)
1745
+ try:
1746
+ resource = await self._resource_manager.get_resource(uri_str)
1747
+ if self._should_enable_component(resource):
1748
+ content = await self._resource_manager.read_resource(uri_str)
1749
+ return [
1750
+ ReadResourceContents(
1751
+ content=content,
1752
+ mime_type=resource.mime_type,
1753
+ )
1754
+ ]
1755
+ except NotFoundError:
1756
+ pass
806
1757
 
807
- async def _mcp_get_prompt(
1758
+ raise NotFoundError(f"Unknown resource: {uri_str!r}")
1759
+
1760
+ async def _get_prompt_mcp(
808
1761
  self, name: str, arguments: dict[str, Any] | None = None
809
1762
  ) -> GetPromptResult:
810
1763
  """
@@ -820,32 +1773,22 @@ class FastMCP(Generic[LifespanResultT]):
820
1773
 
821
1774
  async with fastmcp.server.context.Context(fastmcp=self):
822
1775
  try:
823
- return await self._get_prompt(name, arguments)
824
- except DisabledError:
1776
+ # Task routing handled by custom handler
1777
+ return await self._get_prompt_middleware(name, arguments)
1778
+ except DisabledError as e:
825
1779
  # convert to NotFoundError to avoid leaking prompt presence
826
- raise NotFoundError(f"Unknown prompt: {name}")
827
- except NotFoundError:
1780
+ raise NotFoundError(f"Unknown prompt: {name}") from e
1781
+ except NotFoundError as e:
828
1782
  # standardize NotFound message
829
- raise NotFoundError(f"Unknown prompt: {name}")
1783
+ raise NotFoundError(f"Unknown prompt: {name}") from e
830
1784
 
831
- async def _get_prompt(
1785
+ async def _get_prompt_middleware(
832
1786
  self, name: str, arguments: dict[str, Any] | None = None
833
1787
  ) -> GetPromptResult:
834
1788
  """
835
1789
  Applies this server's middleware and delegates the filtered call to the manager.
836
1790
  """
837
1791
 
838
- async def _handler(
839
- context: MiddlewareContext[mcp.types.GetPromptRequestParams],
840
- ) -> GetPromptResult:
841
- prompt = await self._prompt_manager.get_prompt(context.message.name)
842
- if not self._should_enable_component(prompt):
843
- raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
844
-
845
- return await self._prompt_manager.render_prompt(
846
- name=context.message.name, arguments=context.message.arguments
847
- )
848
-
849
1792
  mw_context = MiddlewareContext(
850
1793
  message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
851
1794
  source="client",
@@ -853,7 +1796,49 @@ class FastMCP(Generic[LifespanResultT]):
853
1796
  method="prompts/get",
854
1797
  fastmcp_context=fastmcp.server.dependencies.get_context(),
855
1798
  )
856
- return await self._apply_middleware(mw_context, _handler)
1799
+ return await self._apply_middleware(
1800
+ context=mw_context, call_next=self._get_prompt
1801
+ )
1802
+
1803
+ async def _get_prompt(
1804
+ self,
1805
+ context: MiddlewareContext[mcp.types.GetPromptRequestParams],
1806
+ ) -> GetPromptResult:
1807
+ name = context.message.name
1808
+
1809
+ # Try mounted servers in reverse order (later wins)
1810
+ for mounted in reversed(self._mounted_servers):
1811
+ try_name = name
1812
+ if mounted.prefix:
1813
+ if not name.startswith(f"{mounted.prefix}_"):
1814
+ continue
1815
+ try_name = name[len(mounted.prefix) + 1 :]
1816
+
1817
+ try:
1818
+ # First, get the prompt to check if parent's filter allows it
1819
+ # Use get_prompt() instead of _prompt_manager.get_prompt() to support
1820
+ # nested mounted servers (prompts mounted more than 2 levels deep)
1821
+ prompt = await mounted.server.get_prompt(try_name)
1822
+ if not self._should_enable_component(prompt):
1823
+ # Parent filter blocks this prompt, continue searching
1824
+ continue
1825
+ return await mounted.server._get_prompt_middleware(
1826
+ try_name, context.message.arguments
1827
+ )
1828
+ except NotFoundError:
1829
+ continue
1830
+
1831
+ # Try local prompts last (mounted servers override local)
1832
+ try:
1833
+ prompt = await self._prompt_manager.get_prompt(name)
1834
+ if self._should_enable_component(prompt):
1835
+ return await self._prompt_manager.render_prompt(
1836
+ name=name, arguments=context.message.arguments
1837
+ )
1838
+ except NotFoundError:
1839
+ pass
1840
+
1841
+ raise NotFoundError(f"Unknown prompt: {name!r}")
857
1842
 
858
1843
  def add_tool(self, tool: Tool) -> Tool:
859
1844
  """Add a tool to the server.
@@ -918,12 +1903,14 @@ class FastMCP(Generic[LifespanResultT]):
918
1903
  name: str | None = None,
919
1904
  title: str | None = None,
920
1905
  description: str | None = None,
1906
+ icons: list[mcp.types.Icon] | None = None,
921
1907
  tags: set[str] | None = None,
922
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1908
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
923
1909
  annotations: ToolAnnotations | dict[str, Any] | None = None,
924
1910
  exclude_args: list[str] | None = None,
925
1911
  meta: dict[str, Any] | None = None,
926
1912
  enabled: bool | None = None,
1913
+ task: bool | TaskConfig | None = None,
927
1914
  ) -> FunctionTool: ...
928
1915
 
929
1916
  @overload
@@ -934,12 +1921,14 @@ class FastMCP(Generic[LifespanResultT]):
934
1921
  name: str | None = None,
935
1922
  title: str | None = None,
936
1923
  description: str | None = None,
1924
+ icons: list[mcp.types.Icon] | None = None,
937
1925
  tags: set[str] | None = None,
938
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1926
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
939
1927
  annotations: ToolAnnotations | dict[str, Any] | None = None,
940
1928
  exclude_args: list[str] | None = None,
941
1929
  meta: dict[str, Any] | None = None,
942
1930
  enabled: bool | None = None,
1931
+ task: bool | TaskConfig | None = None,
943
1932
  ) -> Callable[[AnyFunction], FunctionTool]: ...
944
1933
 
945
1934
  def tool(
@@ -949,12 +1938,14 @@ class FastMCP(Generic[LifespanResultT]):
949
1938
  name: str | None = None,
950
1939
  title: str | None = None,
951
1940
  description: str | None = None,
1941
+ icons: list[mcp.types.Icon] | None = None,
952
1942
  tags: set[str] | None = None,
953
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1943
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
954
1944
  annotations: ToolAnnotations | dict[str, Any] | None = None,
955
1945
  exclude_args: list[str] | None = None,
956
1946
  meta: dict[str, Any] | None = None,
957
1947
  enabled: bool | None = None,
1948
+ task: bool | TaskConfig | None = None,
958
1949
  ) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
959
1950
  """Decorator to register a tool.
960
1951
 
@@ -976,7 +1967,9 @@ class FastMCP(Generic[LifespanResultT]):
976
1967
  tags: Optional set of tags for categorizing the tool
977
1968
  output_schema: Optional JSON schema for the tool's output
978
1969
  annotations: Optional annotations about the tool's behavior
979
- exclude_args: Optional list of argument names to exclude from the tool schema
1970
+ exclude_args: Optional list of argument names to exclude from the tool schema.
1971
+ Note: `exclude_args` will be deprecated in FastMCP 2.14 in favor of dependency
1972
+ injection with `Depends()` for better lifecycle management.
980
1973
  meta: Optional meta information about the tool
981
1974
  enabled: Optional boolean to enable or disable the tool
982
1975
 
@@ -1026,19 +2019,27 @@ class FastMCP(Generic[LifespanResultT]):
1026
2019
  fn = name_or_fn
1027
2020
  tool_name = name # Use keyword name if provided, otherwise None
1028
2021
 
2022
+ # Resolve task parameter
2023
+ supports_task: bool | TaskConfig = (
2024
+ task if task is not None else self._support_tasks_by_default
2025
+ )
2026
+
1029
2027
  # Register the tool immediately and return the tool object
2028
+ # Note: Deprecation warning for exclude_args is handled in Tool.from_function
1030
2029
  tool = Tool.from_function(
1031
2030
  fn,
1032
2031
  name=tool_name,
1033
2032
  title=title,
1034
2033
  description=description,
2034
+ icons=icons,
1035
2035
  tags=tags,
1036
2036
  output_schema=output_schema,
1037
- annotations=cast(ToolAnnotations | None, annotations),
2037
+ annotations=annotations,
1038
2038
  exclude_args=exclude_args,
1039
2039
  meta=meta,
1040
2040
  serializer=self._tool_serializer,
1041
2041
  enabled=enabled,
2042
+ task=supports_task,
1042
2043
  )
1043
2044
  self.add_tool(tool)
1044
2045
  return tool
@@ -1065,12 +2066,14 @@ class FastMCP(Generic[LifespanResultT]):
1065
2066
  name=tool_name,
1066
2067
  title=title,
1067
2068
  description=description,
2069
+ icons=icons,
1068
2070
  tags=tags,
1069
2071
  output_schema=output_schema,
1070
2072
  annotations=annotations,
1071
2073
  exclude_args=exclude_args,
1072
2074
  meta=meta,
1073
2075
  enabled=enabled,
2076
+ task=task,
1074
2077
  )
1075
2078
 
1076
2079
  def add_resource(self, resource: Resource) -> Resource:
@@ -1117,44 +2120,6 @@ class FastMCP(Generic[LifespanResultT]):
1117
2120
 
1118
2121
  return template
1119
2122
 
1120
- def add_resource_fn(
1121
- self,
1122
- fn: AnyFunction,
1123
- uri: str,
1124
- name: str | None = None,
1125
- description: str | None = None,
1126
- mime_type: str | None = None,
1127
- tags: set[str] | None = None,
1128
- ) -> None:
1129
- """Add a resource or template to the server from a function.
1130
-
1131
- If the URI contains parameters (e.g. "resource://{param}") or the function
1132
- has parameters, it will be registered as a template resource.
1133
-
1134
- Args:
1135
- fn: The function to register as a resource
1136
- uri: The URI for the resource
1137
- name: Optional name for the resource
1138
- description: Optional description of the resource
1139
- mime_type: Optional MIME type for the resource
1140
- tags: Optional set of tags for categorizing the resource
1141
- """
1142
- # deprecated since 2.7.0
1143
- if fastmcp.settings.deprecation_warnings:
1144
- warnings.warn(
1145
- "The add_resource_fn method is deprecated. Use the resource decorator instead.",
1146
- DeprecationWarning,
1147
- stacklevel=2,
1148
- )
1149
- self._resource_manager.add_resource_or_template_from_fn(
1150
- fn=fn,
1151
- uri=uri,
1152
- name=name,
1153
- description=description,
1154
- mime_type=mime_type,
1155
- tags=tags,
1156
- )
1157
-
1158
2123
  def resource(
1159
2124
  self,
1160
2125
  uri: str,
@@ -1162,11 +2127,13 @@ class FastMCP(Generic[LifespanResultT]):
1162
2127
  name: str | None = None,
1163
2128
  title: str | None = None,
1164
2129
  description: str | None = None,
2130
+ icons: list[mcp.types.Icon] | None = None,
1165
2131
  mime_type: str | None = None,
1166
2132
  tags: set[str] | None = None,
1167
2133
  enabled: bool | None = None,
1168
2134
  annotations: Annotations | dict[str, Any] | None = None,
1169
2135
  meta: dict[str, Any] | None = None,
2136
+ task: bool | TaskConfig | None = None,
1170
2137
  ) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
1171
2138
  """Decorator to register a function as a resource.
1172
2139
 
@@ -1231,8 +2198,6 @@ class FastMCP(Generic[LifespanResultT]):
1231
2198
  )
1232
2199
 
1233
2200
  def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
1234
- from fastmcp.server.context import Context
1235
-
1236
2201
  if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
1237
2202
  raise ValueError(
1238
2203
  inspect.cleandoc(
@@ -1245,14 +2210,18 @@ class FastMCP(Generic[LifespanResultT]):
1245
2210
  )
1246
2211
  )
1247
2212
 
2213
+ # Resolve task parameter
2214
+ supports_task: bool | TaskConfig = (
2215
+ task if task is not None else self._support_tasks_by_default
2216
+ )
2217
+
1248
2218
  # Check if this should be a template
1249
2219
  has_uri_params = "{" in uri and "}" in uri
1250
- # check if the function has any parameters (other than injected context)
1251
- has_func_params = any(
1252
- p
1253
- for p in inspect.signature(fn).parameters.values()
1254
- if p.annotation is not Context
1255
- )
2220
+ # Use wrapper to check for user-facing parameters
2221
+ from fastmcp.server.dependencies import without_injected_parameters
2222
+
2223
+ wrapper_fn = without_injected_parameters(fn)
2224
+ has_func_params = bool(inspect.signature(wrapper_fn).parameters)
1256
2225
 
1257
2226
  if has_uri_params or has_func_params:
1258
2227
  template = ResourceTemplate.from_function(
@@ -1261,11 +2230,13 @@ class FastMCP(Generic[LifespanResultT]):
1261
2230
  name=name,
1262
2231
  title=title,
1263
2232
  description=description,
2233
+ icons=icons,
1264
2234
  mime_type=mime_type,
1265
2235
  tags=tags,
1266
2236
  enabled=enabled,
1267
- annotations=cast(Annotations | None, annotations),
2237
+ annotations=annotations,
1268
2238
  meta=meta,
2239
+ task=supports_task,
1269
2240
  )
1270
2241
  self.add_template(template)
1271
2242
  return template
@@ -1276,11 +2247,13 @@ class FastMCP(Generic[LifespanResultT]):
1276
2247
  name=name,
1277
2248
  title=title,
1278
2249
  description=description,
2250
+ icons=icons,
1279
2251
  mime_type=mime_type,
1280
2252
  tags=tags,
1281
2253
  enabled=enabled,
1282
- annotations=cast(Annotations | None, annotations),
2254
+ annotations=annotations,
1283
2255
  meta=meta,
2256
+ task=supports_task,
1284
2257
  )
1285
2258
  self.add_resource(resource)
1286
2259
  return resource
@@ -1322,9 +2295,11 @@ class FastMCP(Generic[LifespanResultT]):
1322
2295
  name: str | None = None,
1323
2296
  title: str | None = None,
1324
2297
  description: str | None = None,
2298
+ icons: list[mcp.types.Icon] | None = None,
1325
2299
  tags: set[str] | None = None,
1326
2300
  enabled: bool | None = None,
1327
2301
  meta: dict[str, Any] | None = None,
2302
+ task: bool | TaskConfig | None = None,
1328
2303
  ) -> FunctionPrompt: ...
1329
2304
 
1330
2305
  @overload
@@ -1335,9 +2310,11 @@ class FastMCP(Generic[LifespanResultT]):
1335
2310
  name: str | None = None,
1336
2311
  title: str | None = None,
1337
2312
  description: str | None = None,
2313
+ icons: list[mcp.types.Icon] | None = None,
1338
2314
  tags: set[str] | None = None,
1339
2315
  enabled: bool | None = None,
1340
2316
  meta: dict[str, Any] | None = None,
2317
+ task: bool | TaskConfig | None = None,
1341
2318
  ) -> Callable[[AnyFunction], FunctionPrompt]: ...
1342
2319
 
1343
2320
  def prompt(
@@ -1347,9 +2324,11 @@ class FastMCP(Generic[LifespanResultT]):
1347
2324
  name: str | None = None,
1348
2325
  title: str | None = None,
1349
2326
  description: str | None = None,
2327
+ icons: list[mcp.types.Icon] | None = None,
1350
2328
  tags: set[str] | None = None,
1351
2329
  enabled: bool | None = None,
1352
2330
  meta: dict[str, Any] | None = None,
2331
+ task: bool | TaskConfig | None = None,
1353
2332
  ) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
1354
2333
  """Decorator to register a prompt.
1355
2334
 
@@ -1440,15 +2419,22 @@ class FastMCP(Generic[LifespanResultT]):
1440
2419
  fn = name_or_fn
1441
2420
  prompt_name = name # Use keyword name if provided, otherwise None
1442
2421
 
2422
+ # Resolve task parameter
2423
+ supports_task: bool | TaskConfig = (
2424
+ task if task is not None else self._support_tasks_by_default
2425
+ )
2426
+
1443
2427
  # Register the prompt immediately
1444
2428
  prompt = Prompt.from_function(
1445
2429
  fn=fn,
1446
2430
  name=prompt_name,
1447
2431
  title=title,
1448
2432
  description=description,
2433
+ icons=icons,
1449
2434
  tags=tags,
1450
2435
  enabled=enabled,
1451
2436
  meta=meta,
2437
+ task=supports_task,
1452
2438
  )
1453
2439
  self.add_prompt(prompt)
1454
2440
 
@@ -1476,9 +2462,11 @@ class FastMCP(Generic[LifespanResultT]):
1476
2462
  name=prompt_name,
1477
2463
  title=title,
1478
2464
  description=description,
2465
+ icons=icons,
1479
2466
  tags=tags,
1480
2467
  enabled=enabled,
1481
2468
  meta=meta,
2469
+ task=task,
1482
2470
  )
1483
2471
 
1484
2472
  async def run_stdio_async(
@@ -1498,15 +2486,25 @@ class FastMCP(Generic[LifespanResultT]):
1498
2486
  )
1499
2487
 
1500
2488
  with temporary_log_level(log_level):
1501
- async with stdio_server() as (read_stream, write_stream):
1502
- logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
1503
- await self._mcp_server.run(
1504
- read_stream,
1505
- write_stream,
1506
- self._mcp_server.create_initialization_options(
1507
- NotificationOptions(tools_changed=True)
1508
- ),
1509
- )
2489
+ async with self._lifespan_manager():
2490
+ async with stdio_server() as (read_stream, write_stream):
2491
+ logger.info(
2492
+ f"Starting MCP server {self.name!r} with transport 'stdio'"
2493
+ )
2494
+
2495
+ # Build experimental capabilities
2496
+ experimental_capabilities = get_task_capabilities()
2497
+
2498
+ await self._mcp_server.run(
2499
+ read_stream,
2500
+ write_stream,
2501
+ self._mcp_server.create_initialization_options(
2502
+ notification_options=NotificationOptions(
2503
+ tools_changed=True
2504
+ ),
2505
+ experimental_capabilities=experimental_capabilities,
2506
+ ),
2507
+ )
1510
2508
 
1511
2509
  async def run_http_async(
1512
2510
  self,
@@ -1518,6 +2516,7 @@ class FastMCP(Generic[LifespanResultT]):
1518
2516
  path: str | None = None,
1519
2517
  uvicorn_config: dict[str, Any] | None = None,
1520
2518
  middleware: list[ASGIMiddleware] | None = None,
2519
+ json_response: bool | None = None,
1521
2520
  stateless_http: bool | None = None,
1522
2521
  ) -> None:
1523
2522
  """Run the server using HTTP transport.
@@ -1530,6 +2529,7 @@ class FastMCP(Generic[LifespanResultT]):
1530
2529
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
1531
2530
  uvicorn_config: Additional configuration for the Uvicorn server
1532
2531
  middleware: A list of middleware to apply to the app
2532
+ json_response: Whether to use JSON response format (defaults to settings.json_response)
1533
2533
  stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
1534
2534
  """
1535
2535
  host = host or self._deprecated_settings.host
@@ -1542,6 +2542,7 @@ class FastMCP(Generic[LifespanResultT]):
1542
2542
  path=path,
1543
2543
  transport=transport,
1544
2544
  middleware=middleware,
2545
+ json_response=json_response,
1545
2546
  stateless_http=stateless_http,
1546
2547
  )
1547
2548
 
@@ -1561,106 +2562,28 @@ class FastMCP(Generic[LifespanResultT]):
1561
2562
  port=port,
1562
2563
  path=server_path,
1563
2564
  )
1564
- _uvicorn_config_from_user = uvicorn_config or {}
2565
+ uvicorn_config_from_user = uvicorn_config or {}
1565
2566
 
1566
2567
  config_kwargs: dict[str, Any] = {
1567
2568
  "timeout_graceful_shutdown": 0,
1568
2569
  "lifespan": "on",
2570
+ "ws": "websockets-sansio",
1569
2571
  }
1570
- config_kwargs.update(_uvicorn_config_from_user)
2572
+ config_kwargs.update(uvicorn_config_from_user)
1571
2573
 
1572
2574
  if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
1573
2575
  config_kwargs["log_level"] = default_log_level_to_use
1574
2576
 
1575
2577
  with temporary_log_level(log_level):
1576
- config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
1577
- server = uvicorn.Server(config)
1578
- path = app.state.path.lstrip("/") # type: ignore
1579
- logger.info(
1580
- f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
1581
- )
1582
-
1583
- await server.serve()
1584
-
1585
- async def run_sse_async(
1586
- self,
1587
- host: str | None = None,
1588
- port: int | None = None,
1589
- log_level: str | None = None,
1590
- path: str | None = None,
1591
- uvicorn_config: dict[str, Any] | None = None,
1592
- ) -> None:
1593
- """Run the server using SSE transport."""
1594
-
1595
- # Deprecated since 2.3.2
1596
- if fastmcp.settings.deprecation_warnings:
1597
- warnings.warn(
1598
- "The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
1599
- "modern (non-SSE) alternative, or create an SSE app with "
1600
- "`fastmcp.server.http.create_sse_app` and run it directly.",
1601
- DeprecationWarning,
1602
- stacklevel=2,
1603
- )
1604
- await self.run_http_async(
1605
- transport="sse",
1606
- host=host,
1607
- port=port,
1608
- log_level=log_level,
1609
- path=path,
1610
- uvicorn_config=uvicorn_config,
1611
- )
1612
-
1613
- def sse_app(
1614
- self,
1615
- path: str | None = None,
1616
- message_path: str | None = None,
1617
- middleware: list[ASGIMiddleware] | None = None,
1618
- ) -> StarletteWithLifespan:
1619
- """
1620
- Create a Starlette app for the SSE server.
1621
-
1622
- Args:
1623
- path: The path to the SSE endpoint
1624
- message_path: The path to the message endpoint
1625
- middleware: A list of middleware to apply to the app
1626
- """
1627
- # Deprecated since 2.3.2
1628
- if fastmcp.settings.deprecation_warnings:
1629
- warnings.warn(
1630
- "The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
1631
- "alternative, or call `fastmcp.server.http.create_sse_app` directly.",
1632
- DeprecationWarning,
1633
- stacklevel=2,
1634
- )
1635
- return create_sse_app(
1636
- server=self,
1637
- message_path=message_path or self._deprecated_settings.message_path,
1638
- sse_path=path or self._deprecated_settings.sse_path,
1639
- auth=self.auth,
1640
- debug=self._deprecated_settings.debug,
1641
- middleware=middleware,
1642
- )
1643
-
1644
- def streamable_http_app(
1645
- self,
1646
- path: str | None = None,
1647
- middleware: list[ASGIMiddleware] | None = None,
1648
- ) -> StarletteWithLifespan:
1649
- """
1650
- Create a Starlette app for the StreamableHTTP server.
2578
+ async with self._lifespan_manager():
2579
+ config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
2580
+ server = uvicorn.Server(config)
2581
+ path = app.state.path.lstrip("/") # type: ignore
2582
+ logger.info(
2583
+ f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
2584
+ )
1651
2585
 
1652
- Args:
1653
- path: The path to the StreamableHTTP endpoint
1654
- middleware: A list of middleware to apply to the app
1655
- """
1656
- # Deprecated since 2.3.2
1657
- if fastmcp.settings.deprecation_warnings:
1658
- warnings.warn(
1659
- "The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
1660
- DeprecationWarning,
1661
- stacklevel=2,
1662
- )
1663
- return self.http_app(path=path, middleware=middleware)
2586
+ await server.serve()
1664
2587
 
1665
2588
  def http_app(
1666
2589
  self,
@@ -1669,13 +2592,24 @@ class FastMCP(Generic[LifespanResultT]):
1669
2592
  json_response: bool | None = None,
1670
2593
  stateless_http: bool | None = None,
1671
2594
  transport: Literal["http", "streamable-http", "sse"] = "http",
2595
+ event_store: EventStore | None = None,
2596
+ retry_interval: int | None = None,
1672
2597
  ) -> StarletteWithLifespan:
1673
2598
  """Create a Starlette app using the specified HTTP transport.
1674
2599
 
1675
2600
  Args:
1676
2601
  path: The path for the HTTP endpoint
1677
2602
  middleware: A list of middleware to apply to the app
1678
- transport: Transport protocol to use - either "streamable-http" (default) or "sse"
2603
+ json_response: Whether to use JSON response format
2604
+ stateless_http: Whether to use stateless mode (new transport per request)
2605
+ transport: Transport protocol to use - "http", "streamable-http", or "sse"
2606
+ event_store: Optional event store for SSE polling/resumability. When set,
2607
+ enables clients to reconnect and resume receiving events after
2608
+ server-initiated disconnections. Only used with streamable-http transport.
2609
+ retry_interval: Optional retry interval in milliseconds for SSE polling.
2610
+ Controls how quickly clients should reconnect after server-initiated
2611
+ disconnections. Requires event_store to be set. Only used with
2612
+ streamable-http transport.
1679
2613
 
1680
2614
  Returns:
1681
2615
  A Starlette application configured with the specified transport
@@ -1686,7 +2620,8 @@ class FastMCP(Generic[LifespanResultT]):
1686
2620
  server=self,
1687
2621
  streamable_http_path=path
1688
2622
  or self._deprecated_settings.streamable_http_path,
1689
- event_store=None,
2623
+ event_store=event_store,
2624
+ retry_interval=retry_interval,
1690
2625
  auth=self.auth,
1691
2626
  json_response=(
1692
2627
  json_response
@@ -1711,40 +2646,11 @@ class FastMCP(Generic[LifespanResultT]):
1711
2646
  middleware=middleware,
1712
2647
  )
1713
2648
 
1714
- async def run_streamable_http_async(
1715
- self,
1716
- host: str | None = None,
1717
- port: int | None = None,
1718
- log_level: str | None = None,
1719
- path: str | None = None,
1720
- uvicorn_config: dict[str, Any] | None = None,
1721
- ) -> None:
1722
- # Deprecated since 2.3.2
1723
- if fastmcp.settings.deprecation_warnings:
1724
- warnings.warn(
1725
- "The run_streamable_http_async method is deprecated (as of 2.3.2). "
1726
- "Use run_http_async instead.",
1727
- DeprecationWarning,
1728
- stacklevel=2,
1729
- )
1730
- await self.run_http_async(
1731
- transport="http",
1732
- host=host,
1733
- port=port,
1734
- log_level=log_level,
1735
- path=path,
1736
- uvicorn_config=uvicorn_config,
1737
- )
1738
-
1739
2649
  def mount(
1740
2650
  self,
1741
2651
  server: FastMCP[LifespanResultT],
1742
2652
  prefix: str | None = None,
1743
2653
  as_proxy: bool | None = None,
1744
- *,
1745
- tool_separator: str | None = None,
1746
- resource_separator: str | None = None,
1747
- prompt_separator: str | None = None,
1748
2654
  ) -> None:
1749
2655
  """Mount another FastMCP server on this server with an optional prefix.
1750
2656
 
@@ -1789,82 +2695,33 @@ class FastMCP(Generic[LifespanResultT]):
1789
2695
  as_proxy: Whether to treat the mounted server as a proxy. If None (default),
1790
2696
  automatically determined based on whether the server has a custom lifespan
1791
2697
  (True if it has a custom lifespan, False otherwise).
1792
- tool_separator: Deprecated. Separator character for tool names.
1793
- resource_separator: Deprecated. Separator character for resource URIs.
1794
- prompt_separator: Deprecated. Separator character for prompt names.
1795
2698
  """
1796
2699
  from fastmcp.server.proxy import FastMCPProxy
1797
2700
 
1798
- # Deprecated since 2.9.0
1799
- # Prior to 2.9.0, the first positional argument was the prefix and the
1800
- # second was the server. Here we swap them if needed now that the prefix
1801
- # is optional.
1802
- if isinstance(server, str):
1803
- if fastmcp.settings.deprecation_warnings:
1804
- warnings.warn(
1805
- "Mount prefixes are now optional and the first positional argument "
1806
- "should be the server you want to mount.",
1807
- DeprecationWarning,
1808
- stacklevel=2,
1809
- )
1810
- server, prefix = cast(FastMCP[Any], prefix), server
1811
-
1812
- if tool_separator is not None:
1813
- # Deprecated since 2.4.0
1814
- if fastmcp.settings.deprecation_warnings:
1815
- warnings.warn(
1816
- "The tool_separator parameter is deprecated and will be removed in a future version. "
1817
- "Tools are now prefixed using 'prefix_toolname' format.",
1818
- DeprecationWarning,
1819
- stacklevel=2,
1820
- )
1821
-
1822
- if resource_separator is not None:
1823
- # Deprecated since 2.4.0
1824
- if fastmcp.settings.deprecation_warnings:
1825
- warnings.warn(
1826
- "The resource_separator parameter is deprecated and ignored. "
1827
- "Resource prefixes are now added using the protocol://prefix/path format.",
1828
- DeprecationWarning,
1829
- stacklevel=2,
1830
- )
1831
-
1832
- if prompt_separator is not None:
1833
- # Deprecated since 2.4.0
1834
- if fastmcp.settings.deprecation_warnings:
1835
- warnings.warn(
1836
- "The prompt_separator parameter is deprecated and will be removed in a future version. "
1837
- "Prompts are now prefixed using 'prefix_promptname' format.",
1838
- DeprecationWarning,
1839
- stacklevel=2,
1840
- )
1841
-
1842
2701
  # if as_proxy is not specified and the server has a custom lifespan,
1843
2702
  # we should treat it as a proxy
1844
2703
  if as_proxy is None:
1845
- as_proxy = server._has_lifespan
2704
+ as_proxy = server._lifespan != default_lifespan
1846
2705
 
1847
2706
  if as_proxy and not isinstance(server, FastMCPProxy):
1848
2707
  server = FastMCP.as_proxy(server)
1849
2708
 
2709
+ # Mark the server as mounted so it skips creating its own Docket/Worker.
2710
+ # The parent's Docket handles task execution, avoiding race conditions
2711
+ # with multiple workers competing for tasks from the same queue.
2712
+ server._is_mounted = True
2713
+
1850
2714
  # Delegate mounting to all three managers
1851
2715
  mounted_server = MountedServer(
1852
2716
  prefix=prefix,
1853
2717
  server=server,
1854
- resource_prefix_format=self.resource_prefix_format,
1855
2718
  )
1856
2719
  self._mounted_servers.append(mounted_server)
1857
- self._tool_manager.mount(mounted_server)
1858
- self._resource_manager.mount(mounted_server)
1859
- self._prompt_manager.mount(mounted_server)
1860
2720
 
1861
2721
  async def import_server(
1862
2722
  self,
1863
2723
  server: FastMCP[LifespanResultT],
1864
2724
  prefix: str | None = None,
1865
- tool_separator: str | None = None,
1866
- resource_separator: str | None = None,
1867
- prompt_separator: str | None = None,
1868
2725
  ) -> None:
1869
2726
  """
1870
2727
  Import the MCP objects from another FastMCP server into this one,
@@ -1896,56 +2753,7 @@ class FastMCP(Generic[LifespanResultT]):
1896
2753
  server: The FastMCP server to import
1897
2754
  prefix: Optional prefix to use for the imported server's objects. If None,
1898
2755
  objects are imported with their original names.
1899
- tool_separator: Deprecated. Separator for tool names.
1900
- resource_separator: Deprecated and ignored. Prefix is now
1901
- applied using the protocol://prefix/path format
1902
- prompt_separator: Deprecated. Separator for prompt names.
1903
- """
1904
-
1905
- # Deprecated since 2.9.0
1906
- # Prior to 2.9.0, the first positional argument was the prefix and the
1907
- # second was the server. Here we swap them if needed now that the prefix
1908
- # is optional.
1909
- if isinstance(server, str):
1910
- if fastmcp.settings.deprecation_warnings:
1911
- warnings.warn(
1912
- "Import prefixes are now optional and the first positional argument "
1913
- "should be the server you want to import.",
1914
- DeprecationWarning,
1915
- stacklevel=2,
1916
- )
1917
- server, prefix = cast(FastMCP[Any], prefix), server
1918
-
1919
- if tool_separator is not None:
1920
- # Deprecated since 2.4.0
1921
- if fastmcp.settings.deprecation_warnings:
1922
- warnings.warn(
1923
- "The tool_separator parameter is deprecated and will be removed in a future version. "
1924
- "Tools are now prefixed using 'prefix_toolname' format.",
1925
- DeprecationWarning,
1926
- stacklevel=2,
1927
- )
1928
-
1929
- if resource_separator is not None:
1930
- # Deprecated since 2.4.0
1931
- if fastmcp.settings.deprecation_warnings:
1932
- warnings.warn(
1933
- "The resource_separator parameter is deprecated and ignored. "
1934
- "Resource prefixes are now added using the protocol://prefix/path format.",
1935
- DeprecationWarning,
1936
- stacklevel=2,
1937
- )
1938
-
1939
- if prompt_separator is not None:
1940
- # Deprecated since 2.4.0
1941
- if fastmcp.settings.deprecation_warnings:
1942
- warnings.warn(
1943
- "The prompt_separator parameter is deprecated and will be removed in a future version. "
1944
- "Prompts are now prefixed using 'prefix_promptname' format.",
1945
- DeprecationWarning,
1946
- stacklevel=2,
1947
- )
1948
-
2756
+ """
1949
2757
  # Import tools from the server
1950
2758
  for key, tool in (await server.get_tools()).items():
1951
2759
  if prefix:
@@ -1955,9 +2763,7 @@ class FastMCP(Generic[LifespanResultT]):
1955
2763
  # Import resources and templates from the server
1956
2764
  for key, resource in (await server.get_resources()).items():
1957
2765
  if prefix:
1958
- resource_key = add_resource_prefix(
1959
- key, prefix, self.resource_prefix_format
1960
- )
2766
+ resource_key = add_resource_prefix(key, prefix)
1961
2767
  resource = resource.model_copy(
1962
2768
  update={"name": f"{prefix}_{resource.name}"}, key=resource_key
1963
2769
  )
@@ -1965,9 +2771,7 @@ class FastMCP(Generic[LifespanResultT]):
1965
2771
 
1966
2772
  for key, template in (await server.get_resource_templates()).items():
1967
2773
  if prefix:
1968
- template_key = add_resource_prefix(
1969
- key, prefix, self.resource_prefix_format
1970
- )
2774
+ template_key = add_resource_prefix(key, prefix)
1971
2775
  template = template.model_copy(
1972
2776
  update={"name": f"{prefix}_{template.name}"}, key=template_key
1973
2777
  )
@@ -1979,6 +2783,15 @@ class FastMCP(Generic[LifespanResultT]):
1979
2783
  prompt = prompt.model_copy(key=f"{prefix}_{key}")
1980
2784
  self._prompt_manager.add_prompt(prompt)
1981
2785
 
2786
+ if server._lifespan != default_lifespan:
2787
+ from warnings import warn
2788
+
2789
+ warn(
2790
+ message="When importing from a server with a lifespan, the lifespan from the imported server will not be used.",
2791
+ category=RuntimeWarning,
2792
+ stacklevel=2,
2793
+ )
2794
+
1982
2795
  if prefix:
1983
2796
  logger.debug(
1984
2797
  f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
@@ -1991,66 +2804,46 @@ class FastMCP(Generic[LifespanResultT]):
1991
2804
  cls,
1992
2805
  openapi_spec: dict[str, Any],
1993
2806
  client: httpx.AsyncClient,
1994
- route_maps: list[RouteMap] | list[RouteMapNew] | None = None,
1995
- route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None,
1996
- mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None,
2807
+ route_maps: list[RouteMap] | None = None,
2808
+ route_map_fn: OpenAPIRouteMapFn | None = None,
2809
+ mcp_component_fn: OpenAPIComponentFn | None = None,
1997
2810
  mcp_names: dict[str, str] | None = None,
1998
2811
  tags: set[str] | None = None,
1999
2812
  **settings: Any,
2000
- ) -> FastMCPOpenAPI | FastMCPOpenAPINew:
2813
+ ) -> FastMCPOpenAPI:
2001
2814
  """
2002
2815
  Create a FastMCP server from an OpenAPI specification.
2003
2816
  """
2004
-
2005
- # Check if experimental parser is enabled
2006
- if fastmcp.settings.experimental.enable_new_openapi_parser:
2007
- from fastmcp.experimental.server.openapi import FastMCPOpenAPI
2008
-
2009
- return FastMCPOpenAPI(
2010
- openapi_spec=openapi_spec,
2011
- client=client,
2012
- route_maps=cast(Any, route_maps),
2013
- route_map_fn=cast(Any, route_map_fn),
2014
- mcp_component_fn=cast(Any, mcp_component_fn),
2015
- mcp_names=mcp_names,
2016
- tags=tags,
2017
- **settings,
2018
- )
2019
- else:
2020
- logger.info(
2021
- "Using legacy OpenAPI parser. To use the new parser, set "
2022
- "FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
2023
- "was introduced for testing in 2.11 and will become the default soon."
2024
- )
2025
- from .openapi import FastMCPOpenAPI
2026
-
2027
- return FastMCPOpenAPI(
2028
- openapi_spec=openapi_spec,
2029
- client=client,
2030
- route_maps=cast(Any, route_maps),
2031
- route_map_fn=cast(Any, route_map_fn),
2032
- mcp_component_fn=cast(Any, mcp_component_fn),
2033
- mcp_names=mcp_names,
2034
- tags=tags,
2035
- **settings,
2036
- )
2817
+ from .openapi import FastMCPOpenAPI
2818
+
2819
+ return FastMCPOpenAPI(
2820
+ openapi_spec=openapi_spec,
2821
+ client=client,
2822
+ route_maps=route_maps,
2823
+ route_map_fn=route_map_fn,
2824
+ mcp_component_fn=mcp_component_fn,
2825
+ mcp_names=mcp_names,
2826
+ tags=tags,
2827
+ **settings,
2828
+ )
2037
2829
 
2038
2830
  @classmethod
2039
2831
  def from_fastapi(
2040
2832
  cls,
2041
2833
  app: Any,
2042
2834
  name: str | None = None,
2043
- route_maps: list[RouteMap] | list[RouteMapNew] | None = None,
2044
- route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None,
2045
- mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None,
2835
+ route_maps: list[RouteMap] | None = None,
2836
+ route_map_fn: OpenAPIRouteMapFn | None = None,
2837
+ mcp_component_fn: OpenAPIComponentFn | None = None,
2046
2838
  mcp_names: dict[str, str] | None = None,
2047
2839
  httpx_client_kwargs: dict[str, Any] | None = None,
2048
2840
  tags: set[str] | None = None,
2049
2841
  **settings: Any,
2050
- ) -> FastMCPOpenAPI | FastMCPOpenAPINew:
2842
+ ) -> FastMCPOpenAPI:
2051
2843
  """
2052
2844
  Create a FastMCP server from a FastAPI application.
2053
2845
  """
2846
+ from .openapi import FastMCPOpenAPI
2054
2847
 
2055
2848
  if httpx_client_kwargs is None:
2056
2849
  httpx_client_kwargs = {}
@@ -2063,40 +2856,17 @@ class FastMCP(Generic[LifespanResultT]):
2063
2856
 
2064
2857
  name = name or app.title
2065
2858
 
2066
- # Check if experimental parser is enabled
2067
- if fastmcp.settings.experimental.enable_new_openapi_parser:
2068
- from fastmcp.experimental.server.openapi import FastMCPOpenAPI
2069
-
2070
- return FastMCPOpenAPI(
2071
- openapi_spec=app.openapi(),
2072
- client=client,
2073
- name=name,
2074
- route_maps=cast(Any, route_maps),
2075
- route_map_fn=cast(Any, route_map_fn),
2076
- mcp_component_fn=cast(Any, mcp_component_fn),
2077
- mcp_names=mcp_names,
2078
- tags=tags,
2079
- **settings,
2080
- )
2081
- else:
2082
- logger.info(
2083
- "Using legacy OpenAPI parser. To use the new parser, set "
2084
- "FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
2085
- "was introduced for testing in 2.11 and will become the default soon."
2086
- )
2087
- from .openapi import FastMCPOpenAPI
2088
-
2089
- return FastMCPOpenAPI(
2090
- openapi_spec=app.openapi(),
2091
- client=client,
2092
- name=name,
2093
- route_maps=cast(Any, route_maps),
2094
- route_map_fn=cast(Any, route_map_fn),
2095
- mcp_component_fn=cast(Any, mcp_component_fn),
2096
- mcp_names=mcp_names,
2097
- tags=tags,
2098
- **settings,
2099
- )
2859
+ return FastMCPOpenAPI(
2860
+ openapi_spec=app.openapi(),
2861
+ client=client,
2862
+ name=name,
2863
+ route_maps=route_maps,
2864
+ route_map_fn=route_map_fn,
2865
+ mcp_component_fn=mcp_component_fn,
2866
+ mcp_names=mcp_names,
2867
+ tags=tags,
2868
+ **settings,
2869
+ )
2100
2870
 
2101
2871
  @classmethod
2102
2872
  def as_proxy(
@@ -2105,6 +2875,7 @@ class FastMCP(Generic[LifespanResultT]):
2105
2875
  Client[ClientTransportT]
2106
2876
  | ClientTransport
2107
2877
  | FastMCP[Any]
2878
+ | FastMCP1Server
2108
2879
  | AnyUrl
2109
2880
  | Path
2110
2881
  | MCPConfig
@@ -2129,8 +2900,8 @@ class FastMCP(Generic[LifespanResultT]):
2129
2900
  # - Connected clients: reuse existing session for all requests
2130
2901
  # - Disconnected clients: create fresh sessions per request for isolation
2131
2902
  if client.is_connected():
2132
- _proxy_logger = get_logger(__name__)
2133
- _proxy_logger.info(
2903
+ proxy_logger = get_logger(__name__)
2904
+ proxy_logger.info(
2134
2905
  "Proxy detected connected client - reusing existing session for all requests. "
2135
2906
  "This may cause context mixing in concurrent scenarios."
2136
2907
  )
@@ -2147,7 +2918,7 @@ class FastMCP(Generic[LifespanResultT]):
2147
2918
 
2148
2919
  client_factory = fresh_client_factory
2149
2920
  else:
2150
- base_client = ProxyClient(backend)
2921
+ base_client = ProxyClient(backend) # type: ignore
2151
2922
 
2152
2923
  # Fresh client created from transport - use fresh sessions per request
2153
2924
  def proxy_client_factory():
@@ -2157,23 +2928,6 @@ class FastMCP(Generic[LifespanResultT]):
2157
2928
 
2158
2929
  return FastMCPProxy(client_factory=client_factory, **settings)
2159
2930
 
2160
- @classmethod
2161
- def from_client(
2162
- cls, client: Client[ClientTransportT], **settings: Any
2163
- ) -> FastMCPProxy:
2164
- """
2165
- Create a FastMCP proxy server from a FastMCP client.
2166
- """
2167
- # Deprecated since 2.3.5
2168
- if fastmcp.settings.deprecation_warnings:
2169
- warnings.warn(
2170
- "FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
2171
- DeprecationWarning,
2172
- stacklevel=2,
2173
- )
2174
-
2175
- return cls.as_proxy(client, **settings)
2176
-
2177
2931
  def _should_enable_component(
2178
2932
  self,
2179
2933
  component: FastMCPComponent,
@@ -2202,10 +2956,7 @@ class FastMCP(Generic[LifespanResultT]):
2202
2956
  return False
2203
2957
 
2204
2958
  if self.include_tags is not None:
2205
- if any(itag in component.tags for itag in self.include_tags):
2206
- return True
2207
- else:
2208
- return False
2959
+ return bool(any(itag in component.tags for itag in self.include_tags))
2209
2960
 
2210
2961
  return True
2211
2962
 
@@ -2223,13 +2974,10 @@ class FastMCP(Generic[LifespanResultT]):
2223
2974
  class MountedServer:
2224
2975
  prefix: str | None
2225
2976
  server: FastMCP[Any]
2226
- resource_prefix_format: Literal["protocol", "path"] | None = None
2227
2977
 
2228
2978
 
2229
- def add_resource_prefix(
2230
- uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
2231
- ) -> str:
2232
- """Add a prefix to a resource URI.
2979
+ def add_resource_prefix(uri: str, prefix: str) -> str:
2980
+ """Add a prefix to a resource URI using path formatting (resource://prefix/path).
2233
2981
 
2234
2982
  Args:
2235
2983
  uri: The original resource URI
@@ -2239,16 +2987,10 @@ def add_resource_prefix(
2239
2987
  The resource URI with the prefix added
2240
2988
 
2241
2989
  Examples:
2242
- With new style:
2243
2990
  ```python
2244
2991
  add_resource_prefix("resource://path/to/resource", "prefix")
2245
2992
  "resource://prefix/path/to/resource"
2246
2993
  ```
2247
- With legacy style:
2248
- ```python
2249
- add_resource_prefix("resource://path/to/resource", "prefix")
2250
- "prefix+resource://path/to/resource"
2251
- ```
2252
2994
  With absolute path:
2253
2995
  ```python
2254
2996
  add_resource_prefix("resource:///absolute/path", "prefix")
@@ -2261,54 +3003,32 @@ def add_resource_prefix(
2261
3003
  if not prefix:
2262
3004
  return uri
2263
3005
 
2264
- # Get the server settings to check for legacy format preference
2265
-
2266
- if prefix_format is None:
2267
- prefix_format = fastmcp.settings.resource_prefix_format
3006
+ # Split the URI into protocol and path
3007
+ match = URI_PATTERN.match(uri)
3008
+ if not match:
3009
+ raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2268
3010
 
2269
- if prefix_format == "protocol":
2270
- # Legacy style: prefix+protocol://path
2271
- return f"{prefix}+{uri}"
2272
- elif prefix_format == "path":
2273
- # New style: protocol://prefix/path
2274
- # Split the URI into protocol and path
2275
- match = URI_PATTERN.match(uri)
2276
- if not match:
2277
- raise ValueError(
2278
- f"Invalid URI format: {uri}. Expected protocol://path format."
2279
- )
3011
+ protocol, path = match.groups()
2280
3012
 
2281
- protocol, path = match.groups()
3013
+ # Add the prefix to the path
3014
+ return f"{protocol}{prefix}/{path}"
2282
3015
 
2283
- # Add the prefix to the path
2284
- return f"{protocol}{prefix}/{path}"
2285
- else:
2286
- raise ValueError(f"Invalid prefix format: {prefix_format}")
2287
3016
 
2288
-
2289
- def remove_resource_prefix(
2290
- uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
2291
- ) -> str:
3017
+ def remove_resource_prefix(uri: str, prefix: str) -> str:
2292
3018
  """Remove a prefix from a resource URI.
2293
3019
 
2294
3020
  Args:
2295
3021
  uri: The resource URI with a prefix
2296
3022
  prefix: The prefix to remove
2297
- prefix_format: The format of the prefix to remove
3023
+
2298
3024
  Returns:
2299
3025
  The resource URI with the prefix removed
2300
3026
 
2301
3027
  Examples:
2302
- With new style:
2303
3028
  ```python
2304
3029
  remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
2305
3030
  "resource://path/to/resource"
2306
3031
  ```
2307
- With legacy style:
2308
- ```python
2309
- remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
2310
- "resource://path/to/resource"
2311
- ```
2312
3032
  With absolute path:
2313
3033
  ```python
2314
3034
  remove_resource_prefix("resource://prefix//absolute/path", "prefix")
@@ -2321,41 +3041,24 @@ def remove_resource_prefix(
2321
3041
  if not prefix:
2322
3042
  return uri
2323
3043
 
2324
- if prefix_format is None:
2325
- prefix_format = fastmcp.settings.resource_prefix_format
3044
+ # Split the URI into protocol and path
3045
+ match = URI_PATTERN.match(uri)
3046
+ if not match:
3047
+ raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2326
3048
 
2327
- if prefix_format == "protocol":
2328
- # Legacy style: prefix+protocol://path
2329
- legacy_prefix = f"{prefix}+"
2330
- if uri.startswith(legacy_prefix):
2331
- return uri[len(legacy_prefix) :]
2332
- return uri
2333
- elif prefix_format == "path":
2334
- # New style: protocol://prefix/path
2335
- # Split the URI into protocol and path
2336
- match = URI_PATTERN.match(uri)
2337
- if not match:
2338
- raise ValueError(
2339
- f"Invalid URI format: {uri}. Expected protocol://path format."
2340
- )
2341
-
2342
- protocol, path = match.groups()
3049
+ protocol, path = match.groups()
2343
3050
 
2344
- # Check if the path starts with the prefix followed by a /
2345
- prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
2346
- path_match = re.match(prefix_pattern, path)
2347
- if not path_match:
2348
- return uri
3051
+ # Check if the path starts with the prefix followed by a /
3052
+ prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
3053
+ path_match = re.match(prefix_pattern, path)
3054
+ if not path_match:
3055
+ return uri
2349
3056
 
2350
- # Return the URI without the prefix
2351
- return f"{protocol}{path_match.group(1)}"
2352
- else:
2353
- raise ValueError(f"Invalid prefix format: {prefix_format}")
3057
+ # Return the URI without the prefix
3058
+ return f"{protocol}{path_match.group(1)}"
2354
3059
 
2355
3060
 
2356
- def has_resource_prefix(
2357
- uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
2358
- ) -> bool:
3061
+ def has_resource_prefix(uri: str, prefix: str) -> bool:
2359
3062
  """Check if a resource URI has a specific prefix.
2360
3063
 
2361
3064
  Args:
@@ -2366,16 +3069,10 @@ def has_resource_prefix(
2366
3069
  True if the URI has the specified prefix, False otherwise
2367
3070
 
2368
3071
  Examples:
2369
- With new style:
2370
3072
  ```python
2371
3073
  has_resource_prefix("resource://prefix/path/to/resource", "prefix")
2372
3074
  True
2373
3075
  ```
2374
- With legacy style:
2375
- ```python
2376
- has_resource_prefix("prefix+resource://path/to/resource", "prefix")
2377
- True
2378
- ```
2379
3076
  With other path:
2380
3077
  ```python
2381
3078
  has_resource_prefix("resource://other/path/to/resource", "prefix")
@@ -2388,28 +3085,13 @@ def has_resource_prefix(
2388
3085
  if not prefix:
2389
3086
  return False
2390
3087
 
2391
- # Get the server settings to check for legacy format preference
2392
-
2393
- if prefix_format is None:
2394
- prefix_format = fastmcp.settings.resource_prefix_format
2395
-
2396
- if prefix_format == "protocol":
2397
- # Legacy style: prefix+protocol://path
2398
- legacy_prefix = f"{prefix}+"
2399
- return uri.startswith(legacy_prefix)
2400
- elif prefix_format == "path":
2401
- # New style: protocol://prefix/path
2402
- # Split the URI into protocol and path
2403
- match = URI_PATTERN.match(uri)
2404
- if not match:
2405
- raise ValueError(
2406
- f"Invalid URI format: {uri}. Expected protocol://path format."
2407
- )
3088
+ # Split the URI into protocol and path
3089
+ match = URI_PATTERN.match(uri)
3090
+ if not match:
3091
+ raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2408
3092
 
2409
- _, path = match.groups()
3093
+ _, path = match.groups()
2410
3094
 
2411
- # Check if the path starts with the prefix followed by a /
2412
- prefix_pattern = f"^{re.escape(prefix)}/"
2413
- return bool(re.match(prefix_pattern, path))
2414
- else:
2415
- raise ValueError(f"Invalid prefix format: {prefix_format}")
3095
+ # Check if the path starts with the prefix followed by a /
3096
+ prefix_pattern = f"^{re.escape(prefix)}/"
3097
+ return bool(re.match(prefix_pattern, path))