fastmcp 2.13.2__py3-none-any.whl → 2.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) 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 +665 -129
  9. fastmcp/client/elicitation.py +11 -5
  10. fastmcp/client/messages.py +7 -5
  11. fastmcp/client/roots.py +2 -1
  12. fastmcp/client/tasks.py +614 -0
  13. fastmcp/client/transports.py +37 -5
  14. fastmcp/contrib/component_manager/component_service.py +4 -20
  15. fastmcp/dependencies.py +25 -0
  16. fastmcp/experimental/sampling/handlers/openai.py +1 -1
  17. fastmcp/experimental/server/openapi/__init__.py +15 -13
  18. fastmcp/experimental/utilities/openapi/__init__.py +12 -38
  19. fastmcp/prompts/prompt.py +33 -33
  20. fastmcp/resources/resource.py +29 -12
  21. fastmcp/resources/template.py +64 -54
  22. fastmcp/server/auth/__init__.py +0 -9
  23. fastmcp/server/auth/auth.py +127 -3
  24. fastmcp/server/auth/oauth_proxy.py +47 -97
  25. fastmcp/server/auth/oidc_proxy.py +7 -0
  26. fastmcp/server/auth/providers/in_memory.py +2 -2
  27. fastmcp/server/auth/providers/oci.py +2 -2
  28. fastmcp/server/context.py +66 -72
  29. fastmcp/server/dependencies.py +464 -6
  30. fastmcp/server/elicitation.py +285 -47
  31. fastmcp/server/event_store.py +177 -0
  32. fastmcp/server/http.py +15 -3
  33. fastmcp/server/low_level.py +56 -12
  34. fastmcp/server/middleware/middleware.py +2 -2
  35. fastmcp/server/openapi/__init__.py +35 -0
  36. fastmcp/{experimental/server → server}/openapi/components.py +4 -3
  37. fastmcp/{experimental/server → server}/openapi/routing.py +1 -1
  38. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  39. fastmcp/server/proxy.py +50 -37
  40. fastmcp/server/server.py +731 -532
  41. fastmcp/server/tasks/__init__.py +21 -0
  42. fastmcp/server/tasks/capabilities.py +22 -0
  43. fastmcp/server/tasks/config.py +89 -0
  44. fastmcp/server/tasks/converters.py +205 -0
  45. fastmcp/server/tasks/handlers.py +356 -0
  46. fastmcp/server/tasks/keys.py +93 -0
  47. fastmcp/server/tasks/protocol.py +355 -0
  48. fastmcp/server/tasks/subscriptions.py +205 -0
  49. fastmcp/settings.py +101 -103
  50. fastmcp/tools/tool.py +80 -44
  51. fastmcp/tools/tool_transform.py +1 -12
  52. fastmcp/utilities/components.py +3 -3
  53. fastmcp/utilities/json_schema_type.py +4 -4
  54. fastmcp/utilities/mcp_config.py +1 -2
  55. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
  56. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  57. fastmcp/utilities/openapi/__init__.py +63 -0
  58. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  59. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +1 -1
  60. fastmcp/utilities/tests.py +11 -5
  61. fastmcp/utilities/types.py +8 -0
  62. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/METADATA +5 -4
  63. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/RECORD +71 -59
  64. fastmcp/server/auth/providers/bearer.py +0 -25
  65. fastmcp/server/openapi.py +0 -1087
  66. fastmcp/utilities/openapi.py +0 -1568
  67. /fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  68. /fastmcp/{experimental/utilities → utilities}/openapi/director.py +0 -0
  69. /fastmcp/{experimental/utilities → utilities}/openapi/models.py +0 -0
  70. /fastmcp/{experimental/utilities → utilities}/openapi/parser.py +0 -0
  71. /fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +0 -0
  72. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +0 -0
  73. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  74. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.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,14 +31,18 @@ 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
  )
@@ -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
@@ -81,14 +95,6 @@ if TYPE_CHECKING:
81
95
  from fastmcp.client import Client
82
96
  from fastmcp.client.client import FastMCP1Server
83
97
  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
98
  from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
93
99
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
94
100
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
@@ -98,6 +104,24 @@ if TYPE_CHECKING:
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 ---
@@ -188,12 +211,17 @@ class FastMCP(Generic[LifespanResultT]):
188
211
  sampling_handler: ServerSamplingHandler[LifespanResultT] | 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,24 +288,6 @@ 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
291
  self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
281
292
  sampling_handler
282
293
  )
@@ -290,7 +301,6 @@ class FastMCP(Generic[LifespanResultT]):
290
301
  else fastmcp.settings.include_fastmcp_meta
291
302
  )
292
303
 
293
- # handle deprecated settings
294
304
  self._handle_deprecated_settings(
295
305
  log_level=log_level,
296
306
  debug=debug,
@@ -383,14 +393,197 @@ class FastMCP(Generic[LifespanResultT]):
383
393
  else:
384
394
  return list(self._mcp_server.icons)
385
395
 
396
+ @property
397
+ def docket(self) -> Docket | None:
398
+ """Get the Docket instance if Docket support is enabled.
399
+
400
+ Returns None if Docket is not enabled or server hasn't been started yet.
401
+ """
402
+ return self._docket
403
+
404
+ @asynccontextmanager
405
+ async def _docket_lifespan(self) -> AsyncIterator[None]:
406
+ """Manage Docket instance and Worker for background task execution."""
407
+ from fastmcp import settings
408
+
409
+ # Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
410
+ from fastmcp.server.dependencies import (
411
+ _current_docket,
412
+ _current_server,
413
+ _current_worker,
414
+ )
415
+
416
+ server_token = _current_server.set(weakref.ref(self))
417
+
418
+ try:
419
+ # For directly mounted servers, the parent's Docket/Worker handles all
420
+ # task execution. Skip creating our own to avoid race conditions with
421
+ # multiple workers competing for tasks from the same queue.
422
+ if self._is_mounted:
423
+ yield
424
+ return
425
+
426
+ # Create Docket instance using configured name and URL
427
+ async with Docket(
428
+ name=settings.docket.name,
429
+ url=settings.docket.url,
430
+ ) as docket:
431
+ # Store on server instance for cross-task access (FastMCPTransport)
432
+ self._docket = docket
433
+
434
+ # Register local task-enabled tools/prompts/resources with Docket
435
+ # Only function-based variants support background tasks
436
+ # Register components where task execution is not "forbidden"
437
+ for tool in self._tool_manager._tools.values():
438
+ if (
439
+ isinstance(tool, FunctionTool)
440
+ and tool.task_config.mode != "forbidden"
441
+ ):
442
+ docket.register(tool.fn)
443
+
444
+ for prompt in self._prompt_manager._prompts.values():
445
+ if (
446
+ isinstance(prompt, FunctionPrompt)
447
+ and prompt.task_config.mode != "forbidden"
448
+ ):
449
+ # task execution requires async fn (validated at creation time)
450
+ docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
451
+
452
+ for resource in self._resource_manager._resources.values():
453
+ if (
454
+ isinstance(resource, FunctionResource)
455
+ and resource.task_config.mode != "forbidden"
456
+ ):
457
+ docket.register(resource.fn)
458
+
459
+ for template in self._resource_manager._templates.values():
460
+ if (
461
+ isinstance(template, FunctionResourceTemplate)
462
+ and template.task_config.mode != "forbidden"
463
+ ):
464
+ docket.register(template.fn)
465
+
466
+ # Also register functions from mounted servers so tasks can
467
+ # execute in the parent's Docket context
468
+ for mounted in self._mounted_servers:
469
+ await self._register_mounted_server_functions(
470
+ mounted.server, docket, mounted.prefix
471
+ )
472
+
473
+ # Set Docket in ContextVar so CurrentDocket can access it
474
+ docket_token = _current_docket.set(docket)
475
+ try:
476
+ # Build worker kwargs from settings
477
+ worker_kwargs: dict[str, Any] = {
478
+ "concurrency": settings.docket.concurrency,
479
+ "redelivery_timeout": settings.docket.redelivery_timeout,
480
+ "reconnection_delay": settings.docket.reconnection_delay,
481
+ }
482
+ if settings.docket.worker_name:
483
+ worker_kwargs["name"] = settings.docket.worker_name
484
+
485
+ # Create and start Worker
486
+ async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
487
+ # Set Worker in ContextVar so CurrentWorker can access it
488
+ worker_token = _current_worker.set(worker)
489
+ try:
490
+ worker_task = asyncio.create_task(worker.run_forever())
491
+ try:
492
+ yield
493
+ finally:
494
+ # Cancel worker task on exit with timeout to prevent hanging
495
+ worker_task.cancel()
496
+ with suppress(
497
+ asyncio.CancelledError, asyncio.TimeoutError
498
+ ):
499
+ await asyncio.wait_for(worker_task, timeout=2.0)
500
+ finally:
501
+ _current_worker.reset(worker_token)
502
+ finally:
503
+ # Reset ContextVar
504
+ _current_docket.reset(docket_token)
505
+ # Clear instance attribute
506
+ self._docket = None
507
+ finally:
508
+ # Reset server ContextVar
509
+ _current_server.reset(server_token)
510
+
511
+ async def _register_mounted_server_functions(
512
+ self, server: FastMCP, docket: Docket, prefix: str | None
513
+ ) -> None:
514
+ """Register task-enabled functions from a mounted server with Docket.
515
+
516
+ This enables background task execution for mounted server components
517
+ through the parent server's Docket context.
518
+
519
+ Args:
520
+ server: The mounted server whose functions to register
521
+ docket: The Docket instance to register with
522
+ prefix: The mount prefix to prepend to function names (matches
523
+ client-facing tool/prompt names)
524
+ """
525
+ # Register tools with prefixed names to avoid collisions
526
+ for tool in server._tool_manager._tools.values():
527
+ if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
528
+ # Use same naming as client-facing tool keys
529
+ fn_name = f"{prefix}_{tool.key}" if prefix else tool.key
530
+ named_fn = _create_named_fn_wrapper(tool.fn, fn_name)
531
+ docket.register(named_fn)
532
+
533
+ # Register prompts with prefixed names
534
+ for prompt in server._prompt_manager._prompts.values():
535
+ if (
536
+ isinstance(prompt, FunctionPrompt)
537
+ and prompt.task_config.mode != "forbidden"
538
+ ):
539
+ fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
540
+ named_fn = _create_named_fn_wrapper(
541
+ cast(Callable[..., Awaitable[Any]], prompt.fn), fn_name
542
+ )
543
+ docket.register(named_fn)
544
+
545
+ # Register resources with prefixed names (use name, not key/URI)
546
+ for resource in server._resource_manager._resources.values():
547
+ if (
548
+ isinstance(resource, FunctionResource)
549
+ and resource.task_config.mode != "forbidden"
550
+ ):
551
+ fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
552
+ named_fn = _create_named_fn_wrapper(resource.fn, fn_name)
553
+ docket.register(named_fn)
554
+
555
+ # Register resource templates with prefixed names (use name, not key/URI)
556
+ for template in server._resource_manager._templates.values():
557
+ if (
558
+ isinstance(template, FunctionResourceTemplate)
559
+ and template.task_config.mode != "forbidden"
560
+ ):
561
+ fn_name = f"{prefix}_{template.name}" if prefix else template.name
562
+ named_fn = _create_named_fn_wrapper(template.fn, fn_name)
563
+ docket.register(named_fn)
564
+
565
+ # Recursively register from nested mounted servers with accumulated prefix
566
+ for nested in server._mounted_servers:
567
+ nested_prefix = (
568
+ f"{prefix}_{nested.prefix}"
569
+ if prefix and nested.prefix
570
+ else (prefix or nested.prefix)
571
+ )
572
+ await self._register_mounted_server_functions(
573
+ nested.server, docket, nested_prefix
574
+ )
575
+
386
576
  @asynccontextmanager
387
577
  async def _lifespan_manager(self) -> AsyncIterator[None]:
388
578
  if self._lifespan_result_set:
389
579
  yield
390
580
  return
391
581
 
392
- async with self._lifespan(self) as lifespan_result:
393
- self._lifespan_result = lifespan_result
582
+ async with (
583
+ self._lifespan(self) as user_lifespan_result,
584
+ self._docket_lifespan(),
585
+ ):
586
+ self._lifespan_result = user_lifespan_result
394
587
  self._lifespan_result_set = True
395
588
 
396
589
  async with AsyncExitStack[bool | None]() as stack:
@@ -399,7 +592,11 @@ class FastMCP(Generic[LifespanResultT]):
399
592
  cm=server.server._lifespan_manager()
400
593
  )
401
594
 
402
- yield
595
+ self._started.set()
596
+ try:
597
+ yield
598
+ finally:
599
+ self._started.clear()
403
600
 
404
601
  self._lifespan_result_set = False
405
602
  self._lifespan_result = None
@@ -464,8 +661,260 @@ class FastMCP(Generic[LifespanResultT]):
464
661
  self._mcp_server.call_tool(validate_input=self.strict_input_validation)(
465
662
  self._call_tool_mcp
466
663
  )
467
- self._mcp_server.read_resource()(self._read_resource_mcp)
468
- self._mcp_server.get_prompt()(self._get_prompt_mcp)
664
+ # Register custom read_resource handler (SDK decorator doesn't support CreateTaskResult)
665
+ self._setup_read_resource_handler()
666
+ # Register custom get_prompt handler (SDK decorator doesn't support CreateTaskResult)
667
+ self._setup_get_prompt_handler()
668
+ # Register custom SEP-1686 task protocol handlers
669
+ self._setup_task_protocol_handlers()
670
+
671
+ def _setup_read_resource_handler(self) -> None:
672
+ """
673
+ Set up custom read_resource handler that supports task-augmented responses.
674
+
675
+ The SDK's read_resource decorator doesn't support CreateTaskResult returns,
676
+ so we register a custom handler that checks request_context.experimental.is_task.
677
+ """
678
+
679
+ async def handler(req: mcp.types.ReadResourceRequest) -> mcp.types.ServerResult:
680
+ uri = req.params.uri
681
+
682
+ # Check for task metadata via SDK's request context
683
+ task_meta = None
684
+ try:
685
+ ctx = self._mcp_server.request_context
686
+ if ctx.experimental.is_task:
687
+ task_meta = ctx.experimental.task_metadata
688
+ except (AttributeError, LookupError):
689
+ pass
690
+
691
+ # Check for task metadata and route appropriately
692
+ async with fastmcp.server.context.Context(fastmcp=self):
693
+ # Get resource including from mounted servers
694
+ resource = await self._get_resource_or_template_or_none(str(uri))
695
+ if (
696
+ resource
697
+ and self._should_enable_component(resource)
698
+ and hasattr(resource, "task_config")
699
+ ):
700
+ task_mode = resource.task_config.mode # type: ignore[union-attr]
701
+
702
+ # Enforce mode="required" - must have task metadata
703
+ if task_mode == "required" and not task_meta:
704
+ raise McpError(
705
+ ErrorData(
706
+ code=METHOD_NOT_FOUND,
707
+ message=f"Resource '{uri}' requires task-augmented execution",
708
+ )
709
+ )
710
+
711
+ # Route to background if task metadata present and mode allows
712
+ if task_meta and task_mode != "forbidden":
713
+ # For FunctionResource/FunctionResourceTemplate, use Docket
714
+ if isinstance(
715
+ resource,
716
+ FunctionResource | FunctionResourceTemplate,
717
+ ):
718
+ task_meta_dict = task_meta.model_dump(exclude_none=True)
719
+ return await handle_resource_as_task(
720
+ self, str(uri), resource, task_meta_dict
721
+ )
722
+
723
+ # Forbidden mode: task requested but mode="forbidden"
724
+ # Raise error since resources don't have isError field
725
+ if task_meta and task_mode == "forbidden":
726
+ raise McpError(
727
+ ErrorData(
728
+ code=METHOD_NOT_FOUND,
729
+ message=f"Resource '{uri}' does not support task-augmented execution",
730
+ )
731
+ )
732
+
733
+ # Synchronous execution
734
+ result = await self._read_resource_mcp(uri)
735
+
736
+ # Graceful degradation: if we got here with task_meta, something went wrong
737
+ # (This should be unreachable now that forbidden raises)
738
+ if task_meta:
739
+ mcp_contents = []
740
+ for item in result:
741
+ if isinstance(item.content, str):
742
+ mcp_contents.append(
743
+ mcp.types.TextResourceContents(
744
+ uri=uri,
745
+ text=item.content,
746
+ mimeType=item.mime_type or "text/plain",
747
+ )
748
+ )
749
+ elif isinstance(item.content, bytes):
750
+ import base64
751
+
752
+ mcp_contents.append(
753
+ mcp.types.BlobResourceContents(
754
+ uri=uri,
755
+ blob=base64.b64encode(item.content).decode(),
756
+ mimeType=item.mime_type or "application/octet-stream",
757
+ )
758
+ )
759
+ return mcp.types.ServerResult(
760
+ mcp.types.ReadResourceResult(
761
+ contents=mcp_contents,
762
+ _meta={
763
+ "modelcontextprotocol.io/task": {
764
+ "returned_immediately": True
765
+ }
766
+ },
767
+ )
768
+ )
769
+
770
+ # Convert to proper ServerResult
771
+ if isinstance(result, mcp.types.ServerResult):
772
+ return result
773
+
774
+ mcp_contents = []
775
+ for item in result:
776
+ if isinstance(item.content, str):
777
+ mcp_contents.append(
778
+ mcp.types.TextResourceContents(
779
+ uri=uri,
780
+ text=item.content,
781
+ mimeType=item.mime_type or "text/plain",
782
+ )
783
+ )
784
+ elif isinstance(item.content, bytes):
785
+ import base64
786
+
787
+ mcp_contents.append(
788
+ mcp.types.BlobResourceContents(
789
+ uri=uri,
790
+ blob=base64.b64encode(item.content).decode(),
791
+ mimeType=item.mime_type or "application/octet-stream",
792
+ )
793
+ )
794
+
795
+ return mcp.types.ServerResult(
796
+ mcp.types.ReadResourceResult(contents=mcp_contents)
797
+ )
798
+
799
+ self._mcp_server.request_handlers[mcp.types.ReadResourceRequest] = handler
800
+
801
+ def _setup_get_prompt_handler(self) -> None:
802
+ """
803
+ Set up custom get_prompt handler that supports task-augmented responses.
804
+
805
+ The SDK's get_prompt decorator doesn't support CreateTaskResult returns,
806
+ so we register a custom handler that checks request_context.experimental.is_task.
807
+ """
808
+
809
+ async def handler(req: mcp.types.GetPromptRequest) -> mcp.types.ServerResult:
810
+ name = req.params.name
811
+ arguments = req.params.arguments
812
+
813
+ # Check for task metadata via SDK's request context
814
+ task_meta = None
815
+ try:
816
+ ctx = self._mcp_server.request_context
817
+ if ctx.experimental.is_task:
818
+ task_meta = ctx.experimental.task_metadata
819
+ except (AttributeError, LookupError):
820
+ pass
821
+
822
+ # Check for task metadata and route appropriately
823
+ async with fastmcp.server.context.Context(fastmcp=self):
824
+ prompts = await self.get_prompts()
825
+ prompt = prompts.get(name)
826
+ if (
827
+ prompt
828
+ and self._should_enable_component(prompt)
829
+ and hasattr(prompt, "task_config")
830
+ and prompt.task_config
831
+ ):
832
+ task_mode = prompt.task_config.mode # type: ignore[union-attr]
833
+
834
+ # Enforce mode="required" - must have task metadata
835
+ if task_mode == "required" and not task_meta:
836
+ raise McpError(
837
+ ErrorData(
838
+ code=METHOD_NOT_FOUND,
839
+ message=f"Prompt '{name}' requires task-augmented execution",
840
+ )
841
+ )
842
+
843
+ # Route to background if task metadata present and mode allows
844
+ if task_meta and task_mode != "forbidden":
845
+ task_meta_dict = task_meta.model_dump(exclude_none=True)
846
+ result = await handle_prompt_as_task(
847
+ self, name, arguments, task_meta_dict
848
+ )
849
+ return mcp.types.ServerResult(result)
850
+
851
+ # Forbidden mode: task requested but mode="forbidden"
852
+ # Raise error since prompts don't have isError field
853
+ if task_meta and task_mode == "forbidden":
854
+ raise McpError(
855
+ ErrorData(
856
+ code=METHOD_NOT_FOUND,
857
+ message=f"Prompt '{name}' does not support task-augmented execution",
858
+ )
859
+ )
860
+
861
+ # Synchronous execution
862
+ result = await self._get_prompt_mcp(name, arguments)
863
+ return mcp.types.ServerResult(result)
864
+
865
+ self._mcp_server.request_handlers[mcp.types.GetPromptRequest] = handler
866
+
867
+ def _setup_task_protocol_handlers(self) -> None:
868
+ """Register SEP-1686 task protocol handlers with SDK."""
869
+ from mcp.types import (
870
+ CancelTaskRequest,
871
+ GetTaskPayloadRequest,
872
+ GetTaskRequest,
873
+ ListTasksRequest,
874
+ ServerResult,
875
+ )
876
+
877
+ from fastmcp.server.tasks.protocol import (
878
+ tasks_cancel_handler,
879
+ tasks_get_handler,
880
+ tasks_list_handler,
881
+ tasks_result_handler,
882
+ )
883
+
884
+ # Manually register handlers (SDK decorators fail with locally-defined functions)
885
+ # SDK expects handlers that receive Request objects and return ServerResult
886
+
887
+ async def handle_get_task(req: GetTaskRequest) -> ServerResult:
888
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
889
+ result = await tasks_get_handler(self, params)
890
+ return ServerResult(result)
891
+
892
+ async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:
893
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
894
+ result = await tasks_result_handler(self, params)
895
+ return ServerResult(result)
896
+
897
+ async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:
898
+ params = (
899
+ req.params.model_dump(by_alias=True, exclude_none=True)
900
+ if req.params
901
+ else {}
902
+ )
903
+ result = await tasks_list_handler(self, params)
904
+ return ServerResult(result)
905
+
906
+ async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:
907
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
908
+ result = await tasks_cancel_handler(self, params)
909
+ return ServerResult(result)
910
+
911
+ # Register directly with SDK (same as what decorators do internally)
912
+ self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task
913
+ self._mcp_server.request_handlers[GetTaskPayloadRequest] = (
914
+ handle_get_task_result
915
+ )
916
+ self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks
917
+ self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task
469
918
 
470
919
  async def _apply_middleware(
471
920
  self,
@@ -507,6 +956,33 @@ class FastMCP(Generic[LifespanResultT]):
507
956
  raise NotFoundError(f"Unknown tool: {key}")
508
957
  return tools[key]
509
958
 
959
+ async def _get_tool_with_task_config(self, key: str) -> Tool | None:
960
+ """Get a tool by key, returning None if not found.
961
+
962
+ Used for task config checking where we need the actual tool object
963
+ (including from mounted servers and proxies) but don't want to raise.
964
+ """
965
+ try:
966
+ return await self.get_tool(key)
967
+ except NotFoundError:
968
+ return None
969
+
970
+ async def _get_resource_or_template_or_none(
971
+ self, uri: str
972
+ ) -> Resource | ResourceTemplate | None:
973
+ """Get a resource or template by URI, searching recursively. Returns None if not found."""
974
+ try:
975
+ return await self.get_resource(uri)
976
+ except NotFoundError:
977
+ pass
978
+
979
+ templates = await self.get_resource_templates()
980
+ for template in templates.values():
981
+ if template.matches(uri):
982
+ return template
983
+
984
+ return None
985
+
510
986
  async def get_resources(self) -> dict[str, Resource]:
511
987
  """Get all resources (unfiltered), including mounted servers, indexed by key."""
512
988
  all_resources = dict(await self._resource_manager.get_resources())
@@ -516,9 +992,7 @@ class FastMCP(Generic[LifespanResultT]):
516
992
  child_resources = await mounted.server.get_resources()
517
993
  for key, resource in child_resources.items():
518
994
  new_key = (
519
- add_resource_prefix(
520
- key, mounted.prefix, mounted.resource_prefix_format
521
- )
995
+ add_resource_prefix(key, mounted.prefix)
522
996
  if mounted.prefix
523
997
  else key
524
998
  )
@@ -555,17 +1029,16 @@ class FastMCP(Generic[LifespanResultT]):
555
1029
  child_templates = await mounted.server.get_resource_templates()
556
1030
  for key, template in child_templates.items():
557
1031
  new_key = (
558
- add_resource_prefix(
559
- key, mounted.prefix, mounted.resource_prefix_format
560
- )
1032
+ add_resource_prefix(key, mounted.prefix)
561
1033
  if mounted.prefix
562
1034
  else key
563
1035
  )
564
- update = (
565
- {"name": f"{mounted.prefix}_{template.name}"}
566
- if mounted.prefix and template.name
567
- else {}
568
- )
1036
+ update: dict[str, Any] = {}
1037
+ if mounted.prefix:
1038
+ if template.name:
1039
+ update["name"] = f"{mounted.prefix}_{template.name}"
1040
+ # Update uri_template so matches() works with prefixed URIs
1041
+ update["uri_template"] = new_key
569
1042
  all_templates[new_key] = template.model_copy(
570
1043
  key=new_key, update=update
571
1044
  )
@@ -835,11 +1308,7 @@ class FastMCP(Generic[LifespanResultT]):
835
1308
 
836
1309
  key = resource.key
837
1310
  if mounted.prefix:
838
- key = add_resource_prefix(
839
- resource.key,
840
- mounted.prefix,
841
- mounted.resource_prefix_format,
842
- )
1311
+ key = add_resource_prefix(resource.key, mounted.prefix)
843
1312
  resource = resource.model_copy(
844
1313
  key=key,
845
1314
  update={"name": f"{mounted.prefix}_{resource.name}"},
@@ -931,11 +1400,7 @@ class FastMCP(Generic[LifespanResultT]):
931
1400
 
932
1401
  key = template.key
933
1402
  if mounted.prefix:
934
- key = add_resource_prefix(
935
- template.key,
936
- mounted.prefix,
937
- mounted.resource_prefix_format,
938
- )
1403
+ key = add_resource_prefix(template.key, mounted.prefix)
939
1404
  template = template.model_copy(
940
1405
  key=key,
941
1406
  update={"name": f"{mounted.prefix}_{template.name}"},
@@ -1054,7 +1519,7 @@ class FastMCP(Generic[LifespanResultT]):
1054
1519
  """
1055
1520
  Handle MCP 'callTool' requests.
1056
1521
 
1057
- Delegates to _call_tool, which should be overridden by FastMCP subclasses.
1522
+ Detects SEP-1686 task metadata and routes to background execution if supported.
1058
1523
 
1059
1524
  Args:
1060
1525
  key: The name of the tool to call
@@ -1069,6 +1534,65 @@ class FastMCP(Generic[LifespanResultT]):
1069
1534
 
1070
1535
  async with fastmcp.server.context.Context(fastmcp=self):
1071
1536
  try:
1537
+ # Check for SEP-1686 task metadata via request context
1538
+ task_meta = None
1539
+ try:
1540
+ # Access task metadata from SDK's request context
1541
+ ctx = self._mcp_server.request_context
1542
+ if ctx.experimental.is_task:
1543
+ task_meta = ctx.experimental.task_metadata
1544
+ except (AttributeError, LookupError):
1545
+ # No request context available - proceed without task metadata
1546
+ pass
1547
+
1548
+ # Get tool from local manager, mounted servers, or proxy
1549
+ tool = await self._get_tool_with_task_config(key)
1550
+ if (
1551
+ tool
1552
+ and self._should_enable_component(tool)
1553
+ and hasattr(tool, "task_config")
1554
+ ):
1555
+ task_mode = tool.task_config.mode # type: ignore[union-attr]
1556
+
1557
+ # Enforce mode="required" - must have task metadata
1558
+ if task_mode == "required" and not task_meta:
1559
+ raise McpError(
1560
+ ErrorData(
1561
+ code=METHOD_NOT_FOUND,
1562
+ message=f"Tool '{key}' requires task-augmented execution",
1563
+ )
1564
+ )
1565
+
1566
+ # Route to background if task metadata present and mode allows
1567
+ if task_meta and task_mode != "forbidden":
1568
+ # For FunctionTool, use Docket for background execution
1569
+ if isinstance(tool, FunctionTool):
1570
+ task_meta_dict = task_meta.model_dump(exclude_none=True)
1571
+ return await handle_tool_as_task(
1572
+ self, key, arguments, task_meta_dict
1573
+ )
1574
+ # For ProxyTool/mounted tools, proceed with normal execution
1575
+ # They will forward task metadata to their backend
1576
+
1577
+ # Forbidden mode: task requested but mode="forbidden"
1578
+ # Return error result with returned_immediately=True
1579
+ if task_meta and task_mode == "forbidden":
1580
+ return mcp.types.CallToolResult(
1581
+ content=[
1582
+ mcp.types.TextContent(
1583
+ type="text",
1584
+ text=f"Tool '{key}' does not support task-augmented execution",
1585
+ )
1586
+ ],
1587
+ isError=True,
1588
+ _meta={
1589
+ "modelcontextprotocol.io/task": {
1590
+ "returned_immediately": True
1591
+ }
1592
+ },
1593
+ )
1594
+
1595
+ # Synchronous execution (normal path)
1072
1596
  result = await self._call_tool_middleware(key, arguments)
1073
1597
  return result.to_mcp_result()
1074
1598
  except DisabledError as e:
@@ -1115,7 +1639,9 @@ class FastMCP(Generic[LifespanResultT]):
1115
1639
 
1116
1640
  try:
1117
1641
  # First, get the tool to check if parent's filter allows it
1118
- tool = await mounted.server._tool_manager.get_tool(try_name)
1642
+ # Use get_tool() instead of _tool_manager.get_tool() to support
1643
+ # nested mounted servers (tools mounted more than 2 levels deep)
1644
+ tool = await mounted.server.get_tool(try_name)
1119
1645
  if not self._should_enable_component(tool):
1120
1646
  # Parent filter blocks this tool, continue searching
1121
1647
  continue
@@ -1148,6 +1674,7 @@ class FastMCP(Generic[LifespanResultT]):
1148
1674
 
1149
1675
  async with fastmcp.server.context.Context(fastmcp=self):
1150
1676
  try:
1677
+ # Task routing handled by custom handler
1151
1678
  return list[ReadResourceContents](
1152
1679
  await self._read_resource_middleware(uri)
1153
1680
  )
@@ -1195,20 +1722,20 @@ class FastMCP(Generic[LifespanResultT]):
1195
1722
  for mounted in reversed(self._mounted_servers):
1196
1723
  key = uri_str
1197
1724
  if mounted.prefix:
1198
- if not has_resource_prefix(
1199
- key, mounted.prefix, mounted.resource_prefix_format
1200
- ):
1725
+ if not has_resource_prefix(key, mounted.prefix):
1201
1726
  continue
1202
- key = remove_resource_prefix(
1203
- key, mounted.prefix, mounted.resource_prefix_format
1204
- )
1727
+ key = remove_resource_prefix(key, mounted.prefix)
1205
1728
 
1729
+ # First, get the resource/template to check if parent's filter allows it
1730
+ # Use get_resource_or_template to support nested mounted servers
1731
+ # (resources/templates mounted more than 2 levels deep)
1732
+ resource = await mounted.server._get_resource_or_template_or_none(key)
1733
+ if resource is None:
1734
+ continue
1735
+ if not self._should_enable_component(resource):
1736
+ # Parent filter blocks this resource, continue searching
1737
+ continue
1206
1738
  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
1739
  result = list(await mounted.server._read_resource_middleware(key))
1213
1740
  return result
1214
1741
  except NotFoundError:
@@ -1246,6 +1773,7 @@ class FastMCP(Generic[LifespanResultT]):
1246
1773
 
1247
1774
  async with fastmcp.server.context.Context(fastmcp=self):
1248
1775
  try:
1776
+ # Task routing handled by custom handler
1249
1777
  return await self._get_prompt_middleware(name, arguments)
1250
1778
  except DisabledError as e:
1251
1779
  # convert to NotFoundError to avoid leaking prompt presence
@@ -1288,7 +1816,9 @@ class FastMCP(Generic[LifespanResultT]):
1288
1816
 
1289
1817
  try:
1290
1818
  # First, get the prompt to check if parent's filter allows it
1291
- prompt = await mounted.server._prompt_manager.get_prompt(try_name)
1819
+ # Use get_prompt() instead of _prompt_manager.get_prompt() to support
1820
+ # nested mounted servers (prompts mounted more than 2 levels deep)
1821
+ prompt = await mounted.server.get_prompt(try_name)
1292
1822
  if not self._should_enable_component(prompt):
1293
1823
  # Parent filter blocks this prompt, continue searching
1294
1824
  continue
@@ -1380,6 +1910,7 @@ class FastMCP(Generic[LifespanResultT]):
1380
1910
  exclude_args: list[str] | None = None,
1381
1911
  meta: dict[str, Any] | None = None,
1382
1912
  enabled: bool | None = None,
1913
+ task: bool | TaskConfig | None = None,
1383
1914
  ) -> FunctionTool: ...
1384
1915
 
1385
1916
  @overload
@@ -1397,6 +1928,7 @@ class FastMCP(Generic[LifespanResultT]):
1397
1928
  exclude_args: list[str] | None = None,
1398
1929
  meta: dict[str, Any] | None = None,
1399
1930
  enabled: bool | None = None,
1931
+ task: bool | TaskConfig | None = None,
1400
1932
  ) -> Callable[[AnyFunction], FunctionTool]: ...
1401
1933
 
1402
1934
  def tool(
@@ -1413,6 +1945,7 @@ class FastMCP(Generic[LifespanResultT]):
1413
1945
  exclude_args: list[str] | None = None,
1414
1946
  meta: dict[str, Any] | None = None,
1415
1947
  enabled: bool | None = None,
1948
+ task: bool | TaskConfig | None = None,
1416
1949
  ) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
1417
1950
  """Decorator to register a tool.
1418
1951
 
@@ -1486,6 +2019,11 @@ class FastMCP(Generic[LifespanResultT]):
1486
2019
  fn = name_or_fn
1487
2020
  tool_name = name # Use keyword name if provided, otherwise None
1488
2021
 
2022
+ # Resolve task parameter
2023
+ supports_task: bool | TaskConfig = (
2024
+ task if task is not None else self._support_tasks_by_default
2025
+ )
2026
+
1489
2027
  # Register the tool immediately and return the tool object
1490
2028
  # Note: Deprecation warning for exclude_args is handled in Tool.from_function
1491
2029
  tool = Tool.from_function(
@@ -1501,6 +2039,7 @@ class FastMCP(Generic[LifespanResultT]):
1501
2039
  meta=meta,
1502
2040
  serializer=self._tool_serializer,
1503
2041
  enabled=enabled,
2042
+ task=supports_task,
1504
2043
  )
1505
2044
  self.add_tool(tool)
1506
2045
  return tool
@@ -1534,6 +2073,7 @@ class FastMCP(Generic[LifespanResultT]):
1534
2073
  exclude_args=exclude_args,
1535
2074
  meta=meta,
1536
2075
  enabled=enabled,
2076
+ task=task,
1537
2077
  )
1538
2078
 
1539
2079
  def add_resource(self, resource: Resource) -> Resource:
@@ -1580,44 +2120,6 @@ class FastMCP(Generic[LifespanResultT]):
1580
2120
 
1581
2121
  return template
1582
2122
 
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
2123
  def resource(
1622
2124
  self,
1623
2125
  uri: str,
@@ -1631,6 +2133,7 @@ class FastMCP(Generic[LifespanResultT]):
1631
2133
  enabled: bool | None = None,
1632
2134
  annotations: Annotations | dict[str, Any] | None = None,
1633
2135
  meta: dict[str, Any] | None = None,
2136
+ task: bool | TaskConfig | None = None,
1634
2137
  ) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
1635
2138
  """Decorator to register a function as a resource.
1636
2139
 
@@ -1695,8 +2198,6 @@ class FastMCP(Generic[LifespanResultT]):
1695
2198
  )
1696
2199
 
1697
2200
  def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
1698
- from fastmcp.server.context import Context
1699
-
1700
2201
  if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
1701
2202
  raise ValueError(
1702
2203
  inspect.cleandoc(
@@ -1709,14 +2210,18 @@ class FastMCP(Generic[LifespanResultT]):
1709
2210
  )
1710
2211
  )
1711
2212
 
2213
+ # Resolve task parameter
2214
+ supports_task: bool | TaskConfig = (
2215
+ task if task is not None else self._support_tasks_by_default
2216
+ )
2217
+
1712
2218
  # Check if this should be a template
1713
2219
  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
- )
2220
+ # Use wrapper to check for user-facing parameters
2221
+ from fastmcp.server.dependencies import without_injected_parameters
2222
+
2223
+ wrapper_fn = without_injected_parameters(fn)
2224
+ has_func_params = bool(inspect.signature(wrapper_fn).parameters)
1720
2225
 
1721
2226
  if has_uri_params or has_func_params:
1722
2227
  template = ResourceTemplate.from_function(
@@ -1731,6 +2236,7 @@ class FastMCP(Generic[LifespanResultT]):
1731
2236
  enabled=enabled,
1732
2237
  annotations=annotations,
1733
2238
  meta=meta,
2239
+ task=supports_task,
1734
2240
  )
1735
2241
  self.add_template(template)
1736
2242
  return template
@@ -1747,6 +2253,7 @@ class FastMCP(Generic[LifespanResultT]):
1747
2253
  enabled=enabled,
1748
2254
  annotations=annotations,
1749
2255
  meta=meta,
2256
+ task=supports_task,
1750
2257
  )
1751
2258
  self.add_resource(resource)
1752
2259
  return resource
@@ -1792,6 +2299,7 @@ class FastMCP(Generic[LifespanResultT]):
1792
2299
  tags: set[str] | None = None,
1793
2300
  enabled: bool | None = None,
1794
2301
  meta: dict[str, Any] | None = None,
2302
+ task: bool | TaskConfig | None = None,
1795
2303
  ) -> FunctionPrompt: ...
1796
2304
 
1797
2305
  @overload
@@ -1806,6 +2314,7 @@ class FastMCP(Generic[LifespanResultT]):
1806
2314
  tags: set[str] | None = None,
1807
2315
  enabled: bool | None = None,
1808
2316
  meta: dict[str, Any] | None = None,
2317
+ task: bool | TaskConfig | None = None,
1809
2318
  ) -> Callable[[AnyFunction], FunctionPrompt]: ...
1810
2319
 
1811
2320
  def prompt(
@@ -1819,6 +2328,7 @@ class FastMCP(Generic[LifespanResultT]):
1819
2328
  tags: set[str] | None = None,
1820
2329
  enabled: bool | None = None,
1821
2330
  meta: dict[str, Any] | None = None,
2331
+ task: bool | TaskConfig | None = None,
1822
2332
  ) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
1823
2333
  """Decorator to register a prompt.
1824
2334
 
@@ -1909,6 +2419,11 @@ class FastMCP(Generic[LifespanResultT]):
1909
2419
  fn = name_or_fn
1910
2420
  prompt_name = name # Use keyword name if provided, otherwise None
1911
2421
 
2422
+ # Resolve task parameter
2423
+ supports_task: bool | TaskConfig = (
2424
+ task if task is not None else self._support_tasks_by_default
2425
+ )
2426
+
1912
2427
  # Register the prompt immediately
1913
2428
  prompt = Prompt.from_function(
1914
2429
  fn=fn,
@@ -1919,6 +2434,7 @@ class FastMCP(Generic[LifespanResultT]):
1919
2434
  tags=tags,
1920
2435
  enabled=enabled,
1921
2436
  meta=meta,
2437
+ task=supports_task,
1922
2438
  )
1923
2439
  self.add_prompt(prompt)
1924
2440
 
@@ -1950,6 +2466,7 @@ class FastMCP(Generic[LifespanResultT]):
1950
2466
  tags=tags,
1951
2467
  enabled=enabled,
1952
2468
  meta=meta,
2469
+ task=task,
1953
2470
  )
1954
2471
 
1955
2472
  async def run_stdio_async(
@@ -1974,11 +2491,18 @@ class FastMCP(Generic[LifespanResultT]):
1974
2491
  logger.info(
1975
2492
  f"Starting MCP server {self.name!r} with transport 'stdio'"
1976
2493
  )
2494
+
2495
+ # Build experimental capabilities
2496
+ experimental_capabilities = get_task_capabilities()
2497
+
1977
2498
  await self._mcp_server.run(
1978
2499
  read_stream,
1979
2500
  write_stream,
1980
2501
  self._mcp_server.create_initialization_options(
1981
- NotificationOptions(tools_changed=True)
2502
+ notification_options=NotificationOptions(
2503
+ tools_changed=True
2504
+ ),
2505
+ experimental_capabilities=experimental_capabilities,
1982
2506
  ),
1983
2507
  )
1984
2508
 
@@ -2061,86 +2585,6 @@ class FastMCP(Generic[LifespanResultT]):
2061
2585
 
2062
2586
  await server.serve()
2063
2587
 
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
2588
  def http_app(
2145
2589
  self,
2146
2590
  path: str | None = None,
@@ -2148,13 +2592,24 @@ class FastMCP(Generic[LifespanResultT]):
2148
2592
  json_response: bool | None = None,
2149
2593
  stateless_http: bool | None = None,
2150
2594
  transport: Literal["http", "streamable-http", "sse"] = "http",
2595
+ event_store: EventStore | None = None,
2596
+ retry_interval: int | None = None,
2151
2597
  ) -> StarletteWithLifespan:
2152
2598
  """Create a Starlette app using the specified HTTP transport.
2153
2599
 
2154
2600
  Args:
2155
2601
  path: The path for the HTTP endpoint
2156
2602
  middleware: A list of middleware to apply to the app
2157
- transport: Transport protocol to use - either "streamable-http" (default) or "sse"
2603
+ json_response: Whether to use JSON response format
2604
+ stateless_http: Whether to use stateless mode (new transport per request)
2605
+ transport: Transport protocol to use - "http", "streamable-http", or "sse"
2606
+ event_store: Optional event store for SSE polling/resumability. When set,
2607
+ enables clients to reconnect and resume receiving events after
2608
+ server-initiated disconnections. Only used with streamable-http transport.
2609
+ retry_interval: Optional retry interval in milliseconds for SSE polling.
2610
+ Controls how quickly clients should reconnect after server-initiated
2611
+ disconnections. Requires event_store to be set. Only used with
2612
+ streamable-http transport.
2158
2613
 
2159
2614
  Returns:
2160
2615
  A Starlette application configured with the specified transport
@@ -2165,7 +2620,8 @@ class FastMCP(Generic[LifespanResultT]):
2165
2620
  server=self,
2166
2621
  streamable_http_path=path
2167
2622
  or self._deprecated_settings.streamable_http_path,
2168
- event_store=None,
2623
+ event_store=event_store,
2624
+ retry_interval=retry_interval,
2169
2625
  auth=self.auth,
2170
2626
  json_response=(
2171
2627
  json_response
@@ -2190,40 +2646,11 @@ class FastMCP(Generic[LifespanResultT]):
2190
2646
  middleware=middleware,
2191
2647
  )
2192
2648
 
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
2649
  def mount(
2219
2650
  self,
2220
2651
  server: FastMCP[LifespanResultT],
2221
2652
  prefix: str | None = None,
2222
2653
  as_proxy: bool | None = None,
2223
- *,
2224
- tool_separator: str | None = None,
2225
- resource_separator: str | None = None,
2226
- prompt_separator: str | None = None,
2227
2654
  ) -> None:
2228
2655
  """Mount another FastMCP server on this server with an optional prefix.
2229
2656
 
@@ -2268,56 +2695,9 @@ class FastMCP(Generic[LifespanResultT]):
2268
2695
  as_proxy: Whether to treat the mounted server as a proxy. If None (default),
2269
2696
  automatically determined based on whether the server has a custom lifespan
2270
2697
  (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.
2274
2698
  """
2275
2699
  from fastmcp.server.proxy import FastMCPProxy
2276
2700
 
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
2701
  # if as_proxy is not specified and the server has a custom lifespan,
2322
2702
  # we should treat it as a proxy
2323
2703
  if as_proxy is None:
@@ -2326,11 +2706,15 @@ class FastMCP(Generic[LifespanResultT]):
2326
2706
  if as_proxy and not isinstance(server, FastMCPProxy):
2327
2707
  server = FastMCP.as_proxy(server)
2328
2708
 
2709
+ # Mark the server as mounted so it skips creating its own Docket/Worker.
2710
+ # The parent's Docket handles task execution, avoiding race conditions
2711
+ # with multiple workers competing for tasks from the same queue.
2712
+ server._is_mounted = True
2713
+
2329
2714
  # Delegate mounting to all three managers
2330
2715
  mounted_server = MountedServer(
2331
2716
  prefix=prefix,
2332
2717
  server=server,
2333
- resource_prefix_format=self.resource_prefix_format,
2334
2718
  )
2335
2719
  self._mounted_servers.append(mounted_server)
2336
2720
 
@@ -2338,9 +2722,6 @@ class FastMCP(Generic[LifespanResultT]):
2338
2722
  self,
2339
2723
  server: FastMCP[LifespanResultT],
2340
2724
  prefix: str | None = None,
2341
- tool_separator: str | None = None,
2342
- resource_separator: str | None = None,
2343
- prompt_separator: str | None = None,
2344
2725
  ) -> None:
2345
2726
  """
2346
2727
  Import the MCP objects from another FastMCP server into this one,
@@ -2372,56 +2753,7 @@ class FastMCP(Generic[LifespanResultT]):
2372
2753
  server: The FastMCP server to import
2373
2754
  prefix: Optional prefix to use for the imported server's objects. If None,
2374
2755
  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
-
2756
+ """
2425
2757
  # Import tools from the server
2426
2758
  for key, tool in (await server.get_tools()).items():
2427
2759
  if prefix:
@@ -2431,9 +2763,7 @@ class FastMCP(Generic[LifespanResultT]):
2431
2763
  # Import resources and templates from the server
2432
2764
  for key, resource in (await server.get_resources()).items():
2433
2765
  if prefix:
2434
- resource_key = add_resource_prefix(
2435
- key, prefix, self.resource_prefix_format
2436
- )
2766
+ resource_key = add_resource_prefix(key, prefix)
2437
2767
  resource = resource.model_copy(
2438
2768
  update={"name": f"{prefix}_{resource.name}"}, key=resource_key
2439
2769
  )
@@ -2441,9 +2771,7 @@ class FastMCP(Generic[LifespanResultT]):
2441
2771
 
2442
2772
  for key, template in (await server.get_resource_templates()).items():
2443
2773
  if prefix:
2444
- template_key = add_resource_prefix(
2445
- key, prefix, self.resource_prefix_format
2446
- )
2774
+ template_key = add_resource_prefix(key, prefix)
2447
2775
  template = template.model_copy(
2448
2776
  update={"name": f"{prefix}_{template.name}"}, key=template_key
2449
2777
  )
@@ -2476,66 +2804,46 @@ class FastMCP(Generic[LifespanResultT]):
2476
2804
  cls,
2477
2805
  openapi_spec: dict[str, Any],
2478
2806
  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,
2807
+ route_maps: list[RouteMap] | None = None,
2808
+ route_map_fn: OpenAPIRouteMapFn | None = None,
2809
+ mcp_component_fn: OpenAPIComponentFn | None = None,
2482
2810
  mcp_names: dict[str, str] | None = None,
2483
2811
  tags: set[str] | None = None,
2484
2812
  **settings: Any,
2485
- ) -> FastMCPOpenAPI | FastMCPOpenAPINew:
2813
+ ) -> FastMCPOpenAPI:
2486
2814
  """
2487
2815
  Create a FastMCP server from an OpenAPI specification.
2488
2816
  """
2817
+ from .openapi import FastMCPOpenAPI
2489
2818
 
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
- )
2819
+ return FastMCPOpenAPI(
2820
+ openapi_spec=openapi_spec,
2821
+ client=client,
2822
+ route_maps=route_maps,
2823
+ route_map_fn=route_map_fn,
2824
+ mcp_component_fn=mcp_component_fn,
2825
+ mcp_names=mcp_names,
2826
+ tags=tags,
2827
+ **settings,
2828
+ )
2522
2829
 
2523
2830
  @classmethod
2524
2831
  def from_fastapi(
2525
2832
  cls,
2526
2833
  app: Any,
2527
2834
  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,
2835
+ route_maps: list[RouteMap] | None = None,
2836
+ route_map_fn: OpenAPIRouteMapFn | None = None,
2837
+ mcp_component_fn: OpenAPIComponentFn | None = None,
2531
2838
  mcp_names: dict[str, str] | None = None,
2532
2839
  httpx_client_kwargs: dict[str, Any] | None = None,
2533
2840
  tags: set[str] | None = None,
2534
2841
  **settings: Any,
2535
- ) -> FastMCPOpenAPI | FastMCPOpenAPINew:
2842
+ ) -> FastMCPOpenAPI:
2536
2843
  """
2537
2844
  Create a FastMCP server from a FastAPI application.
2538
2845
  """
2846
+ from .openapi import FastMCPOpenAPI
2539
2847
 
2540
2848
  if httpx_client_kwargs is None:
2541
2849
  httpx_client_kwargs = {}
@@ -2548,40 +2856,17 @@ class FastMCP(Generic[LifespanResultT]):
2548
2856
 
2549
2857
  name = name or app.title
2550
2858
 
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
- )
2859
+ return FastMCPOpenAPI(
2860
+ openapi_spec=app.openapi(),
2861
+ client=client,
2862
+ name=name,
2863
+ route_maps=route_maps,
2864
+ route_map_fn=route_map_fn,
2865
+ mcp_component_fn=mcp_component_fn,
2866
+ mcp_names=mcp_names,
2867
+ tags=tags,
2868
+ **settings,
2869
+ )
2585
2870
 
2586
2871
  @classmethod
2587
2872
  def as_proxy(
@@ -2643,23 +2928,6 @@ class FastMCP(Generic[LifespanResultT]):
2643
2928
 
2644
2929
  return FastMCPProxy(client_factory=client_factory, **settings)
2645
2930
 
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
2931
  def _should_enable_component(
2664
2932
  self,
2665
2933
  component: FastMCPComponent,
@@ -2706,13 +2974,10 @@ class FastMCP(Generic[LifespanResultT]):
2706
2974
  class MountedServer:
2707
2975
  prefix: str | None
2708
2976
  server: FastMCP[Any]
2709
- resource_prefix_format: Literal["protocol", "path"] | None = None
2710
2977
 
2711
2978
 
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.
2979
+ def add_resource_prefix(uri: str, prefix: str) -> str:
2980
+ """Add a prefix to a resource URI using path formatting (resource://prefix/path).
2716
2981
 
2717
2982
  Args:
2718
2983
  uri: The original resource URI
@@ -2722,16 +2987,10 @@ def add_resource_prefix(
2722
2987
  The resource URI with the prefix added
2723
2988
 
2724
2989
  Examples:
2725
- With new style:
2726
2990
  ```python
2727
2991
  add_resource_prefix("resource://path/to/resource", "prefix")
2728
2992
  "resource://prefix/path/to/resource"
2729
2993
  ```
2730
- With legacy style:
2731
- ```python
2732
- add_resource_prefix("resource://path/to/resource", "prefix")
2733
- "prefix+resource://path/to/resource"
2734
- ```
2735
2994
  With absolute path:
2736
2995
  ```python
2737
2996
  add_resource_prefix("resource:///absolute/path", "prefix")
@@ -2744,54 +3003,32 @@ def add_resource_prefix(
2744
3003
  if not prefix:
2745
3004
  return uri
2746
3005
 
2747
- # Get the server settings to check for legacy format preference
3006
+ # Split the URI into protocol and path
3007
+ match = URI_PATTERN.match(uri)
3008
+ if not match:
3009
+ raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2748
3010
 
2749
- if prefix_format is None:
2750
- prefix_format = fastmcp.settings.resource_prefix_format
3011
+ protocol, path = match.groups()
2751
3012
 
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
- )
3013
+ # Add the prefix to the path
3014
+ return f"{protocol}{prefix}/{path}"
2763
3015
 
2764
- protocol, path = match.groups()
2765
3016
 
2766
- # Add the prefix to the path
2767
- return f"{protocol}{prefix}/{path}"
2768
- else:
2769
- raise ValueError(f"Invalid prefix format: {prefix_format}")
2770
-
2771
-
2772
- def remove_resource_prefix(
2773
- uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
2774
- ) -> str:
3017
+ def remove_resource_prefix(uri: str, prefix: str) -> str:
2775
3018
  """Remove a prefix from a resource URI.
2776
3019
 
2777
3020
  Args:
2778
3021
  uri: The resource URI with a prefix
2779
3022
  prefix: The prefix to remove
2780
- prefix_format: The format of the prefix to remove
3023
+
2781
3024
  Returns:
2782
3025
  The resource URI with the prefix removed
2783
3026
 
2784
3027
  Examples:
2785
- With new style:
2786
3028
  ```python
2787
3029
  remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
2788
3030
  "resource://path/to/resource"
2789
3031
  ```
2790
- With legacy style:
2791
- ```python
2792
- remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
2793
- "resource://path/to/resource"
2794
- ```
2795
3032
  With absolute path:
2796
3033
  ```python
2797
3034
  remove_resource_prefix("resource://prefix//absolute/path", "prefix")
@@ -2804,41 +3041,24 @@ def remove_resource_prefix(
2804
3041
  if not prefix:
2805
3042
  return uri
2806
3043
 
2807
- if prefix_format is None:
2808
- prefix_format = fastmcp.settings.resource_prefix_format
2809
-
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
- )
3044
+ # Split the URI into protocol and path
3045
+ match = URI_PATTERN.match(uri)
3046
+ if not match:
3047
+ raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2824
3048
 
2825
- protocol, path = match.groups()
3049
+ protocol, path = match.groups()
2826
3050
 
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
3051
+ # Check if the path starts with the prefix followed by a /
3052
+ prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
3053
+ path_match = re.match(prefix_pattern, path)
3054
+ if not path_match:
3055
+ return uri
2832
3056
 
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}")
3057
+ # Return the URI without the prefix
3058
+ return f"{protocol}{path_match.group(1)}"
2837
3059
 
2838
3060
 
2839
- def has_resource_prefix(
2840
- uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
2841
- ) -> bool:
3061
+ def has_resource_prefix(uri: str, prefix: str) -> bool:
2842
3062
  """Check if a resource URI has a specific prefix.
2843
3063
 
2844
3064
  Args:
@@ -2849,16 +3069,10 @@ def has_resource_prefix(
2849
3069
  True if the URI has the specified prefix, False otherwise
2850
3070
 
2851
3071
  Examples:
2852
- With new style:
2853
3072
  ```python
2854
3073
  has_resource_prefix("resource://prefix/path/to/resource", "prefix")
2855
3074
  True
2856
3075
  ```
2857
- With legacy style:
2858
- ```python
2859
- has_resource_prefix("prefix+resource://path/to/resource", "prefix")
2860
- True
2861
- ```
2862
3076
  With other path:
2863
3077
  ```python
2864
3078
  has_resource_prefix("resource://other/path/to/resource", "prefix")
@@ -2871,28 +3085,13 @@ def has_resource_prefix(
2871
3085
  if not prefix:
2872
3086
  return False
2873
3087
 
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
- )
3088
+ # Split the URI into protocol and path
3089
+ match = URI_PATTERN.match(uri)
3090
+ if not match:
3091
+ raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.")
2891
3092
 
2892
- _, path = match.groups()
3093
+ _, path = match.groups()
2893
3094
 
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}")
3095
+ # Check if the path starts with the prefix followed by a /
3096
+ prefix_pattern = f"^{re.escape(prefix)}/"
3097
+ return bool(re.match(prefix_pattern, path))