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/server/tasks/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ This module implements protocol-level background task execution for MCP servers.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from fastmcp.server.tasks.capabilities import get_task_capabilities
|
|
7
|
-
from fastmcp.server.tasks.config import TaskConfig, TaskMode
|
|
7
|
+
from fastmcp.server.tasks.config import TaskConfig, TaskMeta, TaskMode
|
|
8
8
|
from fastmcp.server.tasks.keys import (
|
|
9
9
|
build_task_key,
|
|
10
10
|
get_client_task_id_from_key,
|
|
@@ -13,6 +13,7 @@ from fastmcp.server.tasks.keys import (
|
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
15
|
"TaskConfig",
|
|
16
|
+
"TaskMeta",
|
|
16
17
|
"TaskMode",
|
|
17
18
|
"build_task_key",
|
|
18
19
|
"get_client_task_id_from_key",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""SEP-1686 task capabilities declaration."""
|
|
2
2
|
|
|
3
|
+
from importlib.util import find_spec
|
|
4
|
+
|
|
3
5
|
from mcp.types import (
|
|
4
6
|
ServerTasksCapability,
|
|
5
7
|
ServerTasksRequestsCapability,
|
|
@@ -10,15 +12,25 @@ from mcp.types import (
|
|
|
10
12
|
)
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
def
|
|
15
|
+
def _is_docket_available() -> bool:
|
|
16
|
+
"""Check if pydocket is installed (local to avoid circular import)."""
|
|
17
|
+
return find_spec("docket") is not None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_task_capabilities() -> ServerTasksCapability | None:
|
|
14
21
|
"""Return the SEP-1686 task capabilities.
|
|
15
22
|
|
|
16
23
|
Returns task capabilities as a first-class ServerCapabilities field,
|
|
17
24
|
declaring support for list, cancel, and request operations per SEP-1686.
|
|
18
25
|
|
|
26
|
+
Returns None if pydocket is not installed (no task support).
|
|
27
|
+
|
|
19
28
|
Note: prompts/resources are passed via extra_data since the SDK types
|
|
20
29
|
don't include them yet (FastMCP supports them ahead of the spec).
|
|
21
30
|
"""
|
|
31
|
+
if not _is_docket_available():
|
|
32
|
+
return None
|
|
33
|
+
|
|
22
34
|
return ServerTasksCapability(
|
|
23
35
|
list=TasksListCapability(),
|
|
24
36
|
cancel=TasksCancelCapability(),
|
fastmcp/server/tasks/config.py
CHANGED
|
@@ -7,13 +7,36 @@ handle task-augmented execution as specified in SEP-1686.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import inspect
|
|
10
|
+
import warnings
|
|
10
11
|
from collections.abc import Callable
|
|
11
12
|
from dataclasses import dataclass
|
|
13
|
+
from datetime import timedelta
|
|
12
14
|
from typing import Any, Literal
|
|
13
15
|
|
|
14
16
|
# Task execution modes per SEP-1686 / MCP ToolExecution.taskSupport
|
|
15
17
|
TaskMode = Literal["forbidden", "optional", "required"]
|
|
16
18
|
|
|
19
|
+
# Default values for task metadata (single source of truth)
|
|
20
|
+
DEFAULT_POLL_INTERVAL = timedelta(seconds=5) # Default poll interval
|
|
21
|
+
DEFAULT_POLL_INTERVAL_MS = int(DEFAULT_POLL_INTERVAL.total_seconds() * 1000)
|
|
22
|
+
DEFAULT_TTL_MS = 60_000 # Default TTL in milliseconds
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TaskMeta:
|
|
27
|
+
"""Metadata for task-augmented execution requests.
|
|
28
|
+
|
|
29
|
+
When passed to call_tool/read_resource/get_prompt, signals that
|
|
30
|
+
the operation should be submitted as a background task.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
ttl: Client-requested TTL in milliseconds. If None, uses server default.
|
|
34
|
+
fn_key: Docket routing key. Auto-derived from component name if None.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
ttl: int | None = None
|
|
38
|
+
fn_key: str | None = None
|
|
39
|
+
|
|
17
40
|
|
|
18
41
|
@dataclass
|
|
19
42
|
class TaskConfig:
|
|
@@ -28,6 +51,11 @@ class TaskConfig:
|
|
|
28
51
|
- "required": Component requires task execution. Clients must request task
|
|
29
52
|
augmentation; server returns -32601 if they don't.
|
|
30
53
|
|
|
54
|
+
Important:
|
|
55
|
+
Task-enabled components must be available at server startup to be
|
|
56
|
+
registered with all Docket workers. Components added dynamically after
|
|
57
|
+
startup will not be registered for background execution.
|
|
58
|
+
|
|
31
59
|
Example:
|
|
32
60
|
```python
|
|
33
61
|
from fastmcp import FastMCP
|
|
@@ -46,6 +74,7 @@ class TaskConfig:
|
|
|
46
74
|
"""
|
|
47
75
|
|
|
48
76
|
mode: TaskMode = "optional"
|
|
77
|
+
poll_interval: timedelta = DEFAULT_POLL_INTERVAL
|
|
49
78
|
|
|
50
79
|
@classmethod
|
|
51
80
|
def from_bool(cls, value: bool) -> TaskConfig:
|
|
@@ -59,22 +88,41 @@ class TaskConfig:
|
|
|
59
88
|
"""
|
|
60
89
|
return cls(mode="optional" if value else "forbidden")
|
|
61
90
|
|
|
91
|
+
def supports_tasks(self) -> bool:
|
|
92
|
+
"""Check if this component supports task execution.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if mode is "optional" or "required", False if "forbidden".
|
|
96
|
+
"""
|
|
97
|
+
return self.mode != "forbidden"
|
|
98
|
+
|
|
62
99
|
def validate_function(self, fn: Callable[..., Any], name: str) -> None:
|
|
63
100
|
"""Validate that function is compatible with this task config.
|
|
64
101
|
|
|
65
|
-
Task execution requires
|
|
66
|
-
|
|
102
|
+
Task execution requires:
|
|
103
|
+
1. fastmcp[tasks] to be installed (pydocket)
|
|
104
|
+
2. Async functions
|
|
105
|
+
|
|
106
|
+
Raises ImportError if mode is "optional" or "required" but pydocket
|
|
107
|
+
is not installed. Raises ValueError if function is synchronous.
|
|
67
108
|
|
|
68
109
|
Args:
|
|
69
110
|
fn: The function to validate (handles callable classes and staticmethods).
|
|
70
111
|
name: Name for error messages.
|
|
71
112
|
|
|
72
113
|
Raises:
|
|
114
|
+
ImportError: If task execution is enabled but pydocket not installed.
|
|
73
115
|
ValueError: If task execution is enabled but function is sync.
|
|
74
116
|
"""
|
|
75
|
-
if self.
|
|
117
|
+
if not self.supports_tasks():
|
|
76
118
|
return
|
|
77
119
|
|
|
120
|
+
# Check that docket is available for task execution
|
|
121
|
+
# Lazy import to avoid circular: dependencies.py → http.py → tasks/__init__.py → config.py
|
|
122
|
+
from fastmcp.server.dependencies import require_docket
|
|
123
|
+
|
|
124
|
+
require_docket(f"`task=True` on function '{name}'")
|
|
125
|
+
|
|
78
126
|
# Unwrap callable classes and staticmethods
|
|
79
127
|
fn_to_check = fn
|
|
80
128
|
if not inspect.isroutine(fn) and callable(fn):
|
|
@@ -87,3 +135,18 @@ class TaskConfig:
|
|
|
87
135
|
f"'{name}' uses a sync function but has task execution enabled. "
|
|
88
136
|
"Background tasks require async functions."
|
|
89
137
|
)
|
|
138
|
+
|
|
139
|
+
# Warn if function uses Context - it won't be available in workers
|
|
140
|
+
from fastmcp.server.context import Context
|
|
141
|
+
from fastmcp.utilities.types import find_kwarg_by_type
|
|
142
|
+
|
|
143
|
+
context_kwarg = find_kwarg_by_type(fn_to_check, Context)
|
|
144
|
+
if context_kwarg:
|
|
145
|
+
warnings.warn(
|
|
146
|
+
f"'{name}' uses Context but has task execution enabled. "
|
|
147
|
+
"Context is not available in background task workers because "
|
|
148
|
+
"there is no active MCP session. Consider using Docket dependencies "
|
|
149
|
+
"like Progress() instead for worker-compatible functionality.",
|
|
150
|
+
UserWarning,
|
|
151
|
+
stacklevel=4,
|
|
152
|
+
)
|
fastmcp/server/tasks/handlers.py
CHANGED
|
@@ -8,55 +8,66 @@ from __future__ import annotations
|
|
|
8
8
|
import uuid
|
|
9
9
|
from contextlib import suppress
|
|
10
10
|
from datetime import datetime, timezone
|
|
11
|
-
from typing import TYPE_CHECKING, Any
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
12
12
|
|
|
13
13
|
import mcp.types
|
|
14
14
|
from mcp.shared.exceptions import McpError
|
|
15
15
|
from mcp.types import INTERNAL_ERROR, ErrorData
|
|
16
16
|
|
|
17
17
|
from fastmcp.server.dependencies import _current_docket, get_context
|
|
18
|
+
from fastmcp.server.tasks.config import TaskMeta
|
|
18
19
|
from fastmcp.server.tasks.keys import build_task_key
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
|
-
from fastmcp.
|
|
22
|
+
from fastmcp.prompts.prompt import Prompt
|
|
23
|
+
from fastmcp.resources.resource import Resource
|
|
24
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
25
|
+
from fastmcp.tools.tool import Tool
|
|
22
26
|
|
|
23
27
|
# Redis mapping TTL buffer: Add 15 minutes to Docket's execution_ttl
|
|
24
28
|
TASK_MAPPING_TTL_BUFFER_SECONDS = 15 * 60
|
|
25
29
|
|
|
26
30
|
|
|
27
|
-
async def
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
async def submit_to_docket(
|
|
32
|
+
task_type: Literal["tool", "resource", "template", "prompt"],
|
|
33
|
+
key: str,
|
|
34
|
+
component: Tool | Resource | ResourceTemplate | Prompt,
|
|
35
|
+
arguments: dict[str, Any] | None = None,
|
|
36
|
+
task_meta: TaskMeta | None = None,
|
|
37
|
+
) -> mcp.types.CreateTaskResult:
|
|
38
|
+
"""Submit any component to Docket for background execution (SEP-1686).
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
Unified handler for all component types. Called by component's internal
|
|
41
|
+
methods (_run, _read, _render) when task metadata is present and mode allows.
|
|
42
|
+
|
|
43
|
+
Queues the component's method to Docket, stores raw return values,
|
|
44
|
+
and converts to MCP types on retrieval.
|
|
37
45
|
|
|
38
46
|
Args:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
task_type: Component type for task key construction
|
|
48
|
+
key: The component key as seen by MCP layer (with namespace prefix)
|
|
49
|
+
component: The component instance (Tool, Resource, ResourceTemplate, Prompt)
|
|
50
|
+
arguments: Arguments/params (None for Resource which has no args)
|
|
51
|
+
task_meta: Task execution metadata. If task_meta.ttl is provided, it
|
|
52
|
+
overrides the server default (docket.execution_ttl).
|
|
43
53
|
|
|
44
54
|
Returns:
|
|
45
|
-
|
|
55
|
+
CreateTaskResult: Task stub with proper Task object
|
|
46
56
|
"""
|
|
47
57
|
# Generate server-side task ID per SEP-1686 final spec (line 375-377)
|
|
48
58
|
# Server MUST generate task IDs, clients no longer provide them
|
|
49
59
|
server_task_id = str(uuid.uuid4())
|
|
50
60
|
|
|
51
61
|
# Record creation timestamp per SEP-1686 final spec (line 430)
|
|
52
|
-
|
|
53
|
-
created_at = datetime.now(timezone.utc).isoformat()
|
|
62
|
+
created_at = datetime.now(timezone.utc)
|
|
54
63
|
|
|
55
|
-
# Get session ID
|
|
64
|
+
# Get session ID - use "internal" for programmatic calls without MCP session
|
|
56
65
|
ctx = get_context()
|
|
57
|
-
|
|
66
|
+
try:
|
|
67
|
+
session_id = ctx.session_id
|
|
68
|
+
except RuntimeError:
|
|
69
|
+
session_id = "internal"
|
|
58
70
|
|
|
59
|
-
# Get Docket from ContextVar (set by Context.__aenter__ at request time)
|
|
60
71
|
docket = _current_docket.get()
|
|
61
72
|
if docket is None:
|
|
62
73
|
raise McpError(
|
|
@@ -67,22 +78,28 @@ async def handle_tool_as_task(
|
|
|
67
78
|
)
|
|
68
79
|
|
|
69
80
|
# Build full task key with embedded metadata
|
|
70
|
-
task_key = build_task_key(session_id, server_task_id,
|
|
81
|
+
task_key = build_task_key(session_id, server_task_id, task_type, key)
|
|
71
82
|
|
|
72
|
-
#
|
|
73
|
-
|
|
83
|
+
# Determine TTL: use task_meta.ttl if provided, else docket default
|
|
84
|
+
if task_meta is not None and task_meta.ttl is not None:
|
|
85
|
+
ttl_ms = task_meta.ttl
|
|
86
|
+
else:
|
|
87
|
+
ttl_ms = int(docket.execution_ttl.total_seconds() * 1000)
|
|
88
|
+
ttl_seconds = int(ttl_ms / 1000) + TASK_MAPPING_TTL_BUFFER_SECONDS
|
|
74
89
|
|
|
75
|
-
# Store task
|
|
90
|
+
# Store task metadata in Redis for protocol handlers
|
|
76
91
|
task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
|
|
77
92
|
created_at_key = docket.key(
|
|
78
93
|
f"fastmcp:task:{session_id}:{server_task_id}:created_at"
|
|
79
94
|
)
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
poll_interval_key = docket.key(
|
|
96
|
+
f"fastmcp:task:{session_id}:{server_task_id}:poll_interval"
|
|
82
97
|
)
|
|
98
|
+
poll_interval_ms = int(component.task_config.poll_interval.total_seconds() * 1000)
|
|
83
99
|
async with docket.redis() as redis:
|
|
84
100
|
await redis.set(task_meta_key, task_key, ex=ttl_seconds)
|
|
85
|
-
await redis.set(created_at_key, created_at, ex=ttl_seconds)
|
|
101
|
+
await redis.set(created_at_key, created_at.isoformat(), ex=ttl_seconds)
|
|
102
|
+
await redis.set(poll_interval_key, str(poll_interval_ms), ex=ttl_seconds)
|
|
86
103
|
|
|
87
104
|
# Send notifications/tasks/created per SEP-1686 (mandatory)
|
|
88
105
|
# Send BEFORE queuing to avoid race where task completes before notification
|
|
@@ -90,257 +107,32 @@ async def handle_tool_as_task(
|
|
|
90
107
|
jsonrpc="2.0",
|
|
91
108
|
method="notifications/tasks/created",
|
|
92
109
|
params={}, # Empty params per spec
|
|
93
|
-
_meta={ #
|
|
110
|
+
_meta={ # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
|
94
111
|
"modelcontextprotocol.io/related-task": {
|
|
95
112
|
"taskId": server_task_id,
|
|
96
113
|
}
|
|
97
114
|
},
|
|
98
115
|
)
|
|
99
|
-
|
|
100
|
-
ctx = get_context()
|
|
101
116
|
with suppress(Exception):
|
|
102
117
|
# Don't let notification failures break task creation
|
|
103
118
|
await ctx.session.send_notification(notification) # type: ignore[arg-type]
|
|
104
119
|
|
|
105
|
-
# Queue function to Docket by
|
|
106
|
-
# Use
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
# Spawn subscription task to send status notifications (SEP-1686 optional feature)
|
|
113
|
-
from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
|
|
114
|
-
|
|
115
|
-
# Start subscription in session's task group (persists for connection lifetime)
|
|
116
|
-
if hasattr(ctx.session, "_subscription_task_group"):
|
|
117
|
-
tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
|
|
118
|
-
if tg:
|
|
119
|
-
tg.start_soon( # type: ignore[union-attr]
|
|
120
|
-
subscribe_to_task_updates,
|
|
121
|
-
server_task_id,
|
|
122
|
-
task_key,
|
|
123
|
-
ctx.session,
|
|
124
|
-
docket,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
# Return task stub
|
|
128
|
-
# Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
|
|
129
|
-
return mcp.types.CallToolResult(
|
|
130
|
-
content=[],
|
|
131
|
-
_meta={
|
|
132
|
-
"modelcontextprotocol.io/task": {
|
|
133
|
-
"taskId": server_task_id,
|
|
134
|
-
"status": "working",
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
async def handle_prompt_as_task(
|
|
141
|
-
server: FastMCP,
|
|
142
|
-
prompt_name: str,
|
|
143
|
-
arguments: dict[str, Any] | None,
|
|
144
|
-
task_meta: dict[str, Any],
|
|
145
|
-
) -> mcp.types.GetPromptResult:
|
|
146
|
-
"""Handle prompt execution as background task (SEP-1686).
|
|
147
|
-
|
|
148
|
-
Queues the user's actual function to Docket (preserving signature for DI).
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
server: FastMCP server instance
|
|
152
|
-
prompt_name: Name of the prompt to execute
|
|
153
|
-
arguments: Prompt arguments
|
|
154
|
-
task_meta: Task metadata from request (contains ttl)
|
|
155
|
-
|
|
156
|
-
Returns:
|
|
157
|
-
GetPromptResult: Task stub with task metadata in _meta
|
|
158
|
-
"""
|
|
159
|
-
# Generate server-side task ID per SEP-1686 final spec (line 375-377)
|
|
160
|
-
# Server MUST generate task IDs, clients no longer provide them
|
|
161
|
-
server_task_id = str(uuid.uuid4())
|
|
162
|
-
|
|
163
|
-
# Record creation timestamp per SEP-1686 final spec (line 430)
|
|
164
|
-
# Format as ISO 8601 / RFC 3339 timestamp
|
|
165
|
-
created_at = datetime.now(timezone.utc).isoformat()
|
|
166
|
-
|
|
167
|
-
# Get session ID and Docket
|
|
168
|
-
ctx = get_context()
|
|
169
|
-
session_id = ctx.session_id
|
|
170
|
-
|
|
171
|
-
# Get Docket from ContextVar (set by Context.__aenter__ at request time)
|
|
172
|
-
docket = _current_docket.get()
|
|
173
|
-
if docket is None:
|
|
174
|
-
raise McpError(
|
|
175
|
-
ErrorData(
|
|
176
|
-
code=INTERNAL_ERROR,
|
|
177
|
-
message="Background tasks require a running FastMCP server context",
|
|
178
|
-
)
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
# Build full task key with embedded metadata
|
|
182
|
-
task_key = build_task_key(session_id, server_task_id, "prompt", prompt_name)
|
|
183
|
-
|
|
184
|
-
# Get the prompt
|
|
185
|
-
prompt = await server.get_prompt(prompt_name)
|
|
186
|
-
|
|
187
|
-
# Store task key mapping and creation timestamp in Redis for protocol handlers
|
|
188
|
-
task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
|
|
189
|
-
created_at_key = docket.key(
|
|
190
|
-
f"fastmcp:task:{session_id}:{server_task_id}:created_at"
|
|
191
|
-
)
|
|
192
|
-
ttl_seconds = int(
|
|
193
|
-
docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
|
|
194
|
-
)
|
|
195
|
-
async with docket.redis() as redis:
|
|
196
|
-
await redis.set(task_meta_key, task_key, ex=ttl_seconds)
|
|
197
|
-
await redis.set(created_at_key, created_at, ex=ttl_seconds)
|
|
198
|
-
|
|
199
|
-
# Send notifications/tasks/created per SEP-1686 (mandatory)
|
|
200
|
-
# Send BEFORE queuing to avoid race where task completes before notification
|
|
201
|
-
notification = mcp.types.JSONRPCNotification(
|
|
202
|
-
jsonrpc="2.0",
|
|
203
|
-
method="notifications/tasks/created",
|
|
204
|
-
params={},
|
|
205
|
-
_meta={
|
|
206
|
-
"modelcontextprotocol.io/related-task": {
|
|
207
|
-
"taskId": server_task_id,
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
)
|
|
211
|
-
with suppress(Exception):
|
|
212
|
-
await ctx.session.send_notification(notification) # type: ignore[arg-type]
|
|
213
|
-
|
|
214
|
-
# Queue function to Docket by name (result storage via execution_ttl)
|
|
215
|
-
# Use prompt.key which matches what was registered - prefixed for mounted prompts
|
|
216
|
-
await docket.add(
|
|
217
|
-
prompt.key,
|
|
218
|
-
key=task_key,
|
|
219
|
-
)(**(arguments or {}))
|
|
220
|
-
|
|
221
|
-
# Spawn subscription task to send status notifications (SEP-1686 optional feature)
|
|
222
|
-
from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
|
|
223
|
-
|
|
224
|
-
# Start subscription in session's task group (persists for connection lifetime)
|
|
225
|
-
if hasattr(ctx.session, "_subscription_task_group"):
|
|
226
|
-
tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
|
|
227
|
-
if tg:
|
|
228
|
-
tg.start_soon( # type: ignore[union-attr]
|
|
229
|
-
subscribe_to_task_updates,
|
|
230
|
-
server_task_id,
|
|
231
|
-
task_key,
|
|
232
|
-
ctx.session,
|
|
233
|
-
docket,
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
# Return task stub
|
|
237
|
-
# Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
|
|
238
|
-
return mcp.types.GetPromptResult(
|
|
239
|
-
description="",
|
|
240
|
-
messages=[],
|
|
241
|
-
_meta={
|
|
242
|
-
"modelcontextprotocol.io/task": {
|
|
243
|
-
"taskId": server_task_id,
|
|
244
|
-
"status": "working",
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
async def handle_resource_as_task(
|
|
251
|
-
server: FastMCP,
|
|
252
|
-
uri: str,
|
|
253
|
-
resource, # Resource or ResourceTemplate
|
|
254
|
-
task_meta: dict[str, Any],
|
|
255
|
-
) -> mcp.types.ServerResult:
|
|
256
|
-
"""Handle resource read as background task (SEP-1686).
|
|
257
|
-
|
|
258
|
-
Queues the user's actual function to Docket.
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
server: FastMCP server instance
|
|
262
|
-
uri: Resource URI
|
|
263
|
-
resource: Resource or ResourceTemplate object
|
|
264
|
-
task_meta: Task metadata from request (contains ttl)
|
|
265
|
-
|
|
266
|
-
Returns:
|
|
267
|
-
ServerResult with ReadResourceResult stub
|
|
268
|
-
"""
|
|
269
|
-
# Generate server-side task ID per SEP-1686 final spec (line 375-377)
|
|
270
|
-
# Server MUST generate task IDs, clients no longer provide them
|
|
271
|
-
server_task_id = str(uuid.uuid4())
|
|
272
|
-
|
|
273
|
-
# Record creation timestamp per SEP-1686 final spec (line 430)
|
|
274
|
-
# Format as ISO 8601 / RFC 3339 timestamp
|
|
275
|
-
created_at = datetime.now(timezone.utc).isoformat()
|
|
276
|
-
|
|
277
|
-
# Get session ID and Docket
|
|
278
|
-
ctx = get_context()
|
|
279
|
-
session_id = ctx.session_id
|
|
280
|
-
|
|
281
|
-
# Get Docket from ContextVar (set by Context.__aenter__ at request time)
|
|
282
|
-
docket = _current_docket.get()
|
|
283
|
-
if docket is None:
|
|
284
|
-
raise McpError(
|
|
285
|
-
ErrorData(
|
|
286
|
-
code=INTERNAL_ERROR,
|
|
287
|
-
message="Background tasks require a running FastMCP server context",
|
|
288
|
-
)
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
# Build full task key with embedded metadata (use original URI)
|
|
292
|
-
task_key = build_task_key(session_id, server_task_id, "resource", str(uri))
|
|
293
|
-
|
|
294
|
-
# Store task key mapping and creation timestamp in Redis for protocol handlers
|
|
295
|
-
task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
|
|
296
|
-
created_at_key = docket.key(
|
|
297
|
-
f"fastmcp:task:{session_id}:{server_task_id}:created_at"
|
|
298
|
-
)
|
|
299
|
-
ttl_seconds = int(
|
|
300
|
-
docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
|
|
301
|
-
)
|
|
302
|
-
async with docket.redis() as redis:
|
|
303
|
-
await redis.set(task_meta_key, task_key, ex=ttl_seconds)
|
|
304
|
-
await redis.set(created_at_key, created_at, ex=ttl_seconds)
|
|
305
|
-
|
|
306
|
-
# Send notifications/tasks/created per SEP-1686 (mandatory)
|
|
307
|
-
# Send BEFORE queuing to avoid race where task completes before notification
|
|
308
|
-
notification = mcp.types.JSONRPCNotification(
|
|
309
|
-
jsonrpc="2.0",
|
|
310
|
-
method="notifications/tasks/created",
|
|
311
|
-
params={},
|
|
312
|
-
_meta={
|
|
313
|
-
"modelcontextprotocol.io/related-task": {
|
|
314
|
-
"taskId": server_task_id,
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
|
-
)
|
|
318
|
-
with suppress(Exception):
|
|
319
|
-
await ctx.session.send_notification(notification) # type: ignore[arg-type]
|
|
320
|
-
|
|
321
|
-
# Queue function to Docket by name (result storage via execution_ttl)
|
|
322
|
-
# Use resource.name which matches what was registered - prefixed for mounted resources
|
|
323
|
-
# For templates, extract URI params and pass them to the function
|
|
324
|
-
from fastmcp.resources.template import FunctionResourceTemplate, match_uri_template
|
|
325
|
-
|
|
326
|
-
if isinstance(resource, FunctionResourceTemplate):
|
|
327
|
-
params = match_uri_template(uri, resource.uri_template) or {}
|
|
328
|
-
await docket.add(
|
|
329
|
-
resource.name,
|
|
330
|
-
key=task_key,
|
|
331
|
-
)(**params)
|
|
120
|
+
# Queue function to Docket by key (result storage via execution_ttl)
|
|
121
|
+
# Use component.add_to_docket() which handles calling conventions
|
|
122
|
+
# `fn_key` is the function lookup key (e.g., "child_multiply")
|
|
123
|
+
# `task_key` is the task result key (e.g., "fastmcp:task:{session}:{task_id}:tool:child_multiply")
|
|
124
|
+
# Resources don't take arguments; tools/prompts/templates always pass arguments (even if None/empty)
|
|
125
|
+
if task_type == "resource":
|
|
126
|
+
await component.add_to_docket(docket, fn_key=key, task_key=task_key) # type: ignore[call-arg]
|
|
332
127
|
else:
|
|
333
|
-
await
|
|
334
|
-
resource.name,
|
|
335
|
-
key=task_key,
|
|
336
|
-
)()
|
|
128
|
+
await component.add_to_docket(docket, arguments, fn_key=key, task_key=task_key) # type: ignore[call-arg]
|
|
337
129
|
|
|
338
130
|
# Spawn subscription task to send status notifications (SEP-1686 optional feature)
|
|
339
131
|
from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
|
|
340
132
|
|
|
341
133
|
# Start subscription in session's task group (persists for connection lifetime)
|
|
342
134
|
if hasattr(ctx.session, "_subscription_task_group"):
|
|
343
|
-
tg = ctx.session._subscription_task_group
|
|
135
|
+
tg = ctx.session._subscription_task_group
|
|
344
136
|
if tg:
|
|
345
137
|
tg.start_soon( # type: ignore[union-attr]
|
|
346
138
|
subscribe_to_task_updates,
|
|
@@ -348,18 +140,18 @@ async def handle_resource_as_task(
|
|
|
348
140
|
task_key,
|
|
349
141
|
ctx.session,
|
|
350
142
|
docket,
|
|
143
|
+
poll_interval_ms,
|
|
351
144
|
)
|
|
352
145
|
|
|
353
|
-
# Return
|
|
146
|
+
# Return CreateTaskResult with proper Task object
|
|
354
147
|
# Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
|
|
355
|
-
return mcp.types.
|
|
356
|
-
mcp.types.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
},
|
|
148
|
+
return mcp.types.CreateTaskResult(
|
|
149
|
+
task=mcp.types.Task(
|
|
150
|
+
taskId=server_task_id,
|
|
151
|
+
status="working",
|
|
152
|
+
createdAt=created_at,
|
|
153
|
+
lastUpdatedAt=created_at,
|
|
154
|
+
ttl=ttl_ms,
|
|
155
|
+
pollInterval=poll_interval_ms,
|
|
364
156
|
)
|
|
365
157
|
)
|
fastmcp/server/tasks/keys.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Task key management for SEP-1686 background tasks.
|
|
2
2
|
|
|
3
3
|
Task keys encode security scoping and metadata in the Docket key format:
|
|
4
|
-
{session_id}:{client_task_id}:{task_type}:{component_identifier}
|
|
4
|
+
`{session_id}:{client_task_id}:{task_type}:{component_identifier}`
|
|
5
5
|
|
|
6
6
|
This format provides:
|
|
7
7
|
- Session-based security scoping (prevents cross-session access)
|
|
@@ -20,7 +20,7 @@ def build_task_key(
|
|
|
20
20
|
) -> str:
|
|
21
21
|
"""Build Docket task key with embedded metadata.
|
|
22
22
|
|
|
23
|
-
Format: {session_id}:{client_task_id}:{task_type}:{component_identifier}
|
|
23
|
+
Format: `{session_id}:{client_task_id}:{task_type}:{component_identifier}`
|
|
24
24
|
|
|
25
25
|
The component_identifier is URI-encoded to handle special characters (colons, slashes, etc.).
|
|
26
26
|
|
|
@@ -55,12 +55,10 @@ def parse_task_key(task_key: str) -> dict[str, str]:
|
|
|
55
55
|
|
|
56
56
|
Examples:
|
|
57
57
|
>>> parse_task_key("session123:task456:tool:my_tool")
|
|
58
|
-
{'session_id': 'session123', 'client_task_id': 'task456',
|
|
59
|
-
'task_type': 'tool', 'component_identifier': 'my_tool'}
|
|
58
|
+
`{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'tool', 'component_identifier': 'my_tool'}`
|
|
60
59
|
|
|
61
60
|
>>> parse_task_key("session123:task456:resource:file%3A%2F%2Fdata.txt")
|
|
62
|
-
{'session_id': 'session123', 'client_task_id': 'task456',
|
|
63
|
-
'task_type': 'resource', 'component_identifier': 'file://data.txt'}
|
|
61
|
+
`{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'resource', 'component_identifier': 'file://data.txt'}`
|
|
64
62
|
"""
|
|
65
63
|
parts = task_key.split(":", 3)
|
|
66
64
|
if len(parts) != 4:
|