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.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fastmcp/tools/tool_transform.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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[
|
|
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
|
fastmcp/utilities/components.py
CHANGED
|
@@ -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
|
|
7
|
+
from pydantic import BeforeValidator, Field
|
|
8
8
|
from typing_extensions import Self, TypeVar
|
|
9
9
|
|
|
10
|
-
import
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
description="
|
|
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
|
-
|
|
115
|
+
Args:
|
|
116
|
+
identifier: The raw identifier (name for tools/prompts, uri for resources)
|
|
60
117
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
141
|
+
def get_meta(self) -> dict[str, Any]:
|
|
142
|
+
"""Get the meta information about the component.
|
|
87
143
|
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
152
|
+
fastmcp_meta: FastMCPMeta = {"tags": sorted(self.tags)}
|
|
153
|
+
if self.version is not None:
|
|
154
|
+
fastmcp_meta["version"] = self.version
|
|
98
155
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
134
|
-
|
|
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
|
-
"""
|
|
138
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
"""
|
|
160
|
-
if self.
|
|
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
|
|
163
|
-
f"
|
|
230
|
+
f"Cannot add {self.__class__.__name__} '{self.name}' to docket: "
|
|
231
|
+
f"task execution not supported"
|
|
164
232
|
)
|
|
165
|
-
|
|
233
|
+
raise NotImplementedError(
|
|
234
|
+
f"{self.__class__.__name__} does not implement add_to_docket()"
|
|
235
|
+
)
|
|
166
236
|
|
|
167
|
-
def
|
|
168
|
-
"""
|
|
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
|
-
|
|
177
|
-
"""
|
|
178
|
-
|
|
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}
|