fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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 (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -3,11 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import inspect
7
6
  import re
8
7
  import secrets
9
8
  import warnings
10
- import weakref
11
9
  from collections.abc import (
12
10
  AsyncIterator,
13
11
  Awaitable,
@@ -18,103 +16,155 @@ from collections.abc import (
18
16
  )
19
17
  from contextlib import (
20
18
  AbstractAsyncContextManager,
21
- AsyncExitStack,
22
19
  asynccontextmanager,
23
- suppress,
24
20
  )
25
- from dataclasses import dataclass
21
+ from dataclasses import replace
26
22
  from functools import partial
27
23
  from pathlib import Path
28
24
  from typing import TYPE_CHECKING, Any, Generic, Literal, cast, overload
29
25
 
30
- import anyio
31
26
  import httpx
32
27
  import mcp.types
33
- import uvicorn
34
- from docket import Docket, Worker
35
- from mcp.server.lowlevel.helper_types import ReadResourceContents
36
- from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
37
- from mcp.server.stdio import stdio_server
28
+ from key_value.aio.adapters.pydantic import PydanticAdapter
29
+ from key_value.aio.protocols import AsyncKeyValue
30
+ from key_value.aio.stores.memory import MemoryStore
31
+ from mcp.server.lowlevel.server import LifespanResultT
38
32
  from mcp.shared.exceptions import McpError
39
33
  from mcp.types import (
40
- METHOD_NOT_FOUND,
41
34
  Annotations,
42
35
  AnyFunction,
43
36
  CallToolRequestParams,
44
- ContentBlock,
45
- ErrorData,
46
- GetPromptResult,
47
37
  ToolAnnotations,
48
38
  )
49
- from mcp.types import Prompt as SDKPrompt
50
- from mcp.types import Resource as SDKResource
51
- from mcp.types import ResourceTemplate as SDKResourceTemplate
52
- from mcp.types import Tool as SDKTool
53
39
  from pydantic import AnyUrl
54
- from starlette.middleware import Middleware as ASGIMiddleware
55
- from starlette.requests import Request
56
- from starlette.responses import Response
57
- from starlette.routing import BaseRoute, Route
40
+ from pydantic import ValidationError as PydanticValidationError
41
+ from starlette.routing import BaseRoute
42
+ from typing_extensions import Self
58
43
 
59
44
  import fastmcp
60
45
  import fastmcp.server
61
- from fastmcp.exceptions import DisabledError, NotFoundError
46
+ from fastmcp.exceptions import (
47
+ AuthorizationError,
48
+ FastMCPError,
49
+ NotFoundError,
50
+ PromptError,
51
+ ResourceError,
52
+ ToolError,
53
+ ValidationError,
54
+ )
62
55
  from fastmcp.mcp_config import MCPConfig
63
56
  from fastmcp.prompts import Prompt
64
- from fastmcp.prompts.prompt import FunctionPrompt
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
69
- from fastmcp.server.auth import AuthProvider
70
- from fastmcp.server.event_store import EventStore
71
- from fastmcp.server.http import (
72
- StarletteWithLifespan,
73
- create_sse_app,
74
- create_streamable_http_app,
75
- )
57
+ from fastmcp.prompts.function_prompt import FunctionPrompt
58
+ from fastmcp.prompts.prompt import PromptResult
59
+ from fastmcp.resources.resource import Resource, ResourceResult
60
+ from fastmcp.resources.template import ResourceTemplate
61
+ from fastmcp.server.auth import AuthContext, AuthProvider, run_auth_checks
62
+ from fastmcp.server.dependencies import get_access_token
63
+ from fastmcp.server.lifespan import Lifespan
76
64
  from fastmcp.server.low_level import LowLevelServer
77
65
  from fastmcp.server.middleware import Middleware, MiddlewareContext
78
- from fastmcp.server.tasks.config import TaskConfig
79
- from fastmcp.server.tasks.handlers import (
80
- handle_prompt_as_task,
81
- handle_resource_as_task,
82
- handle_tool_as_task,
66
+ from fastmcp.server.mixins import LifespanMixin, MCPOperationsMixin, TransportMixin
67
+ from fastmcp.server.providers import LocalProvider, Provider
68
+ from fastmcp.server.providers.aggregate import AggregateProvider
69
+ from fastmcp.server.tasks.config import TaskConfig, TaskMeta
70
+ from fastmcp.server.telemetry import server_span
71
+ from fastmcp.server.transforms import (
72
+ ToolTransform,
73
+ Transform,
83
74
  )
75
+ from fastmcp.server.transforms.visibility import apply_session_transforms, is_enabled
76
+ from fastmcp.settings import DuplicateBehavior as DuplicateBehaviorSetting
84
77
  from fastmcp.settings import Settings
85
- from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
86
- from fastmcp.tools.tool_manager import ToolManager
78
+ from fastmcp.tools.function_tool import FunctionTool
79
+ from fastmcp.tools.tool import AuthCheckCallable, Tool, ToolResult
87
80
  from fastmcp.tools.tool_transform import ToolTransformConfig
88
- from fastmcp.utilities.cli import log_server_banner
89
81
  from fastmcp.utilities.components import FastMCPComponent
90
- from fastmcp.utilities.logging import get_logger, temporary_log_level
91
- from fastmcp.utilities.types import NotSet, NotSetT
82
+ from fastmcp.utilities.logging import get_logger
83
+ from fastmcp.utilities.types import FastMCPBaseModel, NotSet, NotSetT
84
+ from fastmcp.utilities.versions import (
85
+ VersionSpec,
86
+ )
92
87
 
93
88
  if TYPE_CHECKING:
94
89
  from fastmcp.client import Client
95
90
  from fastmcp.client.client import FastMCP1Server
96
91
  from fastmcp.client.sampling import SamplingHandler
97
92
  from fastmcp.client.transports import ClientTransport, ClientTransportT
98
- from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
99
- from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
100
- from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
101
- from fastmcp.server.proxy import FastMCPProxy
93
+ from fastmcp.server.providers.openapi import ComponentFn as OpenAPIComponentFn
94
+ from fastmcp.server.providers.openapi import RouteMap
95
+ from fastmcp.server.providers.openapi import RouteMapFn as OpenAPIRouteMapFn
96
+ from fastmcp.server.providers.proxy import FastMCPProxy
102
97
  from fastmcp.tools.tool import ToolResultSerializerType
103
98
 
104
99
  logger = get_logger(__name__)
105
100
 
106
101
 
107
102
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
103
+
104
+
105
+ def _resolve_on_duplicate(
106
+ on_duplicate: DuplicateBehavior | None,
107
+ on_duplicate_tools: DuplicateBehavior | None,
108
+ on_duplicate_resources: DuplicateBehavior | None,
109
+ on_duplicate_prompts: DuplicateBehavior | None,
110
+ ) -> DuplicateBehavior:
111
+ """Resolve on_duplicate from deprecated per-type params.
112
+
113
+ Takes the most strict value if multiple are provided.
114
+ Delete this function when removing deprecated params.
115
+ """
116
+ strictness_order: list[DuplicateBehavior] = ["error", "warn", "replace", "ignore"]
117
+ deprecated_values: list[DuplicateBehavior] = []
118
+
119
+ deprecated_params: list[tuple[str, DuplicateBehavior | None]] = [
120
+ ("on_duplicate_tools", on_duplicate_tools),
121
+ ("on_duplicate_resources", on_duplicate_resources),
122
+ ("on_duplicate_prompts", on_duplicate_prompts),
123
+ ]
124
+ for name, value in deprecated_params:
125
+ if value is not None:
126
+ if fastmcp.settings.deprecation_warnings:
127
+ warnings.warn(
128
+ f"{name} is deprecated, use on_duplicate instead",
129
+ DeprecationWarning,
130
+ stacklevel=4,
131
+ )
132
+ deprecated_values.append(value)
133
+
134
+ if on_duplicate is None and deprecated_values:
135
+ return min(deprecated_values, key=lambda x: strictness_order.index(x))
136
+
137
+ return on_duplicate or "warn"
138
+
139
+
108
140
  Transport = Literal["stdio", "http", "sse", "streamable-http"]
109
141
 
110
142
  # Compiled URI parsing regex to split a URI into protocol and path components
111
143
  URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
112
144
 
145
+
113
146
  LifespanCallable = Callable[
114
147
  ["FastMCP[LifespanResultT]"], AbstractAsyncContextManager[LifespanResultT]
115
148
  ]
116
149
 
117
150
 
151
+ def _get_auth_context() -> tuple[bool, Any]:
152
+ """Get auth context for the current request.
153
+
154
+ Returns a tuple of (skip_auth, token) where:
155
+ - skip_auth=True means auth checks should be skipped (STDIO transport)
156
+ - token is the access token for HTTP transports (may be None if unauthenticated)
157
+
158
+ Uses late import to avoid circular import with context.py.
159
+ """
160
+ from fastmcp.server.context import _current_transport
161
+
162
+ is_stdio = _current_transport.get() == "stdio"
163
+ if is_stdio:
164
+ return (True, None)
165
+ return (False, get_access_token())
166
+
167
+
118
168
  @asynccontextmanager
119
169
  async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
120
170
  """Default lifespan context manager that does nothing.
@@ -152,7 +202,19 @@ def _lifespan_proxy(
152
202
  return wrap
153
203
 
154
204
 
155
- class FastMCP(Generic[LifespanResultT]):
205
+ class StateValue(FastMCPBaseModel):
206
+ """Wrapper for stored context state values."""
207
+
208
+ value: Any
209
+
210
+
211
+ class FastMCP(
212
+ AggregateProvider,
213
+ LifespanMixin,
214
+ MCPOperationsMixin,
215
+ TransportMixin,
216
+ Generic[LifespanResultT],
217
+ ):
156
218
  def __init__(
157
219
  self,
158
220
  name: str | None = None,
@@ -161,26 +223,26 @@ class FastMCP(Generic[LifespanResultT]):
161
223
  version: str | None = None,
162
224
  website_url: str | None = None,
163
225
  icons: list[mcp.types.Icon] | None = None,
164
- auth: AuthProvider | NotSetT | None = NotSet,
226
+ auth: AuthProvider | None = None,
165
227
  middleware: Sequence[Middleware] | None = None,
166
- lifespan: LifespanCallable | None = None,
228
+ providers: Sequence[Provider] | None = None,
229
+ lifespan: LifespanCallable | Lifespan | None = None,
167
230
  mask_error_details: bool | None = None,
168
231
  tools: Sequence[Tool | Callable[..., Any]] | None = None,
169
- tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
170
232
  tool_serializer: ToolResultSerializerType | None = None,
171
233
  include_tags: Collection[str] | None = None,
172
234
  exclude_tags: Collection[str] | None = None,
173
- include_fastmcp_meta: bool | None = None,
174
- on_duplicate_tools: DuplicateBehavior | None = None,
175
- on_duplicate_resources: DuplicateBehavior | None = None,
176
- on_duplicate_prompts: DuplicateBehavior | None = None,
235
+ on_duplicate: DuplicateBehavior | None = None,
177
236
  strict_input_validation: bool | None = None,
237
+ list_page_size: int | None = None,
178
238
  tasks: bool | None = None,
239
+ session_state_store: AsyncKeyValue | None = None,
179
240
  # ---
241
+ # --- DEPRECATED parameters ---
180
242
  # ---
181
- # --- The following arguments are DEPRECATED ---
182
- # ---
183
- # ---
243
+ on_duplicate_tools: DuplicateBehavior | None = None,
244
+ on_duplicate_resources: DuplicateBehavior | None = None,
245
+ on_duplicate_prompts: DuplicateBehavior | None = None,
184
246
  log_level: str | None = None,
185
247
  debug: bool | None = None,
186
248
  host: str | None = None,
@@ -192,7 +254,19 @@ class FastMCP(Generic[LifespanResultT]):
192
254
  stateless_http: bool | None = None,
193
255
  sampling_handler: SamplingHandler | None = None,
194
256
  sampling_handler_behavior: Literal["always", "fallback"] | None = None,
257
+ tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
195
258
  ):
259
+ # Initialize Provider (sets up _transforms)
260
+ super().__init__()
261
+
262
+ # Resolve on_duplicate from deprecated params (delete when removing deprecation)
263
+ self._on_duplicate: DuplicateBehaviorSetting = _resolve_on_duplicate(
264
+ on_duplicate,
265
+ on_duplicate_tools,
266
+ on_duplicate_resources,
267
+ on_duplicate_prompts,
268
+ )
269
+
196
270
  # Resolve server default for background task support
197
271
  self._support_tasks_by_default: bool = tasks if tasks is not None else False
198
272
 
@@ -201,26 +275,53 @@ class FastMCP(Generic[LifespanResultT]):
201
275
  self._worker = None
202
276
 
203
277
  self._additional_http_routes: list[BaseRoute] = []
204
- self._mounted_servers: list[MountedServer] = []
205
- self._is_mounted: bool = (
206
- False # Set to True when this server is mounted on another
207
- )
208
- self._tool_manager: ToolManager = ToolManager(
209
- duplicate_behavior=on_duplicate_tools,
210
- mask_error_details=mask_error_details,
211
- transformations=tool_transformations,
278
+
279
+ # Session-scoped state store (shared across all requests)
280
+ self._state_storage: AsyncKeyValue = session_state_store or MemoryStore()
281
+ self._state_store: PydanticAdapter[StateValue] = PydanticAdapter[StateValue](
282
+ key_value=self._state_storage,
283
+ pydantic_model=StateValue,
284
+ default_collection="fastmcp_state",
212
285
  )
213
- self._resource_manager: ResourceManager = ResourceManager(
214
- duplicate_behavior=on_duplicate_resources,
215
- mask_error_details=mask_error_details,
286
+
287
+ # Create LocalProvider for local components
288
+ self._local_provider: LocalProvider = LocalProvider(
289
+ on_duplicate=self._on_duplicate
216
290
  )
217
- self._prompt_manager: PromptManager = PromptManager(
218
- duplicate_behavior=on_duplicate_prompts,
219
- mask_error_details=mask_error_details,
291
+
292
+ # Add providers using AggregateProvider's add_provider
293
+ # LocalProvider is always first (no namespace)
294
+ self.add_provider(self._local_provider)
295
+ for p in providers or []:
296
+ self.add_provider(p)
297
+
298
+ # Store mask_error_details for execution error handling
299
+ self._mask_error_details: bool = (
300
+ mask_error_details
301
+ if mask_error_details is not None
302
+ else fastmcp.settings.mask_error_details
220
303
  )
304
+
305
+ # Store list_page_size for pagination of list operations
306
+ if list_page_size is not None and list_page_size <= 0:
307
+ raise ValueError("list_page_size must be a positive integer")
308
+ self._list_page_size: int | None = list_page_size
309
+
310
+ if tool_serializer is not None and fastmcp.settings.deprecation_warnings:
311
+ warnings.warn(
312
+ "The `tool_serializer` parameter is deprecated. "
313
+ "Return ToolResult from your tools for full control over serialization. "
314
+ "See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
315
+ DeprecationWarning,
316
+ stacklevel=2,
317
+ )
221
318
  self._tool_serializer: Callable[[Any], str] | None = tool_serializer
222
319
 
223
- self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
320
+ # Handle Lifespan instances (they're callable) or regular lifespan functions
321
+ if lifespan is not None:
322
+ self._lifespan: LifespanCallable[LifespanResultT] = lifespan
323
+ else:
324
+ self._lifespan = cast(LifespanCallable[LifespanResultT], default_lifespan)
224
325
  self._lifespan_result: LifespanResultT | None = None
225
326
  self._lifespan_result_set: bool = False
226
327
  self._started: asyncio.Event = asyncio.Event()
@@ -238,14 +339,7 @@ class FastMCP(Generic[LifespanResultT]):
238
339
  lifespan=_lifespan_proxy(fastmcp_server=self),
239
340
  )
240
341
 
241
- # if auth is `NotSet`, try to create a provider from the environment
242
- if auth is NotSet:
243
- if fastmcp.settings.server_auth is not None:
244
- # server_auth_class returns the class itself
245
- auth = fastmcp.settings.server_auth_class()
246
- else:
247
- auth = None
248
- self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
342
+ self.auth: AuthProvider | None = auth
249
343
 
250
344
  if tools:
251
345
  for tool in tools:
@@ -253,12 +347,34 @@ class FastMCP(Generic[LifespanResultT]):
253
347
  tool = Tool.from_function(tool, serializer=self._tool_serializer)
254
348
  self.add_tool(tool)
255
349
 
256
- self.include_tags: set[str] | None = (
257
- set(include_tags) if include_tags is not None else None
258
- )
259
- self.exclude_tags: set[str] | None = (
260
- set(exclude_tags) if exclude_tags is not None else None
261
- )
350
+ # Handle deprecated include_tags and exclude_tags parameters
351
+ if include_tags is not None:
352
+ warnings.warn(
353
+ "include_tags is deprecated. Use server.enable(tags=..., only=True) instead.",
354
+ DeprecationWarning,
355
+ stacklevel=2,
356
+ )
357
+ # For backwards compatibility, initialize allowlist from include_tags
358
+ self.enable(tags=set(include_tags), only=True)
359
+ if exclude_tags is not None:
360
+ warnings.warn(
361
+ "exclude_tags is deprecated. Use server.disable(tags=...) instead.",
362
+ DeprecationWarning,
363
+ stacklevel=2,
364
+ )
365
+ # For backwards compatibility, initialize blocklist from exclude_tags
366
+ self.disable(tags=set(exclude_tags))
367
+
368
+ # Handle deprecated tool_transformations parameter
369
+ if tool_transformations:
370
+ if fastmcp.settings.deprecation_warnings:
371
+ warnings.warn(
372
+ "The tool_transformations parameter is deprecated. Use "
373
+ "server.add_transform(ToolTransform({...})) instead.",
374
+ DeprecationWarning,
375
+ stacklevel=2,
376
+ )
377
+ self._transforms.append(ToolTransform(dict(tool_transformations)))
262
378
 
263
379
  self.strict_input_validation: bool = (
264
380
  strict_input_validation
@@ -276,12 +392,6 @@ class FastMCP(Generic[LifespanResultT]):
276
392
  sampling_handler_behavior or "fallback"
277
393
  )
278
394
 
279
- self.include_fastmcp_meta: bool = (
280
- include_fastmcp_meta
281
- if include_fastmcp_meta is not None
282
- else fastmcp.settings.include_fastmcp_meta
283
- )
284
-
285
395
  self._handle_deprecated_settings(
286
396
  log_level=log_level,
287
397
  debug=debug,
@@ -374,539 +484,7 @@ class FastMCP(Generic[LifespanResultT]):
374
484
  else:
375
485
  return list(self._mcp_server.icons)
376
486
 
377
- @property
378
- def docket(self) -> Docket | None:
379
- """Get the Docket instance if Docket support is enabled.
380
-
381
- Returns None if Docket is not enabled or server hasn't been started yet.
382
- """
383
- return self._docket
384
-
385
- @asynccontextmanager
386
- async def _docket_lifespan(self) -> AsyncIterator[None]:
387
- """Manage Docket instance and Worker for background task execution."""
388
- from fastmcp import settings
389
-
390
- # Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
391
- from fastmcp.server.dependencies import (
392
- _current_docket,
393
- _current_server,
394
- _current_worker,
395
- )
396
-
397
- server_token = _current_server.set(weakref.ref(self))
398
-
399
- try:
400
- # For directly mounted servers, the parent's Docket/Worker handles all
401
- # task execution. Skip creating our own to avoid race conditions with
402
- # multiple workers competing for tasks from the same queue.
403
- if self._is_mounted:
404
- yield
405
- return
406
-
407
- # Create Docket instance using configured name and URL
408
- async with Docket(
409
- name=settings.docket.name,
410
- url=settings.docket.url,
411
- ) as docket:
412
- # Store on server instance for cross-task access (FastMCPTransport)
413
- self._docket = docket
414
-
415
- # Register local task-enabled tools/prompts/resources with Docket
416
- # Only function-based variants support background tasks
417
- # Register components where task execution is not "forbidden"
418
- for tool in self._tool_manager._tools.values():
419
- if (
420
- isinstance(tool, FunctionTool)
421
- and tool.task_config.mode != "forbidden"
422
- ):
423
- docket.register(tool.fn, names=[tool.key])
424
-
425
- for prompt in self._prompt_manager._prompts.values():
426
- if (
427
- isinstance(prompt, FunctionPrompt)
428
- and prompt.task_config.mode != "forbidden"
429
- ):
430
- # task execution requires async fn (validated at creation time)
431
- docket.register(
432
- cast(Callable[..., Awaitable[Any]], prompt.fn),
433
- names=[prompt.key],
434
- )
435
-
436
- for resource in self._resource_manager._resources.values():
437
- if (
438
- isinstance(resource, FunctionResource)
439
- and resource.task_config.mode != "forbidden"
440
- ):
441
- docket.register(resource.fn, names=[resource.name])
442
-
443
- for template in self._resource_manager._templates.values():
444
- if (
445
- isinstance(template, FunctionResourceTemplate)
446
- and template.task_config.mode != "forbidden"
447
- ):
448
- docket.register(template.fn, names=[template.name])
449
-
450
- # Also register functions from mounted servers so tasks can
451
- # execute in the parent's Docket context
452
- for mounted in self._mounted_servers:
453
- await self._register_mounted_server_functions(
454
- mounted.server, docket, mounted.prefix, mounted.tool_names
455
- )
456
-
457
- # Set Docket in ContextVar so CurrentDocket can access it
458
- docket_token = _current_docket.set(docket)
459
- try:
460
- # Build worker kwargs from settings
461
- worker_kwargs: dict[str, Any] = {
462
- "concurrency": settings.docket.concurrency,
463
- "redelivery_timeout": settings.docket.redelivery_timeout,
464
- "reconnection_delay": settings.docket.reconnection_delay,
465
- }
466
- if settings.docket.worker_name:
467
- worker_kwargs["name"] = settings.docket.worker_name
468
-
469
- # Create and start Worker
470
- async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
471
- # Store on server instance for cross-context access
472
- self._worker = worker
473
- # Set Worker in ContextVar so CurrentWorker can access it
474
- worker_token = _current_worker.set(worker)
475
- try:
476
- worker_task = asyncio.create_task(worker.run_forever())
477
- try:
478
- yield
479
- finally:
480
- worker_task.cancel()
481
- with suppress(asyncio.CancelledError):
482
- await worker_task
483
- finally:
484
- _current_worker.reset(worker_token)
485
- self._worker = None
486
- finally:
487
- _current_docket.reset(docket_token)
488
- self._docket = None
489
- finally:
490
- _current_server.reset(server_token)
491
-
492
- async def _register_mounted_server_functions(
493
- self,
494
- server: FastMCP,
495
- docket: Docket,
496
- prefix: str | None,
497
- tool_names: dict[str, str] | None = None,
498
- ) -> None:
499
- """Register task-enabled functions from a mounted server with Docket.
500
-
501
- This enables background task execution for mounted server components
502
- through the parent server's Docket context.
503
-
504
- Args:
505
- server: The mounted server whose functions to register
506
- docket: The Docket instance to register with
507
- prefix: The mount prefix to prepend to function names (matches
508
- client-facing tool/prompt names)
509
- tool_names: Optional mapping of original tool names to custom names
510
- """
511
- # Register tools with prefixed names to avoid collisions
512
- for tool in server._tool_manager._tools.values():
513
- if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
514
- # Apply tool_names override first, then prefix (matches get_tools logic)
515
- if tool_names and tool.key in tool_names:
516
- fn_name = tool_names[tool.key]
517
- elif prefix:
518
- fn_name = f"{prefix}_{tool.key}"
519
- else:
520
- fn_name = tool.key
521
- docket.register(tool.fn, names=[fn_name])
522
-
523
- # Register prompts with prefixed names
524
- for prompt in server._prompt_manager._prompts.values():
525
- if (
526
- isinstance(prompt, FunctionPrompt)
527
- and prompt.task_config.mode != "forbidden"
528
- ):
529
- fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
530
- docket.register(
531
- cast(Callable[..., Awaitable[Any]], prompt.fn),
532
- names=[fn_name],
533
- )
534
-
535
- # Register resources with prefixed names (use name, not key/URI)
536
- for resource in server._resource_manager._resources.values():
537
- if (
538
- isinstance(resource, FunctionResource)
539
- and resource.task_config.mode != "forbidden"
540
- ):
541
- fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
542
- docket.register(resource.fn, names=[fn_name])
543
-
544
- # Register resource templates with prefixed names (use name, not key/URI)
545
- for template in server._resource_manager._templates.values():
546
- if (
547
- isinstance(template, FunctionResourceTemplate)
548
- and template.task_config.mode != "forbidden"
549
- ):
550
- fn_name = f"{prefix}_{template.name}" if prefix else template.name
551
- docket.register(template.fn, names=[fn_name])
552
-
553
- # Recursively register from nested mounted servers with accumulated prefix
554
- for nested in server._mounted_servers:
555
- nested_prefix = (
556
- f"{prefix}_{nested.prefix}"
557
- if prefix and nested.prefix
558
- else (prefix or nested.prefix)
559
- )
560
- await self._register_mounted_server_functions(
561
- nested.server, docket, nested_prefix, nested.tool_names
562
- )
563
-
564
- @asynccontextmanager
565
- async def _lifespan_manager(self) -> AsyncIterator[None]:
566
- if self._lifespan_result_set:
567
- # Lifespan already ran - ContextVars will be set by Context.__aenter__
568
- # at request time, so we just yield here.
569
- yield
570
- return
571
-
572
- async with (
573
- self._lifespan(self) as user_lifespan_result,
574
- self._docket_lifespan(),
575
- ):
576
- self._lifespan_result = user_lifespan_result
577
- self._lifespan_result_set = True
578
-
579
- async with AsyncExitStack[bool | None]() as stack:
580
- for server in self._mounted_servers:
581
- await stack.enter_async_context(
582
- cm=server.server._lifespan_manager()
583
- )
584
-
585
- self._started.set()
586
- try:
587
- yield
588
- finally:
589
- self._started.clear()
590
-
591
- self._lifespan_result_set = False
592
- self._lifespan_result = None
593
-
594
- async def run_async(
595
- self,
596
- transport: Transport | None = None,
597
- show_banner: bool = True,
598
- **transport_kwargs: Any,
599
- ) -> None:
600
- """Run the FastMCP server asynchronously.
601
-
602
- Args:
603
- transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
604
- """
605
- if transport is None:
606
- transport = "stdio"
607
- if transport not in {"stdio", "http", "sse", "streamable-http"}:
608
- raise ValueError(f"Unknown transport: {transport}")
609
-
610
- if transport == "stdio":
611
- await self.run_stdio_async(
612
- show_banner=show_banner,
613
- **transport_kwargs,
614
- )
615
- elif transport in {"http", "sse", "streamable-http"}:
616
- await self.run_http_async(
617
- transport=transport,
618
- show_banner=show_banner,
619
- **transport_kwargs,
620
- )
621
- else:
622
- raise ValueError(f"Unknown transport: {transport}")
623
-
624
- def run(
625
- self,
626
- transport: Transport | None = None,
627
- show_banner: bool = True,
628
- **transport_kwargs: Any,
629
- ) -> None:
630
- """Run the FastMCP server. Note this is a synchronous function.
631
-
632
- Args:
633
- transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
634
- """
635
-
636
- anyio.run(
637
- partial(
638
- self.run_async,
639
- transport,
640
- show_banner=show_banner,
641
- **transport_kwargs,
642
- )
643
- )
644
-
645
- def _setup_handlers(self) -> None:
646
- """Set up core MCP protocol handlers."""
647
- self._mcp_server.list_tools()(self._list_tools_mcp)
648
- self._mcp_server.list_resources()(self._list_resources_mcp)
649
- self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
650
- self._mcp_server.list_prompts()(self._list_prompts_mcp)
651
- self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
652
- self._call_tool_mcp
653
- )
654
- # Register custom read_resource handler (SDK decorator doesn't support CreateTaskResult)
655
- self._setup_read_resource_handler()
656
- # Register custom get_prompt handler (SDK decorator doesn't support CreateTaskResult)
657
- self._setup_get_prompt_handler()
658
- # Register custom SEP-1686 task protocol handlers
659
- self._setup_task_protocol_handlers()
660
-
661
- def _setup_read_resource_handler(self) -> None:
662
- """
663
- Set up custom read_resource handler that supports task-augmented responses.
664
-
665
- The SDK's read_resource decorator doesn't support CreateTaskResult returns,
666
- so we register a custom handler that checks request_context.experimental.is_task.
667
- """
668
-
669
- async def handler(req: mcp.types.ReadResourceRequest) -> mcp.types.ServerResult:
670
- uri = req.params.uri
671
-
672
- # Check for task metadata via SDK's request context
673
- task_meta = None
674
- try:
675
- ctx = self._mcp_server.request_context
676
- if ctx.experimental.is_task:
677
- task_meta = ctx.experimental.task_metadata
678
- except (AttributeError, LookupError):
679
- pass
680
-
681
- # Check for task metadata and route appropriately
682
- async with fastmcp.server.context.Context(fastmcp=self):
683
- # Get resource including from mounted servers
684
- resource = await self._get_resource_or_template_or_none(str(uri))
685
- if (
686
- resource
687
- and self._should_enable_component(resource)
688
- and hasattr(resource, "task_config")
689
- ):
690
- task_mode = resource.task_config.mode # type: ignore[union-attr]
691
-
692
- # Enforce mode="required" - must have task metadata
693
- if task_mode == "required" and not task_meta:
694
- raise McpError(
695
- ErrorData(
696
- code=METHOD_NOT_FOUND,
697
- message=f"Resource '{uri}' requires task-augmented execution",
698
- )
699
- )
700
-
701
- # Route to background if task metadata present and mode allows
702
- if task_meta and task_mode != "forbidden":
703
- # For FunctionResource/FunctionResourceTemplate, use Docket
704
- if isinstance(
705
- resource,
706
- FunctionResource | FunctionResourceTemplate,
707
- ):
708
- task_meta_dict = task_meta.model_dump(exclude_none=True)
709
- return await handle_resource_as_task(
710
- self, str(uri), resource, task_meta_dict
711
- )
712
-
713
- # Forbidden mode: task requested but mode="forbidden"
714
- # Raise error since resources don't have isError field
715
- if task_meta and task_mode == "forbidden":
716
- raise McpError(
717
- ErrorData(
718
- code=METHOD_NOT_FOUND,
719
- message=f"Resource '{uri}' does not support task-augmented execution",
720
- )
721
- )
722
-
723
- # Synchronous execution
724
- result = await self._read_resource_mcp(uri)
725
-
726
- # Graceful degradation: if we got here with task_meta, something went wrong
727
- # (This should be unreachable now that forbidden raises)
728
- if task_meta:
729
- mcp_contents = []
730
- for item in result:
731
- if isinstance(item.content, str):
732
- mcp_contents.append(
733
- mcp.types.TextResourceContents(
734
- uri=uri,
735
- text=item.content,
736
- mimeType=item.mime_type or "text/plain",
737
- )
738
- )
739
- elif isinstance(item.content, bytes):
740
- import base64
741
-
742
- mcp_contents.append(
743
- mcp.types.BlobResourceContents(
744
- uri=uri,
745
- blob=base64.b64encode(item.content).decode(),
746
- mimeType=item.mime_type or "application/octet-stream",
747
- )
748
- )
749
- return mcp.types.ServerResult(
750
- mcp.types.ReadResourceResult(
751
- contents=mcp_contents,
752
- _meta={
753
- "modelcontextprotocol.io/task": {
754
- "returned_immediately": True
755
- }
756
- },
757
- )
758
- )
759
-
760
- # Convert to proper ServerResult
761
- if isinstance(result, mcp.types.ServerResult):
762
- return result
763
-
764
- mcp_contents = []
765
- for item in result:
766
- if isinstance(item.content, str):
767
- mcp_contents.append(
768
- mcp.types.TextResourceContents(
769
- uri=uri,
770
- text=item.content,
771
- mimeType=item.mime_type or "text/plain",
772
- )
773
- )
774
- elif isinstance(item.content, bytes):
775
- import base64
776
-
777
- mcp_contents.append(
778
- mcp.types.BlobResourceContents(
779
- uri=uri,
780
- blob=base64.b64encode(item.content).decode(),
781
- mimeType=item.mime_type or "application/octet-stream",
782
- )
783
- )
784
-
785
- return mcp.types.ServerResult(
786
- mcp.types.ReadResourceResult(contents=mcp_contents)
787
- )
788
-
789
- self._mcp_server.request_handlers[mcp.types.ReadResourceRequest] = handler
790
-
791
- def _setup_get_prompt_handler(self) -> None:
792
- """
793
- Set up custom get_prompt handler that supports task-augmented responses.
794
-
795
- The SDK's get_prompt decorator doesn't support CreateTaskResult returns,
796
- so we register a custom handler that checks request_context.experimental.is_task.
797
- """
798
-
799
- async def handler(req: mcp.types.GetPromptRequest) -> mcp.types.ServerResult:
800
- name = req.params.name
801
- arguments = req.params.arguments
802
-
803
- # Check for task metadata via SDK's request context
804
- task_meta = None
805
- try:
806
- ctx = self._mcp_server.request_context
807
- if ctx.experimental.is_task:
808
- task_meta = ctx.experimental.task_metadata
809
- except (AttributeError, LookupError):
810
- pass
811
-
812
- # Check for task metadata and route appropriately
813
- async with fastmcp.server.context.Context(fastmcp=self):
814
- prompts = await self.get_prompts()
815
- prompt = prompts.get(name)
816
- if (
817
- prompt
818
- and self._should_enable_component(prompt)
819
- and hasattr(prompt, "task_config")
820
- and prompt.task_config
821
- ):
822
- task_mode = prompt.task_config.mode # type: ignore[union-attr]
823
-
824
- # Enforce mode="required" - must have task metadata
825
- if task_mode == "required" and not task_meta:
826
- raise McpError(
827
- ErrorData(
828
- code=METHOD_NOT_FOUND,
829
- message=f"Prompt '{name}' requires task-augmented execution",
830
- )
831
- )
832
-
833
- # Route to background if task metadata present and mode allows
834
- if task_meta and task_mode != "forbidden":
835
- task_meta_dict = task_meta.model_dump(exclude_none=True)
836
- result = await handle_prompt_as_task(
837
- self, name, arguments, task_meta_dict
838
- )
839
- return mcp.types.ServerResult(result)
840
-
841
- # Forbidden mode: task requested but mode="forbidden"
842
- # Raise error since prompts don't have isError field
843
- if task_meta and task_mode == "forbidden":
844
- raise McpError(
845
- ErrorData(
846
- code=METHOD_NOT_FOUND,
847
- message=f"Prompt '{name}' does not support task-augmented execution",
848
- )
849
- )
850
-
851
- # Synchronous execution
852
- result = await self._get_prompt_mcp(name, arguments)
853
- return mcp.types.ServerResult(result)
854
-
855
- self._mcp_server.request_handlers[mcp.types.GetPromptRequest] = handler
856
-
857
- def _setup_task_protocol_handlers(self) -> None:
858
- """Register SEP-1686 task protocol handlers with SDK."""
859
- from mcp.types import (
860
- CancelTaskRequest,
861
- GetTaskPayloadRequest,
862
- GetTaskRequest,
863
- ListTasksRequest,
864
- ServerResult,
865
- )
866
-
867
- from fastmcp.server.tasks.protocol import (
868
- tasks_cancel_handler,
869
- tasks_get_handler,
870
- tasks_list_handler,
871
- tasks_result_handler,
872
- )
873
-
874
- # Manually register handlers (SDK decorators fail with locally-defined functions)
875
- # SDK expects handlers that receive Request objects and return ServerResult
876
-
877
- async def handle_get_task(req: GetTaskRequest) -> ServerResult:
878
- params = req.params.model_dump(by_alias=True, exclude_none=True)
879
- result = await tasks_get_handler(self, params)
880
- return ServerResult(result)
881
-
882
- async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:
883
- params = req.params.model_dump(by_alias=True, exclude_none=True)
884
- result = await tasks_result_handler(self, params)
885
- return ServerResult(result)
886
-
887
- async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:
888
- params = (
889
- req.params.model_dump(by_alias=True, exclude_none=True)
890
- if req.params
891
- else {}
892
- )
893
- result = await tasks_list_handler(self, params)
894
- return ServerResult(result)
895
-
896
- async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:
897
- params = req.params.model_dump(by_alias=True, exclude_none=True)
898
- result = await tasks_cancel_handler(self, params)
899
- return ServerResult(result)
900
-
901
- # Register directly with SDK (same as what decorators do internally)
902
- self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task
903
- self._mcp_server.request_handlers[GetTaskPayloadRequest] = (
904
- handle_get_task_result
905
- )
906
- self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks
907
- self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task
908
-
909
- async def _apply_middleware(
487
+ async def _run_middleware(
910
488
  self,
911
489
  context: MiddlewareContext[Any],
912
490
  call_next: Callable[[MiddlewareContext[Any]], Awaitable[Any]],
@@ -920,999 +498,862 @@ class FastMCP(Generic[LifespanResultT]):
920
498
  def add_middleware(self, middleware: Middleware) -> None:
921
499
  self.middleware.append(middleware)
922
500
 
923
- async def get_tools(self) -> dict[str, Tool]:
924
- """Get all tools (unfiltered), including mounted servers, indexed by key."""
925
- all_tools = dict(await self._tool_manager.get_tools())
501
+ def add_provider(self, provider: Provider, *, namespace: str = "") -> None:
502
+ """Add a provider for dynamic tools, resources, and prompts.
926
503
 
927
- for mounted in self._mounted_servers:
928
- try:
929
- child_tools = await mounted.server.get_tools()
930
- for key, tool in child_tools.items():
931
- # Check for manual override first, then apply prefix
932
- if mounted.tool_names and key in mounted.tool_names:
933
- new_key = mounted.tool_names[key]
934
- elif mounted.prefix:
935
- new_key = f"{mounted.prefix}_{key}"
936
- else:
937
- new_key = key
938
- all_tools[new_key] = tool.model_copy(key=new_key)
939
- except Exception as e:
940
- logger.warning(
941
- f"Failed to get tools from mounted server {mounted.server.name!r}: {e}"
942
- )
943
- if fastmcp.settings.mounted_components_raise_on_load_error:
944
- raise
945
- continue
504
+ Providers are queried in registration order. The first provider to return
505
+ a non-None result wins. Static components (registered via decorators)
506
+ always take precedence over providers.
946
507
 
947
- return all_tools
508
+ Args:
509
+ provider: A Provider instance that will provide components dynamically.
510
+ namespace: Optional namespace prefix. When set:
511
+ - Tools become "namespace_toolname"
512
+ - Resources become "protocol://namespace/path"
513
+ - Prompts become "namespace_promptname"
514
+ """
515
+ super().add_provider(provider, namespace=namespace)
516
+
517
+ # -------------------------------------------------------------------------
518
+ # Provider interface overrides - inherited from AggregateProvider
519
+ # -------------------------------------------------------------------------
520
+ # _list_tools, _list_resources, _list_resource_templates, _list_prompts
521
+ # are inherited from AggregateProvider which handles aggregation and namespacing
522
+
523
+ async def get_tasks(self) -> Sequence[FastMCPComponent]:
524
+ """Get task-eligible components with all transforms applied.
525
+
526
+ Overrides AggregateProvider.get_tasks() to apply server-level transforms
527
+ after aggregation. AggregateProvider handles provider-level namespacing.
528
+ """
529
+ # Get tasks from AggregateProvider (handles aggregation and namespacing)
530
+ components = list(await super().get_tasks())
531
+
532
+ # Separate by component type for server-level transform application
533
+ tools = [c for c in components if isinstance(c, Tool)]
534
+ resources = [c for c in components if isinstance(c, Resource)]
535
+ templates = [c for c in components if isinstance(c, ResourceTemplate)]
536
+ prompts = [c for c in components if isinstance(c, Prompt)]
537
+
538
+ # Apply server-level transforms sequentially
539
+ for transform in self.transforms:
540
+ tools = await transform.list_tools(tools)
541
+ resources = await transform.list_resources(resources)
542
+ templates = await transform.list_resource_templates(templates)
543
+ prompts = await transform.list_prompts(prompts)
544
+
545
+ return [
546
+ *tools,
547
+ *resources,
548
+ *templates,
549
+ *prompts,
550
+ ]
551
+
552
+ def add_transform(self, transform: Transform) -> None:
553
+ """Add a server-level transform.
948
554
 
949
- async def get_tool(self, key: str) -> Tool:
950
- tools = await self.get_tools()
951
- if key not in tools:
952
- raise NotFoundError(f"Unknown tool: {key}")
953
- return tools[key]
555
+ Server-level transforms are applied after all providers are aggregated.
556
+ They transform tools, resources, and prompts from ALL providers.
954
557
 
955
- async def _get_tool_with_task_config(self, key: str) -> Tool | None:
956
- """Get a tool by key, returning None if not found.
558
+ Args:
559
+ transform: The transform to add.
957
560
 
958
- Used for task config checking where we need the actual tool object
959
- (including from mounted servers and proxies) but don't want to raise.
561
+ Example:
562
+ ```python
563
+ from fastmcp.server.transforms import Namespace
564
+
565
+ server = FastMCP("Server")
566
+ server.add_transform(Namespace("api"))
567
+ # All tools from all providers become "api_toolname"
568
+ ```
960
569
  """
961
- try:
962
- return await self.get_tool(key)
963
- except NotFoundError:
964
- return None
570
+ self._transforms.append(transform)
965
571
 
966
- async def _get_resource_or_template_or_none(
967
- self, uri: str
968
- ) -> Resource | ResourceTemplate | None:
969
- """Get a resource or template by URI, searching recursively. Returns None if not found."""
970
- try:
971
- return await self.get_resource(uri)
972
- except NotFoundError:
973
- pass
572
+ def add_tool_transformation(
573
+ self, tool_name: str, transformation: ToolTransformConfig
574
+ ) -> None:
575
+ """Add a tool transformation.
974
576
 
975
- templates = await self.get_resource_templates()
976
- for template in templates.values():
977
- if template.matches(uri):
978
- return template
577
+ .. deprecated::
578
+ Use ``add_transform(ToolTransform({...}))`` instead.
579
+ """
580
+ if fastmcp.settings.deprecation_warnings:
581
+ warnings.warn(
582
+ "add_tool_transformation is deprecated. Use "
583
+ "server.add_transform(ToolTransform({tool_name: config})) instead.",
584
+ DeprecationWarning,
585
+ stacklevel=2,
586
+ )
587
+ self.add_transform(ToolTransform({tool_name: transformation}))
979
588
 
980
- return None
589
+ def remove_tool_transformation(self, _tool_name: str) -> None:
590
+ """Remove a tool transformation.
981
591
 
982
- async def get_resources(self) -> dict[str, Resource]:
983
- """Get all resources (unfiltered), including mounted servers, indexed by key."""
984
- all_resources = dict(await self._resource_manager.get_resources())
592
+ .. deprecated::
593
+ Tool transformations are now immutable. Use enable/disable controls instead.
594
+ """
595
+ if fastmcp.settings.deprecation_warnings:
596
+ warnings.warn(
597
+ "remove_tool_transformation is deprecated and has no effect. "
598
+ "Transforms are immutable once added. Use server.disable(keys=[...]) "
599
+ "to hide tools instead.",
600
+ DeprecationWarning,
601
+ stacklevel=2,
602
+ )
985
603
 
986
- for mounted in self._mounted_servers:
987
- try:
988
- child_resources = await mounted.server.get_resources()
989
- for key, resource in child_resources.items():
990
- new_key = (
991
- add_resource_prefix(key, mounted.prefix)
992
- if mounted.prefix
993
- else key
994
- )
995
- update = (
996
- {"name": f"{mounted.prefix}_{resource.name}"}
997
- if mounted.prefix and resource.name
998
- else {}
999
- )
1000
- all_resources[new_key] = resource.model_copy(
1001
- key=new_key, update=update
1002
- )
1003
- except Exception as e:
1004
- logger.warning(
1005
- f"Failed to get resources from mounted server {mounted.server.name!r}: {e}"
604
+ async def list_tools(self, *, run_middleware: bool = True) -> Sequence[Tool]:
605
+ """List all enabled tools from providers.
606
+
607
+ Overrides Provider.list_tools() to add visibility filtering, auth filtering,
608
+ and middleware execution. Returns all versions (no deduplication).
609
+ Protocol handlers deduplicate for MCP wire format.
610
+ """
611
+ async with fastmcp.server.context.Context(fastmcp=self) as ctx:
612
+ if run_middleware:
613
+ mw_context = MiddlewareContext(
614
+ message=mcp.types.ListToolsRequest(method="tools/list"),
615
+ source="client",
616
+ type="request",
617
+ method="tools/list",
618
+ fastmcp_context=ctx,
619
+ )
620
+ return await self._run_middleware(
621
+ context=mw_context,
622
+ call_next=lambda context: self.list_tools(run_middleware=False),
1006
623
  )
1007
- if fastmcp.settings.mounted_components_raise_on_load_error:
1008
- raise
1009
- continue
1010
-
1011
- return all_resources
1012
624
 
1013
- async def get_resource(self, key: str) -> Resource:
1014
- resources = await self.get_resources()
1015
- if key not in resources:
1016
- raise NotFoundError(f"Unknown resource: {key}")
1017
- return resources[key]
625
+ # Get all tools, apply session transforms, then filter enabled
626
+ tools = list(await super().list_tools())
627
+ tools = await apply_session_transforms(tools)
628
+ tools = [t for t in tools if is_enabled(t)]
1018
629
 
1019
- async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
1020
- """Get all resource templates (unfiltered), including mounted servers, indexed by key."""
1021
- all_templates = dict(await self._resource_manager.get_resource_templates())
630
+ skip_auth, token = _get_auth_context()
631
+ authorized: list[Tool] = []
632
+ for tool in tools:
633
+ if not skip_auth and tool.auth is not None:
634
+ ctx = AuthContext(token=token, component=tool)
635
+ try:
636
+ if not run_auth_checks(tool.auth, ctx):
637
+ continue
638
+ except AuthorizationError:
639
+ continue
640
+ authorized.append(tool)
641
+ return authorized
1022
642
 
1023
- for mounted in self._mounted_servers:
1024
- try:
1025
- child_templates = await mounted.server.get_resource_templates()
1026
- for key, template in child_templates.items():
1027
- new_key = (
1028
- add_resource_prefix(key, mounted.prefix)
1029
- if mounted.prefix
1030
- else key
1031
- )
1032
- update: dict[str, Any] = {}
1033
- if mounted.prefix:
1034
- if template.name:
1035
- update["name"] = f"{mounted.prefix}_{template.name}"
1036
- # Update uri_template so matches() works with prefixed URIs
1037
- update["uri_template"] = new_key
1038
- all_templates[new_key] = template.model_copy(
1039
- key=new_key, update=update
1040
- )
1041
- except Exception as e:
1042
- logger.warning(
1043
- f"Failed to get resource templates from mounted server {mounted.server.name!r}: {e}"
1044
- )
1045
- if fastmcp.settings.mounted_components_raise_on_load_error:
1046
- raise
1047
- continue
643
+ async def _get_tool(
644
+ self, name: str, version: VersionSpec | None = None
645
+ ) -> Tool | None:
646
+ """Get a tool by name via aggregation from providers.
1048
647
 
1049
- return all_templates
648
+ Extends AggregateProvider._get_tool() with component-level auth checks.
1050
649
 
1051
- async def get_resource_template(self, key: str) -> ResourceTemplate:
1052
- """Get a registered resource template by key."""
1053
- templates = await self.get_resource_templates()
1054
- if key not in templates:
1055
- raise NotFoundError(f"Unknown resource template: {key}")
1056
- return templates[key]
650
+ Args:
651
+ name: The tool name.
652
+ version: Version filter (None returns highest version).
1057
653
 
1058
- async def get_prompts(self) -> dict[str, Prompt]:
1059
- """Get all prompts (unfiltered), including mounted servers, indexed by key."""
1060
- all_prompts = dict(await self._prompt_manager.get_prompts())
654
+ Returns:
655
+ The tool if found and authorized, None if not found or unauthorized.
656
+ """
657
+ # Get tool from AggregateProvider (handles aggregation and namespacing)
658
+ tool = await super()._get_tool(name, version)
659
+ if tool is None:
660
+ return None
1061
661
 
1062
- for mounted in self._mounted_servers:
662
+ # Component auth - return None if unauthorized (consistent with list filtering)
663
+ skip_auth, token = _get_auth_context()
664
+ if not skip_auth and tool.auth is not None:
665
+ ctx = AuthContext(token=token, component=tool)
1063
666
  try:
1064
- child_prompts = await mounted.server.get_prompts()
1065
- for key, prompt in child_prompts.items():
1066
- new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
1067
- all_prompts[new_key] = prompt.model_copy(key=new_key)
1068
- except Exception as e:
1069
- logger.warning(
1070
- f"Failed to get prompts from mounted server {mounted.server.name!r}: {e}"
1071
- )
1072
- if fastmcp.settings.mounted_components_raise_on_load_error:
1073
- raise
1074
- continue
667
+ if not run_auth_checks(tool.auth, ctx):
668
+ return None
669
+ except AuthorizationError:
670
+ return None
1075
671
 
1076
- return all_prompts
672
+ return tool
1077
673
 
1078
- async def get_prompt(self, key: str) -> Prompt:
1079
- prompts = await self.get_prompts()
1080
- if key not in prompts:
1081
- raise NotFoundError(f"Unknown prompt: {key}")
1082
- return prompts[key]
674
+ async def get_tool(
675
+ self, name: str, version: VersionSpec | None = None
676
+ ) -> Tool | None:
677
+ """Get a tool by name, filtering disabled tools.
1083
678
 
1084
- def custom_route(
1085
- self,
1086
- path: str,
1087
- methods: list[str],
1088
- name: str | None = None,
1089
- include_in_schema: bool = True,
1090
- ) -> Callable[
1091
- [Callable[[Request], Awaitable[Response]]],
1092
- Callable[[Request], Awaitable[Response]],
1093
- ]:
1094
- """
1095
- Decorator to register a custom HTTP route on the FastMCP server.
1096
-
1097
- Allows adding arbitrary HTTP endpoints outside the standard MCP protocol,
1098
- which can be useful for OAuth callbacks, health checks, or admin APIs.
1099
- The handler function must be an async function that accepts a Starlette
1100
- Request and returns a Response.
679
+ Overrides Provider.get_tool() to add visibility filtering after all
680
+ transforms (including session-level) have been applied. This ensures
681
+ session transforms can override provider-level disables.
1101
682
 
1102
683
  Args:
1103
- path: URL path for the route (e.g., "/auth/callback")
1104
- methods: List of HTTP methods to support (e.g., ["GET", "POST"])
1105
- name: Optional name for the route (to reference this route with
1106
- Starlette's reverse URL lookup feature)
1107
- include_in_schema: Whether to include in OpenAPI schema, defaults to True
684
+ name: The tool name.
685
+ version: Version filter (None returns highest version).
1108
686
 
1109
- Example:
1110
- Register a custom HTTP route for a health check endpoint:
1111
- ```python
1112
- @server.custom_route("/health", methods=["GET"])
1113
- async def health_check(request: Request) -> Response:
1114
- return JSONResponse({"status": "ok"})
1115
- ```
687
+ Returns:
688
+ The tool if found and enabled, None otherwise.
1116
689
  """
690
+ tool = await super().get_tool(name, version)
691
+ if tool is None:
692
+ return None
1117
693
 
1118
- def decorator(
1119
- fn: Callable[[Request], Awaitable[Response]],
1120
- ) -> Callable[[Request], Awaitable[Response]]:
1121
- self._additional_http_routes.append(
1122
- Route(
1123
- path,
1124
- endpoint=fn,
1125
- methods=methods,
1126
- name=name,
1127
- include_in_schema=include_in_schema,
694
+ # Apply session transforms to single item
695
+ tools = await apply_session_transforms([tool])
696
+ if not tools or not is_enabled(tools[0]):
697
+ return None
698
+ return tools[0]
699
+
700
+ async def list_resources(
701
+ self, *, run_middleware: bool = True
702
+ ) -> Sequence[Resource]:
703
+ """List all enabled resources from providers.
704
+
705
+ Overrides Provider.list_resources() to add visibility filtering, auth filtering,
706
+ and middleware execution. Returns all versions (no deduplication).
707
+ Protocol handlers deduplicate for MCP wire format.
708
+ """
709
+ async with fastmcp.server.context.Context(fastmcp=self) as ctx:
710
+ if run_middleware:
711
+ mw_context = MiddlewareContext(
712
+ message={},
713
+ source="client",
714
+ type="request",
715
+ method="resources/list",
716
+ fastmcp_context=ctx,
717
+ )
718
+ return await self._run_middleware(
719
+ context=mw_context,
720
+ call_next=lambda context: self.list_resources(run_middleware=False),
1128
721
  )
1129
- )
1130
- return fn
1131
722
 
1132
- return decorator
723
+ # Get all resources, apply session transforms, then filter enabled
724
+ resources = list(await super().list_resources())
725
+ resources = await apply_session_transforms(resources)
726
+ resources = [r for r in resources if is_enabled(r)]
727
+
728
+ skip_auth, token = _get_auth_context()
729
+ authorized: list[Resource] = []
730
+ for resource in resources:
731
+ if not skip_auth and resource.auth is not None:
732
+ ctx = AuthContext(token=token, component=resource)
733
+ try:
734
+ if not run_auth_checks(resource.auth, ctx):
735
+ continue
736
+ except AuthorizationError:
737
+ continue
738
+ authorized.append(resource)
739
+ return authorized
740
+
741
+ async def _get_resource(
742
+ self, uri: str, version: VersionSpec | None = None
743
+ ) -> Resource | None:
744
+ """Get a resource by URI via aggregation from providers.
1133
745
 
1134
- def _get_additional_http_routes(self) -> list[BaseRoute]:
1135
- """Get all additional HTTP routes including from mounted servers.
746
+ Extends AggregateProvider._get_resource() with component-level auth checks.
1136
747
 
1137
- Returns a list of all custom HTTP routes from this server and
1138
- recursively from all mounted servers.
748
+ Args:
749
+ uri: The resource URI.
750
+ version: Version filter (None returns highest version).
1139
751
 
1140
752
  Returns:
1141
- List of Starlette BaseRoute objects
753
+ The resource if found and authorized, None if not found or unauthorized.
1142
754
  """
1143
- routes = list(self._additional_http_routes)
1144
-
1145
- # Recursively get routes from mounted servers
1146
- for mounted_server in self._mounted_servers:
1147
- mounted_routes = mounted_server.server._get_additional_http_routes()
1148
- routes.extend(mounted_routes)
755
+ # Get resource from AggregateProvider (handles aggregation and namespacing)
756
+ resource = await super()._get_resource(uri, version)
757
+ if resource is None:
758
+ return None
1149
759
 
1150
- return routes
760
+ # Component auth - return None if unauthorized (consistent with list filtering)
761
+ skip_auth, token = _get_auth_context()
762
+ if not skip_auth and resource.auth is not None:
763
+ ctx = AuthContext(token=token, component=resource)
764
+ try:
765
+ if not run_auth_checks(resource.auth, ctx):
766
+ return None
767
+ except AuthorizationError:
768
+ return None
1151
769
 
1152
- async def _list_tools_mcp(self) -> list[SDKTool]:
1153
- """
1154
- List all available tools, in the format expected by the low-level MCP
1155
- server.
1156
- """
1157
- logger.debug(f"[{self.name}] Handler called: list_tools")
1158
-
1159
- async with fastmcp.server.context.Context(fastmcp=self):
1160
- tools = await self._list_tools_middleware()
1161
- return [
1162
- tool.to_mcp_tool(
1163
- name=tool.key,
1164
- include_fastmcp_meta=self.include_fastmcp_meta,
1165
- )
1166
- for tool in tools
1167
- ]
770
+ return resource
1168
771
 
1169
- async def _list_tools_middleware(self) -> list[Tool]:
1170
- """
1171
- List all available tools, applying MCP middleware.
1172
- """
772
+ async def get_resource(
773
+ self, uri: str, version: VersionSpec | None = None
774
+ ) -> Resource | None:
775
+ """Get a resource by URI, filtering disabled resources.
1173
776
 
1174
- async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
1175
- # Create the middleware context.
1176
- mw_context = MiddlewareContext(
1177
- message=mcp.types.ListToolsRequest(method="tools/list"),
1178
- source="client",
1179
- type="request",
1180
- method="tools/list",
1181
- fastmcp_context=fastmcp_ctx,
1182
- )
777
+ Overrides Provider.get_resource() to add visibility filtering after all
778
+ transforms (including session-level) have been applied.
1183
779
 
1184
- # Apply the middleware chain.
1185
- return list(
1186
- await self._apply_middleware(
1187
- context=mw_context, call_next=self._list_tools
1188
- )
1189
- )
780
+ Args:
781
+ uri: The resource URI.
782
+ version: Version filter (None returns highest version).
1190
783
 
1191
- async def _list_tools(
1192
- self,
1193
- context: MiddlewareContext[mcp.types.ListToolsRequest],
1194
- ) -> list[Tool]:
1195
- """
1196
- List all available tools.
784
+ Returns:
785
+ The resource if found and enabled, None otherwise.
1197
786
  """
1198
- # 1. Get local tools and filter them
1199
- local_tools = await self._tool_manager.get_tools()
1200
- filtered_local = [
1201
- tool for tool in local_tools.values() if self._should_enable_component(tool)
1202
- ]
1203
-
1204
- # 2. Get tools from mounted servers
1205
- # Mounted servers apply their own filtering, but we also apply parent's filtering
1206
- # Use a dict to implement "later wins" deduplication by key
1207
- all_tools: dict[str, Tool] = {tool.key: tool for tool in filtered_local}
1208
-
1209
- for mounted in self._mounted_servers:
1210
- try:
1211
- child_tools = await mounted.server._list_tools_middleware()
1212
- for tool in child_tools:
1213
- # Apply parent server's filtering to mounted components
1214
- if not self._should_enable_component(tool):
1215
- continue
787
+ resource = await super().get_resource(uri, version)
788
+ if resource is None:
789
+ return None
1216
790
 
1217
- # Check for manual override first, then apply prefix
1218
- if mounted.tool_names and tool.key in mounted.tool_names:
1219
- key = mounted.tool_names[tool.key]
1220
- elif mounted.prefix:
1221
- key = f"{mounted.prefix}_{tool.key}"
1222
- else:
1223
- key = tool.key
1224
-
1225
- if key != tool.key:
1226
- tool = tool.model_copy(key=key)
1227
- # Later mounted servers override earlier ones
1228
- all_tools[key] = tool
1229
- except Exception as e:
1230
- server_name = getattr(
1231
- getattr(mounted, "server", None), "name", repr(mounted)
791
+ # Apply session transforms to single item
792
+ resources = await apply_session_transforms([resource])
793
+ if not resources or not is_enabled(resources[0]):
794
+ return None
795
+ return resources[0]
796
+
797
+ async def list_resource_templates(
798
+ self, *, run_middleware: bool = True
799
+ ) -> Sequence[ResourceTemplate]:
800
+ """List all enabled resource templates from providers.
801
+
802
+ Overrides Provider.list_resource_templates() to add visibility filtering,
803
+ auth filtering, and middleware execution. Returns all versions (no deduplication).
804
+ Protocol handlers deduplicate for MCP wire format.
805
+ """
806
+ async with fastmcp.server.context.Context(fastmcp=self) as ctx:
807
+ if run_middleware:
808
+ mw_context = MiddlewareContext(
809
+ message={},
810
+ source="client",
811
+ type="request",
812
+ method="resources/templates/list",
813
+ fastmcp_context=ctx,
1232
814
  )
1233
- logger.warning(
1234
- f"Failed to list tools from mounted server {server_name!r}: {e}"
815
+ return await self._run_middleware(
816
+ context=mw_context,
817
+ call_next=lambda context: self.list_resource_templates(
818
+ run_middleware=False
819
+ ),
1235
820
  )
1236
- if fastmcp.settings.mounted_components_raise_on_load_error:
1237
- raise
1238
- continue
1239
821
 
1240
- return list(all_tools.values())
822
+ # Get all templates, apply session transforms, then filter enabled
823
+ templates = list(await super().list_resource_templates())
824
+ templates = await apply_session_transforms(templates)
825
+ templates = [t for t in templates if is_enabled(t)]
826
+
827
+ skip_auth, token = _get_auth_context()
828
+ authorized: list[ResourceTemplate] = []
829
+ for template in templates:
830
+ if not skip_auth and template.auth is not None:
831
+ ctx = AuthContext(token=token, component=template)
832
+ try:
833
+ if not run_auth_checks(template.auth, ctx):
834
+ continue
835
+ except AuthorizationError:
836
+ continue
837
+ authorized.append(template)
838
+ return authorized
1241
839
 
1242
- async def _list_resources_mcp(self) -> list[SDKResource]:
1243
- """
1244
- List all available resources, in the format expected by the low-level MCP
1245
- server.
1246
- """
1247
- logger.debug(f"[{self.name}] Handler called: list_resources")
1248
-
1249
- async with fastmcp.server.context.Context(fastmcp=self):
1250
- resources = await self._list_resources_middleware()
1251
- return [
1252
- resource.to_mcp_resource(
1253
- uri=resource.key,
1254
- include_fastmcp_meta=self.include_fastmcp_meta,
1255
- )
1256
- for resource in resources
1257
- ]
840
+ async def _get_resource_template(
841
+ self, uri: str, version: VersionSpec | None = None
842
+ ) -> ResourceTemplate | None:
843
+ """Get a resource template by URI via aggregation from providers.
1258
844
 
1259
- async def _list_resources_middleware(self) -> list[Resource]:
1260
- """
1261
- List all available resources, applying MCP middleware.
1262
- """
1263
-
1264
- async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
1265
- # Create the middleware context.
1266
- mw_context = MiddlewareContext(
1267
- message={}, # List resources doesn't have parameters
1268
- source="client",
1269
- type="request",
1270
- method="resources/list",
1271
- fastmcp_context=fastmcp_ctx,
1272
- )
845
+ Extends AggregateProvider._get_resource_template() with component-level auth checks.
1273
846
 
1274
- # Apply the middleware chain.
1275
- return list(
1276
- await self._apply_middleware(
1277
- context=mw_context, call_next=self._list_resources
1278
- )
1279
- )
847
+ Args:
848
+ uri: The template URI to match.
849
+ version: Version filter (None returns highest version).
1280
850
 
1281
- async def _list_resources(
1282
- self,
1283
- context: MiddlewareContext[dict[str, Any]],
1284
- ) -> list[Resource]:
1285
- """
1286
- List all available resources.
851
+ Returns:
852
+ The template if found and authorized, None if not found or unauthorized.
1287
853
  """
1288
- # 1. Filter local resources
1289
- local_resources = await self._resource_manager.get_resources()
1290
- filtered_local = [
1291
- resource
1292
- for resource in local_resources.values()
1293
- if self._should_enable_component(resource)
1294
- ]
1295
-
1296
- # 2. Get from mounted servers with resource prefix handling
1297
- # Mounted servers apply their own filtering, but we also apply parent's filtering
1298
- # Use a dict to implement "later wins" deduplication by key
1299
- all_resources: dict[str, Resource] = {
1300
- resource.key: resource for resource in filtered_local
1301
- }
854
+ # Get template from AggregateProvider (handles aggregation and namespacing)
855
+ template = await super()._get_resource_template(uri, version)
856
+ if template is None:
857
+ return None
1302
858
 
1303
- for mounted in self._mounted_servers:
859
+ # Component auth - return None if unauthorized (consistent with list filtering)
860
+ skip_auth, token = _get_auth_context()
861
+ if not skip_auth and template.auth is not None:
862
+ ctx = AuthContext(token=token, component=template)
1304
863
  try:
1305
- child_resources = await mounted.server._list_resources_middleware()
1306
- for resource in child_resources:
1307
- # Apply parent server's filtering to mounted components
1308
- if not self._should_enable_component(resource):
1309
- continue
1310
-
1311
- key = resource.key
1312
- if mounted.prefix:
1313
- key = add_resource_prefix(resource.key, mounted.prefix)
1314
- resource = resource.model_copy(
1315
- key=key,
1316
- update={"name": f"{mounted.prefix}_{resource.name}"},
1317
- )
1318
- # Later mounted servers override earlier ones
1319
- all_resources[key] = resource
1320
- except Exception as e:
1321
- server_name = getattr(
1322
- getattr(mounted, "server", None), "name", repr(mounted)
1323
- )
1324
- logger.warning(f"Failed to list resources from {server_name!r}: {e}")
1325
- if fastmcp.settings.mounted_components_raise_on_load_error:
1326
- raise
1327
- continue
864
+ if not run_auth_checks(template.auth, ctx):
865
+ return None
866
+ except AuthorizationError:
867
+ return None
1328
868
 
1329
- return list(all_resources.values())
1330
-
1331
- async def _list_resource_templates_mcp(self) -> list[SDKResourceTemplate]:
1332
- """
1333
- List all available resource templates, in the format expected by the low-level MCP
1334
- server.
1335
- """
1336
- logger.debug(f"[{self.name}] Handler called: list_resource_templates")
1337
-
1338
- async with fastmcp.server.context.Context(fastmcp=self):
1339
- templates = await self._list_resource_templates_middleware()
1340
- return [
1341
- template.to_mcp_template(
1342
- uriTemplate=template.key,
1343
- include_fastmcp_meta=self.include_fastmcp_meta,
1344
- )
1345
- for template in templates
1346
- ]
1347
-
1348
- async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
1349
- """
1350
- List all available resource templates, applying MCP middleware.
869
+ return template
1351
870
 
1352
- """
871
+ async def get_resource_template(
872
+ self, uri: str, version: VersionSpec | None = None
873
+ ) -> ResourceTemplate | None:
874
+ """Get a resource template by URI, filtering disabled templates.
1353
875
 
1354
- async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
1355
- # Create the middleware context.
1356
- mw_context = MiddlewareContext(
1357
- message={}, # List resource templates doesn't have parameters
1358
- source="client",
1359
- type="request",
1360
- method="resources/templates/list",
1361
- fastmcp_context=fastmcp_ctx,
1362
- )
876
+ Overrides Provider.get_resource_template() to add visibility filtering after
877
+ all transforms (including session-level) have been applied.
1363
878
 
1364
- # Apply the middleware chain.
1365
- return list(
1366
- await self._apply_middleware(
1367
- context=mw_context, call_next=self._list_resource_templates
1368
- )
1369
- )
879
+ Args:
880
+ uri: The template URI.
881
+ version: Version filter (None returns highest version).
1370
882
 
1371
- async def _list_resource_templates(
1372
- self,
1373
- context: MiddlewareContext[dict[str, Any]],
1374
- ) -> list[ResourceTemplate]:
1375
- """
1376
- List all available resource templates.
883
+ Returns:
884
+ The template if found and enabled, None otherwise.
1377
885
  """
1378
- # 1. Filter local templates
1379
- local_templates = await self._resource_manager.get_resource_templates()
1380
- filtered_local = [
1381
- template
1382
- for template in local_templates.values()
1383
- if self._should_enable_component(template)
1384
- ]
1385
-
1386
- # 2. Get from mounted servers with resource prefix handling
1387
- # Mounted servers apply their own filtering, but we also apply parent's filtering
1388
- # Use a dict to implement "later wins" deduplication by key
1389
- all_templates: dict[str, ResourceTemplate] = {
1390
- template.key: template for template in filtered_local
1391
- }
1392
-
1393
- for mounted in self._mounted_servers:
1394
- try:
1395
- child_templates = (
1396
- await mounted.server._list_resource_templates_middleware()
1397
- )
1398
- for template in child_templates:
1399
- # Apply parent server's filtering to mounted components
1400
- if not self._should_enable_component(template):
1401
- continue
886
+ template = await super().get_resource_template(uri, version)
887
+ if template is None:
888
+ return None
1402
889
 
1403
- key = template.key
1404
- if mounted.prefix:
1405
- key = add_resource_prefix(template.key, mounted.prefix)
1406
- template = template.model_copy(
1407
- key=key,
1408
- update={"name": f"{mounted.prefix}_{template.name}"},
1409
- )
1410
- # Later mounted servers override earlier ones
1411
- all_templates[key] = template
1412
- except Exception as e:
1413
- server_name = getattr(
1414
- getattr(mounted, "server", None), "name", repr(mounted)
890
+ # Apply session transforms to single item
891
+ templates = await apply_session_transforms([template])
892
+ if not templates or not is_enabled(templates[0]):
893
+ return None
894
+ return templates[0]
895
+
896
+ async def list_prompts(self, *, run_middleware: bool = True) -> Sequence[Prompt]:
897
+ """List all enabled prompts from providers.
898
+
899
+ Overrides Provider.list_prompts() to add visibility filtering, auth filtering,
900
+ and middleware execution. Returns all versions (no deduplication).
901
+ Protocol handlers deduplicate for MCP wire format.
902
+ """
903
+ async with fastmcp.server.context.Context(fastmcp=self) as ctx:
904
+ if run_middleware:
905
+ mw_context = MiddlewareContext(
906
+ message={},
907
+ source="client",
908
+ type="request",
909
+ method="prompts/list",
910
+ fastmcp_context=ctx,
1415
911
  )
1416
- logger.warning(
1417
- f"Failed to list resource templates from {server_name!r}: {e}"
912
+ return await self._run_middleware(
913
+ context=mw_context,
914
+ call_next=lambda context: self.list_prompts(run_middleware=False),
1418
915
  )
1419
- if fastmcp.settings.mounted_components_raise_on_load_error:
1420
- raise
1421
- continue
1422
-
1423
- return list(all_templates.values())
1424
916
 
1425
- async def _list_prompts_mcp(self) -> list[SDKPrompt]:
1426
- """
1427
- List all available prompts, in the format expected by the low-level MCP
1428
- server.
1429
- """
1430
- logger.debug(f"[{self.name}] Handler called: list_prompts")
1431
-
1432
- async with fastmcp.server.context.Context(fastmcp=self):
1433
- prompts = await self._list_prompts_middleware()
1434
- return [
1435
- prompt.to_mcp_prompt(
1436
- name=prompt.key,
1437
- include_fastmcp_meta=self.include_fastmcp_meta,
1438
- )
1439
- for prompt in prompts
1440
- ]
917
+ # Get all prompts, apply session transforms, then filter enabled
918
+ prompts = list(await super().list_prompts())
919
+ prompts = await apply_session_transforms(prompts)
920
+ prompts = [p for p in prompts if is_enabled(p)]
921
+
922
+ skip_auth, token = _get_auth_context()
923
+ authorized: list[Prompt] = []
924
+ for prompt in prompts:
925
+ if not skip_auth and prompt.auth is not None:
926
+ ctx = AuthContext(token=token, component=prompt)
927
+ try:
928
+ if not run_auth_checks(prompt.auth, ctx):
929
+ continue
930
+ except AuthorizationError:
931
+ continue
932
+ authorized.append(prompt)
933
+ return authorized
1441
934
 
1442
- async def _list_prompts_middleware(self) -> list[Prompt]:
1443
- """
1444
- List all available prompts, applying MCP middleware.
935
+ async def _get_prompt(
936
+ self, name: str, version: VersionSpec | None = None
937
+ ) -> Prompt | None:
938
+ """Get a prompt by name via aggregation from providers.
1445
939
 
1446
- """
940
+ Extends AggregateProvider._get_prompt() with component-level auth checks.
1447
941
 
1448
- async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
1449
- # Create the middleware context.
1450
- mw_context = MiddlewareContext(
1451
- message=mcp.types.ListPromptsRequest(method="prompts/list"),
1452
- source="client",
1453
- type="request",
1454
- method="prompts/list",
1455
- fastmcp_context=fastmcp_ctx,
1456
- )
1457
-
1458
- # Apply the middleware chain.
1459
- return list(
1460
- await self._apply_middleware(
1461
- context=mw_context, call_next=self._list_prompts
1462
- )
1463
- )
942
+ Args:
943
+ name: The prompt name.
944
+ version: Version filter (None returns highest version).
1464
945
 
1465
- async def _list_prompts(
1466
- self,
1467
- context: MiddlewareContext[mcp.types.ListPromptsRequest],
1468
- ) -> list[Prompt]:
1469
- """
1470
- List all available prompts.
946
+ Returns:
947
+ The prompt if found and authorized, None if not found or unauthorized.
1471
948
  """
1472
- # 1. Filter local prompts
1473
- local_prompts = await self._prompt_manager.get_prompts()
1474
- filtered_local = [
1475
- prompt
1476
- for prompt in local_prompts.values()
1477
- if self._should_enable_component(prompt)
1478
- ]
1479
-
1480
- # 2. Get from mounted servers
1481
- # Mounted servers apply their own filtering, but we also apply parent's filtering
1482
- # Use a dict to implement "later wins" deduplication by key
1483
- all_prompts: dict[str, Prompt] = {
1484
- prompt.key: prompt for prompt in filtered_local
1485
- }
949
+ # Get prompt from AggregateProvider (handles aggregation and namespacing)
950
+ prompt = await super()._get_prompt(name, version)
951
+ if prompt is None:
952
+ return None
1486
953
 
1487
- for mounted in self._mounted_servers:
954
+ # Component auth - return None if unauthorized (consistent with list filtering)
955
+ skip_auth, token = _get_auth_context()
956
+ if not skip_auth and prompt.auth is not None:
957
+ ctx = AuthContext(token=token, component=prompt)
1488
958
  try:
1489
- child_prompts = await mounted.server._list_prompts_middleware()
1490
- for prompt in child_prompts:
1491
- # Apply parent server's filtering to mounted components
1492
- if not self._should_enable_component(prompt):
1493
- continue
959
+ if not run_auth_checks(prompt.auth, ctx):
960
+ return None
961
+ except AuthorizationError:
962
+ return None
1494
963
 
1495
- # Apply prefix to prompt key
1496
- if mounted.prefix:
1497
- key = f"{mounted.prefix}_{prompt.key}"
1498
- else:
1499
- key = prompt.key
1500
-
1501
- if key != prompt.key:
1502
- prompt = prompt.model_copy(key=key)
1503
- # Later mounted servers override earlier ones
1504
- all_prompts[key] = prompt
1505
- except Exception as e:
1506
- server_name = getattr(
1507
- getattr(mounted, "server", None), "name", repr(mounted)
1508
- )
1509
- logger.warning(
1510
- f"Failed to list prompts from mounted server {server_name!r}: {e}"
1511
- )
1512
- if fastmcp.settings.mounted_components_raise_on_load_error:
1513
- raise
1514
- continue
964
+ return prompt
1515
965
 
1516
- return list(all_prompts.values())
966
+ async def get_prompt(
967
+ self, name: str, version: VersionSpec | None = None
968
+ ) -> Prompt | None:
969
+ """Get a prompt by name, filtering disabled prompts.
1517
970
 
1518
- async def _call_tool_mcp(
1519
- self, key: str, arguments: dict[str, Any]
1520
- ) -> (
1521
- list[ContentBlock]
1522
- | tuple[list[ContentBlock], dict[str, Any]]
1523
- | mcp.types.CallToolResult
1524
- ):
1525
- """
1526
- Handle MCP 'callTool' requests.
1527
-
1528
- Detects SEP-1686 task metadata and routes to background execution if supported.
971
+ Overrides Provider.get_prompt() to add visibility filtering after all
972
+ transforms (including session-level) have been applied.
1529
973
 
1530
974
  Args:
1531
- key: The name of the tool to call
1532
- arguments: Arguments to pass to the tool
975
+ name: The prompt name.
976
+ version: Version filter (None returns highest version).
1533
977
 
1534
978
  Returns:
1535
- List of MCP Content objects containing the tool results
979
+ The prompt if found and enabled, None otherwise.
1536
980
  """
1537
- logger.debug(
1538
- f"[{self.name}] Handler called: call_tool %s with %s", key, arguments
1539
- )
981
+ prompt = await super().get_prompt(name, version)
982
+ if prompt is None:
983
+ return None
1540
984
 
1541
- async with fastmcp.server.context.Context(fastmcp=self):
1542
- try:
1543
- # Check for SEP-1686 task metadata via request context
1544
- task_meta = None
1545
- try:
1546
- # Access task metadata from SDK's request context
1547
- ctx = self._mcp_server.request_context
1548
- if ctx.experimental.is_task:
1549
- task_meta = ctx.experimental.task_metadata
1550
- except (AttributeError, LookupError):
1551
- # No request context available - proceed without task metadata
1552
- pass
1553
-
1554
- # Get tool from local manager, mounted servers, or proxy
1555
- tool = await self._get_tool_with_task_config(key)
1556
- if (
1557
- tool
1558
- and self._should_enable_component(tool)
1559
- and hasattr(tool, "task_config")
1560
- ):
1561
- task_mode = tool.task_config.mode # type: ignore[union-attr]
1562
-
1563
- # Enforce mode="required" - must have task metadata
1564
- if task_mode == "required" and not task_meta:
1565
- raise McpError(
1566
- ErrorData(
1567
- code=METHOD_NOT_FOUND,
1568
- message=f"Tool '{key}' requires task-augmented execution",
1569
- )
1570
- )
1571
-
1572
- # Route to background if task metadata present and mode allows
1573
- if task_meta and task_mode != "forbidden":
1574
- # For FunctionTool, use Docket for background execution
1575
- if isinstance(tool, FunctionTool):
1576
- task_meta_dict = task_meta.model_dump(exclude_none=True)
1577
- return await handle_tool_as_task(
1578
- self, key, arguments, task_meta_dict
1579
- )
1580
- # For ProxyTool/mounted tools, proceed with normal execution
1581
- # They will forward task metadata to their backend
1582
-
1583
- # Forbidden mode: task requested but mode="forbidden"
1584
- # Return error result with returned_immediately=True
1585
- if task_meta and task_mode == "forbidden":
1586
- return mcp.types.CallToolResult(
1587
- content=[
1588
- mcp.types.TextContent(
1589
- type="text",
1590
- text=f"Tool '{key}' does not support task-augmented execution",
1591
- )
1592
- ],
1593
- isError=True,
1594
- _meta={
1595
- "modelcontextprotocol.io/task": {
1596
- "returned_immediately": True
1597
- }
1598
- },
1599
- )
1600
-
1601
- # Synchronous execution (normal path)
1602
- result = await self._call_tool_middleware(key, arguments)
1603
- return result.to_mcp_result()
1604
- except DisabledError as e:
1605
- raise NotFoundError(f"Unknown tool: {key}") from e
1606
- except NotFoundError as e:
1607
- raise NotFoundError(f"Unknown tool: {key}") from e
1608
-
1609
- async def _call_tool_middleware(
985
+ # Apply session transforms to single item
986
+ prompts = await apply_session_transforms([prompt])
987
+ if not prompts or not is_enabled(prompts[0]):
988
+ return None
989
+ return prompts[0]
990
+
991
+ @overload
992
+ async def call_tool(
1610
993
  self,
1611
- key: str,
1612
- arguments: dict[str, Any],
1613
- ) -> ToolResult:
1614
- """
1615
- Applies this server's middleware and delegates the filtered call to the manager.
1616
- """
994
+ name: str,
995
+ arguments: dict[str, Any] | None = None,
996
+ *,
997
+ version: VersionSpec | None = None,
998
+ run_middleware: bool = True,
999
+ task_meta: None = None,
1000
+ ) -> ToolResult: ...
1617
1001
 
1618
- mw_context = MiddlewareContext[CallToolRequestParams](
1619
- message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
1620
- source="client",
1621
- type="request",
1622
- method="tools/call",
1623
- fastmcp_context=fastmcp.server.dependencies.get_context(),
1624
- )
1625
- return await self._apply_middleware(
1626
- context=mw_context, call_next=self._call_tool
1627
- )
1002
+ @overload
1003
+ async def call_tool(
1004
+ self,
1005
+ name: str,
1006
+ arguments: dict[str, Any] | None = None,
1007
+ *,
1008
+ version: VersionSpec | None = None,
1009
+ run_middleware: bool = True,
1010
+ task_meta: TaskMeta,
1011
+ ) -> mcp.types.CreateTaskResult: ...
1628
1012
 
1629
- async def _call_tool(
1013
+ async def call_tool(
1630
1014
  self,
1631
- context: MiddlewareContext[mcp.types.CallToolRequestParams],
1632
- ) -> ToolResult:
1633
- """
1634
- Call a tool
1635
- """
1636
- tool_name = context.message.name
1637
-
1638
- # Try mounted servers in reverse order (later wins)
1639
- for mounted in reversed(self._mounted_servers):
1640
- try_name = tool_name
1641
-
1642
- # First check if tool_name is an overridden name (reverse lookup)
1643
- if mounted.tool_names:
1644
- for orig_key, override_name in mounted.tool_names.items():
1645
- if override_name == tool_name:
1646
- try_name = orig_key
1647
- break
1648
- else:
1649
- # Not an override, try standard prefix stripping
1650
- if mounted.prefix:
1651
- if not tool_name.startswith(f"{mounted.prefix}_"):
1652
- continue
1653
- try_name = tool_name[len(mounted.prefix) + 1 :]
1654
- elif mounted.prefix:
1655
- if not tool_name.startswith(f"{mounted.prefix}_"):
1656
- continue
1657
- try_name = tool_name[len(mounted.prefix) + 1 :]
1015
+ name: str,
1016
+ arguments: dict[str, Any] | None = None,
1017
+ *,
1018
+ version: VersionSpec | None = None,
1019
+ run_middleware: bool = True,
1020
+ task_meta: TaskMeta | None = None,
1021
+ ) -> ToolResult | mcp.types.CreateTaskResult:
1022
+ """Call a tool by name.
1658
1023
 
1659
- try:
1660
- # First, get the tool to check if parent's filter allows it
1661
- # Use get_tool() instead of _tool_manager.get_tool() to support
1662
- # nested mounted servers (tools mounted more than 2 levels deep)
1663
- tool = await mounted.server.get_tool(try_name)
1664
- if not self._should_enable_component(tool):
1665
- # Parent filter blocks this tool, continue searching
1666
- continue
1667
-
1668
- return await mounted.server._call_tool_middleware(
1669
- try_name, context.message.arguments or {}
1670
- )
1671
- except NotFoundError:
1672
- continue
1024
+ This is the public API for executing tools. By default, middleware is applied.
1673
1025
 
1674
- # Try local tools last (mounted servers override local)
1675
- try:
1676
- tool = await self._tool_manager.get_tool(tool_name)
1677
- if self._should_enable_component(tool):
1678
- return await self._tool_manager.call_tool(
1679
- key=tool_name, arguments=context.message.arguments or {}
1680
- )
1681
- except NotFoundError:
1682
- pass
1026
+ Args:
1027
+ name: The tool name
1028
+ arguments: Tool arguments (optional)
1029
+ version: Specific version to call. If None, calls highest version.
1030
+ run_middleware: If True (default), apply the middleware chain.
1031
+ Set to False when called from middleware to avoid re-applying.
1032
+ task_meta: If provided, execute as a background task and return
1033
+ CreateTaskResult. If None (default), execute synchronously and
1034
+ return ToolResult.
1683
1035
 
1684
- raise NotFoundError(f"Unknown tool: {tool_name!r}")
1036
+ Returns:
1037
+ ToolResult when task_meta is None.
1038
+ CreateTaskResult when task_meta is provided.
1685
1039
 
1686
- async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
1687
- """
1688
- Handle MCP 'readResource' requests.
1040
+ Raises:
1041
+ NotFoundError: If tool not found or disabled
1042
+ ToolError: If tool execution fails
1043
+ ValidationError: If arguments fail validation
1044
+ """
1045
+ # Note: fn_key enrichment happens here after finding the tool.
1046
+ # For mounted servers, the parent's provider sets fn_key to the
1047
+ # namespaced key before delegating, ensuring correct Docket routing.
1048
+
1049
+ async with fastmcp.server.context.Context(fastmcp=self) as ctx:
1050
+ if run_middleware:
1051
+ mw_context = MiddlewareContext[CallToolRequestParams](
1052
+ message=mcp.types.CallToolRequestParams(
1053
+ name=name, arguments=arguments or {}
1054
+ ),
1055
+ source="client",
1056
+ type="request",
1057
+ method="tools/call",
1058
+ fastmcp_context=ctx,
1059
+ )
1060
+ return await self._run_middleware(
1061
+ context=mw_context,
1062
+ call_next=lambda context: self.call_tool(
1063
+ context.message.name,
1064
+ context.message.arguments or {},
1065
+ version=version,
1066
+ run_middleware=False,
1067
+ task_meta=task_meta,
1068
+ ),
1069
+ )
1689
1070
 
1690
- Delegates to _read_resource, which should be overridden by FastMCP subclasses.
1691
- """
1692
- logger.debug(f"[{self.name}] Handler called: read_resource %s", uri)
1071
+ # Core logic: find and execute tool (providers queried in parallel)
1072
+ # Use get_tool to apply transforms and filter disabled
1073
+ with server_span(
1074
+ f"tools/call {name}", "tools/call", self.name, "tool", name
1075
+ ) as span:
1076
+ tool = await self.get_tool(name, version=version)
1077
+ if tool is None:
1078
+ raise NotFoundError(f"Unknown tool: {name!r}")
1079
+ span.set_attributes(tool.get_span_attributes())
1080
+ if task_meta is not None and task_meta.fn_key is None:
1081
+ task_meta = replace(task_meta, fn_key=tool.key)
1082
+ try:
1083
+ return await tool._run(arguments or {}, task_meta=task_meta)
1084
+ except FastMCPError:
1085
+ logger.exception(f"Error calling tool {name!r}")
1086
+ raise
1087
+ except (ValidationError, PydanticValidationError):
1088
+ logger.exception(f"Error validating tool {name!r}")
1089
+ raise
1090
+ except Exception as e:
1091
+ logger.exception(f"Error calling tool {name!r}")
1092
+ if self._mask_error_details:
1093
+ raise ToolError(f"Error calling tool {name!r}") from e
1094
+ raise ToolError(f"Error calling tool {name!r}: {e}") from e
1693
1095
 
1694
- async with fastmcp.server.context.Context(fastmcp=self):
1695
- try:
1696
- # Task routing handled by custom handler
1697
- return list[ReadResourceContents](
1698
- await self._read_resource_middleware(uri)
1699
- )
1700
- except DisabledError as e:
1701
- # convert to NotFoundError to avoid leaking resource presence
1702
- raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
1703
- except NotFoundError as e:
1704
- # standardize NotFound message
1705
- raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
1706
-
1707
- async def _read_resource_middleware(
1096
+ @overload
1097
+ async def read_resource(
1708
1098
  self,
1709
- uri: AnyUrl | str,
1710
- ) -> list[ReadResourceContents]:
1711
- """
1712
- Applies this server's middleware and delegates the filtered call to the manager.
1713
- """
1714
-
1715
- # Convert string URI to AnyUrl if needed
1716
- uri_param = AnyUrl(uri) if isinstance(uri, str) else uri
1099
+ uri: str,
1100
+ *,
1101
+ version: VersionSpec | None = None,
1102
+ run_middleware: bool = True,
1103
+ task_meta: None = None,
1104
+ ) -> ResourceResult: ...
1717
1105
 
1718
- mw_context = MiddlewareContext(
1719
- message=mcp.types.ReadResourceRequestParams(uri=uri_param),
1720
- source="client",
1721
- type="request",
1722
- method="resources/read",
1723
- fastmcp_context=fastmcp.server.dependencies.get_context(),
1724
- )
1725
- return list(
1726
- await self._apply_middleware(
1727
- context=mw_context, call_next=self._read_resource
1728
- )
1729
- )
1106
+ @overload
1107
+ async def read_resource(
1108
+ self,
1109
+ uri: str,
1110
+ *,
1111
+ version: VersionSpec | None = None,
1112
+ run_middleware: bool = True,
1113
+ task_meta: TaskMeta,
1114
+ ) -> mcp.types.CreateTaskResult: ...
1730
1115
 
1731
- async def _read_resource(
1116
+ async def read_resource(
1732
1117
  self,
1733
- context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
1734
- ) -> list[ReadResourceContents]:
1735
- """
1736
- Read a resource
1737
- """
1738
- uri_str = str(context.message.uri)
1739
-
1740
- # Try mounted servers in reverse order (later wins)
1741
- for mounted in reversed(self._mounted_servers):
1742
- key = uri_str
1743
- if mounted.prefix:
1744
- if not has_resource_prefix(key, mounted.prefix):
1745
- continue
1746
- key = remove_resource_prefix(key, mounted.prefix)
1747
-
1748
- # First, get the resource/template to check if parent's filter allows it
1749
- # Use get_resource_or_template to support nested mounted servers
1750
- # (resources/templates mounted more than 2 levels deep)
1751
- resource = await mounted.server._get_resource_or_template_or_none(key)
1752
- if resource is None:
1753
- continue
1754
- if not self._should_enable_component(resource):
1755
- # Parent filter blocks this resource, continue searching
1756
- continue
1757
- try:
1758
- result = list(await mounted.server._read_resource_middleware(key))
1759
- return result
1760
- except NotFoundError:
1761
- continue
1118
+ uri: str,
1119
+ *,
1120
+ version: VersionSpec | None = None,
1121
+ run_middleware: bool = True,
1122
+ task_meta: TaskMeta | None = None,
1123
+ ) -> ResourceResult | mcp.types.CreateTaskResult:
1124
+ """Read a resource by URI.
1762
1125
 
1763
- # Try local resources last (mounted servers override local)
1764
- try:
1765
- resource = await self._resource_manager.get_resource(uri_str)
1766
- if self._should_enable_component(resource):
1767
- content = await self._resource_manager.read_resource(uri_str)
1768
- return [
1769
- ReadResourceContents(
1770
- content=content,
1771
- mime_type=resource.mime_type,
1772
- )
1773
- ]
1774
- except NotFoundError:
1775
- pass
1126
+ This is the public API for reading resources. By default, middleware is applied.
1127
+ Checks concrete resources first, then templates.
1776
1128
 
1777
- raise NotFoundError(f"Unknown resource: {uri_str!r}")
1129
+ Args:
1130
+ uri: The resource URI
1131
+ version: Specific version to read. If None, reads highest version.
1132
+ run_middleware: If True (default), apply the middleware chain.
1133
+ Set to False when called from middleware to avoid re-applying.
1134
+ task_meta: If provided, execute as a background task and return
1135
+ CreateTaskResult. If None (default), execute synchronously and
1136
+ return ResourceResult.
1778
1137
 
1779
- async def _get_prompt_mcp(
1780
- self, name: str, arguments: dict[str, Any] | None = None
1781
- ) -> GetPromptResult:
1782
- """
1783
- Handle MCP 'getPrompt' requests.
1138
+ Returns:
1139
+ ResourceResult when task_meta is None.
1140
+ CreateTaskResult when task_meta is provided.
1784
1141
 
1785
- Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
1786
- """
1787
- import fastmcp.server.context
1142
+ Raises:
1143
+ NotFoundError: If resource not found or disabled
1144
+ ResourceError: If resource read fails
1145
+ """
1146
+ # Note: fn_key enrichment happens here after finding the resource/template.
1147
+ # Resources and templates use different key formats:
1148
+ # - Resources use resource.key (derived from the concrete URI)
1149
+ # - Templates use template.key (the template pattern)
1150
+ # For mounted servers, the parent's provider sets fn_key to the
1151
+ # namespaced key before delegating, ensuring correct Docket routing.
1152
+
1153
+ async with fastmcp.server.context.Context(fastmcp=self) as ctx:
1154
+ if run_middleware:
1155
+ uri_param = AnyUrl(uri)
1156
+ mw_context = MiddlewareContext(
1157
+ message=mcp.types.ReadResourceRequestParams(uri=uri_param),
1158
+ source="client",
1159
+ type="request",
1160
+ method="resources/read",
1161
+ fastmcp_context=ctx,
1162
+ )
1163
+ return await self._run_middleware(
1164
+ context=mw_context,
1165
+ call_next=lambda context: self.read_resource(
1166
+ str(context.message.uri),
1167
+ version=version,
1168
+ run_middleware=False,
1169
+ task_meta=task_meta,
1170
+ ),
1171
+ )
1788
1172
 
1789
- logger.debug(
1790
- f"[{self.name}] Handler called: get_prompt %s with %s", name, arguments
1791
- )
1173
+ # Core logic: find and read resource (providers queried in parallel)
1174
+ with server_span(
1175
+ f"resources/read {uri}",
1176
+ "resources/read",
1177
+ self.name,
1178
+ "resource",
1179
+ uri,
1180
+ resource_uri=uri,
1181
+ ) as span:
1182
+ # Try concrete resources first (transforms + auth via _get_resource)
1183
+ resource = await self.get_resource(uri, version=version)
1184
+ if resource is not None:
1185
+ span.set_attributes(resource.get_span_attributes())
1186
+ if task_meta is not None and task_meta.fn_key is None:
1187
+ task_meta = replace(task_meta, fn_key=resource.key)
1188
+ try:
1189
+ return await resource._read(task_meta=task_meta)
1190
+ except (FastMCPError, McpError):
1191
+ logger.exception(f"Error reading resource {uri!r}")
1192
+ raise
1193
+ except Exception as e:
1194
+ logger.exception(f"Error reading resource {uri!r}")
1195
+ if self._mask_error_details:
1196
+ raise ResourceError(
1197
+ f"Error reading resource {uri!r}"
1198
+ ) from e
1199
+ raise ResourceError(
1200
+ f"Error reading resource {uri!r}: {e}"
1201
+ ) from e
1202
+
1203
+ # Try templates (transforms + auth via get_resource_template)
1204
+ template = await self.get_resource_template(uri, version=version)
1205
+ if template is None:
1206
+ if version is None:
1207
+ raise NotFoundError(f"Unknown resource: {uri!r}")
1208
+ raise NotFoundError(
1209
+ f"Unknown resource: {uri!r} version {version!r}"
1210
+ )
1211
+ span.set_attributes(template.get_span_attributes())
1212
+ params = template.matches(uri)
1213
+ assert params is not None
1214
+ if task_meta is not None and task_meta.fn_key is None:
1215
+ task_meta = replace(task_meta, fn_key=template.key)
1216
+ try:
1217
+ return await template._read(uri, params, task_meta=task_meta)
1218
+ except (FastMCPError, McpError):
1219
+ logger.exception(f"Error reading resource {uri!r}")
1220
+ raise
1221
+ except Exception as e:
1222
+ logger.exception(f"Error reading resource {uri!r}")
1223
+ if self._mask_error_details:
1224
+ raise ResourceError(f"Error reading resource {uri!r}") from e
1225
+ raise ResourceError(f"Error reading resource {uri!r}: {e}") from e
1792
1226
 
1793
- async with fastmcp.server.context.Context(fastmcp=self):
1794
- try:
1795
- # Task routing handled by custom handler
1796
- return await self._get_prompt_middleware(name, arguments)
1797
- except DisabledError as e:
1798
- # convert to NotFoundError to avoid leaking prompt presence
1799
- raise NotFoundError(f"Unknown prompt: {name}") from e
1800
- except NotFoundError as e:
1801
- # standardize NotFound message
1802
- raise NotFoundError(f"Unknown prompt: {name}") from e
1803
-
1804
- async def _get_prompt_middleware(
1805
- self, name: str, arguments: dict[str, Any] | None = None
1806
- ) -> GetPromptResult:
1807
- """
1808
- Applies this server's middleware and delegates the filtered call to the manager.
1809
- """
1227
+ @overload
1228
+ async def render_prompt(
1229
+ self,
1230
+ name: str,
1231
+ arguments: dict[str, Any] | None = None,
1232
+ *,
1233
+ version: VersionSpec | None = None,
1234
+ run_middleware: bool = True,
1235
+ task_meta: None = None,
1236
+ ) -> PromptResult: ...
1810
1237
 
1811
- mw_context = MiddlewareContext(
1812
- message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
1813
- source="client",
1814
- type="request",
1815
- method="prompts/get",
1816
- fastmcp_context=fastmcp.server.dependencies.get_context(),
1817
- )
1818
- return await self._apply_middleware(
1819
- context=mw_context, call_next=self._get_prompt
1820
- )
1238
+ @overload
1239
+ async def render_prompt(
1240
+ self,
1241
+ name: str,
1242
+ arguments: dict[str, Any] | None = None,
1243
+ *,
1244
+ version: VersionSpec | None = None,
1245
+ run_middleware: bool = True,
1246
+ task_meta: TaskMeta,
1247
+ ) -> mcp.types.CreateTaskResult: ...
1821
1248
 
1822
- async def _get_prompt(
1249
+ async def render_prompt(
1823
1250
  self,
1824
- context: MiddlewareContext[mcp.types.GetPromptRequestParams],
1825
- ) -> GetPromptResult:
1826
- name = context.message.name
1827
-
1828
- # Try mounted servers in reverse order (later wins)
1829
- for mounted in reversed(self._mounted_servers):
1830
- try_name = name
1831
- if mounted.prefix:
1832
- if not name.startswith(f"{mounted.prefix}_"):
1833
- continue
1834
- try_name = name[len(mounted.prefix) + 1 :]
1251
+ name: str,
1252
+ arguments: dict[str, Any] | None = None,
1253
+ *,
1254
+ version: VersionSpec | None = None,
1255
+ run_middleware: bool = True,
1256
+ task_meta: TaskMeta | None = None,
1257
+ ) -> PromptResult | mcp.types.CreateTaskResult:
1258
+ """Render a prompt by name.
1835
1259
 
1836
- try:
1837
- # First, get the prompt to check if parent's filter allows it
1838
- # Use get_prompt() instead of _prompt_manager.get_prompt() to support
1839
- # nested mounted servers (prompts mounted more than 2 levels deep)
1840
- prompt = await mounted.server.get_prompt(try_name)
1841
- if not self._should_enable_component(prompt):
1842
- # Parent filter blocks this prompt, continue searching
1843
- continue
1844
- return await mounted.server._get_prompt_middleware(
1845
- try_name, context.message.arguments
1846
- )
1847
- except NotFoundError:
1848
- continue
1260
+ This is the public API for rendering prompts. By default, middleware is applied.
1261
+ Use get_prompt() to retrieve the prompt definition without rendering.
1849
1262
 
1850
- # Try local prompts last (mounted servers override local)
1851
- try:
1852
- prompt = await self._prompt_manager.get_prompt(name)
1853
- if self._should_enable_component(prompt):
1854
- return await self._prompt_manager.render_prompt(
1855
- name=name, arguments=context.message.arguments
1263
+ Args:
1264
+ name: The prompt name
1265
+ arguments: Prompt arguments (optional)
1266
+ version: Specific version to render. If None, renders highest version.
1267
+ run_middleware: If True (default), apply the middleware chain.
1268
+ Set to False when called from middleware to avoid re-applying.
1269
+ task_meta: If provided, execute as a background task and return
1270
+ CreateTaskResult. If None (default), execute synchronously and
1271
+ return PromptResult.
1272
+
1273
+ Returns:
1274
+ PromptResult when task_meta is None.
1275
+ CreateTaskResult when task_meta is provided.
1276
+
1277
+ Raises:
1278
+ NotFoundError: If prompt not found or disabled
1279
+ PromptError: If prompt rendering fails
1280
+ """
1281
+ async with fastmcp.server.context.Context(fastmcp=self) as ctx:
1282
+ if run_middleware:
1283
+ mw_context = MiddlewareContext(
1284
+ message=mcp.types.GetPromptRequestParams(
1285
+ name=name, arguments=arguments
1286
+ ),
1287
+ source="client",
1288
+ type="request",
1289
+ method="prompts/get",
1290
+ fastmcp_context=ctx,
1291
+ )
1292
+ return await self._run_middleware(
1293
+ context=mw_context,
1294
+ call_next=lambda context: self.render_prompt(
1295
+ context.message.name,
1296
+ context.message.arguments,
1297
+ version=version,
1298
+ run_middleware=False,
1299
+ task_meta=task_meta,
1300
+ ),
1856
1301
  )
1857
- except NotFoundError:
1858
- pass
1859
1302
 
1860
- raise NotFoundError(f"Unknown prompt: {name!r}")
1303
+ # Core logic: find and render prompt (providers queried in parallel)
1304
+ # Use get_prompt to apply transforms and filter disabled
1305
+ with server_span(
1306
+ f"prompts/get {name}", "prompts/get", self.name, "prompt", name
1307
+ ) as span:
1308
+ prompt = await self.get_prompt(name, version=version)
1309
+ if prompt is None:
1310
+ raise NotFoundError(f"Unknown prompt: {name!r}")
1311
+ span.set_attributes(prompt.get_span_attributes())
1312
+ if task_meta is not None and task_meta.fn_key is None:
1313
+ task_meta = replace(task_meta, fn_key=prompt.key)
1314
+ try:
1315
+ return await prompt._render(arguments, task_meta=task_meta)
1316
+ except (FastMCPError, McpError):
1317
+ logger.exception(f"Error rendering prompt {name!r}")
1318
+ raise
1319
+ except Exception as e:
1320
+ logger.exception(f"Error rendering prompt {name!r}")
1321
+ if self._mask_error_details:
1322
+ raise PromptError(f"Error rendering prompt {name!r}") from e
1323
+ raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
1861
1324
 
1862
- def add_tool(self, tool: Tool) -> Tool:
1325
+ def add_tool(self, tool: Tool | Callable[..., Any]) -> Tool:
1863
1326
  """Add a tool to the server.
1864
1327
 
1865
1328
  The tool function can optionally request a Context object by adding a parameter
1866
1329
  with the Context type annotation. See the @tool decorator for examples.
1867
1330
 
1868
1331
  Args:
1869
- tool: The Tool instance to register
1332
+ tool: The Tool instance or @tool-decorated function to register
1870
1333
 
1871
1334
  Returns:
1872
1335
  The tool instance that was added to the server.
1873
1336
  """
1874
- self._tool_manager.add_tool(tool)
1875
-
1876
- # Send notification if we're in a request context
1877
- try:
1878
- from fastmcp.server.dependencies import get_context
1879
-
1880
- context = get_context()
1881
- context._queue_tool_list_changed() # type: ignore[private-use]
1882
- except RuntimeError:
1883
- pass # No context available
1337
+ return self._local_provider.add_tool(tool)
1884
1338
 
1885
- return tool
1886
-
1887
- def remove_tool(self, name: str) -> None:
1888
- """Remove a tool from the server.
1339
+ def remove_tool(self, name: str, version: str | None = None) -> None:
1340
+ """Remove tool(s) from the server.
1889
1341
 
1890
1342
  Args:
1891
- name: The name of the tool to remove
1343
+ name: The name of the tool to remove.
1344
+ version: If None, removes ALL versions. If specified, removes only that version.
1892
1345
 
1893
1346
  Raises:
1894
- NotFoundError: If the tool is not found
1347
+ NotFoundError: If no matching tool is found.
1895
1348
  """
1896
- self._tool_manager.remove_tool(name)
1897
-
1898
- # Send notification if we're in a request context
1899
1349
  try:
1900
- from fastmcp.server.dependencies import get_context
1901
-
1902
- context = get_context()
1903
- context._queue_tool_list_changed() # type: ignore[private-use]
1904
- except RuntimeError:
1905
- pass # No context available
1906
-
1907
- def add_tool_transformation(
1908
- self, tool_name: str, transformation: ToolTransformConfig
1909
- ) -> None:
1910
- """Add a tool transformation."""
1911
- self._tool_manager.add_tool_transformation(tool_name, transformation)
1912
-
1913
- def remove_tool_transformation(self, tool_name: str) -> None:
1914
- """Remove a tool transformation."""
1915
- self._tool_manager.remove_tool_transformation(tool_name)
1350
+ self._local_provider.remove_tool(name, version)
1351
+ except KeyError:
1352
+ if version is None:
1353
+ raise NotFoundError(f"Tool {name!r} not found") from None
1354
+ raise NotFoundError(
1355
+ f"Tool {name!r} version {version!r} not found"
1356
+ ) from None
1916
1357
 
1917
1358
  @overload
1918
1359
  def tool(
@@ -1920,6 +1361,7 @@ class FastMCP(Generic[LifespanResultT]):
1920
1361
  name_or_fn: AnyFunction,
1921
1362
  *,
1922
1363
  name: str | None = None,
1364
+ version: str | int | None = None,
1923
1365
  title: str | None = None,
1924
1366
  description: str | None = None,
1925
1367
  icons: list[mcp.types.Icon] | None = None,
@@ -1928,8 +1370,9 @@ class FastMCP(Generic[LifespanResultT]):
1928
1370
  annotations: ToolAnnotations | dict[str, Any] | None = None,
1929
1371
  exclude_args: list[str] | None = None,
1930
1372
  meta: dict[str, Any] | None = None,
1931
- enabled: bool | None = None,
1932
1373
  task: bool | TaskConfig | None = None,
1374
+ timeout: float | None = None,
1375
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
1933
1376
  ) -> FunctionTool: ...
1934
1377
 
1935
1378
  @overload
@@ -1938,6 +1381,7 @@ class FastMCP(Generic[LifespanResultT]):
1938
1381
  name_or_fn: str | None = None,
1939
1382
  *,
1940
1383
  name: str | None = None,
1384
+ version: str | int | None = None,
1941
1385
  title: str | None = None,
1942
1386
  description: str | None = None,
1943
1387
  icons: list[mcp.types.Icon] | None = None,
@@ -1946,8 +1390,9 @@ class FastMCP(Generic[LifespanResultT]):
1946
1390
  annotations: ToolAnnotations | dict[str, Any] | None = None,
1947
1391
  exclude_args: list[str] | None = None,
1948
1392
  meta: dict[str, Any] | None = None,
1949
- enabled: bool | None = None,
1950
1393
  task: bool | TaskConfig | None = None,
1394
+ timeout: float | None = None,
1395
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
1951
1396
  ) -> Callable[[AnyFunction], FunctionTool]: ...
1952
1397
 
1953
1398
  def tool(
@@ -1955,6 +1400,7 @@ class FastMCP(Generic[LifespanResultT]):
1955
1400
  name_or_fn: str | AnyFunction | None = None,
1956
1401
  *,
1957
1402
  name: str | None = None,
1403
+ version: str | int | None = None,
1958
1404
  title: str | None = None,
1959
1405
  description: str | None = None,
1960
1406
  icons: list[mcp.types.Icon] | None = None,
@@ -1963,9 +1409,14 @@ class FastMCP(Generic[LifespanResultT]):
1963
1409
  annotations: ToolAnnotations | dict[str, Any] | None = None,
1964
1410
  exclude_args: list[str] | None = None,
1965
1411
  meta: dict[str, Any] | None = None,
1966
- enabled: bool | None = None,
1967
1412
  task: bool | TaskConfig | None = None,
1968
- ) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
1413
+ timeout: float | None = None,
1414
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
1415
+ ) -> (
1416
+ Callable[[AnyFunction], FunctionTool]
1417
+ | FunctionTool
1418
+ | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
1419
+ ):
1969
1420
  """Decorator to register a tool.
1970
1421
 
1971
1422
  Tools can optionally request a Context object by adding a parameter with the
@@ -1989,7 +1440,6 @@ class FastMCP(Generic[LifespanResultT]):
1989
1440
  exclude_args: Optional list of argument names to exclude from the tool schema.
1990
1441
  Deprecated: Use `Depends()` for dependency injection instead.
1991
1442
  meta: Optional meta information about the tool
1992
- enabled: Optional boolean to enable or disable the tool
1993
1443
 
1994
1444
  Examples:
1995
1445
  Register a tool with a custom name:
@@ -2015,73 +1465,11 @@ class FastMCP(Generic[LifespanResultT]):
2015
1465
  server.tool(my_function, name="custom_name")
2016
1466
  ```
2017
1467
  """
2018
- if isinstance(annotations, dict):
2019
- annotations = ToolAnnotations(**annotations)
2020
-
2021
- if isinstance(name_or_fn, classmethod):
2022
- raise ValueError(
2023
- inspect.cleandoc(
2024
- """
2025
- To decorate a classmethod, first define the method and then call
2026
- tool() directly on the method instead of using it as a
2027
- decorator. See https://gofastmcp.com/patterns/decorating-methods
2028
- for examples and more information.
2029
- """
2030
- )
2031
- )
2032
-
2033
- # Determine the actual name and function based on the calling pattern
2034
- if inspect.isroutine(name_or_fn):
2035
- # Case 1: @tool (without parens) - function passed directly
2036
- # Case 2: direct call like tool(fn, name="something")
2037
- fn = name_or_fn
2038
- tool_name = name # Use keyword name if provided, otherwise None
2039
-
2040
- # Resolve task parameter
2041
- supports_task: bool | TaskConfig = (
2042
- task if task is not None else self._support_tasks_by_default
2043
- )
2044
-
2045
- # Register the tool immediately and return the tool object
2046
- # Note: Deprecation warning for exclude_args is handled in Tool.from_function
2047
- tool = Tool.from_function(
2048
- fn,
2049
- name=tool_name,
2050
- title=title,
2051
- description=description,
2052
- icons=icons,
2053
- tags=tags,
2054
- output_schema=output_schema,
2055
- annotations=annotations,
2056
- exclude_args=exclude_args,
2057
- meta=meta,
2058
- serializer=self._tool_serializer,
2059
- enabled=enabled,
2060
- task=supports_task,
2061
- )
2062
- self.add_tool(tool)
2063
- return tool
2064
-
2065
- elif isinstance(name_or_fn, str):
2066
- # Case 3: @tool("custom_name") - name passed as first argument
2067
- if name is not None:
2068
- raise TypeError(
2069
- "Cannot specify both a name as first argument and as keyword argument. "
2070
- f"Use either @tool('{name_or_fn}') or @tool(name='{name}'), not both."
2071
- )
2072
- tool_name = name_or_fn
2073
- elif name_or_fn is None:
2074
- # Case 4: @tool or @tool(name="something") - use keyword name
2075
- tool_name = name
2076
- else:
2077
- raise TypeError(
2078
- f"First argument to @tool must be a function, string, or None, got {type(name_or_fn)}"
2079
- )
2080
-
2081
- # Return partial for cases where we need to wait for the function
2082
- return partial(
2083
- self.tool,
2084
- name=tool_name,
1468
+ # Delegate to LocalProvider with server-level defaults
1469
+ result = self._local_provider.tool(
1470
+ name_or_fn,
1471
+ name=name,
1472
+ version=version,
2085
1473
  title=title,
2086
1474
  description=description,
2087
1475
  icons=icons,
@@ -2090,31 +1478,26 @@ class FastMCP(Generic[LifespanResultT]):
2090
1478
  annotations=annotations,
2091
1479
  exclude_args=exclude_args,
2092
1480
  meta=meta,
2093
- enabled=enabled,
2094
- task=task,
1481
+ task=task if task is not None else self._support_tasks_by_default,
1482
+ timeout=timeout,
1483
+ serializer=self._tool_serializer,
1484
+ auth=auth,
2095
1485
  )
2096
1486
 
2097
- def add_resource(self, resource: Resource) -> Resource:
1487
+ return result
1488
+
1489
+ def add_resource(
1490
+ self, resource: Resource | Callable[..., Any]
1491
+ ) -> Resource | ResourceTemplate:
2098
1492
  """Add a resource to the server.
2099
1493
 
2100
1494
  Args:
2101
- resource: A Resource instance to add
1495
+ resource: A Resource instance or @resource-decorated function to add
2102
1496
 
2103
1497
  Returns:
2104
1498
  The resource instance that was added to the server.
2105
1499
  """
2106
- self._resource_manager.add_resource(resource)
2107
-
2108
- # Send notification if we're in a request context
2109
- try:
2110
- from fastmcp.server.dependencies import get_context
2111
-
2112
- context = get_context()
2113
- context._queue_resource_list_changed() # type: ignore[private-use]
2114
- except RuntimeError:
2115
- pass # No context available
2116
-
2117
- return resource
1500
+ return self._local_provider.add_resource(resource)
2118
1501
 
2119
1502
  def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
2120
1503
  """Add a resource template to the server.
@@ -2125,34 +1508,24 @@ class FastMCP(Generic[LifespanResultT]):
2125
1508
  Returns:
2126
1509
  The template instance that was added to the server.
2127
1510
  """
2128
- self._resource_manager.add_template(template)
2129
-
2130
- # Send notification if we're in a request context
2131
- try:
2132
- from fastmcp.server.dependencies import get_context
2133
-
2134
- context = get_context()
2135
- context._queue_resource_list_changed() # type: ignore[private-use]
2136
- except RuntimeError:
2137
- pass # No context available
2138
-
2139
- return template
1511
+ return self._local_provider.add_template(template)
2140
1512
 
2141
1513
  def resource(
2142
1514
  self,
2143
1515
  uri: str,
2144
1516
  *,
2145
1517
  name: str | None = None,
1518
+ version: str | int | None = None,
2146
1519
  title: str | None = None,
2147
1520
  description: str | None = None,
2148
1521
  icons: list[mcp.types.Icon] | None = None,
2149
1522
  mime_type: str | None = None,
2150
1523
  tags: set[str] | None = None,
2151
- enabled: bool | None = None,
2152
1524
  annotations: Annotations | dict[str, Any] | None = None,
2153
1525
  meta: dict[str, Any] | None = None,
2154
1526
  task: bool | TaskConfig | None = None,
2155
- ) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
1527
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
1528
+ ) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]:
2156
1529
  """Decorator to register a function as a resource.
2157
1530
 
2158
1531
  The function will be called when the resource is read to generate its content.
@@ -2174,7 +1547,6 @@ class FastMCP(Generic[LifespanResultT]):
2174
1547
  description: Optional description of the resource
2175
1548
  mime_type: Optional MIME type for the resource
2176
1549
  tags: Optional set of tags for categorizing the resource
2177
- enabled: Optional boolean to enable or disable the resource
2178
1550
  annotations: Optional annotations about the resource's behavior
2179
1551
  meta: Optional meta information about the resource
2180
1552
 
@@ -2205,105 +1577,37 @@ class FastMCP(Generic[LifespanResultT]):
2205
1577
  return f"Weather for {city}: {data}"
2206
1578
  ```
2207
1579
  """
2208
- if isinstance(annotations, dict):
2209
- annotations = Annotations(**annotations)
2210
-
2211
- # Check if user passed function directly instead of calling decorator
2212
- if inspect.isroutine(uri):
2213
- raise TypeError(
2214
- "The @resource decorator was used incorrectly. "
2215
- "Did you forget to call it? Use @resource('uri') instead of @resource"
2216
- )
2217
-
2218
- def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
2219
- if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
2220
- raise ValueError(
2221
- inspect.cleandoc(
2222
- """
2223
- To decorate a classmethod, first define the method and then call
2224
- resource() directly on the method instead of using it as a
2225
- decorator. See https://gofastmcp.com/patterns/decorating-methods
2226
- for examples and more information.
2227
- """
2228
- )
2229
- )
2230
-
2231
- # Resolve task parameter
2232
- supports_task: bool | TaskConfig = (
2233
- task if task is not None else self._support_tasks_by_default
2234
- )
1580
+ # Delegate to LocalProvider with server-level defaults
1581
+ inner_decorator = self._local_provider.resource(
1582
+ uri,
1583
+ name=name,
1584
+ version=version,
1585
+ title=title,
1586
+ description=description,
1587
+ icons=icons,
1588
+ mime_type=mime_type,
1589
+ tags=tags,
1590
+ annotations=annotations,
1591
+ meta=meta,
1592
+ task=task if task is not None else self._support_tasks_by_default,
1593
+ auth=auth,
1594
+ )
2235
1595
 
2236
- # Check if this should be a template
2237
- has_uri_params = "{" in uri and "}" in uri
2238
- # Use wrapper to check for user-facing parameters
2239
- from fastmcp.server.dependencies import without_injected_parameters
2240
-
2241
- wrapper_fn = without_injected_parameters(fn)
2242
- has_func_params = bool(inspect.signature(wrapper_fn).parameters)
2243
-
2244
- if has_uri_params or has_func_params:
2245
- template = ResourceTemplate.from_function(
2246
- fn=fn,
2247
- uri_template=uri,
2248
- name=name,
2249
- title=title,
2250
- description=description,
2251
- icons=icons,
2252
- mime_type=mime_type,
2253
- tags=tags,
2254
- enabled=enabled,
2255
- annotations=annotations,
2256
- meta=meta,
2257
- task=supports_task,
2258
- )
2259
- self.add_template(template)
2260
- return template
2261
- elif not has_uri_params and not has_func_params:
2262
- resource = Resource.from_function(
2263
- fn=fn,
2264
- uri=uri,
2265
- name=name,
2266
- title=title,
2267
- description=description,
2268
- icons=icons,
2269
- mime_type=mime_type,
2270
- tags=tags,
2271
- enabled=enabled,
2272
- annotations=annotations,
2273
- meta=meta,
2274
- task=supports_task,
2275
- )
2276
- self.add_resource(resource)
2277
- return resource
2278
- else:
2279
- raise ValueError(
2280
- "Invalid resource or template definition due to a "
2281
- "mismatch between URI parameters and function parameters."
2282
- )
1596
+ def decorator(fn: AnyFunction) -> Resource | ResourceTemplate | AnyFunction:
1597
+ return inner_decorator(fn)
2283
1598
 
2284
1599
  return decorator
2285
1600
 
2286
- def add_prompt(self, prompt: Prompt) -> Prompt:
1601
+ def add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt:
2287
1602
  """Add a prompt to the server.
2288
1603
 
2289
1604
  Args:
2290
- prompt: A Prompt instance to add
1605
+ prompt: A Prompt instance or @prompt-decorated function to add
2291
1606
 
2292
1607
  Returns:
2293
1608
  The prompt instance that was added to the server.
2294
1609
  """
2295
- self._prompt_manager.add_prompt(prompt)
2296
-
2297
- # Send notification if we're in a request context
2298
- try:
2299
- from fastmcp.server.dependencies import get_context
2300
-
2301
- context = get_context()
2302
- context._queue_prompt_list_changed() # type: ignore[private-use]
2303
- except RuntimeError:
2304
- pass # No context available
2305
-
2306
- return prompt
1610
+ return self._local_provider.add_prompt(prompt)
2307
1611
 
2308
1612
  @overload
2309
1613
  def prompt(
@@ -2311,13 +1615,14 @@ class FastMCP(Generic[LifespanResultT]):
2311
1615
  name_or_fn: AnyFunction,
2312
1616
  *,
2313
1617
  name: str | None = None,
1618
+ version: str | int | None = None,
2314
1619
  title: str | None = None,
2315
1620
  description: str | None = None,
2316
1621
  icons: list[mcp.types.Icon] | None = None,
2317
1622
  tags: set[str] | None = None,
2318
- enabled: bool | None = None,
2319
1623
  meta: dict[str, Any] | None = None,
2320
1624
  task: bool | TaskConfig | None = None,
1625
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
2321
1626
  ) -> FunctionPrompt: ...
2322
1627
 
2323
1628
  @overload
@@ -2326,13 +1631,14 @@ class FastMCP(Generic[LifespanResultT]):
2326
1631
  name_or_fn: str | None = None,
2327
1632
  *,
2328
1633
  name: str | None = None,
1634
+ version: str | int | None = None,
2329
1635
  title: str | None = None,
2330
1636
  description: str | None = None,
2331
1637
  icons: list[mcp.types.Icon] | None = None,
2332
1638
  tags: set[str] | None = None,
2333
- enabled: bool | None = None,
2334
1639
  meta: dict[str, Any] | None = None,
2335
1640
  task: bool | TaskConfig | None = None,
1641
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
2336
1642
  ) -> Callable[[AnyFunction], FunctionPrompt]: ...
2337
1643
 
2338
1644
  def prompt(
@@ -2340,14 +1646,19 @@ class FastMCP(Generic[LifespanResultT]):
2340
1646
  name_or_fn: str | AnyFunction | None = None,
2341
1647
  *,
2342
1648
  name: str | None = None,
1649
+ version: str | int | None = None,
2343
1650
  title: str | None = None,
2344
1651
  description: str | None = None,
2345
1652
  icons: list[mcp.types.Icon] | None = None,
2346
1653
  tags: set[str] | None = None,
2347
- enabled: bool | None = None,
2348
1654
  meta: dict[str, Any] | None = None,
2349
1655
  task: bool | TaskConfig | None = None,
2350
- ) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
1656
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
1657
+ ) -> (
1658
+ Callable[[AnyFunction], FunctionPrompt]
1659
+ | FunctionPrompt
1660
+ | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]
1661
+ ):
2351
1662
  """Decorator to register a prompt.
2352
1663
 
2353
1664
  Prompts can optionally request a Context object by adding a parameter with the
@@ -2366,7 +1677,6 @@ class FastMCP(Generic[LifespanResultT]):
2366
1677
  name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
2367
1678
  description: Optional description of what the prompt does
2368
1679
  tags: Optional set of tags for categorizing the prompt
2369
- enabled: Optional boolean to enable or disable the prompt
2370
1680
  meta: Optional meta information about the prompt
2371
1681
 
2372
1682
  Examples:
@@ -2417,241 +1727,29 @@ class FastMCP(Generic[LifespanResultT]):
2417
1727
  server.prompt(my_function, name="custom_name")
2418
1728
  ```
2419
1729
  """
2420
-
2421
- if isinstance(name_or_fn, classmethod):
2422
- raise ValueError(
2423
- inspect.cleandoc(
2424
- """
2425
- To decorate a classmethod, first define the method and then call
2426
- prompt() directly on the method instead of using it as a
2427
- decorator. See https://gofastmcp.com/patterns/decorating-methods
2428
- for examples and more information.
2429
- """
2430
- )
2431
- )
2432
-
2433
- # Determine the actual name and function based on the calling pattern
2434
- if inspect.isroutine(name_or_fn):
2435
- # Case 1: @prompt (without parens) - function passed directly as decorator
2436
- # Case 2: direct call like prompt(fn, name="something")
2437
- fn = name_or_fn
2438
- prompt_name = name # Use keyword name if provided, otherwise None
2439
-
2440
- # Resolve task parameter
2441
- supports_task: bool | TaskConfig = (
2442
- task if task is not None else self._support_tasks_by_default
2443
- )
2444
-
2445
- # Register the prompt immediately
2446
- prompt = Prompt.from_function(
2447
- fn=fn,
2448
- name=prompt_name,
2449
- title=title,
2450
- description=description,
2451
- icons=icons,
2452
- tags=tags,
2453
- enabled=enabled,
2454
- meta=meta,
2455
- task=supports_task,
2456
- )
2457
- self.add_prompt(prompt)
2458
-
2459
- return prompt
2460
-
2461
- elif isinstance(name_or_fn, str):
2462
- # Case 3: @prompt("custom_name") - name passed as first argument
2463
- if name is not None:
2464
- raise TypeError(
2465
- "Cannot specify both a name as first argument and as keyword argument. "
2466
- f"Use either @prompt('{name_or_fn}') or @prompt(name='{name}'), not both."
2467
- )
2468
- prompt_name = name_or_fn
2469
- elif name_or_fn is None:
2470
- # Case 4: @prompt() or @prompt(name="something") - use keyword name
2471
- prompt_name = name
2472
- else:
2473
- raise TypeError(
2474
- f"First argument to @prompt must be a function, string, or None, got {type(name_or_fn)}"
2475
- )
2476
-
2477
- # Return partial for cases where we need to wait for the function
2478
- return partial(
2479
- self.prompt,
2480
- name=prompt_name,
1730
+ # Delegate to LocalProvider with server-level defaults
1731
+ return self._local_provider.prompt(
1732
+ name_or_fn,
1733
+ name=name,
1734
+ version=version,
2481
1735
  title=title,
2482
1736
  description=description,
2483
1737
  icons=icons,
2484
1738
  tags=tags,
2485
- enabled=enabled,
2486
1739
  meta=meta,
2487
- task=task,
2488
- )
2489
-
2490
- async def run_stdio_async(
2491
- self, show_banner: bool = True, log_level: str | None = None
2492
- ) -> None:
2493
- """Run the server using stdio transport.
2494
-
2495
- Args:
2496
- show_banner: Whether to display the server banner
2497
- log_level: Log level for the server
2498
- """
2499
- # Display server banner
2500
- if show_banner:
2501
- log_server_banner(server=self)
2502
-
2503
- with temporary_log_level(log_level):
2504
- async with self._lifespan_manager():
2505
- async with stdio_server() as (read_stream, write_stream):
2506
- logger.info(
2507
- f"Starting MCP server {self.name!r} with transport 'stdio'"
2508
- )
2509
-
2510
- await self._mcp_server.run(
2511
- read_stream,
2512
- write_stream,
2513
- self._mcp_server.create_initialization_options(
2514
- notification_options=NotificationOptions(
2515
- tools_changed=True
2516
- ),
2517
- ),
2518
- )
2519
-
2520
- async def run_http_async(
2521
- self,
2522
- show_banner: bool = True,
2523
- transport: Literal["http", "streamable-http", "sse"] = "http",
2524
- host: str | None = None,
2525
- port: int | None = None,
2526
- log_level: str | None = None,
2527
- path: str | None = None,
2528
- uvicorn_config: dict[str, Any] | None = None,
2529
- middleware: list[ASGIMiddleware] | None = None,
2530
- json_response: bool | None = None,
2531
- stateless_http: bool | None = None,
2532
- ) -> None:
2533
- """Run the server using HTTP transport.
2534
-
2535
- Args:
2536
- transport: Transport protocol to use - either "streamable-http" (default) or "sse"
2537
- host: Host address to bind to (defaults to settings.host)
2538
- port: Port to bind to (defaults to settings.port)
2539
- log_level: Log level for the server (defaults to settings.log_level)
2540
- path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
2541
- uvicorn_config: Additional configuration for the Uvicorn server
2542
- middleware: A list of middleware to apply to the app
2543
- json_response: Whether to use JSON response format (defaults to settings.json_response)
2544
- stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
2545
- """
2546
- host = host or self._deprecated_settings.host
2547
- port = port or self._deprecated_settings.port
2548
- default_log_level_to_use = (
2549
- log_level or self._deprecated_settings.log_level
2550
- ).lower()
2551
-
2552
- app = self.http_app(
2553
- path=path,
2554
- transport=transport,
2555
- middleware=middleware,
2556
- json_response=json_response,
2557
- stateless_http=stateless_http,
1740
+ task=task if task is not None else self._support_tasks_by_default,
1741
+ auth=auth,
2558
1742
  )
2559
1743
 
2560
- # Display server banner
2561
- if show_banner:
2562
- log_server_banner(server=self)
2563
- uvicorn_config_from_user = uvicorn_config or {}
2564
-
2565
- config_kwargs: dict[str, Any] = {
2566
- "timeout_graceful_shutdown": 0,
2567
- "lifespan": "on",
2568
- "ws": "websockets-sansio",
2569
- }
2570
- config_kwargs.update(uvicorn_config_from_user)
2571
-
2572
- if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
2573
- config_kwargs["log_level"] = default_log_level_to_use
2574
-
2575
- with temporary_log_level(log_level):
2576
- async with self._lifespan_manager():
2577
- config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
2578
- server = uvicorn.Server(config)
2579
- path = app.state.path.lstrip("/") # type: ignore
2580
- logger.info(
2581
- f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
2582
- )
2583
-
2584
- await server.serve()
2585
-
2586
- def http_app(
2587
- self,
2588
- path: str | None = None,
2589
- middleware: list[ASGIMiddleware] | None = None,
2590
- json_response: bool | None = None,
2591
- stateless_http: bool | None = None,
2592
- transport: Literal["http", "streamable-http", "sse"] = "http",
2593
- event_store: EventStore | None = None,
2594
- retry_interval: int | None = None,
2595
- ) -> StarletteWithLifespan:
2596
- """Create a Starlette app using the specified HTTP transport.
2597
-
2598
- Args:
2599
- path: The path for the HTTP endpoint
2600
- middleware: A list of middleware to apply to the app
2601
- json_response: Whether to use JSON response format
2602
- stateless_http: Whether to use stateless mode (new transport per request)
2603
- transport: Transport protocol to use - "http", "streamable-http", or "sse"
2604
- event_store: Optional event store for SSE polling/resumability. When set,
2605
- enables clients to reconnect and resume receiving events after
2606
- server-initiated disconnections. Only used with streamable-http transport.
2607
- retry_interval: Optional retry interval in milliseconds for SSE polling.
2608
- Controls how quickly clients should reconnect after server-initiated
2609
- disconnections. Requires event_store to be set. Only used with
2610
- streamable-http transport.
2611
-
2612
- Returns:
2613
- A Starlette application configured with the specified transport
2614
- """
2615
-
2616
- if transport in ("streamable-http", "http"):
2617
- return create_streamable_http_app(
2618
- server=self,
2619
- streamable_http_path=path
2620
- or self._deprecated_settings.streamable_http_path,
2621
- event_store=event_store,
2622
- retry_interval=retry_interval,
2623
- auth=self.auth,
2624
- json_response=(
2625
- json_response
2626
- if json_response is not None
2627
- else self._deprecated_settings.json_response
2628
- ),
2629
- stateless_http=(
2630
- stateless_http
2631
- if stateless_http is not None
2632
- else self._deprecated_settings.stateless_http
2633
- ),
2634
- debug=self._deprecated_settings.debug,
2635
- middleware=middleware,
2636
- )
2637
- elif transport == "sse":
2638
- return create_sse_app(
2639
- server=self,
2640
- message_path=self._deprecated_settings.message_path,
2641
- sse_path=path or self._deprecated_settings.sse_path,
2642
- auth=self.auth,
2643
- debug=self._deprecated_settings.debug,
2644
- middleware=middleware,
2645
- )
2646
-
2647
1744
  def mount(
2648
1745
  self,
2649
1746
  server: FastMCP[LifespanResultT],
2650
- prefix: str | None = None,
1747
+ namespace: str | None = None,
2651
1748
  as_proxy: bool | None = None,
2652
1749
  tool_names: dict[str, str] | None = None,
1750
+ prefix: str | None = None, # deprecated, use namespace
2653
1751
  ) -> None:
2654
- """Mount another FastMCP server on this server with an optional prefix.
1752
+ """Mount another FastMCP server on this server with an optional namespace.
2655
1753
 
2656
1754
  Unlike importing (with import_server), mounting establishes a dynamic connection
2657
1755
  between servers. When a client interacts with a mounted server's objects through
@@ -2659,67 +1757,83 @@ class FastMCP(Generic[LifespanResultT]):
2659
1757
  This means changes to the mounted server are immediately reflected when accessed
2660
1758
  through the parent.
2661
1759
 
2662
- When a server is mounted with a prefix:
2663
- - Tools from the mounted server are accessible with prefixed names.
2664
- Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
2665
- - Resources are accessible with prefixed URIs.
1760
+ When a server is mounted with a namespace:
1761
+ - Tools from the mounted server are accessible with namespaced names.
1762
+ Example: If server has a tool named "get_weather", it will be available as "namespace_get_weather".
1763
+ - Resources are accessible with namespaced URIs.
2666
1764
  Example: If server has a resource with URI "weather://forecast", it will be available as
2667
- "weather://prefix/forecast".
2668
- - Templates are accessible with prefixed URI templates.
1765
+ "weather://namespace/forecast".
1766
+ - Templates are accessible with namespaced URI templates.
2669
1767
  Example: If server has a template with URI "weather://location/{id}", it will be available
2670
- as "weather://prefix/location/{id}".
2671
- - Prompts are accessible with prefixed names.
1768
+ as "weather://namespace/location/{id}".
1769
+ - Prompts are accessible with namespaced names.
2672
1770
  Example: If server has a prompt named "weather_prompt", it will be available as
2673
- "prefix_weather_prompt".
1771
+ "namespace_weather_prompt".
2674
1772
 
2675
- When a server is mounted without a prefix (prefix=None), its tools, resources, templates,
1773
+ When a server is mounted without a namespace (namespace=None), its tools, resources, templates,
2676
1774
  and prompts are accessible with their original names. Multiple servers can be mounted
2677
- without prefixes, and they will be tried in order until a match is found.
1775
+ without namespaces, and they will be tried in order until a match is found.
2678
1776
 
2679
- There are two modes for mounting servers:
2680
- 1. Direct mounting (default when server has no custom lifespan): The parent server
2681
- directly accesses the mounted server's objects in-memory for better performance.
2682
- In this mode, no client lifecycle events occur on the mounted server, including
2683
- lifespan execution.
2684
-
2685
- 2. Proxy mounting (default when server has a custom lifespan): The parent server
2686
- treats the mounted server as a separate entity and communicates with it via a
2687
- Client transport. This preserves all client-facing behaviors, including lifespan
2688
- execution, but with slightly higher overhead.
1777
+ The mounted server's lifespan is executed when the parent server starts, and its
1778
+ middleware chain is invoked for all operations (tool calls, resource reads, prompts).
2689
1779
 
2690
1780
  Args:
2691
1781
  server: The FastMCP server to mount.
2692
- prefix: Optional prefix to use for the mounted server's objects. If None,
1782
+ namespace: Optional namespace to use for the mounted server's objects. If None,
2693
1783
  the server's objects are accessible with their original names.
2694
- as_proxy: Whether to treat the mounted server as a proxy. If None (default),
2695
- automatically determined based on whether the server has a custom lifespan
2696
- (True if it has a custom lifespan, False otherwise).
1784
+ as_proxy: Deprecated. Mounted servers now always have their lifespan and
1785
+ middleware invoked. To create a proxy server, use create_proxy()
1786
+ explicitly before mounting.
2697
1787
  tool_names: Optional mapping of original tool names to custom names. Use this
2698
- to override prefixed names. Keys are the original tool names from the
1788
+ to override namespaced names. Keys are the original tool names from the
2699
1789
  mounted server.
1790
+ prefix: Deprecated. Use namespace instead.
2700
1791
  """
2701
- from fastmcp.server.proxy import FastMCPProxy
2702
-
2703
- # if as_proxy is not specified and the server has a custom lifespan,
2704
- # we should treat it as a proxy
2705
- if as_proxy is None:
2706
- as_proxy = server._lifespan != default_lifespan
2707
-
2708
- if as_proxy and not isinstance(server, FastMCPProxy):
2709
- server = FastMCP.as_proxy(server)
2710
-
2711
- # Mark the server as mounted so it skips creating its own Docket/Worker.
2712
- # The parent's Docket handles task execution, avoiding race conditions
2713
- # with multiple workers competing for tasks from the same queue.
2714
- server._is_mounted = True
2715
-
2716
- # Delegate mounting to all three managers
2717
- mounted_server = MountedServer(
2718
- prefix=prefix,
2719
- server=server,
2720
- tool_names=tool_names,
2721
- )
2722
- self._mounted_servers.append(mounted_server)
1792
+ import warnings
1793
+
1794
+ from fastmcp.server.providers.fastmcp_provider import FastMCPProvider
1795
+
1796
+ # Handle deprecated prefix parameter
1797
+ if prefix is not None:
1798
+ warnings.warn(
1799
+ "The 'prefix' parameter is deprecated, use 'namespace' instead",
1800
+ DeprecationWarning,
1801
+ stacklevel=2,
1802
+ )
1803
+ if namespace is None:
1804
+ namespace = prefix
1805
+ else:
1806
+ raise ValueError("Cannot specify both 'prefix' and 'namespace'")
1807
+
1808
+ if as_proxy is not None:
1809
+ warnings.warn(
1810
+ "as_proxy is deprecated and will be removed in a future version. "
1811
+ "Mounted servers now always have their lifespan and middleware invoked. "
1812
+ "To create a proxy server, use create_proxy() explicitly.",
1813
+ DeprecationWarning,
1814
+ stacklevel=2,
1815
+ )
1816
+ # Still honor the flag for backward compatibility
1817
+ if as_proxy:
1818
+ from fastmcp.server.providers.proxy import FastMCPProxy
1819
+
1820
+ if not isinstance(server, FastMCPProxy):
1821
+ server = FastMCP.as_proxy(server)
1822
+
1823
+ # Create provider and add it with namespace
1824
+ provider: Provider = FastMCPProvider(server)
1825
+
1826
+ # Apply tool renames first (scoped to this provider), then namespace
1827
+ # So foo → bar with namespace="baz" becomes baz_bar
1828
+ if tool_names:
1829
+ transforms = {
1830
+ old_name: ToolTransformConfig(name=new_name)
1831
+ for old_name, new_name in tool_names.items()
1832
+ }
1833
+ provider = provider.wrap_transform(ToolTransform(transforms))
1834
+
1835
+ # Use add_provider with namespace (applies namespace in AggregateProvider)
1836
+ self.add_provider(provider, namespace=namespace or "")
2723
1837
 
2724
1838
  async def import_server(
2725
1839
  self,
@@ -2730,6 +1844,10 @@ class FastMCP(Generic[LifespanResultT]):
2730
1844
  Import the MCP objects from another FastMCP server into this one,
2731
1845
  optionally with a given prefix.
2732
1846
 
1847
+ .. deprecated::
1848
+ Use :meth:`mount` instead. ``import_server`` will be removed in a
1849
+ future version.
1850
+
2733
1851
  Note that when a server is *imported*, its objects are immediately
2734
1852
  registered to the importing server. This is a one-time operation and
2735
1853
  future changes to the imported server will not be reflected in the
@@ -2757,34 +1875,48 @@ class FastMCP(Generic[LifespanResultT]):
2757
1875
  prefix: Optional prefix to use for the imported server's objects. If None,
2758
1876
  objects are imported with their original names.
2759
1877
  """
1878
+ import warnings
1879
+
1880
+ warnings.warn(
1881
+ "import_server is deprecated, use mount() instead",
1882
+ DeprecationWarning,
1883
+ stacklevel=2,
1884
+ )
1885
+
1886
+ def add_resource_prefix(uri: str, prefix: str) -> str:
1887
+ """Add prefix to resource URI: protocol://path → protocol://prefix/path."""
1888
+ match = URI_PATTERN.match(uri)
1889
+ if match:
1890
+ protocol, path = match.groups()
1891
+ return f"{protocol}{prefix}/{path}"
1892
+ return uri
1893
+
2760
1894
  # Import tools from the server
2761
- for key, tool in (await server.get_tools()).items():
1895
+ for tool in await server.list_tools():
2762
1896
  if prefix:
2763
- tool = tool.model_copy(key=f"{prefix}_{key}")
2764
- self._tool_manager.add_tool(tool)
1897
+ tool = tool.model_copy(update={"name": f"{prefix}_{tool.name}"})
1898
+ self.add_tool(tool)
2765
1899
 
2766
1900
  # Import resources and templates from the server
2767
- for key, resource in (await server.get_resources()).items():
1901
+ for resource in await server.list_resources():
2768
1902
  if prefix:
2769
- resource_key = add_resource_prefix(key, prefix)
2770
- resource = resource.model_copy(
2771
- update={"name": f"{prefix}_{resource.name}"}, key=resource_key
2772
- )
2773
- self._resource_manager.add_resource(resource)
1903
+ new_uri = add_resource_prefix(str(resource.uri), prefix)
1904
+ resource = resource.model_copy(update={"uri": new_uri})
1905
+ self.add_resource(resource)
2774
1906
 
2775
- for key, template in (await server.get_resource_templates()).items():
1907
+ for template in await server.list_resource_templates():
2776
1908
  if prefix:
2777
- template_key = add_resource_prefix(key, prefix)
1909
+ new_uri_template = add_resource_prefix(template.uri_template, prefix)
2778
1910
  template = template.model_copy(
2779
- update={"name": f"{prefix}_{template.name}"}, key=template_key
1911
+ update={"uri_template": new_uri_template}
2780
1912
  )
2781
- self._resource_manager.add_template(template)
1913
+ self.add_template(template)
2782
1914
 
2783
1915
  # Import prompts from the server
2784
- for key, prompt in (await server.get_prompts()).items():
1916
+ for prompt in await server.list_prompts():
2785
1917
  if prefix:
2786
- prompt = prompt.model_copy(key=f"{prefix}_{key}")
2787
- self._prompt_manager.add_prompt(prompt)
1918
+ prompt = prompt.model_copy(update={"name": f"{prefix}_{prompt.name}"})
1919
+ self.add_prompt(prompt)
2788
1920
 
2789
1921
  if server._lifespan != default_lifespan:
2790
1922
  from warnings import warn
@@ -2807,19 +1939,36 @@ class FastMCP(Generic[LifespanResultT]):
2807
1939
  cls,
2808
1940
  openapi_spec: dict[str, Any],
2809
1941
  client: httpx.AsyncClient,
1942
+ name: str = "OpenAPI Server",
2810
1943
  route_maps: list[RouteMap] | None = None,
2811
1944
  route_map_fn: OpenAPIRouteMapFn | None = None,
2812
1945
  mcp_component_fn: OpenAPIComponentFn | None = None,
2813
1946
  mcp_names: dict[str, str] | None = None,
2814
1947
  tags: set[str] | None = None,
1948
+ timeout: float | None = None,
2815
1949
  **settings: Any,
2816
- ) -> FastMCPOpenAPI:
1950
+ ) -> Self:
2817
1951
  """
2818
1952
  Create a FastMCP server from an OpenAPI specification.
1953
+
1954
+ Args:
1955
+ openapi_spec: OpenAPI schema as a dictionary
1956
+ client: httpx AsyncClient for making HTTP requests
1957
+ name: Name for the MCP server
1958
+ route_maps: Optional list of RouteMap objects defining route mappings
1959
+ route_map_fn: Optional callable for advanced route type mapping
1960
+ mcp_component_fn: Optional callable for component customization
1961
+ mcp_names: Optional dictionary mapping operationId to component names
1962
+ tags: Optional set of tags to add to all components
1963
+ timeout: Optional timeout (in seconds) for all requests
1964
+ **settings: Additional settings passed to FastMCP
1965
+
1966
+ Returns:
1967
+ A FastMCP server with an OpenAPIProvider attached.
2819
1968
  """
2820
- from .openapi import FastMCPOpenAPI
1969
+ from .providers.openapi import OpenAPIProvider
2821
1970
 
2822
- return FastMCPOpenAPI(
1971
+ provider: Provider = OpenAPIProvider(
2823
1972
  openapi_spec=openapi_spec,
2824
1973
  client=client,
2825
1974
  route_maps=route_maps,
@@ -2827,8 +1976,9 @@ class FastMCP(Generic[LifespanResultT]):
2827
1976
  mcp_component_fn=mcp_component_fn,
2828
1977
  mcp_names=mcp_names,
2829
1978
  tags=tags,
2830
- **settings,
1979
+ timeout=timeout,
2831
1980
  )
1981
+ return cls(name=name, providers=[provider], **settings)
2832
1982
 
2833
1983
  @classmethod
2834
1984
  def from_fastapi(
@@ -2841,12 +1991,28 @@ class FastMCP(Generic[LifespanResultT]):
2841
1991
  mcp_names: dict[str, str] | None = None,
2842
1992
  httpx_client_kwargs: dict[str, Any] | None = None,
2843
1993
  tags: set[str] | None = None,
1994
+ timeout: float | None = None,
2844
1995
  **settings: Any,
2845
- ) -> FastMCPOpenAPI:
1996
+ ) -> Self:
2846
1997
  """
2847
1998
  Create a FastMCP server from a FastAPI application.
1999
+
2000
+ Args:
2001
+ app: FastAPI application instance
2002
+ name: Name for the MCP server (defaults to app.title)
2003
+ route_maps: Optional list of RouteMap objects defining route mappings
2004
+ route_map_fn: Optional callable for advanced route type mapping
2005
+ mcp_component_fn: Optional callable for component customization
2006
+ mcp_names: Optional dictionary mapping operationId to component names
2007
+ httpx_client_kwargs: Optional kwargs passed to httpx.AsyncClient
2008
+ tags: Optional set of tags to add to all components
2009
+ timeout: Optional timeout (in seconds) for all requests
2010
+ **settings: Additional settings passed to FastMCP
2011
+
2012
+ Returns:
2013
+ A FastMCP server with an OpenAPIProvider attached.
2848
2014
  """
2849
- from .openapi import FastMCPOpenAPI
2015
+ from .providers.openapi import OpenAPIProvider
2850
2016
 
2851
2017
  if httpx_client_kwargs is None:
2852
2018
  httpx_client_kwargs = {}
@@ -2857,19 +2023,19 @@ class FastMCP(Generic[LifespanResultT]):
2857
2023
  **httpx_client_kwargs,
2858
2024
  )
2859
2025
 
2860
- name = name or app.title
2026
+ server_name = name or app.title
2861
2027
 
2862
- return FastMCPOpenAPI(
2028
+ provider: Provider = OpenAPIProvider(
2863
2029
  openapi_spec=app.openapi(),
2864
2030
  client=client,
2865
- name=name,
2866
2031
  route_maps=route_maps,
2867
2032
  route_map_fn=route_map_fn,
2868
2033
  mcp_component_fn=mcp_component_fn,
2869
2034
  mcp_names=mcp_names,
2870
2035
  tags=tags,
2871
- **settings,
2036
+ timeout=timeout,
2872
2037
  )
2038
+ return cls(name=server_name, providers=[provider], **settings)
2873
2039
 
2874
2040
  @classmethod
2875
2041
  def as_proxy(
@@ -2889,79 +2055,24 @@ class FastMCP(Generic[LifespanResultT]):
2889
2055
  ) -> FastMCPProxy:
2890
2056
  """Create a FastMCP proxy server for the given backend.
2891
2057
 
2058
+ .. deprecated::
2059
+ Use :func:`fastmcp.server.create_proxy` instead.
2060
+ This method will be removed in a future version.
2061
+
2892
2062
  The `backend` argument can be either an existing `fastmcp.client.Client`
2893
2063
  instance or any value accepted as the `transport` argument of
2894
2064
  `fastmcp.client.Client`. This mirrors the convenience of the
2895
2065
  `fastmcp.client.Client` constructor.
2896
2066
  """
2897
- from fastmcp.client.client import Client
2898
- from fastmcp.server.proxy import FastMCPProxy, ProxyClient
2899
-
2900
- if isinstance(backend, Client):
2901
- client = backend
2902
- # Session strategy based on client connection state:
2903
- # - Connected clients: reuse existing session for all requests
2904
- # - Disconnected clients: create fresh sessions per request for isolation
2905
- if client.is_connected():
2906
- proxy_logger = get_logger(__name__)
2907
- proxy_logger.info(
2908
- "Proxy detected connected client - reusing existing session for all requests. "
2909
- "This may cause context mixing in concurrent scenarios."
2910
- )
2911
-
2912
- # Reuse sessions - return the same client instance
2913
- def reuse_client_factory():
2914
- return client
2915
-
2916
- client_factory = reuse_client_factory
2917
- else:
2918
- # Fresh sessions per request
2919
- def fresh_client_factory():
2920
- return client.new()
2921
-
2922
- client_factory = fresh_client_factory
2923
- else:
2924
- base_client = ProxyClient(backend) # type: ignore
2925
-
2926
- # Fresh client created from transport - use fresh sessions per request
2927
- def proxy_client_factory():
2928
- return base_client.new()
2929
-
2930
- client_factory = proxy_client_factory
2931
-
2932
- return FastMCPProxy(client_factory=client_factory, **settings)
2933
-
2934
- def _should_enable_component(
2935
- self,
2936
- component: FastMCPComponent,
2937
- ) -> bool:
2938
- """
2939
- Given a component, determine if it should be enabled. Returns True if it should be enabled; False if it should not.
2940
-
2941
- Rules:
2942
- - If the component's enabled property is False, always return False.
2943
- - If both include_tags and exclude_tags are None, return True.
2944
- - If exclude_tags is provided, check each exclude tag:
2945
- - If the exclude tag is a string, it must be present in the input tags to exclude.
2946
- - If include_tags is provided, check each include tag:
2947
- - If the include tag is a string, it must be present in the input tags to include.
2948
- - If include_tags is provided and none of the include tags match, return False.
2949
- - If include_tags is not provided, return True.
2950
- """
2951
- if not component.enabled:
2952
- return False
2953
-
2954
- if self.include_tags is None and self.exclude_tags is None:
2955
- return True
2956
-
2957
- if self.exclude_tags is not None:
2958
- if any(etag in component.tags for etag in self.exclude_tags):
2959
- return False
2960
-
2961
- if self.include_tags is not None:
2962
- return bool(any(itag in component.tags for itag in self.include_tags))
2963
-
2964
- return True
2067
+ if fastmcp.settings.deprecation_warnings:
2068
+ warnings.warn(
2069
+ "FastMCP.as_proxy() is deprecated. Use create_proxy() from "
2070
+ "fastmcp.server instead: `from fastmcp.server import create_proxy`",
2071
+ DeprecationWarning,
2072
+ stacklevel=2,
2073
+ )
2074
+ # Call the module-level create_proxy function directly
2075
+ return create_proxy(backend, **settings)
2965
2076
 
2966
2077
  @classmethod
2967
2078
  def generate_name(cls, name: str | None = None) -> str:
@@ -2973,129 +2084,61 @@ class FastMCP(Generic[LifespanResultT]):
2973
2084
  return f"{class_name}-{name}-{secrets.token_hex(2)}"
2974
2085
 
2975
2086
 
2976
- @dataclass
2977
- class MountedServer:
2978
- prefix: str | None
2979
- server: FastMCP[Any]
2980
- tool_names: dict[str, str] | None = None
2981
-
2982
-
2983
- def add_resource_prefix(uri: str, prefix: str) -> str:
2984
- """Add a prefix to a resource URI using path formatting (resource://prefix/path).
2985
-
2986
- Args:
2987
- uri: The original resource URI
2988
- prefix: The prefix to add
2989
-
2990
- Returns:
2991
- The resource URI with the prefix added
2992
-
2993
- Examples:
2994
- ```python
2995
- add_resource_prefix("resource://path/to/resource", "prefix")
2996
- "resource://prefix/path/to/resource"
2997
- ```
2998
- With absolute path:
2999
- ```python
3000
- add_resource_prefix("resource:///absolute/path", "prefix")
3001
- "resource://prefix//absolute/path"
3002
- ```
3003
-
3004
- Raises:
3005
- ValueError: If the URI doesn't match the expected protocol://path format
3006
- """
3007
- if not prefix:
3008
- return uri
3009
-
3010
- # Split the URI into protocol and path
3011
- match = URI_PATTERN.match(uri)
3012
- if not match:
3013
- raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2087
+ # -----------------------------------------------------------------------------
2088
+ # Module-level Factory Functions
2089
+ # -----------------------------------------------------------------------------
3014
2090
 
3015
- protocol, path = match.groups()
3016
2091
 
3017
- # Add the prefix to the path
3018
- return f"{protocol}{prefix}/{path}"
2092
+ def create_proxy(
2093
+ target: (
2094
+ Client[ClientTransportT]
2095
+ | ClientTransport
2096
+ | FastMCP[Any]
2097
+ | FastMCP1Server
2098
+ | AnyUrl
2099
+ | Path
2100
+ | MCPConfig
2101
+ | dict[str, Any]
2102
+ | str
2103
+ ),
2104
+ **settings: Any,
2105
+ ) -> FastMCPProxy:
2106
+ """Create a FastMCP proxy server for the given target.
3019
2107
 
3020
-
3021
- def remove_resource_prefix(uri: str, prefix: str) -> str:
3022
- """Remove a prefix from a resource URI.
2108
+ This is the recommended way to create a proxy server. For lower-level control,
2109
+ use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.proxy`.
3023
2110
 
3024
2111
  Args:
3025
- uri: The resource URI with a prefix
3026
- prefix: The prefix to remove
2112
+ target: The backend to proxy to. Can be:
2113
+ - A Client instance (connected or disconnected)
2114
+ - A ClientTransport
2115
+ - A FastMCP server instance
2116
+ - A URL string or AnyUrl
2117
+ - A Path to a server script
2118
+ - An MCPConfig or dict
2119
+ **settings: Additional settings passed to FastMCPProxy (name, etc.)
3027
2120
 
3028
2121
  Returns:
3029
- The resource URI with the prefix removed
2122
+ A FastMCPProxy server that proxies to the target.
3030
2123
 
3031
- Examples:
3032
- ```python
3033
- remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
3034
- "resource://path/to/resource"
3035
- ```
3036
- With absolute path:
2124
+ Example:
3037
2125
  ```python
3038
- remove_resource_prefix("resource://prefix//absolute/path", "prefix")
3039
- "resource:///absolute/path"
3040
- ```
3041
-
3042
- Raises:
3043
- ValueError: If the URI doesn't match the expected protocol://path format
3044
- """
3045
- if not prefix:
3046
- return uri
2126
+ from fastmcp.server import create_proxy
3047
2127
 
3048
- # Split the URI into protocol and path
3049
- match = URI_PATTERN.match(uri)
3050
- if not match:
3051
- raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2128
+ # Create a proxy to a remote server
2129
+ proxy = create_proxy("http://remote-server/mcp")
3052
2130
 
3053
- protocol, path = match.groups()
3054
-
3055
- # Check if the path starts with the prefix followed by a /
3056
- prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
3057
- path_match = re.match(prefix_pattern, path)
3058
- if not path_match:
3059
- return uri
3060
-
3061
- # Return the URI without the prefix
3062
- return f"{protocol}{path_match.group(1)}"
3063
-
3064
-
3065
- def has_resource_prefix(uri: str, prefix: str) -> bool:
3066
- """Check if a resource URI has a specific prefix.
3067
-
3068
- Args:
3069
- uri: The resource URI to check
3070
- prefix: The prefix to look for
3071
-
3072
- Returns:
3073
- True if the URI has the specified prefix, False otherwise
3074
-
3075
- Examples:
3076
- ```python
3077
- has_resource_prefix("resource://prefix/path/to/resource", "prefix")
3078
- True
3079
- ```
3080
- With other path:
3081
- ```python
3082
- has_resource_prefix("resource://other/path/to/resource", "prefix")
3083
- False
2131
+ # Create a proxy to another FastMCP server
2132
+ proxy = create_proxy(other_server)
3084
2133
  ```
3085
-
3086
- Raises:
3087
- ValueError: If the URI doesn't match the expected protocol://path format
3088
2134
  """
3089
- if not prefix:
3090
- return False
3091
-
3092
- # Split the URI into protocol and path
3093
- match = URI_PATTERN.match(uri)
3094
- if not match:
3095
- raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
3096
-
3097
- _, path = match.groups()
3098
-
3099
- # Check if the path starts with the prefix followed by a /
3100
- prefix_pattern = f"^{re.escape(prefix)}/"
3101
- return bool(re.match(prefix_pattern, path))
2135
+ from fastmcp.server.providers.proxy import (
2136
+ FastMCPProxy,
2137
+ _create_client_factory,
2138
+ )
2139
+
2140
+ client_factory = _create_client_factory(target)
2141
+ return FastMCPProxy(
2142
+ client_factory=client_factory,
2143
+ **settings,
2144
+ )