fastmcp 2.12.5__py3-none-any.whl → 2.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +2 -23
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +19 -33
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/run.py +13 -8
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +123 -225
- fastmcp/client/client.py +697 -95
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/logging.py +18 -14
- fastmcp/client/messages.py +7 -5
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +117 -30
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +10 -26
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +3 -3
- fastmcp/experimental/server/openapi/__init__.py +20 -21
- fastmcp/experimental/utilities/openapi/__init__.py +16 -47
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +54 -51
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +43 -21
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +161 -61
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -14
- fastmcp/server/auth/auth.py +197 -46
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1469 -298
- fastmcp/server/auth/oidc_proxy.py +91 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +29 -5
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +236 -116
- fastmcp/server/dependencies.py +503 -18
- fastmcp/server/elicitation.py +286 -48
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +71 -20
- fastmcp/server/low_level.py +165 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +15 -10
- fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +72 -48
- fastmcp/server/server.py +1415 -733
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +125 -113
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +138 -55
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -21
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +10 -5
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +8 -8
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
- fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
- fastmcp/utilities/tests.py +92 -5
- fastmcp/utilities/types.py +86 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
- fastmcp-2.14.0.dist-info/RECORD +156 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1083
- fastmcp/utilities/openapi.py +0 -1568
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
- fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""MCP SEP-1686 background tasks support.
|
|
2
|
+
|
|
3
|
+
This module implements protocol-level background task execution for MCP servers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from fastmcp.server.tasks.capabilities import get_task_capabilities
|
|
7
|
+
from fastmcp.server.tasks.config import TaskConfig, TaskMode
|
|
8
|
+
from fastmcp.server.tasks.keys import (
|
|
9
|
+
build_task_key,
|
|
10
|
+
get_client_task_id_from_key,
|
|
11
|
+
parse_task_key,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"TaskConfig",
|
|
16
|
+
"TaskMode",
|
|
17
|
+
"build_task_key",
|
|
18
|
+
"get_client_task_id_from_key",
|
|
19
|
+
"get_task_capabilities",
|
|
20
|
+
"parse_task_key",
|
|
21
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""SEP-1686 task capabilities declaration."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_task_capabilities() -> dict[str, Any]:
|
|
7
|
+
"""Return the SEP-1686 task capabilities structure.
|
|
8
|
+
|
|
9
|
+
This is the standard capabilities map advertised to clients,
|
|
10
|
+
declaring support for list, cancel, and request operations.
|
|
11
|
+
"""
|
|
12
|
+
return {
|
|
13
|
+
"tasks": {
|
|
14
|
+
"list": {},
|
|
15
|
+
"cancel": {},
|
|
16
|
+
"requests": {
|
|
17
|
+
"tools": {"call": {}},
|
|
18
|
+
"prompts": {"get": {}},
|
|
19
|
+
"resources": {"read": {}},
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""TaskConfig for MCP SEP-1686 background task execution modes.
|
|
2
|
+
|
|
3
|
+
This module defines the configuration for how tools, resources, and prompts
|
|
4
|
+
handle task-augmented execution as specified in SEP-1686.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
# Task execution modes per SEP-1686 / MCP ToolExecution.taskSupport
|
|
15
|
+
TaskMode = Literal["forbidden", "optional", "required"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class TaskConfig:
|
|
20
|
+
"""Configuration for MCP background task execution (SEP-1686).
|
|
21
|
+
|
|
22
|
+
Controls how a component handles task-augmented requests:
|
|
23
|
+
|
|
24
|
+
- "forbidden": Component does not support task execution. Clients must not
|
|
25
|
+
request task augmentation; server returns -32601 if they do.
|
|
26
|
+
- "optional": Component supports both synchronous and task execution.
|
|
27
|
+
Client may request task augmentation or call normally.
|
|
28
|
+
- "required": Component requires task execution. Clients must request task
|
|
29
|
+
augmentation; server returns -32601 if they don't.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
```python
|
|
33
|
+
from fastmcp import FastMCP
|
|
34
|
+
from fastmcp.server.tasks import TaskConfig
|
|
35
|
+
|
|
36
|
+
mcp = FastMCP("MyServer")
|
|
37
|
+
|
|
38
|
+
# Background execution required
|
|
39
|
+
@mcp.tool(task=TaskConfig(mode="required"))
|
|
40
|
+
async def long_running_task(): ...
|
|
41
|
+
|
|
42
|
+
# Supports both modes (default when task=True)
|
|
43
|
+
@mcp.tool(task=TaskConfig(mode="optional"))
|
|
44
|
+
async def flexible_task(): ...
|
|
45
|
+
```
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
mode: TaskMode = "optional"
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_bool(cls, value: bool) -> TaskConfig:
|
|
52
|
+
"""Convert boolean task flag to TaskConfig.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
value: True for "optional" mode, False for "forbidden" mode.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
TaskConfig with appropriate mode.
|
|
59
|
+
"""
|
|
60
|
+
return cls(mode="optional" if value else "forbidden")
|
|
61
|
+
|
|
62
|
+
def validate_function(self, fn: Callable[..., Any], name: str) -> None:
|
|
63
|
+
"""Validate that function is compatible with this task config.
|
|
64
|
+
|
|
65
|
+
Task execution requires async functions. Raises ValueError if mode
|
|
66
|
+
is "optional" or "required" but function is synchronous.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
fn: The function to validate (handles callable classes and staticmethods).
|
|
70
|
+
name: Name for error messages.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If task execution is enabled but function is sync.
|
|
74
|
+
"""
|
|
75
|
+
if self.mode == "forbidden":
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Unwrap callable classes and staticmethods
|
|
79
|
+
fn_to_check = fn
|
|
80
|
+
if not inspect.isroutine(fn) and callable(fn):
|
|
81
|
+
fn_to_check = fn.__call__
|
|
82
|
+
if isinstance(fn_to_check, staticmethod):
|
|
83
|
+
fn_to_check = fn_to_check.__func__
|
|
84
|
+
|
|
85
|
+
if not inspect.iscoroutinefunction(fn_to_check):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"'{name}' uses a sync function but has task execution enabled. "
|
|
88
|
+
"Background tasks require async functions."
|
|
89
|
+
)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""SEP-1686 task result converters.
|
|
2
|
+
|
|
3
|
+
Converts raw task return values to MCP result types.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
import mcp.types
|
|
13
|
+
import pydantic_core
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from fastmcp.server.server import FastMCP
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def convert_tool_result(
|
|
20
|
+
server: FastMCP, raw_value: Any, tool_name: str, client_task_id: str
|
|
21
|
+
) -> mcp.types.CallToolResult:
|
|
22
|
+
"""Convert raw tool return value to MCP CallToolResult.
|
|
23
|
+
|
|
24
|
+
Replicates the serialization logic from tool.run() to properly handle
|
|
25
|
+
output_schema, structured content, etc.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
server: FastMCP server instance
|
|
29
|
+
raw_value: The raw return value from user's tool function
|
|
30
|
+
tool_name: Name of the tool (to get output_schema and serializer)
|
|
31
|
+
client_task_id: Client task ID for related-task metadata
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
CallToolResult with properly formatted content and structured content
|
|
35
|
+
"""
|
|
36
|
+
# Import here to avoid circular import:
|
|
37
|
+
# tools/tool.py -> tasks/config.py -> tasks/__init__.py -> converters.py -> tools/tool.py
|
|
38
|
+
from fastmcp.tools.tool import ToolResult, _convert_to_content
|
|
39
|
+
|
|
40
|
+
# Get the tool to access its configuration
|
|
41
|
+
tool = await server.get_tool(tool_name)
|
|
42
|
+
|
|
43
|
+
# Build related-task metadata
|
|
44
|
+
related_task_meta = {
|
|
45
|
+
"modelcontextprotocol.io/related-task": {
|
|
46
|
+
"taskId": client_task_id,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# If raw value is already ToolResult, use it directly
|
|
51
|
+
if isinstance(raw_value, ToolResult):
|
|
52
|
+
mcp_result = raw_value.to_mcp_result()
|
|
53
|
+
if isinstance(mcp_result, mcp.types.CallToolResult):
|
|
54
|
+
# Add metadata
|
|
55
|
+
mcp_result._meta = related_task_meta
|
|
56
|
+
return mcp_result
|
|
57
|
+
elif isinstance(mcp_result, tuple):
|
|
58
|
+
content, structured_content = mcp_result
|
|
59
|
+
return mcp.types.CallToolResult(
|
|
60
|
+
content=content,
|
|
61
|
+
structuredContent=structured_content,
|
|
62
|
+
_meta=related_task_meta,
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
return mcp.types.CallToolResult(content=mcp_result, _meta=related_task_meta)
|
|
66
|
+
|
|
67
|
+
# Convert raw value to content blocks
|
|
68
|
+
unstructured_result = _convert_to_content(raw_value, serializer=tool.serializer)
|
|
69
|
+
|
|
70
|
+
# Handle structured content creation (same logic as tool.run())
|
|
71
|
+
structured_content = None
|
|
72
|
+
|
|
73
|
+
if tool.output_schema is None:
|
|
74
|
+
# Try to serialize as dict for structured content
|
|
75
|
+
try:
|
|
76
|
+
sc = pydantic_core.to_jsonable_python(raw_value)
|
|
77
|
+
if isinstance(sc, dict):
|
|
78
|
+
structured_content = sc
|
|
79
|
+
except pydantic_core.PydanticSerializationError:
|
|
80
|
+
pass
|
|
81
|
+
else:
|
|
82
|
+
# Has output_schema - convert to JSON-able types
|
|
83
|
+
jsonable_value = pydantic_core.to_jsonable_python(raw_value)
|
|
84
|
+
wrap_result = tool.output_schema.get("x-fastmcp-wrap-result")
|
|
85
|
+
structured_content = (
|
|
86
|
+
{"result": jsonable_value} if wrap_result else jsonable_value
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return mcp.types.CallToolResult(
|
|
90
|
+
content=unstructured_result,
|
|
91
|
+
structuredContent=structured_content,
|
|
92
|
+
_meta=related_task_meta,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def convert_prompt_result(
|
|
97
|
+
server: FastMCP, raw_value: Any, prompt_name: str, client_task_id: str
|
|
98
|
+
) -> mcp.types.GetPromptResult:
|
|
99
|
+
"""Convert raw prompt return value to MCP GetPromptResult.
|
|
100
|
+
|
|
101
|
+
The user function returns raw values (strings, dicts, lists) that need
|
|
102
|
+
to be converted to PromptMessage objects.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
server: FastMCP server instance
|
|
106
|
+
raw_value: The raw return value from user's prompt function
|
|
107
|
+
prompt_name: Name of the prompt
|
|
108
|
+
client_task_id: Client task ID for related-task metadata
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
GetPromptResult with properly formatted messages
|
|
112
|
+
"""
|
|
113
|
+
from fastmcp.prompts.prompt import PromptMessage
|
|
114
|
+
|
|
115
|
+
# Get the prompt for metadata
|
|
116
|
+
prompt = await server.get_prompt(prompt_name)
|
|
117
|
+
|
|
118
|
+
# Normalize to list
|
|
119
|
+
if not isinstance(raw_value, list | tuple):
|
|
120
|
+
raw_value = [raw_value]
|
|
121
|
+
|
|
122
|
+
# Convert to PromptMessages
|
|
123
|
+
messages: list[mcp.types.PromptMessage] = []
|
|
124
|
+
for msg in raw_value:
|
|
125
|
+
if isinstance(msg, PromptMessage):
|
|
126
|
+
messages.append(msg.to_mcp())
|
|
127
|
+
elif isinstance(msg, str):
|
|
128
|
+
messages.append(
|
|
129
|
+
mcp.types.PromptMessage(
|
|
130
|
+
role="user",
|
|
131
|
+
content=mcp.types.TextContent(type="text", text=msg),
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
elif isinstance(msg, dict):
|
|
135
|
+
messages.append(mcp.types.PromptMessage.model_validate(msg))
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError(f"Invalid message type: {type(msg)}")
|
|
138
|
+
|
|
139
|
+
return mcp.types.GetPromptResult(
|
|
140
|
+
description=prompt.description or "",
|
|
141
|
+
messages=messages,
|
|
142
|
+
_meta={
|
|
143
|
+
"modelcontextprotocol.io/related-task": {
|
|
144
|
+
"taskId": client_task_id,
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def convert_resource_result(
|
|
151
|
+
server: FastMCP, raw_value: Any, uri: str, client_task_id: str
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
"""Convert raw resource return value to MCP resource contents dict.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
server: FastMCP server instance
|
|
157
|
+
raw_value: The raw return value from user's resource function (str or bytes)
|
|
158
|
+
uri: Resource URI (for the contents response)
|
|
159
|
+
client_task_id: Client task ID for related-task metadata
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dict with 'contents' key containing list of resource contents
|
|
163
|
+
"""
|
|
164
|
+
# Build related-task metadata
|
|
165
|
+
related_task_meta = {
|
|
166
|
+
"modelcontextprotocol.io/related-task": {
|
|
167
|
+
"taskId": client_task_id,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Resources return str or bytes directly
|
|
172
|
+
if isinstance(raw_value, str):
|
|
173
|
+
return {
|
|
174
|
+
"contents": [
|
|
175
|
+
{
|
|
176
|
+
"uri": uri,
|
|
177
|
+
"text": raw_value,
|
|
178
|
+
"mimeType": "text/plain",
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
"_meta": related_task_meta,
|
|
182
|
+
}
|
|
183
|
+
elif isinstance(raw_value, bytes):
|
|
184
|
+
return {
|
|
185
|
+
"contents": [
|
|
186
|
+
{
|
|
187
|
+
"uri": uri,
|
|
188
|
+
"blob": base64.b64encode(raw_value).decode(),
|
|
189
|
+
"mimeType": "application/octet-stream",
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
"_meta": related_task_meta,
|
|
193
|
+
}
|
|
194
|
+
else:
|
|
195
|
+
# Fallback: convert to JSON string
|
|
196
|
+
return {
|
|
197
|
+
"contents": [
|
|
198
|
+
{
|
|
199
|
+
"uri": uri,
|
|
200
|
+
"text": json.dumps(raw_value),
|
|
201
|
+
"mimeType": "application/json",
|
|
202
|
+
}
|
|
203
|
+
],
|
|
204
|
+
"_meta": related_task_meta,
|
|
205
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""SEP-1686 task execution handlers.
|
|
2
|
+
|
|
3
|
+
Handles queuing tool/prompt/resource executions to Docket as background tasks.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
import mcp.types
|
|
14
|
+
from mcp.shared.exceptions import McpError
|
|
15
|
+
from mcp.types import INTERNAL_ERROR, ErrorData
|
|
16
|
+
|
|
17
|
+
from fastmcp.server.dependencies import _current_docket, get_context
|
|
18
|
+
from fastmcp.server.tasks.keys import build_task_key
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from fastmcp.server.server import FastMCP
|
|
22
|
+
|
|
23
|
+
# Redis mapping TTL buffer: Add 15 minutes to Docket's execution_ttl
|
|
24
|
+
TASK_MAPPING_TTL_BUFFER_SECONDS = 15 * 60
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def handle_tool_as_task(
|
|
28
|
+
server: FastMCP,
|
|
29
|
+
tool_name: str,
|
|
30
|
+
arguments: dict[str, Any],
|
|
31
|
+
task_meta: dict[str, Any],
|
|
32
|
+
) -> mcp.types.CallToolResult:
|
|
33
|
+
"""Handle tool execution as background task (SEP-1686).
|
|
34
|
+
|
|
35
|
+
Queues the user's actual function to Docket (preserving signature for DI),
|
|
36
|
+
stores raw return values, converts to MCP types on retrieval.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
server: FastMCP server instance
|
|
40
|
+
tool_name: Name of the tool to execute
|
|
41
|
+
arguments: Tool arguments
|
|
42
|
+
task_meta: Task metadata from request (contains ttl)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
CallToolResult: Task stub with task metadata in _meta
|
|
46
|
+
"""
|
|
47
|
+
# Generate server-side task ID per SEP-1686 final spec (line 375-377)
|
|
48
|
+
# Server MUST generate task IDs, clients no longer provide them
|
|
49
|
+
server_task_id = str(uuid.uuid4())
|
|
50
|
+
|
|
51
|
+
# Record creation timestamp per SEP-1686 final spec (line 430)
|
|
52
|
+
# Format as ISO 8601 / RFC 3339 timestamp
|
|
53
|
+
created_at = datetime.now(timezone.utc).isoformat()
|
|
54
|
+
|
|
55
|
+
# Get session ID and Docket
|
|
56
|
+
ctx = get_context()
|
|
57
|
+
session_id = ctx.session_id
|
|
58
|
+
|
|
59
|
+
docket = _current_docket.get()
|
|
60
|
+
if docket is None:
|
|
61
|
+
raise McpError(
|
|
62
|
+
ErrorData(
|
|
63
|
+
code=INTERNAL_ERROR,
|
|
64
|
+
message="Background tasks require a running FastMCP server context",
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Build full task key with embedded metadata
|
|
69
|
+
task_key = build_task_key(session_id, server_task_id, "tool", tool_name)
|
|
70
|
+
|
|
71
|
+
# Get the tool to access user's function
|
|
72
|
+
tool = await server.get_tool(tool_name)
|
|
73
|
+
|
|
74
|
+
# Store task key mapping and creation timestamp in Redis for protocol handlers
|
|
75
|
+
redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
|
|
76
|
+
created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
|
|
77
|
+
ttl_seconds = int(
|
|
78
|
+
docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
|
|
79
|
+
)
|
|
80
|
+
async with docket.redis() as redis:
|
|
81
|
+
await redis.set(redis_key, task_key, ex=ttl_seconds)
|
|
82
|
+
await redis.set(created_at_key, created_at, ex=ttl_seconds)
|
|
83
|
+
|
|
84
|
+
# Send notifications/tasks/created per SEP-1686 (mandatory)
|
|
85
|
+
# Send BEFORE queuing to avoid race where task completes before notification
|
|
86
|
+
notification = mcp.types.JSONRPCNotification(
|
|
87
|
+
jsonrpc="2.0",
|
|
88
|
+
method="notifications/tasks/created",
|
|
89
|
+
params={}, # Empty params per spec
|
|
90
|
+
_meta={ # taskId in _meta per spec
|
|
91
|
+
"modelcontextprotocol.io/related-task": {
|
|
92
|
+
"taskId": server_task_id,
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
ctx = get_context()
|
|
98
|
+
with suppress(Exception):
|
|
99
|
+
# Don't let notification failures break task creation
|
|
100
|
+
await ctx.session.send_notification(notification) # type: ignore[arg-type]
|
|
101
|
+
|
|
102
|
+
# Queue function to Docket by name (result storage via execution_ttl)
|
|
103
|
+
# Use tool.key which matches what was registered - prefixed for mounted tools
|
|
104
|
+
await docket.add(
|
|
105
|
+
tool.key,
|
|
106
|
+
key=task_key,
|
|
107
|
+
)(**arguments)
|
|
108
|
+
|
|
109
|
+
# Spawn subscription task to send status notifications (SEP-1686 optional feature)
|
|
110
|
+
from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
|
|
111
|
+
|
|
112
|
+
# Start subscription in session's task group (persists for connection lifetime)
|
|
113
|
+
if hasattr(ctx.session, "_subscription_task_group"):
|
|
114
|
+
tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
|
|
115
|
+
if tg:
|
|
116
|
+
tg.start_soon( # type: ignore[union-attr]
|
|
117
|
+
subscribe_to_task_updates,
|
|
118
|
+
server_task_id,
|
|
119
|
+
task_key,
|
|
120
|
+
ctx.session,
|
|
121
|
+
docket,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Return task stub
|
|
125
|
+
# Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
|
|
126
|
+
return mcp.types.CallToolResult(
|
|
127
|
+
content=[],
|
|
128
|
+
_meta={
|
|
129
|
+
"modelcontextprotocol.io/task": {
|
|
130
|
+
"taskId": server_task_id,
|
|
131
|
+
"status": "working",
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def handle_prompt_as_task(
|
|
138
|
+
server: FastMCP,
|
|
139
|
+
prompt_name: str,
|
|
140
|
+
arguments: dict[str, Any] | None,
|
|
141
|
+
task_meta: dict[str, Any],
|
|
142
|
+
) -> mcp.types.GetPromptResult:
|
|
143
|
+
"""Handle prompt execution as background task (SEP-1686).
|
|
144
|
+
|
|
145
|
+
Queues the user's actual function to Docket (preserving signature for DI).
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
server: FastMCP server instance
|
|
149
|
+
prompt_name: Name of the prompt to execute
|
|
150
|
+
arguments: Prompt arguments
|
|
151
|
+
task_meta: Task metadata from request (contains ttl)
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
GetPromptResult: Task stub with task metadata in _meta
|
|
155
|
+
"""
|
|
156
|
+
# Generate server-side task ID per SEP-1686 final spec (line 375-377)
|
|
157
|
+
# Server MUST generate task IDs, clients no longer provide them
|
|
158
|
+
server_task_id = str(uuid.uuid4())
|
|
159
|
+
|
|
160
|
+
# Record creation timestamp per SEP-1686 final spec (line 430)
|
|
161
|
+
# Format as ISO 8601 / RFC 3339 timestamp
|
|
162
|
+
created_at = datetime.now(timezone.utc).isoformat()
|
|
163
|
+
|
|
164
|
+
# Get session ID and Docket
|
|
165
|
+
ctx = get_context()
|
|
166
|
+
session_id = ctx.session_id
|
|
167
|
+
|
|
168
|
+
docket = _current_docket.get()
|
|
169
|
+
if docket is None:
|
|
170
|
+
raise McpError(
|
|
171
|
+
ErrorData(
|
|
172
|
+
code=INTERNAL_ERROR,
|
|
173
|
+
message="Background tasks require a running FastMCP server context",
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Build full task key with embedded metadata
|
|
178
|
+
task_key = build_task_key(session_id, server_task_id, "prompt", prompt_name)
|
|
179
|
+
|
|
180
|
+
# Get the prompt
|
|
181
|
+
prompt = await server.get_prompt(prompt_name)
|
|
182
|
+
|
|
183
|
+
# Store task key mapping and creation timestamp in Redis for protocol handlers
|
|
184
|
+
redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
|
|
185
|
+
created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
|
|
186
|
+
ttl_seconds = int(
|
|
187
|
+
docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
|
|
188
|
+
)
|
|
189
|
+
async with docket.redis() as redis:
|
|
190
|
+
await redis.set(redis_key, task_key, ex=ttl_seconds)
|
|
191
|
+
await redis.set(created_at_key, created_at, ex=ttl_seconds)
|
|
192
|
+
|
|
193
|
+
# Send notifications/tasks/created per SEP-1686 (mandatory)
|
|
194
|
+
# Send BEFORE queuing to avoid race where task completes before notification
|
|
195
|
+
notification = mcp.types.JSONRPCNotification(
|
|
196
|
+
jsonrpc="2.0",
|
|
197
|
+
method="notifications/tasks/created",
|
|
198
|
+
params={},
|
|
199
|
+
_meta={
|
|
200
|
+
"modelcontextprotocol.io/related-task": {
|
|
201
|
+
"taskId": server_task_id,
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
with suppress(Exception):
|
|
206
|
+
await ctx.session.send_notification(notification) # type: ignore[arg-type]
|
|
207
|
+
|
|
208
|
+
# Queue function to Docket by name (result storage via execution_ttl)
|
|
209
|
+
# Use prompt.key which matches what was registered - prefixed for mounted prompts
|
|
210
|
+
await docket.add(
|
|
211
|
+
prompt.key,
|
|
212
|
+
key=task_key,
|
|
213
|
+
)(**(arguments or {}))
|
|
214
|
+
|
|
215
|
+
# Spawn subscription task to send status notifications (SEP-1686 optional feature)
|
|
216
|
+
from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
|
|
217
|
+
|
|
218
|
+
# Start subscription in session's task group (persists for connection lifetime)
|
|
219
|
+
if hasattr(ctx.session, "_subscription_task_group"):
|
|
220
|
+
tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
|
|
221
|
+
if tg:
|
|
222
|
+
tg.start_soon( # type: ignore[union-attr]
|
|
223
|
+
subscribe_to_task_updates,
|
|
224
|
+
server_task_id,
|
|
225
|
+
task_key,
|
|
226
|
+
ctx.session,
|
|
227
|
+
docket,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Return task stub
|
|
231
|
+
# Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
|
|
232
|
+
return mcp.types.GetPromptResult(
|
|
233
|
+
description="",
|
|
234
|
+
messages=[],
|
|
235
|
+
_meta={
|
|
236
|
+
"modelcontextprotocol.io/task": {
|
|
237
|
+
"taskId": server_task_id,
|
|
238
|
+
"status": "working",
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def handle_resource_as_task(
|
|
245
|
+
server: FastMCP,
|
|
246
|
+
uri: str,
|
|
247
|
+
resource, # Resource or ResourceTemplate
|
|
248
|
+
task_meta: dict[str, Any],
|
|
249
|
+
) -> mcp.types.ServerResult:
|
|
250
|
+
"""Handle resource read as background task (SEP-1686).
|
|
251
|
+
|
|
252
|
+
Queues the user's actual function to Docket.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
server: FastMCP server instance
|
|
256
|
+
uri: Resource URI
|
|
257
|
+
resource: Resource or ResourceTemplate object
|
|
258
|
+
task_meta: Task metadata from request (contains ttl)
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
ServerResult with ReadResourceResult stub
|
|
262
|
+
"""
|
|
263
|
+
# Generate server-side task ID per SEP-1686 final spec (line 375-377)
|
|
264
|
+
# Server MUST generate task IDs, clients no longer provide them
|
|
265
|
+
server_task_id = str(uuid.uuid4())
|
|
266
|
+
|
|
267
|
+
# Record creation timestamp per SEP-1686 final spec (line 430)
|
|
268
|
+
# Format as ISO 8601 / RFC 3339 timestamp
|
|
269
|
+
created_at = datetime.now(timezone.utc).isoformat()
|
|
270
|
+
|
|
271
|
+
# Get session ID and Docket
|
|
272
|
+
ctx = get_context()
|
|
273
|
+
session_id = ctx.session_id
|
|
274
|
+
|
|
275
|
+
docket = _current_docket.get()
|
|
276
|
+
if docket is None:
|
|
277
|
+
raise McpError(
|
|
278
|
+
ErrorData(
|
|
279
|
+
code=INTERNAL_ERROR,
|
|
280
|
+
message="Background tasks require Docket",
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Build full task key with embedded metadata (use original URI)
|
|
285
|
+
task_key = build_task_key(session_id, server_task_id, "resource", str(uri))
|
|
286
|
+
|
|
287
|
+
# Store task key mapping and creation timestamp in Redis for protocol handlers
|
|
288
|
+
redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
|
|
289
|
+
created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
|
|
290
|
+
ttl_seconds = int(
|
|
291
|
+
docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
|
|
292
|
+
)
|
|
293
|
+
async with docket.redis() as redis:
|
|
294
|
+
await redis.set(redis_key, task_key, ex=ttl_seconds)
|
|
295
|
+
await redis.set(created_at_key, created_at, ex=ttl_seconds)
|
|
296
|
+
|
|
297
|
+
# Send notifications/tasks/created per SEP-1686 (mandatory)
|
|
298
|
+
# Send BEFORE queuing to avoid race where task completes before notification
|
|
299
|
+
notification = mcp.types.JSONRPCNotification(
|
|
300
|
+
jsonrpc="2.0",
|
|
301
|
+
method="notifications/tasks/created",
|
|
302
|
+
params={},
|
|
303
|
+
_meta={
|
|
304
|
+
"modelcontextprotocol.io/related-task": {
|
|
305
|
+
"taskId": server_task_id,
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
with suppress(Exception):
|
|
310
|
+
await ctx.session.send_notification(notification) # type: ignore[arg-type]
|
|
311
|
+
|
|
312
|
+
# Queue function to Docket by name (result storage via execution_ttl)
|
|
313
|
+
# Use resource.name which matches what was registered - prefixed for mounted resources
|
|
314
|
+
# For templates, extract URI params and pass them to the function
|
|
315
|
+
from fastmcp.resources.template import FunctionResourceTemplate, match_uri_template
|
|
316
|
+
|
|
317
|
+
if isinstance(resource, FunctionResourceTemplate):
|
|
318
|
+
params = match_uri_template(uri, resource.uri_template) or {}
|
|
319
|
+
await docket.add(
|
|
320
|
+
resource.name,
|
|
321
|
+
key=task_key,
|
|
322
|
+
)(**params)
|
|
323
|
+
else:
|
|
324
|
+
await docket.add(
|
|
325
|
+
resource.name,
|
|
326
|
+
key=task_key,
|
|
327
|
+
)()
|
|
328
|
+
|
|
329
|
+
# Spawn subscription task to send status notifications (SEP-1686 optional feature)
|
|
330
|
+
from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
|
|
331
|
+
|
|
332
|
+
# Start subscription in session's task group (persists for connection lifetime)
|
|
333
|
+
if hasattr(ctx.session, "_subscription_task_group"):
|
|
334
|
+
tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
|
|
335
|
+
if tg:
|
|
336
|
+
tg.start_soon( # type: ignore[union-attr]
|
|
337
|
+
subscribe_to_task_updates,
|
|
338
|
+
server_task_id,
|
|
339
|
+
task_key,
|
|
340
|
+
ctx.session,
|
|
341
|
+
docket,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Return task stub
|
|
345
|
+
# Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
|
|
346
|
+
return mcp.types.ServerResult(
|
|
347
|
+
mcp.types.ReadResourceResult(
|
|
348
|
+
contents=[],
|
|
349
|
+
_meta={
|
|
350
|
+
"modelcontextprotocol.io/task": {
|
|
351
|
+
"taskId": server_task_id,
|
|
352
|
+
"status": "working",
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
)
|
|
356
|
+
)
|