fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import warnings
4
5
  from collections.abc import Callable
5
6
  from contextvars import ContextVar
6
7
  from copy import deepcopy
@@ -13,7 +14,9 @@ from pydantic import ConfigDict
13
14
  from pydantic.fields import Field
14
15
  from pydantic.functional_validators import BeforeValidator
15
16
 
16
- from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content
17
+ import fastmcp
18
+ from fastmcp.tools.function_parsing import ParsedFunction
19
+ from fastmcp.tools.tool import Tool, ToolResult, _convert_to_content
17
20
  from fastmcp.utilities.components import _convert_set_default_none
18
21
  from fastmcp.utilities.json_schema import compress_schema
19
22
  from fastmcp.utilities.logging import get_logger
@@ -28,7 +31,7 @@ logger = get_logger(__name__)
28
31
 
29
32
 
30
33
  # Context variable to store current transformed tool
31
- _current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[assignment]
34
+ _current_tool: ContextVar[TransformedTool | None] = ContextVar(
32
35
  "_current_tool", default=None
33
36
  )
34
37
 
@@ -364,6 +367,7 @@ class TransformedTool(Tool):
364
367
  cls,
365
368
  tool: Tool,
366
369
  name: str | None = None,
370
+ version: str | NotSetT | None = NotSet,
367
371
  title: str | NotSetT | None = NotSet,
368
372
  description: str | NotSetT | None = NotSet,
369
373
  tags: set[str] | None = None,
@@ -371,9 +375,8 @@ class TransformedTool(Tool):
371
375
  transform_args: dict[str, ArgTransform] | None = None,
372
376
  annotations: ToolAnnotations | NotSetT | None = NotSet,
373
377
  output_schema: dict[str, Any] | NotSetT | None = NotSet,
374
- serializer: Callable[[Any], str] | NotSetT | None = NotSet,
378
+ serializer: Callable[[Any], str] | NotSetT | None = NotSet, # Deprecated
375
379
  meta: dict[str, Any] | NotSetT | None = NotSet,
376
- enabled: bool | None = None,
377
380
  ) -> TransformedTool:
378
381
  """Create a transformed tool from a parent tool.
379
382
 
@@ -383,6 +386,7 @@ class TransformedTool(Tool):
383
386
  to call the parent tool. Functions with **kwargs receive transformed
384
387
  argument names.
385
388
  name: New name for the tool. Defaults to parent tool's name.
389
+ version: New version for the tool. Defaults to parent tool's version.
386
390
  title: New title for the tool. Defaults to parent tool's title.
387
391
  transform_args: Optional transformations for parent tool arguments.
388
392
  Only specified arguments are transformed, others pass through unchanged:
@@ -396,7 +400,7 @@ class TransformedTool(Tool):
396
400
  - None (default): Inherit from transform_fn if available, then parent tool
397
401
  - dict: Use custom output schema
398
402
  - False: Disable output schema and structured outputs
399
- serializer: New serializer. Defaults to parent's serializer.
403
+ serializer: Deprecated. Return ToolResult from your tools for full control over serialization.
400
404
  meta: Control meta information:
401
405
  - NotSet (default): Inherit from parent tool
402
406
  - dict: Use custom meta information
@@ -449,6 +453,18 @@ class TransformedTool(Tool):
449
453
  )
450
454
  ```
451
455
  """
456
+ if (
457
+ serializer is not NotSet
458
+ and serializer is not None
459
+ and fastmcp.settings.deprecation_warnings
460
+ ):
461
+ warnings.warn(
462
+ "The `serializer` parameter is deprecated. "
463
+ "Return ToolResult from your tools for full control over serialization. "
464
+ "See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
465
+ DeprecationWarning,
466
+ stacklevel=2,
467
+ )
452
468
  transform_args = transform_args or {}
453
469
 
454
470
  if transform_fn is not None:
@@ -555,6 +571,7 @@ class TransformedTool(Tool):
555
571
  )
556
572
 
557
573
  final_name = name or tool.name
574
+ final_version = version if not isinstance(version, NotSetT) else tool.version
558
575
  final_description = (
559
576
  description if not isinstance(description, NotSetT) else tool.description
560
577
  )
@@ -566,13 +583,13 @@ class TransformedTool(Tool):
566
583
  final_serializer = (
567
584
  serializer if not isinstance(serializer, NotSetT) else tool.serializer
568
585
  )
569
- final_enabled = enabled if enabled is not None else tool.enabled
570
586
 
571
587
  transformed_tool = cls(
572
588
  fn=final_fn,
573
589
  forwarding_fn=forwarding_fn,
574
590
  parent_tool=tool,
575
591
  name=final_name,
592
+ version=final_version,
576
593
  title=final_title,
577
594
  description=final_description,
578
595
  parameters=final_schema,
@@ -582,7 +599,7 @@ class TransformedTool(Tool):
582
599
  serializer=final_serializer,
583
600
  meta=final_meta,
584
601
  transform_args=transform_args,
585
- enabled=final_enabled,
602
+ auth=tool.auth,
586
603
  )
587
604
 
588
605
  return transformed_tool
@@ -669,7 +686,7 @@ class TransformedTool(Tool):
669
686
 
670
687
  if parent_defs:
671
688
  schema["$defs"] = parent_defs
672
- schema = compress_schema(schema, prune_defs=True)
689
+ schema = compress_schema(schema)
673
690
 
674
691
  # Create forwarding function that closes over everything it needs
675
692
  async def _forward(**kwargs: Any):
@@ -852,7 +869,7 @@ class TransformedTool(Tool):
852
869
 
853
870
  if merged_defs:
854
871
  result["$defs"] = merged_defs
855
- result = compress_schema(result, prune_defs=True)
872
+ result = compress_schema(result)
856
873
 
857
874
  return result
858
875
 
@@ -879,7 +896,9 @@ class ToolTransformConfig(FastMCPBaseModel):
879
896
  """Provides a way to transform a tool."""
880
897
 
881
898
  name: str | None = Field(default=None, description="The new name for the tool.")
882
-
899
+ version: str | None = Field(
900
+ default=None, description="The new version for the tool."
901
+ )
883
902
  title: str | None = Field(
884
903
  default=None,
885
904
  description="The new title of the tool.",
@@ -897,11 +916,6 @@ class ToolTransformConfig(FastMCPBaseModel):
897
916
  description="The new meta information for the tool.",
898
917
  )
899
918
 
900
- enabled: bool = Field(
901
- default=True,
902
- description="Whether the tool is enabled.",
903
- )
904
-
905
919
  arguments: dict[str, ArgTransformConfig] = Field(
906
920
  default_factory=dict,
907
921
  description="A dictionary of argument transforms to apply to the tool.",
@@ -927,17 +941,20 @@ def apply_transformations_to_tools(
927
941
  ) -> dict[str, Tool]:
928
942
  """Apply a list of transformations to a list of tools. Tools that do not have any transformations
929
943
  are left unchanged.
944
+
945
+ Note: tools dict is keyed by prefixed key (e.g., "tool:my_tool"),
946
+ but transformations are keyed by tool name (e.g., "my_tool").
930
947
  """
931
948
 
932
949
  transformed_tools: dict[str, Tool] = {}
933
950
 
934
- for tool_name, tool in tools.items():
935
- if transformation := transformations.get(tool_name):
936
- transformed_tools[transformation.name or tool_name] = transformation.apply(
937
- tool
938
- )
951
+ for tool_key, tool in tools.items():
952
+ # Look up transformation by tool name, not prefixed key
953
+ if transformation := transformations.get(tool.name):
954
+ transformed = transformation.apply(tool)
955
+ transformed_tools[transformed.key] = transformed
939
956
  continue
940
957
 
941
- transformed_tools[tool_name] = tool
958
+ transformed_tools[tool_key] = tool
942
959
 
943
960
  return transformed_tools
@@ -0,0 +1,69 @@
1
+ """Async utilities for FastMCP."""
2
+
3
+ import functools
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Any, Literal, TypeVar, overload
6
+
7
+ import anyio
8
+ from anyio.to_thread import run_sync as run_sync_in_threadpool
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ async def call_sync_fn_in_threadpool(
14
+ fn: Callable[..., Any], *args: Any, **kwargs: Any
15
+ ) -> Any:
16
+ """Call a sync function in a threadpool to avoid blocking the event loop.
17
+
18
+ Uses anyio.to_thread.run_sync which properly propagates contextvars,
19
+ making this safe for functions that depend on context (like dependency injection).
20
+ """
21
+ return await run_sync_in_threadpool(functools.partial(fn, *args, **kwargs))
22
+
23
+
24
+ @overload
25
+ async def gather(
26
+ *awaitables: Awaitable[T],
27
+ return_exceptions: Literal[True],
28
+ ) -> list[T | BaseException]: ...
29
+
30
+
31
+ @overload
32
+ async def gather(
33
+ *awaitables: Awaitable[T],
34
+ return_exceptions: Literal[False] = ...,
35
+ ) -> list[T]: ...
36
+
37
+
38
+ async def gather(
39
+ *awaitables: Awaitable[T],
40
+ return_exceptions: bool = False,
41
+ ) -> list[T] | list[T | BaseException]:
42
+ """Run awaitables concurrently and return results in order.
43
+
44
+ Uses anyio TaskGroup for structured concurrency.
45
+
46
+ Args:
47
+ *awaitables: Awaitables to run concurrently
48
+ return_exceptions: If True, exceptions are returned in results.
49
+ If False, first exception cancels all and raises.
50
+
51
+ Returns:
52
+ List of results in the same order as input awaitables.
53
+ """
54
+ results: list[T | BaseException] = [None] * len(awaitables) # type: ignore[assignment]
55
+
56
+ async def run_at(i: int, aw: Awaitable[T]) -> None:
57
+ try:
58
+ results[i] = await aw
59
+ except BaseException as e:
60
+ if return_exceptions:
61
+ results[i] = e
62
+ else:
63
+ raise
64
+
65
+ async with anyio.create_task_group() as tg:
66
+ for i, aw in enumerate(awaitables):
67
+ tg.start_soon(run_at, i, aw)
68
+
69
+ return results
@@ -1,20 +1,37 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Sequence
4
- from typing import Annotated, Any, TypedDict, cast
4
+ from typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypedDict, cast
5
5
 
6
6
  from mcp.types import Icon
7
- from pydantic import BeforeValidator, Field, PrivateAttr
7
+ from pydantic import BeforeValidator, Field
8
8
  from typing_extensions import Self, TypeVar
9
9
 
10
- import fastmcp
10
+ from fastmcp.server.tasks.config import TaskConfig
11
11
  from fastmcp.utilities.types import FastMCPBaseModel
12
12
 
13
+ if TYPE_CHECKING:
14
+ from docket import Docket
15
+ from docket.execution import Execution
16
+
13
17
  T = TypeVar("T", default=Any)
14
18
 
15
19
 
16
20
  class FastMCPMeta(TypedDict, total=False):
17
21
  tags: list[str]
22
+ version: str
23
+ versions: list[str]
24
+
25
+
26
+ def get_fastmcp_metadata(meta: dict[str, Any] | None) -> FastMCPMeta:
27
+ """Extract FastMCP metadata from a component's meta dict.
28
+
29
+ Handles both the current `fastmcp` namespace and the legacy `_fastmcp`
30
+ namespace for compatibility with older FastMCP servers.
31
+ """
32
+ if not meta:
33
+ return {}
34
+ return cast(FastMCPMeta, meta.get("fastmcp") or meta.get("_fastmcp") or {})
18
35
 
19
36
 
20
37
  def _convert_set_default_none(maybe_set: set[T] | Sequence[T] | None) -> set[T]:
@@ -26,12 +43,47 @@ def _convert_set_default_none(maybe_set: set[T] | Sequence[T] | None) -> set[T]:
26
43
  return set(maybe_set)
27
44
 
28
45
 
46
+ def _coerce_version(v: str | int | None) -> str | None:
47
+ """Coerce version to string, accepting int or str.
48
+
49
+ Raises ValueError if version contains '@' (used as key delimiter).
50
+ """
51
+ if v is None:
52
+ return None
53
+ version = str(v)
54
+ if "@" in version:
55
+ raise ValueError(
56
+ f"Version string cannot contain '@' (used as key delimiter): {version!r}"
57
+ )
58
+ return version
59
+
60
+
29
61
  class FastMCPComponent(FastMCPBaseModel):
30
62
  """Base class for FastMCP tools, prompts, resources, and resource templates."""
31
63
 
64
+ KEY_PREFIX: ClassVar[str] = ""
65
+
66
+ def __init_subclass__(cls, **kwargs: Any) -> None:
67
+ super().__init_subclass__(**kwargs)
68
+ # Warn if a subclass doesn't define KEY_PREFIX (inherited or its own)
69
+ if not cls.KEY_PREFIX:
70
+ import warnings
71
+
72
+ warnings.warn(
73
+ f"{cls.__name__} does not define KEY_PREFIX. "
74
+ f"Component keys will not be type-prefixed, which may cause collisions.",
75
+ UserWarning,
76
+ stacklevel=2,
77
+ )
78
+
32
79
  name: str = Field(
33
80
  description="The name of the component.",
34
81
  )
82
+ version: Annotated[str | None, BeforeValidator(_coerce_version)] = Field(
83
+ default=None,
84
+ description="Optional version identifier for this component. "
85
+ "Multiple versions of the same component (same name) can coexist.",
86
+ )
35
87
  title: str | None = Field(
36
88
  default=None,
37
89
  description="The title of the component for display purposes.",
@@ -51,73 +103,68 @@ class FastMCPComponent(FastMCPBaseModel):
51
103
  meta: dict[str, Any] | None = Field(
52
104
  default=None, description="Meta information about the component"
53
105
  )
54
- enabled: bool = Field(
55
- default=True,
56
- description="Whether the component is enabled.",
57
- )
106
+ task_config: Annotated[
107
+ TaskConfig,
108
+ Field(description="Background task execution configuration (SEP-1686)."),
109
+ ] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
110
+
111
+ @classmethod
112
+ def make_key(cls, identifier: str) -> str:
113
+ """Construct the lookup key for this component type.
58
114
 
59
- _key: str | None = PrivateAttr()
115
+ Args:
116
+ identifier: The raw identifier (name for tools/prompts, uri for resources)
60
117
 
61
- def __init__(self, *, key: str | None = None, **kwargs: Any) -> None:
62
- super().__init__(**kwargs)
63
- self._key = key
118
+ Returns:
119
+ A prefixed key like "tool:name" or "resource:uri"
120
+ """
121
+ if cls.KEY_PREFIX:
122
+ return f"{cls.KEY_PREFIX}:{identifier}"
123
+ return identifier
64
124
 
65
125
  @property
66
126
  def key(self) -> str:
67
- """
68
- The key of the component. This is used for internal bookkeeping
69
- and may reflect e.g. prefixes or other identifiers. You should not depend on
70
- keys having a certain value, as the same tool loaded from different
71
- hierarchies of servers may have different keys.
72
- """
73
- return self._key or self.name
127
+ """The globally unique lookup key for this component.
74
128
 
75
- def get_meta(
76
- self, include_fastmcp_meta: bool | None = None
77
- ) -> dict[str, Any] | None:
78
- """
79
- Get the meta information about the component.
129
+ Format: "{key_prefix}:{identifier}@{version}" or "{key_prefix}:{identifier}@"
130
+ e.g. "tool:my_tool@v2", "tool:my_tool@", "resource:file://x.txt@"
80
131
 
81
- If include_fastmcp_meta is True, a `_fastmcp` key will be added to the
82
- meta, containing a `tags` field with the tags of the component.
132
+ The @ suffix is ALWAYS present to enable unambiguous parsing of keys
133
+ (URIs may contain @ characters, so we always include the delimiter).
134
+
135
+ Subclasses should override this to use their specific identifier.
136
+ Base implementation uses name.
83
137
  """
138
+ base_key = self.make_key(self.name)
139
+ return f"{base_key}@{self.version or ''}"
84
140
 
85
- if include_fastmcp_meta is None:
86
- include_fastmcp_meta = fastmcp.settings.include_fastmcp_meta
141
+ def get_meta(self) -> dict[str, Any]:
142
+ """Get the meta information about the component.
87
143
 
88
- meta = self.meta or {}
144
+ Returns a dict that always includes a `fastmcp` key containing:
145
+ - `tags`: sorted list of component tags
146
+ - `version`: component version (only if set)
89
147
 
90
- if include_fastmcp_meta:
91
- fastmcp_meta = FastMCPMeta(tags=sorted(self.tags))
92
- # overwrite any existing _fastmcp meta with keys from the new one
93
- if upstream_meta := meta.get("_fastmcp"):
94
- fastmcp_meta = upstream_meta | fastmcp_meta
95
- meta["_fastmcp"] = fastmcp_meta
148
+ Internal keys (prefixed with `_`) are stripped from the fastmcp namespace.
149
+ """
150
+ meta = dict(self.meta) if self.meta else {}
96
151
 
97
- return meta or None
152
+ fastmcp_meta: FastMCPMeta = {"tags": sorted(self.tags)}
153
+ if self.version is not None:
154
+ fastmcp_meta["version"] = self.version
98
155
 
99
- def model_copy( # type: ignore[override]
100
- self,
101
- *,
102
- update: dict[str, Any] | None = None,
103
- deep: bool = False,
104
- key: str | None = None,
105
- ) -> Self:
106
- """
107
- Create a copy of the component.
156
+ # Merge with upstream fastmcp meta, stripping internal keys
157
+ if (upstream_meta := meta.get("fastmcp")) is not None:
158
+ if not isinstance(upstream_meta, dict):
159
+ raise TypeError("meta['fastmcp'] must be a dict")
160
+ # Filter out internal keys (e.g., _internal used for enabled state)
161
+ public_upstream = {
162
+ k: v for k, v in upstream_meta.items() if not k.startswith("_")
163
+ }
164
+ fastmcp_meta = cast(FastMCPMeta, public_upstream | fastmcp_meta)
165
+ meta["fastmcp"] = fastmcp_meta
108
166
 
109
- Args:
110
- update: A dictionary of fields to update.
111
- deep: Whether to deep copy the component.
112
- key: The key to use for the copy.
113
- """
114
- # `model_copy` has an `update` parameter but it doesn't work for certain private attributes
115
- # https://github.com/pydantic/pydantic/issues/12116
116
- # So we manually set the private attribute here instead, such as _key
117
- copy = super().model_copy(update=update, deep=deep)
118
- if key is not None:
119
- copy._key = key
120
- return cast(Self, copy)
167
+ return meta
121
168
 
122
169
  def __eq__(self, other: object) -> bool:
123
170
  if type(self) is not type(other):
@@ -127,55 +174,69 @@ class FastMCPComponent(FastMCPBaseModel):
127
174
  return self.model_dump() == other.model_dump()
128
175
 
129
176
  def __repr__(self) -> str:
130
- return f"{self.__class__.__name__}(name={self.name!r}, title={self.title!r}, description={self.description!r}, tags={self.tags}, enabled={self.enabled})"
177
+ parts = [f"name={self.name!r}"]
178
+ if self.version:
179
+ parts.append(f"version={self.version!r}")
180
+ parts.extend(
181
+ [
182
+ f"title={self.title!r}",
183
+ f"description={self.description!r}",
184
+ f"tags={self.tags}",
185
+ ]
186
+ )
187
+ return f"{self.__class__.__name__}({', '.join(parts)})"
131
188
 
132
189
  def enable(self) -> None:
133
- """Enable the component."""
134
- self.enabled = True
190
+ """Removed in 3.0. Use server.enable(keys=[...]) instead."""
191
+ raise NotImplementedError(
192
+ f"Component.enable() was removed in FastMCP 3.0. "
193
+ f"Use server.enable(keys=['{self.key}']) instead."
194
+ )
135
195
 
136
196
  def disable(self) -> None:
137
- """Disable the component."""
138
- self.enabled = False
197
+ """Removed in 3.0. Use server.disable(keys=[...]) instead."""
198
+ raise NotImplementedError(
199
+ f"Component.disable() was removed in FastMCP 3.0. "
200
+ f"Use server.disable(keys=['{self.key}']) instead."
201
+ )
139
202
 
140
203
  def copy(self) -> Self: # type: ignore[override]
141
204
  """Create a copy of the component."""
142
205
  return self.model_copy()
143
206
 
207
+ def register_with_docket(self, docket: Docket) -> None:
208
+ """Register this component with docket for background execution.
144
209
 
145
- class MirroredComponent(FastMCPComponent):
146
- """Base class for components that are mirrored from a remote server.
147
-
148
- Mirrored components cannot be enabled or disabled directly. Call copy() first
149
- to create a local version you can modify.
150
- """
210
+ No-ops if task_config.mode is "forbidden". Subclasses override to
211
+ register their callable (self.run, self.read, self.render, or self.fn).
212
+ """
213
+ # Base implementation: no-op (subclasses override)
151
214
 
152
- _mirrored: bool = PrivateAttr(default=False)
215
+ async def add_to_docket(
216
+ self, docket: Docket, *args: Any, **kwargs: Any
217
+ ) -> Execution:
218
+ """Schedule this component for background execution via docket.
153
219
 
154
- def __init__(self, *, _mirrored: bool = False, **kwargs: Any) -> None:
155
- super().__init__(**kwargs)
156
- self._mirrored = _mirrored
220
+ Subclasses override this to handle their specific calling conventions:
221
+ - Tool: add_to_docket(docket, arguments: dict, **kwargs)
222
+ - Resource: add_to_docket(docket, **kwargs)
223
+ - ResourceTemplate: add_to_docket(docket, params: dict, **kwargs)
224
+ - Prompt: add_to_docket(docket, arguments: dict | None, **kwargs)
157
225
 
158
- def enable(self) -> None:
159
- """Enable the component."""
160
- if self._mirrored:
226
+ The **kwargs are passed through to docket.add() (e.g., key=task_key).
227
+ """
228
+ if not self.task_config.supports_tasks():
161
229
  raise RuntimeError(
162
- f"Cannot enable mirrored component '{self.name}'. "
163
- f"Create a local copy first with {self.name}.copy() and add it to your server."
230
+ f"Cannot add {self.__class__.__name__} '{self.name}' to docket: "
231
+ f"task execution not supported"
164
232
  )
165
- super().enable()
233
+ raise NotImplementedError(
234
+ f"{self.__class__.__name__} does not implement add_to_docket()"
235
+ )
166
236
 
167
- def disable(self) -> None:
168
- """Disable the component."""
169
- if self._mirrored:
170
- raise RuntimeError(
171
- f"Cannot disable mirrored component '{self.name}'. "
172
- f"Create a local copy first with {self.name}.copy() and add it to your server."
173
- )
174
- super().disable()
237
+ def get_span_attributes(self) -> dict[str, Any]:
238
+ """Return span attributes for telemetry.
175
239
 
176
- def copy(self) -> Self: # type: ignore[override]
177
- """Create a copy of the component that can be modified."""
178
- # Create a copy and mark it as not mirrored
179
- copied = self.model_copy()
180
- copied._mirrored = False
181
- return copied
240
+ Subclasses should call super() and merge their specific attributes.
241
+ """
242
+ return {"fastmcp.component.key": self.key}