fastmcp 2.13.3__py3-none-any.whl → 2.14.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. fastmcp/__init__.py +0 -21
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +8 -22
  5. fastmcp/cli/install/shared.py +0 -15
  6. fastmcp/cli/tasks.py +110 -0
  7. fastmcp/client/auth/oauth.py +9 -9
  8. fastmcp/client/client.py +739 -136
  9. fastmcp/client/elicitation.py +11 -5
  10. fastmcp/client/messages.py +7 -5
  11. fastmcp/client/roots.py +2 -1
  12. fastmcp/client/sampling/__init__.py +69 -0
  13. fastmcp/client/sampling/handlers/__init__.py +0 -0
  14. fastmcp/client/sampling/handlers/anthropic.py +387 -0
  15. fastmcp/client/sampling/handlers/openai.py +399 -0
  16. fastmcp/client/tasks.py +551 -0
  17. fastmcp/client/transports.py +72 -21
  18. fastmcp/contrib/component_manager/component_service.py +4 -20
  19. fastmcp/dependencies.py +25 -0
  20. fastmcp/experimental/sampling/handlers/__init__.py +5 -0
  21. fastmcp/experimental/sampling/handlers/openai.py +4 -169
  22. fastmcp/experimental/server/openapi/__init__.py +15 -13
  23. fastmcp/experimental/utilities/openapi/__init__.py +12 -38
  24. fastmcp/prompts/prompt.py +38 -38
  25. fastmcp/resources/resource.py +33 -16
  26. fastmcp/resources/template.py +69 -59
  27. fastmcp/server/auth/__init__.py +0 -9
  28. fastmcp/server/auth/auth.py +127 -3
  29. fastmcp/server/auth/oauth_proxy.py +47 -97
  30. fastmcp/server/auth/oidc_proxy.py +7 -0
  31. fastmcp/server/auth/providers/in_memory.py +2 -2
  32. fastmcp/server/auth/providers/oci.py +2 -2
  33. fastmcp/server/context.py +509 -180
  34. fastmcp/server/dependencies.py +464 -6
  35. fastmcp/server/elicitation.py +285 -47
  36. fastmcp/server/event_store.py +177 -0
  37. fastmcp/server/http.py +15 -3
  38. fastmcp/server/low_level.py +56 -12
  39. fastmcp/server/middleware/middleware.py +2 -2
  40. fastmcp/server/openapi/__init__.py +35 -0
  41. fastmcp/{experimental/server → server}/openapi/components.py +4 -3
  42. fastmcp/{experimental/server → server}/openapi/routing.py +1 -1
  43. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  44. fastmcp/server/proxy.py +53 -40
  45. fastmcp/server/sampling/__init__.py +10 -0
  46. fastmcp/server/sampling/run.py +301 -0
  47. fastmcp/server/sampling/sampling_tool.py +108 -0
  48. fastmcp/server/server.py +793 -552
  49. fastmcp/server/tasks/__init__.py +21 -0
  50. fastmcp/server/tasks/capabilities.py +22 -0
  51. fastmcp/server/tasks/config.py +89 -0
  52. fastmcp/server/tasks/converters.py +206 -0
  53. fastmcp/server/tasks/handlers.py +356 -0
  54. fastmcp/server/tasks/keys.py +93 -0
  55. fastmcp/server/tasks/protocol.py +355 -0
  56. fastmcp/server/tasks/subscriptions.py +205 -0
  57. fastmcp/settings.py +101 -103
  58. fastmcp/tools/tool.py +83 -49
  59. fastmcp/tools/tool_transform.py +1 -12
  60. fastmcp/utilities/components.py +3 -3
  61. fastmcp/utilities/json_schema_type.py +4 -4
  62. fastmcp/utilities/mcp_config.py +1 -2
  63. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
  64. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  65. fastmcp/utilities/openapi/__init__.py +63 -0
  66. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  67. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +1 -1
  68. fastmcp/utilities/tests.py +11 -5
  69. fastmcp/utilities/types.py +8 -0
  70. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/METADATA +7 -4
  71. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/RECORD +79 -63
  72. fastmcp/client/sampling.py +0 -56
  73. fastmcp/experimental/sampling/handlers/base.py +0 -21
  74. fastmcp/server/auth/providers/bearer.py +0 -25
  75. fastmcp/server/openapi.py +0 -1087
  76. fastmcp/server/sampling/handler.py +0 -19
  77. fastmcp/utilities/openapi.py +0 -1568
  78. /fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  79. /fastmcp/{experimental/utilities → utilities}/openapi/director.py +0 -0
  80. /fastmcp/{experimental/utilities → utilities}/openapi/models.py +0 -0
  81. /fastmcp/{experimental/utilities → utilities}/openapi/parser.py +0 -0
  82. /fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +0 -0
  83. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/WHEEL +0 -0
  84. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/entry_points.txt +0 -0
  85. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import inspect
6
- import json
7
7
  import re
8
8
  import secrets
9
9
  import warnings
10
+ import weakref
10
11
  from collections.abc import (
11
12
  AsyncIterator,
12
13
  Awaitable,
@@ -19,6 +20,7 @@ from contextlib import (
19
20
  AbstractAsyncContextManager,
20
21
  AsyncExitStack,
21
22
  asynccontextmanager,
23
+ suppress,
22
24
  )
23
25
  from dataclasses import dataclass
24
26
  from functools import partial
@@ -29,21 +31,25 @@ import anyio
29
31
  import httpx
30
32
  import mcp.types
31
33
  import uvicorn
34
+ from docket import Docket, Worker
32
35
  from mcp.server.lowlevel.helper_types import ReadResourceContents
33
36
  from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
34
37
  from mcp.server.stdio import stdio_server
38
+ from mcp.shared.exceptions import McpError
35
39
  from mcp.types import (
40
+ METHOD_NOT_FOUND,
36
41
  Annotations,
37
42
  AnyFunction,
38
43
  CallToolRequestParams,
39
44
  ContentBlock,
45
+ ErrorData,
40
46
  GetPromptResult,
41
47
  ToolAnnotations,
42
48
  )
43
- from mcp.types import Prompt as MCPPrompt
44
- from mcp.types import Resource as MCPResource
45
- from mcp.types import ResourceTemplate as MCPResourceTemplate
46
- from mcp.types import Tool as MCPTool
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
47
53
  from pydantic import AnyUrl
48
54
  from starlette.middleware import Middleware as ASGIMiddleware
49
55
  from starlette.requests import Request
@@ -57,10 +63,11 @@ from fastmcp.mcp_config import MCPConfig
57
63
  from fastmcp.prompts import Prompt
58
64
  from fastmcp.prompts.prompt import FunctionPrompt
59
65
  from fastmcp.prompts.prompt_manager import PromptManager
60
- from fastmcp.resources.resource import Resource
66
+ from fastmcp.resources.resource import FunctionResource, Resource
61
67
  from fastmcp.resources.resource_manager import ResourceManager
62
- from fastmcp.resources.template import ResourceTemplate
68
+ from fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate
63
69
  from fastmcp.server.auth import AuthProvider
70
+ from fastmcp.server.event_store import EventStore
64
71
  from fastmcp.server.http import (
65
72
  StarletteWithLifespan,
66
73
  create_sse_app,
@@ -68,6 +75,13 @@ from fastmcp.server.http import (
68
75
  )
69
76
  from fastmcp.server.low_level import LowLevelServer
70
77
  from fastmcp.server.middleware import Middleware, MiddlewareContext
78
+ from fastmcp.server.tasks.capabilities import get_task_capabilities
79
+ from fastmcp.server.tasks.config import TaskConfig
80
+ from fastmcp.server.tasks.handlers import (
81
+ handle_prompt_as_task,
82
+ handle_resource_as_task,
83
+ handle_tool_as_task,
84
+ )
71
85
  from fastmcp.settings import Settings
72
86
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
73
87
  from fastmcp.tools.tool_manager import ToolManager
@@ -80,24 +94,34 @@ from fastmcp.utilities.types import NotSet, NotSetT
80
94
  if TYPE_CHECKING:
81
95
  from fastmcp.client import Client
82
96
  from fastmcp.client.client import FastMCP1Server
97
+ from fastmcp.client.sampling import SamplingHandler
83
98
  from fastmcp.client.transports import ClientTransport, ClientTransportT
84
- from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
85
- from fastmcp.experimental.server.openapi.routing import (
86
- ComponentFn as OpenAPIComponentFnNew,
87
- )
88
- from fastmcp.experimental.server.openapi.routing import RouteMap as RouteMapNew
89
- from fastmcp.experimental.server.openapi.routing import (
90
- RouteMapFn as OpenAPIRouteMapFnNew,
91
- )
92
99
  from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
93
100
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
94
101
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
95
102
  from fastmcp.server.proxy import FastMCPProxy
96
- from fastmcp.server.sampling.handler import ServerSamplingHandler
97
103
  from fastmcp.tools.tool import ToolResultSerializerType
98
104
 
99
105
  logger = get_logger(__name__)
100
106
 
107
+
108
+ def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
109
+ """Create a wrapper function with a custom __name__ for Docket registration.
110
+
111
+ Docket uses fn.__name__ as the key for function registration and lookup.
112
+ When mounting servers, we need unique names to avoid collisions between
113
+ mounted servers that have identically-named functions.
114
+ """
115
+ import functools
116
+
117
+ @functools.wraps(fn)
118
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
119
+ return await fn(*args, **kwargs)
120
+
121
+ wrapper.__name__ = name
122
+ return wrapper
123
+
124
+
101
125
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
102
126
  Transport = Literal["stdio", "http", "sse", "streamable-http"]
103
127
 
@@ -158,8 +182,6 @@ class FastMCP(Generic[LifespanResultT]):
158
182
  auth: AuthProvider | NotSetT | None = NotSet,
159
183
  middleware: Sequence[Middleware] | None = None,
160
184
  lifespan: LifespanCallable | None = None,
161
- dependencies: list[str] | None = None,
162
- resource_prefix_format: Literal["protocol", "path"] | None = None,
163
185
  mask_error_details: bool | None = None,
164
186
  tools: Sequence[Tool | Callable[..., Any]] | None = None,
165
187
  tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
@@ -171,6 +193,7 @@ class FastMCP(Generic[LifespanResultT]):
171
193
  on_duplicate_resources: DuplicateBehavior | None = None,
172
194
  on_duplicate_prompts: DuplicateBehavior | None = None,
173
195
  strict_input_validation: bool | None = None,
196
+ tasks: bool | None = None,
174
197
  # ---
175
198
  # ---
176
199
  # --- The following arguments are DEPRECATED ---
@@ -185,15 +208,20 @@ class FastMCP(Generic[LifespanResultT]):
185
208
  streamable_http_path: str | None = None,
186
209
  json_response: bool | None = None,
187
210
  stateless_http: bool | None = None,
188
- sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None,
211
+ sampling_handler: SamplingHandler | None = None,
189
212
  sampling_handler_behavior: Literal["always", "fallback"] | None = None,
190
213
  ):
191
- self.resource_prefix_format: Literal["protocol", "path"] = (
192
- resource_prefix_format or fastmcp.settings.resource_prefix_format
193
- )
214
+ # Resolve server default for background task support
215
+ self._support_tasks_by_default: bool = tasks if tasks is not None else False
216
+
217
+ # Docket instance (set during lifespan for cross-task access)
218
+ self._docket = None
194
219
 
195
220
  self._additional_http_routes: list[BaseRoute] = []
196
221
  self._mounted_servers: list[MountedServer] = []
222
+ self._is_mounted: bool = (
223
+ False # Set to True when this server is mounted on another
224
+ )
197
225
  self._tool_manager: ToolManager = ToolManager(
198
226
  duplicate_behavior=on_duplicate_tools,
199
227
  mask_error_details=mask_error_details,
@@ -212,6 +240,7 @@ class FastMCP(Generic[LifespanResultT]):
212
240
  self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
213
241
  self._lifespan_result: LifespanResultT | None = None
214
242
  self._lifespan_result_set: bool = False
243
+ self._started: asyncio.Event = asyncio.Event()
215
244
 
216
245
  # Generate random ID if no name provided
217
246
  self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
@@ -259,27 +288,7 @@ class FastMCP(Generic[LifespanResultT]):
259
288
  # Set up MCP protocol handlers
260
289
  self._setup_handlers()
261
290
 
262
- # Handle dependencies with deprecation warning
263
- # TODO: Remove dependencies parameter (deprecated in v2.11.4)
264
- if dependencies is not None:
265
- import warnings
266
-
267
- warnings.warn(
268
- "The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. "
269
- "Please specify dependencies in a fastmcp.json configuration file instead:\n"
270
- '{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": '
271
- f"{json.dumps(dependencies)}\n }}\n}}\n"
272
- "See https://gofastmcp.com/docs/deployment/server-configuration for more information.",
273
- DeprecationWarning,
274
- stacklevel=2,
275
- )
276
- self.dependencies: list[str] = (
277
- dependencies or fastmcp.settings.server_dependencies
278
- ) # TODO: Remove (deprecated in v2.11.4)
279
-
280
- self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
281
- sampling_handler
282
- )
291
+ self.sampling_handler: SamplingHandler | None = sampling_handler
283
292
  self.sampling_handler_behavior: Literal["always", "fallback"] = (
284
293
  sampling_handler_behavior or "fallback"
285
294
  )
@@ -290,7 +299,6 @@ class FastMCP(Generic[LifespanResultT]):
290
299
  else fastmcp.settings.include_fastmcp_meta
291
300
  )
292
301
 
293
- # handle deprecated settings
294
302
  self._handle_deprecated_settings(
295
303
  log_level=log_level,
296
304
  debug=debug,
@@ -383,14 +391,207 @@ class FastMCP(Generic[LifespanResultT]):
383
391
  else:
384
392
  return list(self._mcp_server.icons)
385
393
 
394
+ @property
395
+ def docket(self) -> Docket | None:
396
+ """Get the Docket instance if Docket support is enabled.
397
+
398
+ Returns None if Docket is not enabled or server hasn't been started yet.
399
+ """
400
+ return self._docket
401
+
402
+ @asynccontextmanager
403
+ async def _docket_lifespan(self) -> AsyncIterator[None]:
404
+ """Manage Docket instance and Worker for background task execution."""
405
+ from fastmcp import settings
406
+
407
+ # Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
408
+ from fastmcp.server.dependencies import (
409
+ _current_docket,
410
+ _current_server,
411
+ _current_worker,
412
+ )
413
+
414
+ server_token = _current_server.set(weakref.ref(self))
415
+
416
+ try:
417
+ # For directly mounted servers, the parent's Docket/Worker handles all
418
+ # task execution. Skip creating our own to avoid race conditions with
419
+ # multiple workers competing for tasks from the same queue.
420
+ if self._is_mounted:
421
+ yield
422
+ return
423
+
424
+ # Create Docket instance using configured name and URL
425
+ async with Docket(
426
+ name=settings.docket.name,
427
+ url=settings.docket.url,
428
+ ) as docket:
429
+ # Store on server instance for cross-task access (FastMCPTransport)
430
+ self._docket = docket
431
+
432
+ # Register local task-enabled tools/prompts/resources with Docket
433
+ # Only function-based variants support background tasks
434
+ # Register components where task execution is not "forbidden"
435
+ for tool in self._tool_manager._tools.values():
436
+ if (
437
+ isinstance(tool, FunctionTool)
438
+ and tool.task_config.mode != "forbidden"
439
+ ):
440
+ docket.register(tool.fn)
441
+
442
+ for prompt in self._prompt_manager._prompts.values():
443
+ if (
444
+ isinstance(prompt, FunctionPrompt)
445
+ and prompt.task_config.mode != "forbidden"
446
+ ):
447
+ # task execution requires async fn (validated at creation time)
448
+ docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
449
+
450
+ for resource in self._resource_manager._resources.values():
451
+ if (
452
+ isinstance(resource, FunctionResource)
453
+ and resource.task_config.mode != "forbidden"
454
+ ):
455
+ docket.register(resource.fn)
456
+
457
+ for template in self._resource_manager._templates.values():
458
+ if (
459
+ isinstance(template, FunctionResourceTemplate)
460
+ and template.task_config.mode != "forbidden"
461
+ ):
462
+ docket.register(template.fn)
463
+
464
+ # Also register functions from mounted servers so tasks can
465
+ # execute in the parent's Docket context
466
+ for mounted in self._mounted_servers:
467
+ await self._register_mounted_server_functions(
468
+ mounted.server, docket, mounted.prefix, mounted.tool_names
469
+ )
470
+
471
+ # Set Docket in ContextVar so CurrentDocket can access it
472
+ docket_token = _current_docket.set(docket)
473
+ try:
474
+ # Build worker kwargs from settings
475
+ worker_kwargs: dict[str, Any] = {
476
+ "concurrency": settings.docket.concurrency,
477
+ "redelivery_timeout": settings.docket.redelivery_timeout,
478
+ "reconnection_delay": settings.docket.reconnection_delay,
479
+ }
480
+ if settings.docket.worker_name:
481
+ worker_kwargs["name"] = settings.docket.worker_name
482
+
483
+ # Create and start Worker
484
+ async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
485
+ # Set Worker in ContextVar so CurrentWorker can access it
486
+ worker_token = _current_worker.set(worker)
487
+ try:
488
+ worker_task = asyncio.create_task(worker.run_forever())
489
+ try:
490
+ yield
491
+ finally:
492
+ # Cancel worker task on exit with timeout to prevent hanging
493
+ worker_task.cancel()
494
+ with suppress(
495
+ asyncio.CancelledError, asyncio.TimeoutError
496
+ ):
497
+ await asyncio.wait_for(worker_task, timeout=2.0)
498
+ finally:
499
+ _current_worker.reset(worker_token)
500
+ finally:
501
+ # Reset ContextVar
502
+ _current_docket.reset(docket_token)
503
+ # Clear instance attribute
504
+ self._docket = None
505
+ finally:
506
+ # Reset server ContextVar
507
+ _current_server.reset(server_token)
508
+
509
+ async def _register_mounted_server_functions(
510
+ self,
511
+ server: FastMCP,
512
+ docket: Docket,
513
+ prefix: str | None,
514
+ tool_names: dict[str, str] | None = None,
515
+ ) -> None:
516
+ """Register task-enabled functions from a mounted server with Docket.
517
+
518
+ This enables background task execution for mounted server components
519
+ through the parent server's Docket context.
520
+
521
+ Args:
522
+ server: The mounted server whose functions to register
523
+ docket: The Docket instance to register with
524
+ prefix: The mount prefix to prepend to function names (matches
525
+ client-facing tool/prompt names)
526
+ tool_names: Optional mapping of original tool names to custom names
527
+ """
528
+ # Register tools with prefixed names to avoid collisions
529
+ for tool in server._tool_manager._tools.values():
530
+ if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
531
+ # Apply tool_names override first, then prefix (matches get_tools logic)
532
+ if tool_names and tool.key in tool_names:
533
+ fn_name = tool_names[tool.key]
534
+ elif prefix:
535
+ fn_name = f"{prefix}_{tool.key}"
536
+ else:
537
+ fn_name = tool.key
538
+ named_fn = _create_named_fn_wrapper(tool.fn, fn_name)
539
+ docket.register(named_fn)
540
+
541
+ # Register prompts with prefixed names
542
+ for prompt in server._prompt_manager._prompts.values():
543
+ if (
544
+ isinstance(prompt, FunctionPrompt)
545
+ and prompt.task_config.mode != "forbidden"
546
+ ):
547
+ fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
548
+ named_fn = _create_named_fn_wrapper(
549
+ cast(Callable[..., Awaitable[Any]], prompt.fn), fn_name
550
+ )
551
+ docket.register(named_fn)
552
+
553
+ # Register resources with prefixed names (use name, not key/URI)
554
+ for resource in server._resource_manager._resources.values():
555
+ if (
556
+ isinstance(resource, FunctionResource)
557
+ and resource.task_config.mode != "forbidden"
558
+ ):
559
+ fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
560
+ named_fn = _create_named_fn_wrapper(resource.fn, fn_name)
561
+ docket.register(named_fn)
562
+
563
+ # Register resource templates with prefixed names (use name, not key/URI)
564
+ for template in server._resource_manager._templates.values():
565
+ if (
566
+ isinstance(template, FunctionResourceTemplate)
567
+ and template.task_config.mode != "forbidden"
568
+ ):
569
+ fn_name = f"{prefix}_{template.name}" if prefix else template.name
570
+ named_fn = _create_named_fn_wrapper(template.fn, fn_name)
571
+ docket.register(named_fn)
572
+
573
+ # Recursively register from nested mounted servers with accumulated prefix
574
+ for nested in server._mounted_servers:
575
+ nested_prefix = (
576
+ f"{prefix}_{nested.prefix}"
577
+ if prefix and nested.prefix
578
+ else (prefix or nested.prefix)
579
+ )
580
+ await self._register_mounted_server_functions(
581
+ nested.server, docket, nested_prefix, nested.tool_names
582
+ )
583
+
386
584
  @asynccontextmanager
387
585
  async def _lifespan_manager(self) -> AsyncIterator[None]:
388
586
  if self._lifespan_result_set:
389
587
  yield
390
588
  return
391
589
 
392
- async with self._lifespan(self) as lifespan_result:
393
- self._lifespan_result = lifespan_result
590
+ async with (
591
+ self._lifespan(self) as user_lifespan_result,
592
+ self._docket_lifespan(),
593
+ ):
594
+ self._lifespan_result = user_lifespan_result
394
595
  self._lifespan_result_set = True
395
596
 
396
597
  async with AsyncExitStack[bool | None]() as stack:
@@ -399,7 +600,11 @@ class FastMCP(Generic[LifespanResultT]):
399
600
  cm=server.server._lifespan_manager()
400
601
  )
401
602
 
402
- yield
603
+ self._started.set()
604
+ try:
605
+ yield
606
+ finally:
607
+ self._started.clear()
403
608
 
404
609
  self._lifespan_result_set = False
405
610
  self._lifespan_result = None
@@ -464,8 +669,260 @@ class FastMCP(Generic[LifespanResultT]):
464
669
  self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
465
670
  self._call_tool_mcp
466
671
  )
467
- self._mcp_server.read_resource()(self._read_resource_mcp)
468
- self._mcp_server.get_prompt()(self._get_prompt_mcp)
672
+ # Register custom read_resource handler (SDK decorator doesn't support CreateTaskResult)
673
+ self._setup_read_resource_handler()
674
+ # Register custom get_prompt handler (SDK decorator doesn't support CreateTaskResult)
675
+ self._setup_get_prompt_handler()
676
+ # Register custom SEP-1686 task protocol handlers
677
+ self._setup_task_protocol_handlers()
678
+
679
+ def _setup_read_resource_handler(self) -> None:
680
+ """
681
+ Set up custom read_resource handler that supports task-augmented responses.
682
+
683
+ The SDK's read_resource decorator doesn't support CreateTaskResult returns,
684
+ so we register a custom handler that checks request_context.experimental.is_task.
685
+ """
686
+
687
+ async def handler(req: mcp.types.ReadResourceRequest) -> mcp.types.ServerResult:
688
+ uri = req.params.uri
689
+
690
+ # Check for task metadata via SDK's request context
691
+ task_meta = None
692
+ try:
693
+ ctx = self._mcp_server.request_context
694
+ if ctx.experimental.is_task:
695
+ task_meta = ctx.experimental.task_metadata
696
+ except (AttributeError, LookupError):
697
+ pass
698
+
699
+ # Check for task metadata and route appropriately
700
+ async with fastmcp.server.context.Context(fastmcp=self):
701
+ # Get resource including from mounted servers
702
+ resource = await self._get_resource_or_template_or_none(str(uri))
703
+ if (
704
+ resource
705
+ and self._should_enable_component(resource)
706
+ and hasattr(resource, "task_config")
707
+ ):
708
+ task_mode = resource.task_config.mode # type: ignore[union-attr]
709
+
710
+ # Enforce mode="required" - must have task metadata
711
+ if task_mode == "required" and not task_meta:
712
+ raise McpError(
713
+ ErrorData(
714
+ code=METHOD_NOT_FOUND,
715
+ message=f"Resource '{uri}' requires task-augmented execution",
716
+ )
717
+ )
718
+
719
+ # Route to background if task metadata present and mode allows
720
+ if task_meta and task_mode != "forbidden":
721
+ # For FunctionResource/FunctionResourceTemplate, use Docket
722
+ if isinstance(
723
+ resource,
724
+ FunctionResource | FunctionResourceTemplate,
725
+ ):
726
+ task_meta_dict = task_meta.model_dump(exclude_none=True)
727
+ return await handle_resource_as_task(
728
+ self, str(uri), resource, task_meta_dict
729
+ )
730
+
731
+ # Forbidden mode: task requested but mode="forbidden"
732
+ # Raise error since resources don't have isError field
733
+ if task_meta and task_mode == "forbidden":
734
+ raise McpError(
735
+ ErrorData(
736
+ code=METHOD_NOT_FOUND,
737
+ message=f"Resource '{uri}' does not support task-augmented execution",
738
+ )
739
+ )
740
+
741
+ # Synchronous execution
742
+ result = await self._read_resource_mcp(uri)
743
+
744
+ # Graceful degradation: if we got here with task_meta, something went wrong
745
+ # (This should be unreachable now that forbidden raises)
746
+ if task_meta:
747
+ mcp_contents = []
748
+ for item in result:
749
+ if isinstance(item.content, str):
750
+ mcp_contents.append(
751
+ mcp.types.TextResourceContents(
752
+ uri=uri,
753
+ text=item.content,
754
+ mimeType=item.mime_type or "text/plain",
755
+ )
756
+ )
757
+ elif isinstance(item.content, bytes):
758
+ import base64
759
+
760
+ mcp_contents.append(
761
+ mcp.types.BlobResourceContents(
762
+ uri=uri,
763
+ blob=base64.b64encode(item.content).decode(),
764
+ mimeType=item.mime_type or "application/octet-stream",
765
+ )
766
+ )
767
+ return mcp.types.ServerResult(
768
+ mcp.types.ReadResourceResult(
769
+ contents=mcp_contents,
770
+ _meta={
771
+ "modelcontextprotocol.io/task": {
772
+ "returned_immediately": True
773
+ }
774
+ },
775
+ )
776
+ )
777
+
778
+ # Convert to proper ServerResult
779
+ if isinstance(result, mcp.types.ServerResult):
780
+ return result
781
+
782
+ mcp_contents = []
783
+ for item in result:
784
+ if isinstance(item.content, str):
785
+ mcp_contents.append(
786
+ mcp.types.TextResourceContents(
787
+ uri=uri,
788
+ text=item.content,
789
+ mimeType=item.mime_type or "text/plain",
790
+ )
791
+ )
792
+ elif isinstance(item.content, bytes):
793
+ import base64
794
+
795
+ mcp_contents.append(
796
+ mcp.types.BlobResourceContents(
797
+ uri=uri,
798
+ blob=base64.b64encode(item.content).decode(),
799
+ mimeType=item.mime_type or "application/octet-stream",
800
+ )
801
+ )
802
+
803
+ return mcp.types.ServerResult(
804
+ mcp.types.ReadResourceResult(contents=mcp_contents)
805
+ )
806
+
807
+ self._mcp_server.request_handlers[mcp.types.ReadResourceRequest] = handler
808
+
809
+ def _setup_get_prompt_handler(self) -> None:
810
+ """
811
+ Set up custom get_prompt handler that supports task-augmented responses.
812
+
813
+ The SDK's get_prompt decorator doesn't support CreateTaskResult returns,
814
+ so we register a custom handler that checks request_context.experimental.is_task.
815
+ """
816
+
817
+ async def handler(req: mcp.types.GetPromptRequest) -> mcp.types.ServerResult:
818
+ name = req.params.name
819
+ arguments = req.params.arguments
820
+
821
+ # Check for task metadata via SDK's request context
822
+ task_meta = None
823
+ try:
824
+ ctx = self._mcp_server.request_context
825
+ if ctx.experimental.is_task:
826
+ task_meta = ctx.experimental.task_metadata
827
+ except (AttributeError, LookupError):
828
+ pass
829
+
830
+ # Check for task metadata and route appropriately
831
+ async with fastmcp.server.context.Context(fastmcp=self):
832
+ prompts = await self.get_prompts()
833
+ prompt = prompts.get(name)
834
+ if (
835
+ prompt
836
+ and self._should_enable_component(prompt)
837
+ and hasattr(prompt, "task_config")
838
+ and prompt.task_config
839
+ ):
840
+ task_mode = prompt.task_config.mode # type: ignore[union-attr]
841
+
842
+ # Enforce mode="required" - must have task metadata
843
+ if task_mode == "required" and not task_meta:
844
+ raise McpError(
845
+ ErrorData(
846
+ code=METHOD_NOT_FOUND,
847
+ message=f"Prompt '{name}' requires task-augmented execution",
848
+ )
849
+ )
850
+
851
+ # Route to background if task metadata present and mode allows
852
+ if task_meta and task_mode != "forbidden":
853
+ task_meta_dict = task_meta.model_dump(exclude_none=True)
854
+ result = await handle_prompt_as_task(
855
+ self, name, arguments, task_meta_dict
856
+ )
857
+ return mcp.types.ServerResult(result)
858
+
859
+ # Forbidden mode: task requested but mode="forbidden"
860
+ # Raise error since prompts don't have isError field
861
+ if task_meta and task_mode == "forbidden":
862
+ raise McpError(
863
+ ErrorData(
864
+ code=METHOD_NOT_FOUND,
865
+ message=f"Prompt '{name}' does not support task-augmented execution",
866
+ )
867
+ )
868
+
869
+ # Synchronous execution
870
+ result = await self._get_prompt_mcp(name, arguments)
871
+ return mcp.types.ServerResult(result)
872
+
873
+ self._mcp_server.request_handlers[mcp.types.GetPromptRequest] = handler
874
+
875
+ def _setup_task_protocol_handlers(self) -> None:
876
+ """Register SEP-1686 task protocol handlers with SDK."""
877
+ from mcp.types import (
878
+ CancelTaskRequest,
879
+ GetTaskPayloadRequest,
880
+ GetTaskRequest,
881
+ ListTasksRequest,
882
+ ServerResult,
883
+ )
884
+
885
+ from fastmcp.server.tasks.protocol import (
886
+ tasks_cancel_handler,
887
+ tasks_get_handler,
888
+ tasks_list_handler,
889
+ tasks_result_handler,
890
+ )
891
+
892
+ # Manually register handlers (SDK decorators fail with locally-defined functions)
893
+ # SDK expects handlers that receive Request objects and return ServerResult
894
+
895
+ async def handle_get_task(req: GetTaskRequest) -> ServerResult:
896
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
897
+ result = await tasks_get_handler(self, params)
898
+ return ServerResult(result)
899
+
900
+ async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:
901
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
902
+ result = await tasks_result_handler(self, params)
903
+ return ServerResult(result)
904
+
905
+ async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:
906
+ params = (
907
+ req.params.model_dump(by_alias=True, exclude_none=True)
908
+ if req.params
909
+ else {}
910
+ )
911
+ result = await tasks_list_handler(self, params)
912
+ return ServerResult(result)
913
+
914
+ async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:
915
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
916
+ result = await tasks_cancel_handler(self, params)
917
+ return ServerResult(result)
918
+
919
+ # Register directly with SDK (same as what decorators do internally)
920
+ self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task
921
+ self._mcp_server.request_handlers[GetTaskPayloadRequest] = (
922
+ handle_get_task_result
923
+ )
924
+ self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks
925
+ self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task
469
926
 
470
927
  async def _apply_middleware(
471
928
  self,
@@ -489,7 +946,13 @@ class FastMCP(Generic[LifespanResultT]):
489
946
  try:
490
947
  child_tools = await mounted.server.get_tools()
491
948
  for key, tool in child_tools.items():
492
- new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
949
+ # Check for manual override first, then apply prefix
950
+ if mounted.tool_names and key in mounted.tool_names:
951
+ new_key = mounted.tool_names[key]
952
+ elif mounted.prefix:
953
+ new_key = f"{mounted.prefix}_{key}"
954
+ else:
955
+ new_key = key
493
956
  all_tools[new_key] = tool.model_copy(key=new_key)
494
957
  except Exception as e:
495
958
  logger.warning(
@@ -507,6 +970,33 @@ class FastMCP(Generic[LifespanResultT]):
507
970
  raise NotFoundError(f"Unknown tool: {key}")
508
971
  return tools[key]
509
972
 
973
+ async def _get_tool_with_task_config(self, key: str) -> Tool | None:
974
+ """Get a tool by key, returning None if not found.
975
+
976
+ Used for task config checking where we need the actual tool object
977
+ (including from mounted servers and proxies) but don't want to raise.
978
+ """
979
+ try:
980
+ return await self.get_tool(key)
981
+ except NotFoundError:
982
+ return None
983
+
984
+ async def _get_resource_or_template_or_none(
985
+ self, uri: str
986
+ ) -> Resource | ResourceTemplate | None:
987
+ """Get a resource or template by URI, searching recursively. Returns None if not found."""
988
+ try:
989
+ return await self.get_resource(uri)
990
+ except NotFoundError:
991
+ pass
992
+
993
+ templates = await self.get_resource_templates()
994
+ for template in templates.values():
995
+ if template.matches(uri):
996
+ return template
997
+
998
+ return None
999
+
510
1000
  async def get_resources(self) -> dict[str, Resource]:
511
1001
  """Get all resources (unfiltered), including mounted servers, indexed by key."""
512
1002
  all_resources = dict(await self._resource_manager.get_resources())
@@ -516,9 +1006,7 @@ class FastMCP(Generic[LifespanResultT]):
516
1006
  child_resources = await mounted.server.get_resources()
517
1007
  for key, resource in child_resources.items():
518
1008
  new_key = (
519
- add_resource_prefix(
520
- key, mounted.prefix, mounted.resource_prefix_format
521
- )
1009
+ add_resource_prefix(key, mounted.prefix)
522
1010
  if mounted.prefix
523
1011
  else key
524
1012
  )
@@ -555,17 +1043,16 @@ class FastMCP(Generic[LifespanResultT]):
555
1043
  child_templates = await mounted.server.get_resource_templates()
556
1044
  for key, template in child_templates.items():
557
1045
  new_key = (
558
- add_resource_prefix(
559
- key, mounted.prefix, mounted.resource_prefix_format
560
- )
1046
+ add_resource_prefix(key, mounted.prefix)
561
1047
  if mounted.prefix
562
1048
  else key
563
1049
  )
564
- update = (
565
- {"name": f"{mounted.prefix}_{template.name}"}
566
- if mounted.prefix and template.name
567
- else {}
568
- )
1050
+ update: dict[str, Any] = {}
1051
+ if mounted.prefix:
1052
+ if template.name:
1053
+ update["name"] = f"{mounted.prefix}_{template.name}"
1054
+ # Update uri_template so matches() works with prefixed URIs
1055
+ update["uri_template"] = new_key
569
1056
  all_templates[new_key] = template.model_copy(
570
1057
  key=new_key, update=update
571
1058
  )
@@ -680,7 +1167,7 @@ class FastMCP(Generic[LifespanResultT]):
680
1167
 
681
1168
  return routes
682
1169
 
683
- async def _list_tools_mcp(self) -> list[MCPTool]:
1170
+ async def _list_tools_mcp(self) -> list[SDKTool]:
684
1171
  """
685
1172
  List all available tools, in the format expected by the low-level MCP
686
1173
  server.
@@ -745,9 +1232,15 @@ class FastMCP(Generic[LifespanResultT]):
745
1232
  if not self._should_enable_component(tool):
746
1233
  continue
747
1234
 
748
- key = tool.key
749
- if mounted.prefix:
1235
+ # Check for manual override first, then apply prefix
1236
+ if mounted.tool_names and tool.key in mounted.tool_names:
1237
+ key = mounted.tool_names[tool.key]
1238
+ elif mounted.prefix:
750
1239
  key = f"{mounted.prefix}_{tool.key}"
1240
+ else:
1241
+ key = tool.key
1242
+
1243
+ if key != tool.key:
751
1244
  tool = tool.model_copy(key=key)
752
1245
  # Later mounted servers override earlier ones
753
1246
  all_tools[key] = tool
@@ -764,7 +1257,7 @@ class FastMCP(Generic[LifespanResultT]):
764
1257
 
765
1258
  return list(all_tools.values())
766
1259
 
767
- async def _list_resources_mcp(self) -> list[MCPResource]:
1260
+ async def _list_resources_mcp(self) -> list[SDKResource]:
768
1261
  """
769
1262
  List all available resources, in the format expected by the low-level MCP
770
1263
  server.
@@ -835,11 +1328,7 @@ class FastMCP(Generic[LifespanResultT]):
835
1328
 
836
1329
  key = resource.key
837
1330
  if mounted.prefix:
838
- key = add_resource_prefix(
839
- resource.key,
840
- mounted.prefix,
841
- mounted.resource_prefix_format,
842
- )
1331
+ key = add_resource_prefix(resource.key, mounted.prefix)
843
1332
  resource = resource.model_copy(
844
1333
  key=key,
845
1334
  update={"name": f"{mounted.prefix}_{resource.name}"},
@@ -857,7 +1346,7 @@ class FastMCP(Generic[LifespanResultT]):
857
1346
 
858
1347
  return list(all_resources.values())
859
1348
 
860
- async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
1349
+ async def _list_resource_templates_mcp(self) -> list[SDKResourceTemplate]:
861
1350
  """
862
1351
  List all available resource templates, in the format expected by the low-level MCP
863
1352
  server.
@@ -931,11 +1420,7 @@ class FastMCP(Generic[LifespanResultT]):
931
1420
 
932
1421
  key = template.key
933
1422
  if mounted.prefix:
934
- key = add_resource_prefix(
935
- template.key,
936
- mounted.prefix,
937
- mounted.resource_prefix_format,
938
- )
1423
+ key = add_resource_prefix(template.key, mounted.prefix)
939
1424
  template = template.model_copy(
940
1425
  key=key,
941
1426
  update={"name": f"{mounted.prefix}_{template.name}"},
@@ -955,7 +1440,7 @@ class FastMCP(Generic[LifespanResultT]):
955
1440
 
956
1441
  return list(all_templates.values())
957
1442
 
958
- async def _list_prompts_mcp(self) -> list[MCPPrompt]:
1443
+ async def _list_prompts_mcp(self) -> list[SDKPrompt]:
959
1444
  """
960
1445
  List all available prompts, in the format expected by the low-level MCP
961
1446
  server.
@@ -1025,9 +1510,13 @@ class FastMCP(Generic[LifespanResultT]):
1025
1510
  if not self._should_enable_component(prompt):
1026
1511
  continue
1027
1512
 
1028
- key = prompt.key
1513
+ # Apply prefix to prompt key
1029
1514
  if mounted.prefix:
1030
1515
  key = f"{mounted.prefix}_{prompt.key}"
1516
+ else:
1517
+ key = prompt.key
1518
+
1519
+ if key != prompt.key:
1031
1520
  prompt = prompt.model_copy(key=key)
1032
1521
  # Later mounted servers override earlier ones
1033
1522
  all_prompts[key] = prompt
@@ -1054,7 +1543,7 @@ class FastMCP(Generic[LifespanResultT]):
1054
1543
  """
1055
1544
  Handle MCP 'callTool' requests.
1056
1545
 
1057
- Delegates to _call_tool, which should be overridden by FastMCP subclasses.
1546
+ Detects SEP-1686 task metadata and routes to background execution if supported.
1058
1547
 
1059
1548
  Args:
1060
1549
  key: The name of the tool to call
@@ -1069,6 +1558,65 @@ class FastMCP(Generic[LifespanResultT]):
1069
1558
 
1070
1559
  async with fastmcp.server.context.Context(fastmcp=self):
1071
1560
  try:
1561
+ # Check for SEP-1686 task metadata via request context
1562
+ task_meta = None
1563
+ try:
1564
+ # Access task metadata from SDK's request context
1565
+ ctx = self._mcp_server.request_context
1566
+ if ctx.experimental.is_task:
1567
+ task_meta = ctx.experimental.task_metadata
1568
+ except (AttributeError, LookupError):
1569
+ # No request context available - proceed without task metadata
1570
+ pass
1571
+
1572
+ # Get tool from local manager, mounted servers, or proxy
1573
+ tool = await self._get_tool_with_task_config(key)
1574
+ if (
1575
+ tool
1576
+ and self._should_enable_component(tool)
1577
+ and hasattr(tool, "task_config")
1578
+ ):
1579
+ task_mode = tool.task_config.mode # type: ignore[union-attr]
1580
+
1581
+ # Enforce mode="required" - must have task metadata
1582
+ if task_mode == "required" and not task_meta:
1583
+ raise McpError(
1584
+ ErrorData(
1585
+ code=METHOD_NOT_FOUND,
1586
+ message=f"Tool '{key}' requires task-augmented execution",
1587
+ )
1588
+ )
1589
+
1590
+ # Route to background if task metadata present and mode allows
1591
+ if task_meta and task_mode != "forbidden":
1592
+ # For FunctionTool, use Docket for background execution
1593
+ if isinstance(tool, FunctionTool):
1594
+ task_meta_dict = task_meta.model_dump(exclude_none=True)
1595
+ return await handle_tool_as_task(
1596
+ self, key, arguments, task_meta_dict
1597
+ )
1598
+ # For ProxyTool/mounted tools, proceed with normal execution
1599
+ # They will forward task metadata to their backend
1600
+
1601
+ # Forbidden mode: task requested but mode="forbidden"
1602
+ # Return error result with returned_immediately=True
1603
+ if task_meta and task_mode == "forbidden":
1604
+ return mcp.types.CallToolResult(
1605
+ content=[
1606
+ mcp.types.TextContent(
1607
+ type="text",
1608
+ text=f"Tool '{key}' does not support task-augmented execution",
1609
+ )
1610
+ ],
1611
+ isError=True,
1612
+ _meta={
1613
+ "modelcontextprotocol.io/task": {
1614
+ "returned_immediately": True
1615
+ }
1616
+ },
1617
+ )
1618
+
1619
+ # Synchronous execution (normal path)
1072
1620
  result = await self._call_tool_middleware(key, arguments)
1073
1621
  return result.to_mcp_result()
1074
1622
  except DisabledError as e:
@@ -1108,14 +1656,29 @@ class FastMCP(Generic[LifespanResultT]):
1108
1656
  # Try mounted servers in reverse order (later wins)
1109
1657
  for mounted in reversed(self._mounted_servers):
1110
1658
  try_name = tool_name
1111
- if mounted.prefix:
1659
+
1660
+ # First check if tool_name is an overridden name (reverse lookup)
1661
+ if mounted.tool_names:
1662
+ for orig_key, override_name in mounted.tool_names.items():
1663
+ if override_name == tool_name:
1664
+ try_name = orig_key
1665
+ break
1666
+ else:
1667
+ # Not an override, try standard prefix stripping
1668
+ if mounted.prefix:
1669
+ if not tool_name.startswith(f"{mounted.prefix}_"):
1670
+ continue
1671
+ try_name = tool_name[len(mounted.prefix) + 1 :]
1672
+ elif mounted.prefix:
1112
1673
  if not tool_name.startswith(f"{mounted.prefix}_"):
1113
1674
  continue
1114
1675
  try_name = tool_name[len(mounted.prefix) + 1 :]
1115
1676
 
1116
1677
  try:
1117
1678
  # First, get the tool to check if parent's filter allows it
1118
- tool = await mounted.server._tool_manager.get_tool(try_name)
1679
+ # Use get_tool() instead of _tool_manager.get_tool() to support
1680
+ # nested mounted servers (tools mounted more than 2 levels deep)
1681
+ tool = await mounted.server.get_tool(try_name)
1119
1682
  if not self._should_enable_component(tool):
1120
1683
  # Parent filter blocks this tool, continue searching
1121
1684
  continue
@@ -1148,6 +1711,7 @@ class FastMCP(Generic[LifespanResultT]):
1148
1711
 
1149
1712
  async with fastmcp.server.context.Context(fastmcp=self):
1150
1713
  try:
1714
+ # Task routing handled by custom handler
1151
1715
  return list[ReadResourceContents](
1152
1716
  await self._read_resource_middleware(uri)
1153
1717
  )
@@ -1195,20 +1759,20 @@ class FastMCP(Generic[LifespanResultT]):
1195
1759
  for mounted in reversed(self._mounted_servers):
1196
1760
  key = uri_str
1197
1761
  if mounted.prefix:
1198
- if not has_resource_prefix(
1199
- key, mounted.prefix, mounted.resource_prefix_format
1200
- ):
1762
+ if not has_resource_prefix(key, mounted.prefix):
1201
1763
  continue
1202
- key = remove_resource_prefix(
1203
- key, mounted.prefix, mounted.resource_prefix_format
1204
- )
1764
+ key = remove_resource_prefix(key, mounted.prefix)
1205
1765
 
1766
+ # First, get the resource/template to check if parent's filter allows it
1767
+ # Use get_resource_or_template to support nested mounted servers
1768
+ # (resources/templates mounted more than 2 levels deep)
1769
+ resource = await mounted.server._get_resource_or_template_or_none(key)
1770
+ if resource is None:
1771
+ continue
1772
+ if not self._should_enable_component(resource):
1773
+ # Parent filter blocks this resource, continue searching
1774
+ continue
1206
1775
  try:
1207
- # First, get the resource to check if parent's filter allows it
1208
- resource = await mounted.server._resource_manager.get_resource(key)
1209
- if not self._should_enable_component(resource):
1210
- # Parent filter blocks this resource, continue searching
1211
- continue
1212
1776
  result = list(await mounted.server._read_resource_middleware(key))
1213
1777
  return result
1214
1778
  except NotFoundError:
@@ -1246,6 +1810,7 @@ class FastMCP(Generic[LifespanResultT]):
1246
1810
 
1247
1811
  async with fastmcp.server.context.Context(fastmcp=self):
1248
1812
  try:
1813
+ # Task routing handled by custom handler
1249
1814
  return await self._get_prompt_middleware(name, arguments)
1250
1815
  except DisabledError as e:
1251
1816
  # convert to NotFoundError to avoid leaking prompt presence
@@ -1288,7 +1853,9 @@ class FastMCP(Generic[LifespanResultT]):
1288
1853
 
1289
1854
  try:
1290
1855
  # First, get the prompt to check if parent's filter allows it
1291
- prompt = await mounted.server._prompt_manager.get_prompt(try_name)
1856
+ # Use get_prompt() instead of _prompt_manager.get_prompt() to support
1857
+ # nested mounted servers (prompts mounted more than 2 levels deep)
1858
+ prompt = await mounted.server.get_prompt(try_name)
1292
1859
  if not self._should_enable_component(prompt):
1293
1860
  # Parent filter blocks this prompt, continue searching
1294
1861
  continue
@@ -1380,6 +1947,7 @@ class FastMCP(Generic[LifespanResultT]):
1380
1947
  exclude_args: list[str] | None = None,
1381
1948
  meta: dict[str, Any] | None = None,
1382
1949
  enabled: bool | None = None,
1950
+ task: bool | TaskConfig | None = None,
1383
1951
  ) -> FunctionTool: ...
1384
1952
 
1385
1953
  @overload
@@ -1397,6 +1965,7 @@ class FastMCP(Generic[LifespanResultT]):
1397
1965
  exclude_args: list[str] | None = None,
1398
1966
  meta: dict[str, Any] | None = None,
1399
1967
  enabled: bool | None = None,
1968
+ task: bool | TaskConfig | None = None,
1400
1969
  ) -> Callable[[AnyFunction], FunctionTool]: ...
1401
1970
 
1402
1971
  def tool(
@@ -1413,6 +1982,7 @@ class FastMCP(Generic[LifespanResultT]):
1413
1982
  exclude_args: list[str] | None = None,
1414
1983
  meta: dict[str, Any] | None = None,
1415
1984
  enabled: bool | None = None,
1985
+ task: bool | TaskConfig | None = None,
1416
1986
  ) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
1417
1987
  """Decorator to register a tool.
1418
1988
 
@@ -1435,8 +2005,7 @@ class FastMCP(Generic[LifespanResultT]):
1435
2005
  output_schema: Optional JSON schema for the tool's output
1436
2006
  annotations: Optional annotations about the tool's behavior
1437
2007
  exclude_args: Optional list of argument names to exclude from the tool schema.
1438
- Note: `exclude_args` will be deprecated in FastMCP 2.14 in favor of dependency
1439
- injection with `Depends()` for better lifecycle management.
2008
+ Deprecated: Use `Depends()` for dependency injection instead.
1440
2009
  meta: Optional meta information about the tool
1441
2010
  enabled: Optional boolean to enable or disable the tool
1442
2011
 
@@ -1486,6 +2055,11 @@ class FastMCP(Generic[LifespanResultT]):
1486
2055
  fn = name_or_fn
1487
2056
  tool_name = name # Use keyword name if provided, otherwise None
1488
2057
 
2058
+ # Resolve task parameter
2059
+ supports_task: bool | TaskConfig = (
2060
+ task if task is not None else self._support_tasks_by_default
2061
+ )
2062
+
1489
2063
  # Register the tool immediately and return the tool object
1490
2064
  # Note: Deprecation warning for exclude_args is handled in Tool.from_function
1491
2065
  tool = Tool.from_function(
@@ -1501,6 +2075,7 @@ class FastMCP(Generic[LifespanResultT]):
1501
2075
  meta=meta,
1502
2076
  serializer=self._tool_serializer,
1503
2077
  enabled=enabled,
2078
+ task=supports_task,
1504
2079
  )
1505
2080
  self.add_tool(tool)
1506
2081
  return tool
@@ -1534,6 +2109,7 @@ class FastMCP(Generic[LifespanResultT]):
1534
2109
  exclude_args=exclude_args,
1535
2110
  meta=meta,
1536
2111
  enabled=enabled,
2112
+ task=task,
1537
2113
  )
1538
2114
 
1539
2115
  def add_resource(self, resource: Resource) -> Resource:
@@ -1580,44 +2156,6 @@ class FastMCP(Generic[LifespanResultT]):
1580
2156
 
1581
2157
  return template
1582
2158
 
1583
- def add_resource_fn(
1584
- self,
1585
- fn: AnyFunction,
1586
- uri: str,
1587
- name: str | None = None,
1588
- description: str | None = None,
1589
- mime_type: str | None = None,
1590
- tags: set[str] | None = None,
1591
- ) -> None:
1592
- """Add a resource or template to the server from a function.
1593
-
1594
- If the URI contains parameters (e.g. "resource://{param}") or the function
1595
- has parameters, it will be registered as a template resource.
1596
-
1597
- Args:
1598
- fn: The function to register as a resource
1599
- uri: The URI for the resource
1600
- name: Optional name for the resource
1601
- description: Optional description of the resource
1602
- mime_type: Optional MIME type for the resource
1603
- tags: Optional set of tags for categorizing the resource
1604
- """
1605
- # deprecated since 2.7.0
1606
- if fastmcp.settings.deprecation_warnings:
1607
- warnings.warn(
1608
- "The add_resource_fn method is deprecated. Use the resource decorator instead.",
1609
- DeprecationWarning,
1610
- stacklevel=2,
1611
- )
1612
- self._resource_manager.add_resource_or_template_from_fn(
1613
- fn=fn,
1614
- uri=uri,
1615
- name=name,
1616
- description=description,
1617
- mime_type=mime_type,
1618
- tags=tags,
1619
- )
1620
-
1621
2159
  def resource(
1622
2160
  self,
1623
2161
  uri: str,
@@ -1631,6 +2169,7 @@ class FastMCP(Generic[LifespanResultT]):
1631
2169
  enabled: bool | None = None,
1632
2170
  annotations: Annotations | dict[str, Any] | None = None,
1633
2171
  meta: dict[str, Any] | None = None,
2172
+ task: bool | TaskConfig | None = None,
1634
2173
  ) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
1635
2174
  """Decorator to register a function as a resource.
1636
2175
 
@@ -1695,8 +2234,6 @@ class FastMCP(Generic[LifespanResultT]):
1695
2234
  )
1696
2235
 
1697
2236
  def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
1698
- from fastmcp.server.context import Context
1699
-
1700
2237
  if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
1701
2238
  raise ValueError(
1702
2239
  inspect.cleandoc(
@@ -1709,14 +2246,18 @@ class FastMCP(Generic[LifespanResultT]):
1709
2246
  )
1710
2247
  )
1711
2248
 
2249
+ # Resolve task parameter
2250
+ supports_task: bool | TaskConfig = (
2251
+ task if task is not None else self._support_tasks_by_default
2252
+ )
2253
+
1712
2254
  # Check if this should be a template
1713
2255
  has_uri_params = "{" in uri and "}" in uri
1714
- # check if the function has any parameters (other than injected context)
1715
- has_func_params = any(
1716
- p
1717
- for p in inspect.signature(fn).parameters.values()
1718
- if p.annotation is not Context
1719
- )
2256
+ # Use wrapper to check for user-facing parameters
2257
+ from fastmcp.server.dependencies import without_injected_parameters
2258
+
2259
+ wrapper_fn = without_injected_parameters(fn)
2260
+ has_func_params = bool(inspect.signature(wrapper_fn).parameters)
1720
2261
 
1721
2262
  if has_uri_params or has_func_params:
1722
2263
  template = ResourceTemplate.from_function(
@@ -1731,6 +2272,7 @@ class FastMCP(Generic[LifespanResultT]):
1731
2272
  enabled=enabled,
1732
2273
  annotations=annotations,
1733
2274
  meta=meta,
2275
+ task=supports_task,
1734
2276
  )
1735
2277
  self.add_template(template)
1736
2278
  return template
@@ -1747,6 +2289,7 @@ class FastMCP(Generic[LifespanResultT]):
1747
2289
  enabled=enabled,
1748
2290
  annotations=annotations,
1749
2291
  meta=meta,
2292
+ task=supports_task,
1750
2293
  )
1751
2294
  self.add_resource(resource)
1752
2295
  return resource
@@ -1792,6 +2335,7 @@ class FastMCP(Generic[LifespanResultT]):
1792
2335
  tags: set[str] | None = None,
1793
2336
  enabled: bool | None = None,
1794
2337
  meta: dict[str, Any] | None = None,
2338
+ task: bool | TaskConfig | None = None,
1795
2339
  ) -> FunctionPrompt: ...
1796
2340
 
1797
2341
  @overload
@@ -1806,6 +2350,7 @@ class FastMCP(Generic[LifespanResultT]):
1806
2350
  tags: set[str] | None = None,
1807
2351
  enabled: bool | None = None,
1808
2352
  meta: dict[str, Any] | None = None,
2353
+ task: bool | TaskConfig | None = None,
1809
2354
  ) -> Callable[[AnyFunction], FunctionPrompt]: ...
1810
2355
 
1811
2356
  def prompt(
@@ -1819,6 +2364,7 @@ class FastMCP(Generic[LifespanResultT]):
1819
2364
  tags: set[str] | None = None,
1820
2365
  enabled: bool | None = None,
1821
2366
  meta: dict[str, Any] | None = None,
2367
+ task: bool | TaskConfig | None = None,
1822
2368
  ) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
1823
2369
  """Decorator to register a prompt.
1824
2370
 
@@ -1909,6 +2455,11 @@ class FastMCP(Generic[LifespanResultT]):
1909
2455
  fn = name_or_fn
1910
2456
  prompt_name = name # Use keyword name if provided, otherwise None
1911
2457
 
2458
+ # Resolve task parameter
2459
+ supports_task: bool | TaskConfig = (
2460
+ task if task is not None else self._support_tasks_by_default
2461
+ )
2462
+
1912
2463
  # Register the prompt immediately
1913
2464
  prompt = Prompt.from_function(
1914
2465
  fn=fn,
@@ -1919,6 +2470,7 @@ class FastMCP(Generic[LifespanResultT]):
1919
2470
  tags=tags,
1920
2471
  enabled=enabled,
1921
2472
  meta=meta,
2473
+ task=supports_task,
1922
2474
  )
1923
2475
  self.add_prompt(prompt)
1924
2476
 
@@ -1950,6 +2502,7 @@ class FastMCP(Generic[LifespanResultT]):
1950
2502
  tags=tags,
1951
2503
  enabled=enabled,
1952
2504
  meta=meta,
2505
+ task=task,
1953
2506
  )
1954
2507
 
1955
2508
  async def run_stdio_async(
@@ -1974,11 +2527,18 @@ class FastMCP(Generic[LifespanResultT]):
1974
2527
  logger.info(
1975
2528
  f"Starting MCP server {self.name!r} with transport 'stdio'"
1976
2529
  )
2530
+
2531
+ # Build experimental capabilities
2532
+ experimental_capabilities = get_task_capabilities()
2533
+
1977
2534
  await self._mcp_server.run(
1978
2535
  read_stream,
1979
2536
  write_stream,
1980
2537
  self._mcp_server.create_initialization_options(
1981
- NotificationOptions(tools_changed=True)
2538
+ notification_options=NotificationOptions(
2539
+ tools_changed=True
2540
+ ),
2541
+ experimental_capabilities=experimental_capabilities,
1982
2542
  ),
1983
2543
  )
1984
2544
 
@@ -2061,86 +2621,6 @@ class FastMCP(Generic[LifespanResultT]):
2061
2621
 
2062
2622
  await server.serve()
2063
2623
 
2064
- async def run_sse_async(
2065
- self,
2066
- host: str | None = None,
2067
- port: int | None = None,
2068
- log_level: str | None = None,
2069
- path: str | None = None,
2070
- uvicorn_config: dict[str, Any] | None = None,
2071
- ) -> None:
2072
- """Run the server using SSE transport."""
2073
-
2074
- # Deprecated since 2.3.2
2075
- if fastmcp.settings.deprecation_warnings:
2076
- warnings.warn(
2077
- "The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
2078
- "modern (non-SSE) alternative, or create an SSE app with "
2079
- "`fastmcp.server.http.create_sse_app` and run it directly.",
2080
- DeprecationWarning,
2081
- stacklevel=2,
2082
- )
2083
- await self.run_http_async(
2084
- transport="sse",
2085
- host=host,
2086
- port=port,
2087
- log_level=log_level,
2088
- path=path,
2089
- uvicorn_config=uvicorn_config,
2090
- )
2091
-
2092
- def sse_app(
2093
- self,
2094
- path: str | None = None,
2095
- message_path: str | None = None,
2096
- middleware: list[ASGIMiddleware] | None = None,
2097
- ) -> StarletteWithLifespan:
2098
- """
2099
- Create a Starlette app for the SSE server.
2100
-
2101
- Args:
2102
- path: The path to the SSE endpoint
2103
- message_path: The path to the message endpoint
2104
- middleware: A list of middleware to apply to the app
2105
- """
2106
- # Deprecated since 2.3.2
2107
- if fastmcp.settings.deprecation_warnings:
2108
- warnings.warn(
2109
- "The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
2110
- "alternative, or call `fastmcp.server.http.create_sse_app` directly.",
2111
- DeprecationWarning,
2112
- stacklevel=2,
2113
- )
2114
- return create_sse_app(
2115
- server=self,
2116
- message_path=message_path or self._deprecated_settings.message_path,
2117
- sse_path=path or self._deprecated_settings.sse_path,
2118
- auth=self.auth,
2119
- debug=self._deprecated_settings.debug,
2120
- middleware=middleware,
2121
- )
2122
-
2123
- def streamable_http_app(
2124
- self,
2125
- path: str | None = None,
2126
- middleware: list[ASGIMiddleware] | None = None,
2127
- ) -> StarletteWithLifespan:
2128
- """
2129
- Create a Starlette app for the StreamableHTTP server.
2130
-
2131
- Args:
2132
- path: The path to the StreamableHTTP endpoint
2133
- middleware: A list of middleware to apply to the app
2134
- """
2135
- # Deprecated since 2.3.2
2136
- if fastmcp.settings.deprecation_warnings:
2137
- warnings.warn(
2138
- "The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
2139
- DeprecationWarning,
2140
- stacklevel=2,
2141
- )
2142
- return self.http_app(path=path, middleware=middleware)
2143
-
2144
2624
  def http_app(
2145
2625
  self,
2146
2626
  path: str | None = None,
@@ -2148,13 +2628,24 @@ class FastMCP(Generic[LifespanResultT]):
2148
2628
  json_response: bool | None = None,
2149
2629
  stateless_http: bool | None = None,
2150
2630
  transport: Literal["http", "streamable-http", "sse"] = "http",
2631
+ event_store: EventStore | None = None,
2632
+ retry_interval: int | None = None,
2151
2633
  ) -> StarletteWithLifespan:
2152
2634
  """Create a Starlette app using the specified HTTP transport.
2153
2635
 
2154
2636
  Args:
2155
2637
  path: The path for the HTTP endpoint
2156
2638
  middleware: A list of middleware to apply to the app
2157
- transport: Transport protocol to use - either "streamable-http" (default) or "sse"
2639
+ json_response: Whether to use JSON response format
2640
+ stateless_http: Whether to use stateless mode (new transport per request)
2641
+ transport: Transport protocol to use - "http", "streamable-http", or "sse"
2642
+ event_store: Optional event store for SSE polling/resumability. When set,
2643
+ enables clients to reconnect and resume receiving events after
2644
+ server-initiated disconnections. Only used with streamable-http transport.
2645
+ retry_interval: Optional retry interval in milliseconds for SSE polling.
2646
+ Controls how quickly clients should reconnect after server-initiated
2647
+ disconnections. Requires event_store to be set. Only used with
2648
+ streamable-http transport.
2158
2649
 
2159
2650
  Returns:
2160
2651
  A Starlette application configured with the specified transport
@@ -2165,7 +2656,8 @@ class FastMCP(Generic[LifespanResultT]):
2165
2656
  server=self,
2166
2657
  streamable_http_path=path
2167
2658
  or self._deprecated_settings.streamable_http_path,
2168
- event_store=None,
2659
+ event_store=event_store,
2660
+ retry_interval=retry_interval,
2169
2661
  auth=self.auth,
2170
2662
  json_response=(
2171
2663
  json_response
@@ -2190,40 +2682,12 @@ class FastMCP(Generic[LifespanResultT]):
2190
2682
  middleware=middleware,
2191
2683
  )
2192
2684
 
2193
- async def run_streamable_http_async(
2194
- self,
2195
- host: str | None = None,
2196
- port: int | None = None,
2197
- log_level: str | None = None,
2198
- path: str | None = None,
2199
- uvicorn_config: dict[str, Any] | None = None,
2200
- ) -> None:
2201
- # Deprecated since 2.3.2
2202
- if fastmcp.settings.deprecation_warnings:
2203
- warnings.warn(
2204
- "The run_streamable_http_async method is deprecated (as of 2.3.2). "
2205
- "Use run_http_async instead.",
2206
- DeprecationWarning,
2207
- stacklevel=2,
2208
- )
2209
- await self.run_http_async(
2210
- transport="http",
2211
- host=host,
2212
- port=port,
2213
- log_level=log_level,
2214
- path=path,
2215
- uvicorn_config=uvicorn_config,
2216
- )
2217
-
2218
2685
  def mount(
2219
2686
  self,
2220
2687
  server: FastMCP[LifespanResultT],
2221
2688
  prefix: str | None = None,
2222
2689
  as_proxy: bool | None = None,
2223
- *,
2224
- tool_separator: str | None = None,
2225
- resource_separator: str | None = None,
2226
- prompt_separator: str | None = None,
2690
+ tool_names: dict[str, str] | None = None,
2227
2691
  ) -> None:
2228
2692
  """Mount another FastMCP server on this server with an optional prefix.
2229
2693
 
@@ -2268,56 +2732,12 @@ class FastMCP(Generic[LifespanResultT]):
2268
2732
  as_proxy: Whether to treat the mounted server as a proxy. If None (default),
2269
2733
  automatically determined based on whether the server has a custom lifespan
2270
2734
  (True if it has a custom lifespan, False otherwise).
2271
- tool_separator: Deprecated. Separator character for tool names.
2272
- resource_separator: Deprecated. Separator character for resource URIs.
2273
- prompt_separator: Deprecated. Separator character for prompt names.
2735
+ tool_names: Optional mapping of original tool names to custom names. Use this
2736
+ to override prefixed names. Keys are the original tool names from the
2737
+ mounted server.
2274
2738
  """
2275
2739
  from fastmcp.server.proxy import FastMCPProxy
2276
2740
 
2277
- # Deprecated since 2.9.0
2278
- # Prior to 2.9.0, the first positional argument was the prefix and the
2279
- # second was the server. Here we swap them if needed now that the prefix
2280
- # is optional.
2281
- if isinstance(server, str):
2282
- if fastmcp.settings.deprecation_warnings:
2283
- warnings.warn(
2284
- "Mount prefixes are now optional and the first positional argument "
2285
- "should be the server you want to mount.",
2286
- DeprecationWarning,
2287
- stacklevel=2,
2288
- )
2289
- server, prefix = cast(FastMCP[Any], prefix), server
2290
-
2291
- if tool_separator is not None:
2292
- # Deprecated since 2.4.0
2293
- if fastmcp.settings.deprecation_warnings:
2294
- warnings.warn(
2295
- "The tool_separator parameter is deprecated and will be removed in a future version. "
2296
- "Tools are now prefixed using 'prefix_toolname' format.",
2297
- DeprecationWarning,
2298
- stacklevel=2,
2299
- )
2300
-
2301
- if resource_separator is not None:
2302
- # Deprecated since 2.4.0
2303
- if fastmcp.settings.deprecation_warnings:
2304
- warnings.warn(
2305
- "The resource_separator parameter is deprecated and ignored. "
2306
- "Resource prefixes are now added using the protocol://prefix/path format.",
2307
- DeprecationWarning,
2308
- stacklevel=2,
2309
- )
2310
-
2311
- if prompt_separator is not None:
2312
- # Deprecated since 2.4.0
2313
- if fastmcp.settings.deprecation_warnings:
2314
- warnings.warn(
2315
- "The prompt_separator parameter is deprecated and will be removed in a future version. "
2316
- "Prompts are now prefixed using 'prefix_promptname' format.",
2317
- DeprecationWarning,
2318
- stacklevel=2,
2319
- )
2320
-
2321
2741
  # if as_proxy is not specified and the server has a custom lifespan,
2322
2742
  # we should treat it as a proxy
2323
2743
  if as_proxy is None:
@@ -2326,11 +2746,16 @@ class FastMCP(Generic[LifespanResultT]):
2326
2746
  if as_proxy and not isinstance(server, FastMCPProxy):
2327
2747
  server = FastMCP.as_proxy(server)
2328
2748
 
2749
+ # Mark the server as mounted so it skips creating its own Docket/Worker.
2750
+ # The parent's Docket handles task execution, avoiding race conditions
2751
+ # with multiple workers competing for tasks from the same queue.
2752
+ server._is_mounted = True
2753
+
2329
2754
  # Delegate mounting to all three managers
2330
2755
  mounted_server = MountedServer(
2331
2756
  prefix=prefix,
2332
2757
  server=server,
2333
- resource_prefix_format=self.resource_prefix_format,
2758
+ tool_names=tool_names,
2334
2759
  )
2335
2760
  self._mounted_servers.append(mounted_server)
2336
2761
 
@@ -2338,9 +2763,6 @@ class FastMCP(Generic[LifespanResultT]):
2338
2763
  self,
2339
2764
  server: FastMCP[LifespanResultT],
2340
2765
  prefix: str | None = None,
2341
- tool_separator: str | None = None,
2342
- resource_separator: str | None = None,
2343
- prompt_separator: str | None = None,
2344
2766
  ) -> None:
2345
2767
  """
2346
2768
  Import the MCP objects from another FastMCP server into this one,
@@ -2372,56 +2794,7 @@ class FastMCP(Generic[LifespanResultT]):
2372
2794
  server: The FastMCP server to import
2373
2795
  prefix: Optional prefix to use for the imported server's objects. If None,
2374
2796
  objects are imported with their original names.
2375
- tool_separator: Deprecated. Separator for tool names.
2376
- resource_separator: Deprecated and ignored. Prefix is now
2377
- applied using the protocol://prefix/path format
2378
- prompt_separator: Deprecated. Separator for prompt names.
2379
- """
2380
-
2381
- # Deprecated since 2.9.0
2382
- # Prior to 2.9.0, the first positional argument was the prefix and the
2383
- # second was the server. Here we swap them if needed now that the prefix
2384
- # is optional.
2385
- if isinstance(server, str):
2386
- if fastmcp.settings.deprecation_warnings:
2387
- warnings.warn(
2388
- "Import prefixes are now optional and the first positional argument "
2389
- "should be the server you want to import.",
2390
- DeprecationWarning,
2391
- stacklevel=2,
2392
- )
2393
- server, prefix = cast(FastMCP[Any], prefix), server
2394
-
2395
- if tool_separator is not None:
2396
- # Deprecated since 2.4.0
2397
- if fastmcp.settings.deprecation_warnings:
2398
- warnings.warn(
2399
- "The tool_separator parameter is deprecated and will be removed in a future version. "
2400
- "Tools are now prefixed using 'prefix_toolname' format.",
2401
- DeprecationWarning,
2402
- stacklevel=2,
2403
- )
2404
-
2405
- if resource_separator is not None:
2406
- # Deprecated since 2.4.0
2407
- if fastmcp.settings.deprecation_warnings:
2408
- warnings.warn(
2409
- "The resource_separator parameter is deprecated and ignored. "
2410
- "Resource prefixes are now added using the protocol://prefix/path format.",
2411
- DeprecationWarning,
2412
- stacklevel=2,
2413
- )
2414
-
2415
- if prompt_separator is not None:
2416
- # Deprecated since 2.4.0
2417
- if fastmcp.settings.deprecation_warnings:
2418
- warnings.warn(
2419
- "The prompt_separator parameter is deprecated and will be removed in a future version. "
2420
- "Prompts are now prefixed using 'prefix_promptname' format.",
2421
- DeprecationWarning,
2422
- stacklevel=2,
2423
- )
2424
-
2797
+ """
2425
2798
  # Import tools from the server
2426
2799
  for key, tool in (await server.get_tools()).items():
2427
2800
  if prefix:
@@ -2431,9 +2804,7 @@ class FastMCP(Generic[LifespanResultT]):
2431
2804
  # Import resources and templates from the server
2432
2805
  for key, resource in (await server.get_resources()).items():
2433
2806
  if prefix:
2434
- resource_key = add_resource_prefix(
2435
- key, prefix, self.resource_prefix_format
2436
- )
2807
+ resource_key = add_resource_prefix(key, prefix)
2437
2808
  resource = resource.model_copy(
2438
2809
  update={"name": f"{prefix}_{resource.name}"}, key=resource_key
2439
2810
  )
@@ -2441,9 +2812,7 @@ class FastMCP(Generic[LifespanResultT]):
2441
2812
 
2442
2813
  for key, template in (await server.get_resource_templates()).items():
2443
2814
  if prefix:
2444
- template_key = add_resource_prefix(
2445
- key, prefix, self.resource_prefix_format
2446
- )
2815
+ template_key = add_resource_prefix(key, prefix)
2447
2816
  template = template.model_copy(
2448
2817
  update={"name": f"{prefix}_{template.name}"}, key=template_key
2449
2818
  )
@@ -2476,66 +2845,46 @@ class FastMCP(Generic[LifespanResultT]):
2476
2845
  cls,
2477
2846
  openapi_spec: dict[str, Any],
2478
2847
  client: httpx.AsyncClient,
2479
- route_maps: list[RouteMap] | list[RouteMapNew] | None = None,
2480
- route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None,
2481
- mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None,
2848
+ route_maps: list[RouteMap] | None = None,
2849
+ route_map_fn: OpenAPIRouteMapFn | None = None,
2850
+ mcp_component_fn: OpenAPIComponentFn | None = None,
2482
2851
  mcp_names: dict[str, str] | None = None,
2483
2852
  tags: set[str] | None = None,
2484
2853
  **settings: Any,
2485
- ) -> FastMCPOpenAPI | FastMCPOpenAPINew:
2854
+ ) -> FastMCPOpenAPI:
2486
2855
  """
2487
2856
  Create a FastMCP server from an OpenAPI specification.
2488
2857
  """
2858
+ from .openapi import FastMCPOpenAPI
2489
2859
 
2490
- # Check if experimental parser is enabled
2491
- if fastmcp.settings.experimental.enable_new_openapi_parser:
2492
- from fastmcp.experimental.server.openapi import FastMCPOpenAPI
2493
-
2494
- return FastMCPOpenAPI(
2495
- openapi_spec=openapi_spec,
2496
- client=client,
2497
- route_maps=cast(Any, route_maps),
2498
- route_map_fn=cast(Any, route_map_fn),
2499
- mcp_component_fn=cast(Any, mcp_component_fn),
2500
- mcp_names=mcp_names,
2501
- tags=tags,
2502
- **settings,
2503
- )
2504
- else:
2505
- logger.info(
2506
- "Using legacy OpenAPI parser. To use the new parser, set "
2507
- "FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
2508
- "was introduced for testing in 2.11 and will become the default soon."
2509
- )
2510
- from .openapi import FastMCPOpenAPI
2511
-
2512
- return FastMCPOpenAPI(
2513
- openapi_spec=openapi_spec,
2514
- client=client,
2515
- route_maps=cast(Any, route_maps),
2516
- route_map_fn=cast(Any, route_map_fn),
2517
- mcp_component_fn=cast(Any, mcp_component_fn),
2518
- mcp_names=mcp_names,
2519
- tags=tags,
2520
- **settings,
2521
- )
2860
+ return FastMCPOpenAPI(
2861
+ openapi_spec=openapi_spec,
2862
+ client=client,
2863
+ route_maps=route_maps,
2864
+ route_map_fn=route_map_fn,
2865
+ mcp_component_fn=mcp_component_fn,
2866
+ mcp_names=mcp_names,
2867
+ tags=tags,
2868
+ **settings,
2869
+ )
2522
2870
 
2523
2871
  @classmethod
2524
2872
  def from_fastapi(
2525
2873
  cls,
2526
2874
  app: Any,
2527
2875
  name: str | None = None,
2528
- route_maps: list[RouteMap] | list[RouteMapNew] | None = None,
2529
- route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None,
2530
- mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None,
2876
+ route_maps: list[RouteMap] | None = None,
2877
+ route_map_fn: OpenAPIRouteMapFn | None = None,
2878
+ mcp_component_fn: OpenAPIComponentFn | None = None,
2531
2879
  mcp_names: dict[str, str] | None = None,
2532
2880
  httpx_client_kwargs: dict[str, Any] | None = None,
2533
2881
  tags: set[str] | None = None,
2534
2882
  **settings: Any,
2535
- ) -> FastMCPOpenAPI | FastMCPOpenAPINew:
2883
+ ) -> FastMCPOpenAPI:
2536
2884
  """
2537
2885
  Create a FastMCP server from a FastAPI application.
2538
2886
  """
2887
+ from .openapi import FastMCPOpenAPI
2539
2888
 
2540
2889
  if httpx_client_kwargs is None:
2541
2890
  httpx_client_kwargs = {}
@@ -2548,40 +2897,17 @@ class FastMCP(Generic[LifespanResultT]):
2548
2897
 
2549
2898
  name = name or app.title
2550
2899
 
2551
- # Check if experimental parser is enabled
2552
- if fastmcp.settings.experimental.enable_new_openapi_parser:
2553
- from fastmcp.experimental.server.openapi import FastMCPOpenAPI
2554
-
2555
- return FastMCPOpenAPI(
2556
- openapi_spec=app.openapi(),
2557
- client=client,
2558
- name=name,
2559
- route_maps=cast(Any, route_maps),
2560
- route_map_fn=cast(Any, route_map_fn),
2561
- mcp_component_fn=cast(Any, mcp_component_fn),
2562
- mcp_names=mcp_names,
2563
- tags=tags,
2564
- **settings,
2565
- )
2566
- else:
2567
- logger.info(
2568
- "Using legacy OpenAPI parser. To use the new parser, set "
2569
- "FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true. The new parser "
2570
- "was introduced for testing in 2.11 and will become the default soon."
2571
- )
2572
- from .openapi import FastMCPOpenAPI
2573
-
2574
- return FastMCPOpenAPI(
2575
- openapi_spec=app.openapi(),
2576
- client=client,
2577
- name=name,
2578
- route_maps=cast(Any, route_maps),
2579
- route_map_fn=cast(Any, route_map_fn),
2580
- mcp_component_fn=cast(Any, mcp_component_fn),
2581
- mcp_names=mcp_names,
2582
- tags=tags,
2583
- **settings,
2584
- )
2900
+ return FastMCPOpenAPI(
2901
+ openapi_spec=app.openapi(),
2902
+ client=client,
2903
+ name=name,
2904
+ route_maps=route_maps,
2905
+ route_map_fn=route_map_fn,
2906
+ mcp_component_fn=mcp_component_fn,
2907
+ mcp_names=mcp_names,
2908
+ tags=tags,
2909
+ **settings,
2910
+ )
2585
2911
 
2586
2912
  @classmethod
2587
2913
  def as_proxy(
@@ -2643,23 +2969,6 @@ class FastMCP(Generic[LifespanResultT]):
2643
2969
 
2644
2970
  return FastMCPProxy(client_factory=client_factory, **settings)
2645
2971
 
2646
- @classmethod
2647
- def from_client(
2648
- cls, client: Client[ClientTransportT], **settings: Any
2649
- ) -> FastMCPProxy:
2650
- """
2651
- Create a FastMCP proxy server from a FastMCP client.
2652
- """
2653
- # Deprecated since 2.3.5
2654
- if fastmcp.settings.deprecation_warnings:
2655
- warnings.warn(
2656
- "FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
2657
- DeprecationWarning,
2658
- stacklevel=2,
2659
- )
2660
-
2661
- return cls.as_proxy(client, **settings)
2662
-
2663
2972
  def _should_enable_component(
2664
2973
  self,
2665
2974
  component: FastMCPComponent,
@@ -2706,13 +3015,11 @@ class FastMCP(Generic[LifespanResultT]):
2706
3015
  class MountedServer:
2707
3016
  prefix: str | None
2708
3017
  server: FastMCP[Any]
2709
- resource_prefix_format: Literal["protocol", "path"] | None = None
3018
+ tool_names: dict[str, str] | None = None
2710
3019
 
2711
3020
 
2712
- def add_resource_prefix(
2713
- uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
2714
- ) -> str:
2715
- """Add a prefix to a resource URI.
3021
+ def add_resource_prefix(uri: str, prefix: str) -> str:
3022
+ """Add a prefix to a resource URI using path formatting (resource://prefix/path).
2716
3023
 
2717
3024
  Args:
2718
3025
  uri: The original resource URI
@@ -2722,16 +3029,10 @@ def add_resource_prefix(
2722
3029
  The resource URI with the prefix added
2723
3030
 
2724
3031
  Examples:
2725
- With new style:
2726
3032
  ```python
2727
3033
  add_resource_prefix("resource://path/to/resource", "prefix")
2728
3034
  "resource://prefix/path/to/resource"
2729
3035
  ```
2730
- With legacy style:
2731
- ```python
2732
- add_resource_prefix("resource://path/to/resource", "prefix")
2733
- "prefix+resource://path/to/resource"
2734
- ```
2735
3036
  With absolute path:
2736
3037
  ```python
2737
3038
  add_resource_prefix("resource:///absolute/path", "prefix")
@@ -2744,54 +3045,32 @@ def add_resource_prefix(
2744
3045
  if not prefix:
2745
3046
  return uri
2746
3047
 
2747
- # Get the server settings to check for legacy format preference
2748
-
2749
- if prefix_format is None:
2750
- prefix_format = fastmcp.settings.resource_prefix_format
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.")
2751
3052
 
2752
- if prefix_format == "protocol":
2753
- # Legacy style: prefix+protocol://path
2754
- return f"{prefix}+{uri}"
2755
- elif prefix_format == "path":
2756
- # New style: protocol://prefix/path
2757
- # Split the URI into protocol and path
2758
- match = URI_PATTERN.match(uri)
2759
- if not match:
2760
- raise ValueError(
2761
- f"Invalid URI format: {uri}. Expected protocol://path format."
2762
- )
2763
-
2764
- protocol, path = match.groups()
3053
+ protocol, path = match.groups()
2765
3054
 
2766
- # Add the prefix to the path
2767
- return f"{protocol}{prefix}/{path}"
2768
- else:
2769
- raise ValueError(f"Invalid prefix format: {prefix_format}")
3055
+ # Add the prefix to the path
3056
+ return f"{protocol}{prefix}/{path}"
2770
3057
 
2771
3058
 
2772
- def remove_resource_prefix(
2773
- uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
2774
- ) -> str:
3059
+ def remove_resource_prefix(uri: str, prefix: str) -> str:
2775
3060
  """Remove a prefix from a resource URI.
2776
3061
 
2777
3062
  Args:
2778
3063
  uri: The resource URI with a prefix
2779
3064
  prefix: The prefix to remove
2780
- prefix_format: The format of the prefix to remove
3065
+
2781
3066
  Returns:
2782
3067
  The resource URI with the prefix removed
2783
3068
 
2784
3069
  Examples:
2785
- With new style:
2786
3070
  ```python
2787
3071
  remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
2788
3072
  "resource://path/to/resource"
2789
3073
  ```
2790
- With legacy style:
2791
- ```python
2792
- remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
2793
- "resource://path/to/resource"
2794
- ```
2795
3074
  With absolute path:
2796
3075
  ```python
2797
3076
  remove_resource_prefix("resource://prefix//absolute/path", "prefix")
@@ -2804,41 +3083,24 @@ def remove_resource_prefix(
2804
3083
  if not prefix:
2805
3084
  return uri
2806
3085
 
2807
- if prefix_format is None:
2808
- prefix_format = fastmcp.settings.resource_prefix_format
3086
+ # Split the URI into protocol and path
3087
+ match = URI_PATTERN.match(uri)
3088
+ if not match:
3089
+ raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2809
3090
 
2810
- if prefix_format == "protocol":
2811
- # Legacy style: prefix+protocol://path
2812
- legacy_prefix = f"{prefix}+"
2813
- if uri.startswith(legacy_prefix):
2814
- return uri[len(legacy_prefix) :]
2815
- return uri
2816
- elif prefix_format == "path":
2817
- # New style: protocol://prefix/path
2818
- # Split the URI into protocol and path
2819
- match = URI_PATTERN.match(uri)
2820
- if not match:
2821
- raise ValueError(
2822
- f"Invalid URI format: {uri}. Expected protocol://path format."
2823
- )
3091
+ protocol, path = match.groups()
2824
3092
 
2825
- protocol, path = match.groups()
2826
-
2827
- # Check if the path starts with the prefix followed by a /
2828
- prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
2829
- path_match = re.match(prefix_pattern, path)
2830
- if not path_match:
2831
- return uri
3093
+ # Check if the path starts with the prefix followed by a /
3094
+ prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
3095
+ path_match = re.match(prefix_pattern, path)
3096
+ if not path_match:
3097
+ return uri
2832
3098
 
2833
- # Return the URI without the prefix
2834
- return f"{protocol}{path_match.group(1)}"
2835
- else:
2836
- raise ValueError(f"Invalid prefix format: {prefix_format}")
3099
+ # Return the URI without the prefix
3100
+ return f"{protocol}{path_match.group(1)}"
2837
3101
 
2838
3102
 
2839
- def has_resource_prefix(
2840
- uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
2841
- ) -> bool:
3103
+ def has_resource_prefix(uri: str, prefix: str) -> bool:
2842
3104
  """Check if a resource URI has a specific prefix.
2843
3105
 
2844
3106
  Args:
@@ -2849,16 +3111,10 @@ def has_resource_prefix(
2849
3111
  True if the URI has the specified prefix, False otherwise
2850
3112
 
2851
3113
  Examples:
2852
- With new style:
2853
3114
  ```python
2854
3115
  has_resource_prefix("resource://prefix/path/to/resource", "prefix")
2855
3116
  True
2856
3117
  ```
2857
- With legacy style:
2858
- ```python
2859
- has_resource_prefix("prefix+resource://path/to/resource", "prefix")
2860
- True
2861
- ```
2862
3118
  With other path:
2863
3119
  ```python
2864
3120
  has_resource_prefix("resource://other/path/to/resource", "prefix")
@@ -2871,28 +3127,13 @@ def has_resource_prefix(
2871
3127
  if not prefix:
2872
3128
  return False
2873
3129
 
2874
- # Get the server settings to check for legacy format preference
2875
-
2876
- if prefix_format is None:
2877
- prefix_format = fastmcp.settings.resource_prefix_format
2878
-
2879
- if prefix_format == "protocol":
2880
- # Legacy style: prefix+protocol://path
2881
- legacy_prefix = f"{prefix}+"
2882
- return uri.startswith(legacy_prefix)
2883
- elif prefix_format == "path":
2884
- # New style: protocol://prefix/path
2885
- # Split the URI into protocol and path
2886
- match = URI_PATTERN.match(uri)
2887
- if not match:
2888
- raise ValueError(
2889
- f"Invalid URI format: {uri}. Expected protocol://path format."
2890
- )
3130
+ # Split the URI into protocol and path
3131
+ match = URI_PATTERN.match(uri)
3132
+ if not match:
3133
+ raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2891
3134
 
2892
- _, path = match.groups()
3135
+ _, path = match.groups()
2893
3136
 
2894
- # Check if the path starts with the prefix followed by a /
2895
- prefix_pattern = f"^{re.escape(prefix)}/"
2896
- return bool(re.match(prefix_pattern, path))
2897
- else:
2898
- raise ValueError(f"Invalid prefix format: {prefix_format}")
3137
+ # Check if the path starts with the prefix followed by a /
3138
+ prefix_pattern = f"^{re.escape(prefix)}/"
3139
+ return bool(re.match(prefix_pattern, path))