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/dependencies.py
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Dependency injection for FastMCP.
|
|
2
|
+
|
|
3
|
+
DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket
|
|
4
|
+
using a vendored DI engine. Only task-related dependencies (CurrentDocket,
|
|
5
|
+
CurrentWorker) and background task execution require fastmcp[tasks].
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from __future__ import annotations
|
|
2
9
|
|
|
3
10
|
import contextlib
|
|
@@ -7,10 +14,8 @@ from collections.abc import AsyncGenerator, Callable
|
|
|
7
14
|
from contextlib import AsyncExitStack, asynccontextmanager
|
|
8
15
|
from contextvars import ContextVar
|
|
9
16
|
from functools import lru_cache
|
|
10
|
-
from typing import TYPE_CHECKING, Any, cast, get_type_hints
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Protocol, cast, get_type_hints, runtime_checkable
|
|
11
18
|
|
|
12
|
-
from docket.dependencies import Dependency, _Depends, get_dependency_parameters
|
|
13
|
-
from docket.dependencies import Progress as DocketProgress
|
|
14
19
|
from mcp.server.auth.middleware.auth_context import (
|
|
15
20
|
get_access_token as _sdk_get_access_token,
|
|
16
21
|
)
|
|
@@ -24,7 +29,8 @@ from starlette.requests import Request
|
|
|
24
29
|
from fastmcp.exceptions import FastMCPError
|
|
25
30
|
from fastmcp.server.auth import AccessToken
|
|
26
31
|
from fastmcp.server.http import _current_http_request
|
|
27
|
-
from fastmcp.utilities.
|
|
32
|
+
from fastmcp.utilities.async_utils import call_sync_fn_in_threadpool
|
|
33
|
+
from fastmcp.utilities.types import find_kwarg_by_type, is_class_member_of_type
|
|
28
34
|
|
|
29
35
|
if TYPE_CHECKING:
|
|
30
36
|
from docket import Docket
|
|
@@ -33,12 +39,6 @@ if TYPE_CHECKING:
|
|
|
33
39
|
from fastmcp.server.context import Context
|
|
34
40
|
from fastmcp.server.server import FastMCP
|
|
35
41
|
|
|
36
|
-
# ContextVars for tracking Docket infrastructure
|
|
37
|
-
_current_docket: ContextVar[Docket | None] = ContextVar("docket", default=None) # type: ignore[assignment]
|
|
38
|
-
_current_worker: ContextVar[Worker | None] = ContextVar("worker", default=None) # type: ignore[assignment]
|
|
39
|
-
_current_server: ContextVar[weakref.ref[FastMCP] | None] = ContextVar( # type: ignore[invalid-assignment]
|
|
40
|
-
"server", default=None
|
|
41
|
-
)
|
|
42
42
|
|
|
43
43
|
__all__ = [
|
|
44
44
|
"AccessToken",
|
|
@@ -52,34 +52,382 @@ __all__ = [
|
|
|
52
52
|
"get_http_headers",
|
|
53
53
|
"get_http_request",
|
|
54
54
|
"get_server",
|
|
55
|
+
"is_docket_available",
|
|
56
|
+
"require_docket",
|
|
55
57
|
"resolve_dependencies",
|
|
58
|
+
"transform_context_annotations",
|
|
56
59
|
"without_injected_parameters",
|
|
57
60
|
]
|
|
58
61
|
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
# --- ContextVars ---
|
|
64
|
+
|
|
65
|
+
_current_server: ContextVar[weakref.ref[FastMCP] | None] = ContextVar(
|
|
66
|
+
"server", default=None
|
|
67
|
+
)
|
|
68
|
+
_current_docket: ContextVar[Docket | None] = ContextVar("docket", default=None)
|
|
69
|
+
_current_worker: ContextVar[Worker | None] = ContextVar("worker", default=None)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# --- Docket availability check ---
|
|
73
|
+
|
|
74
|
+
_DOCKET_AVAILABLE: bool | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_docket_available() -> bool:
|
|
78
|
+
"""Check if pydocket is installed."""
|
|
79
|
+
global _DOCKET_AVAILABLE
|
|
80
|
+
if _DOCKET_AVAILABLE is None:
|
|
81
|
+
try:
|
|
82
|
+
import docket # noqa: F401
|
|
83
|
+
|
|
84
|
+
_DOCKET_AVAILABLE = True
|
|
85
|
+
except ImportError:
|
|
86
|
+
_DOCKET_AVAILABLE = False
|
|
87
|
+
return _DOCKET_AVAILABLE
|
|
88
|
+
|
|
62
89
|
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
def require_docket(feature: str) -> None:
|
|
91
|
+
"""Raise ImportError with install instructions if docket not available.
|
|
65
92
|
|
|
66
|
-
|
|
93
|
+
Args:
|
|
94
|
+
feature: Description of what requires docket (e.g., "`task=True`",
|
|
95
|
+
"CurrentDocket()"). Will be included in the error message.
|
|
67
96
|
"""
|
|
97
|
+
if not is_docket_available():
|
|
98
|
+
raise ImportError(
|
|
99
|
+
f"FastMCP background tasks require the `tasks` extra. "
|
|
100
|
+
f"Install with: pip install 'fastmcp[tasks]'. "
|
|
101
|
+
f"(Triggered by {feature})"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# --- Dependency injection imports ---
|
|
106
|
+
# Try docket first for isinstance compatibility in worker context,
|
|
107
|
+
# fall back to vendored DI engine when docket is not installed.
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
from docket.dependencies import (
|
|
111
|
+
Dependency,
|
|
112
|
+
_Depends,
|
|
113
|
+
get_dependency_parameters,
|
|
114
|
+
)
|
|
115
|
+
except ImportError:
|
|
116
|
+
from fastmcp._vendor.docket_di import (
|
|
117
|
+
Dependency,
|
|
118
|
+
_Depends,
|
|
119
|
+
get_dependency_parameters,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Import Progress separately to avoid breaking DI fallback if Progress is missing
|
|
123
|
+
try:
|
|
124
|
+
from docket.dependencies import Progress as DocketProgress
|
|
125
|
+
except ImportError:
|
|
126
|
+
DocketProgress = None # type: ignore[assignment]
|
|
68
127
|
|
|
69
|
-
if inspect.ismethod(fn) and hasattr(fn, "__func__"):
|
|
70
|
-
fn = fn.__func__
|
|
71
128
|
|
|
129
|
+
# --- Context utilities ---
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
133
|
+
"""Transform ctx: Context into ctx: Context = CurrentContext().
|
|
134
|
+
|
|
135
|
+
Transforms ALL params typed as Context to use Docket's DI system,
|
|
136
|
+
unless they already have a Dependency-based default (like CurrentContext()).
|
|
137
|
+
|
|
138
|
+
This unifies the legacy type annotation DI with Docket's Depends() system,
|
|
139
|
+
allowing both patterns to work through a single resolution path.
|
|
140
|
+
|
|
141
|
+
Note: Only POSITIONAL_OR_KEYWORD parameters are reordered (params with defaults
|
|
142
|
+
after those without). KEYWORD_ONLY parameters keep their position since Python
|
|
143
|
+
allows them to have defaults in any order.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
fn: Function to transform
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Function with modified signature (same function object, updated __signature__)
|
|
150
|
+
"""
|
|
151
|
+
from fastmcp.server.context import Context
|
|
152
|
+
|
|
153
|
+
# Get the function's signature
|
|
154
|
+
try:
|
|
155
|
+
sig = inspect.signature(fn)
|
|
156
|
+
except (ValueError, TypeError):
|
|
157
|
+
return fn
|
|
158
|
+
|
|
159
|
+
# Get type hints for accurate type checking
|
|
72
160
|
try:
|
|
73
161
|
type_hints = get_type_hints(fn, include_extras=True)
|
|
74
162
|
except Exception:
|
|
75
163
|
type_hints = getattr(fn, "__annotations__", {})
|
|
76
164
|
|
|
77
|
-
|
|
165
|
+
# First pass: identify which params need transformation
|
|
166
|
+
params_to_transform: set[str] = set()
|
|
78
167
|
for name, param in sig.parameters.items():
|
|
79
168
|
annotation = type_hints.get(name, param.annotation)
|
|
80
|
-
if is_class_member_of_type(annotation,
|
|
81
|
-
|
|
82
|
-
|
|
169
|
+
if is_class_member_of_type(annotation, Context):
|
|
170
|
+
if not isinstance(param.default, Dependency):
|
|
171
|
+
params_to_transform.add(name)
|
|
172
|
+
|
|
173
|
+
if not params_to_transform:
|
|
174
|
+
return fn
|
|
175
|
+
|
|
176
|
+
# Second pass: build new param list preserving parameter kind structure
|
|
177
|
+
# Python signature structure: [POSITIONAL_ONLY] / [POSITIONAL_OR_KEYWORD] *args [KEYWORD_ONLY] **kwargs
|
|
178
|
+
# Within POSITIONAL_ONLY and POSITIONAL_OR_KEYWORD: params without defaults must come first
|
|
179
|
+
# KEYWORD_ONLY params can have defaults in any order
|
|
180
|
+
P = inspect.Parameter
|
|
181
|
+
|
|
182
|
+
# Group params by section, preserving order within each
|
|
183
|
+
positional_only_no_default: list[P] = []
|
|
184
|
+
positional_only_with_default: list[P] = []
|
|
185
|
+
positional_or_keyword_no_default: list[P] = []
|
|
186
|
+
positional_or_keyword_with_default: list[P] = []
|
|
187
|
+
var_positional: list[P] = [] # *args (at most one)
|
|
188
|
+
keyword_only: list[P] = [] # After * or *args, order preserved
|
|
189
|
+
var_keyword: list[P] = [] # **kwargs (at most one)
|
|
190
|
+
|
|
191
|
+
for name, param in sig.parameters.items():
|
|
192
|
+
# Transform Context params by adding CurrentContext default
|
|
193
|
+
if name in params_to_transform:
|
|
194
|
+
# We use CurrentContext() instead of Depends(get_context) because
|
|
195
|
+
# get_context() returns the Context which is an AsyncContextManager,
|
|
196
|
+
# and the DI system would try to enter it again (it's already entered)
|
|
197
|
+
param = param.replace(default=CurrentContext())
|
|
198
|
+
|
|
199
|
+
# Sort into buckets based on parameter kind
|
|
200
|
+
if param.kind == P.POSITIONAL_ONLY:
|
|
201
|
+
if param.default is P.empty:
|
|
202
|
+
positional_only_no_default.append(param)
|
|
203
|
+
else:
|
|
204
|
+
positional_only_with_default.append(param)
|
|
205
|
+
elif param.kind == P.POSITIONAL_OR_KEYWORD:
|
|
206
|
+
if param.default is P.empty:
|
|
207
|
+
positional_or_keyword_no_default.append(param)
|
|
208
|
+
else:
|
|
209
|
+
positional_or_keyword_with_default.append(param)
|
|
210
|
+
elif param.kind == P.VAR_POSITIONAL:
|
|
211
|
+
var_positional.append(param)
|
|
212
|
+
elif param.kind == P.KEYWORD_ONLY:
|
|
213
|
+
keyword_only.append(param)
|
|
214
|
+
elif param.kind == P.VAR_KEYWORD:
|
|
215
|
+
var_keyword.append(param)
|
|
216
|
+
|
|
217
|
+
# Reconstruct parameter list maintaining Python's required structure
|
|
218
|
+
new_params: list[P] = (
|
|
219
|
+
positional_only_no_default
|
|
220
|
+
+ positional_only_with_default
|
|
221
|
+
+ positional_or_keyword_no_default
|
|
222
|
+
+ positional_or_keyword_with_default
|
|
223
|
+
+ var_positional
|
|
224
|
+
+ keyword_only
|
|
225
|
+
+ var_keyword
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Update function's signature in place
|
|
229
|
+
# Handle methods by setting signature on the underlying function
|
|
230
|
+
# For bound methods, we need to preserve the 'self' parameter because
|
|
231
|
+
# inspect.signature(bound_method) automatically removes the first param
|
|
232
|
+
if inspect.ismethod(fn):
|
|
233
|
+
# Get the original __func__ signature which includes 'self'
|
|
234
|
+
func_sig = inspect.signature(fn.__func__)
|
|
235
|
+
# Insert 'self' at the beginning of our new params
|
|
236
|
+
self_param = next(iter(func_sig.parameters.values())) # Should be 'self'
|
|
237
|
+
new_sig = func_sig.replace(parameters=[self_param, *new_params])
|
|
238
|
+
fn.__func__.__signature__ = new_sig # type: ignore[union-attr]
|
|
239
|
+
else:
|
|
240
|
+
new_sig = sig.replace(parameters=new_params)
|
|
241
|
+
fn.__signature__ = new_sig # type: ignore[attr-defined]
|
|
242
|
+
|
|
243
|
+
# Clear caches that may have cached the old signature
|
|
244
|
+
# This ensures get_dependency_parameters and without_injected_parameters
|
|
245
|
+
# see the transformed signature
|
|
246
|
+
_clear_signature_caches(fn)
|
|
247
|
+
|
|
248
|
+
return fn
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _clear_signature_caches(fn: Callable[..., Any]) -> None:
|
|
252
|
+
"""Clear signature-related caches for a function.
|
|
253
|
+
|
|
254
|
+
Called after modifying a function's signature to ensure downstream
|
|
255
|
+
code sees the updated signature.
|
|
256
|
+
"""
|
|
257
|
+
# Clear vendored DI caches
|
|
258
|
+
from fastmcp._vendor.docket_di import _parameter_cache, _signature_cache
|
|
259
|
+
|
|
260
|
+
_signature_cache.pop(fn, None)
|
|
261
|
+
_parameter_cache.pop(fn, None)
|
|
262
|
+
|
|
263
|
+
# Also clear for __func__ if it's a method
|
|
264
|
+
if inspect.ismethod(fn):
|
|
265
|
+
_signature_cache.pop(fn.__func__, None)
|
|
266
|
+
_parameter_cache.pop(fn.__func__, None)
|
|
267
|
+
|
|
268
|
+
# Try to clear docket caches if docket is installed
|
|
269
|
+
if is_docket_available():
|
|
270
|
+
try:
|
|
271
|
+
from docket.dependencies import _parameter_cache as docket_param_cache
|
|
272
|
+
from docket.execution import _signature_cache as docket_sig_cache
|
|
273
|
+
|
|
274
|
+
docket_sig_cache.pop(fn, None)
|
|
275
|
+
docket_param_cache.pop(fn, None)
|
|
276
|
+
if inspect.ismethod(fn):
|
|
277
|
+
docket_sig_cache.pop(fn.__func__, None)
|
|
278
|
+
docket_param_cache.pop(fn.__func__, None)
|
|
279
|
+
except (ImportError, AttributeError):
|
|
280
|
+
pass # Cache access not available in this docket version
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def get_context() -> Context:
|
|
284
|
+
"""Get the current FastMCP Context instance directly."""
|
|
285
|
+
from fastmcp.server.context import _current_context
|
|
286
|
+
|
|
287
|
+
context = _current_context.get()
|
|
288
|
+
if context is None:
|
|
289
|
+
raise RuntimeError("No active context found.")
|
|
290
|
+
return context
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_server() -> FastMCP:
|
|
294
|
+
"""Get the current FastMCP server instance directly.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
The active FastMCP server
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
RuntimeError: If no server in context
|
|
301
|
+
"""
|
|
302
|
+
server_ref = _current_server.get()
|
|
303
|
+
if server_ref is None:
|
|
304
|
+
raise RuntimeError("No FastMCP server instance in context")
|
|
305
|
+
server = server_ref()
|
|
306
|
+
if server is None:
|
|
307
|
+
raise RuntimeError("FastMCP server instance is no longer available")
|
|
308
|
+
return server
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_http_request() -> Request:
|
|
312
|
+
"""Get the current HTTP request.
|
|
313
|
+
|
|
314
|
+
Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context.
|
|
315
|
+
"""
|
|
316
|
+
# Try MCP SDK's request_ctx first (set during normal MCP request handling)
|
|
317
|
+
request = None
|
|
318
|
+
with contextlib.suppress(LookupError):
|
|
319
|
+
request = request_ctx.get().request
|
|
320
|
+
|
|
321
|
+
# Fallback to FastMCP's HTTP context variable
|
|
322
|
+
# This is needed during `on_initialize` middleware where request_ctx isn't set yet
|
|
323
|
+
if request is None:
|
|
324
|
+
request = _current_http_request.get()
|
|
325
|
+
|
|
326
|
+
if request is None:
|
|
327
|
+
raise RuntimeError("No active HTTP request found.")
|
|
328
|
+
return request
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def get_http_headers(include_all: bool = False) -> dict[str, str]:
|
|
332
|
+
"""Extract headers from the current HTTP request if available.
|
|
333
|
+
|
|
334
|
+
Never raises an exception, even if there is no active HTTP request (in which case
|
|
335
|
+
an empty dict is returned).
|
|
336
|
+
|
|
337
|
+
By default, strips problematic headers like `content-length` that cause issues
|
|
338
|
+
if forwarded to downstream clients. If `include_all` is True, all headers are returned.
|
|
339
|
+
"""
|
|
340
|
+
if include_all:
|
|
341
|
+
exclude_headers: set[str] = set()
|
|
342
|
+
else:
|
|
343
|
+
exclude_headers = {
|
|
344
|
+
"host",
|
|
345
|
+
"content-length",
|
|
346
|
+
"connection",
|
|
347
|
+
"transfer-encoding",
|
|
348
|
+
"upgrade",
|
|
349
|
+
"te",
|
|
350
|
+
"keep-alive",
|
|
351
|
+
"expect",
|
|
352
|
+
"accept",
|
|
353
|
+
# Proxy-related headers
|
|
354
|
+
"proxy-authenticate",
|
|
355
|
+
"proxy-authorization",
|
|
356
|
+
"proxy-connection",
|
|
357
|
+
# MCP-related headers
|
|
358
|
+
"mcp-session-id",
|
|
359
|
+
}
|
|
360
|
+
# (just in case)
|
|
361
|
+
if not all(h.lower() == h for h in exclude_headers):
|
|
362
|
+
raise ValueError("Excluded headers must be lowercase")
|
|
363
|
+
headers: dict[str, str] = {}
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
request = get_http_request()
|
|
367
|
+
for name, value in request.headers.items():
|
|
368
|
+
lower_name = name.lower()
|
|
369
|
+
if lower_name not in exclude_headers:
|
|
370
|
+
headers[lower_name] = str(value)
|
|
371
|
+
return headers
|
|
372
|
+
except RuntimeError:
|
|
373
|
+
return {}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def get_access_token() -> AccessToken | None:
|
|
377
|
+
"""Get the FastMCP access token from the current context.
|
|
378
|
+
|
|
379
|
+
This function first tries to get the token from the current HTTP request's scope,
|
|
380
|
+
which is more reliable for long-lived connections where the SDK's auth_context_var
|
|
381
|
+
may become stale after token refresh. Falls back to the SDK's context var if no
|
|
382
|
+
request is available.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
The access token if an authenticated user is available, None otherwise.
|
|
386
|
+
"""
|
|
387
|
+
access_token: _SDKAccessToken | None = None
|
|
388
|
+
|
|
389
|
+
# First, try to get from current HTTP request's scope (issue #1863)
|
|
390
|
+
# This is more reliable than auth_context_var for Streamable HTTP sessions
|
|
391
|
+
# where tokens may be refreshed between MCP messages
|
|
392
|
+
try:
|
|
393
|
+
request = get_http_request()
|
|
394
|
+
user = request.scope.get("user")
|
|
395
|
+
if isinstance(user, AuthenticatedUser):
|
|
396
|
+
access_token = user.access_token
|
|
397
|
+
except RuntimeError:
|
|
398
|
+
# No HTTP request available, fall back to context var
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
# Fall back to SDK's context var if we didn't get a token from the request
|
|
402
|
+
if access_token is None:
|
|
403
|
+
access_token = _sdk_get_access_token()
|
|
404
|
+
|
|
405
|
+
if access_token is None or isinstance(access_token, AccessToken):
|
|
406
|
+
return access_token
|
|
407
|
+
|
|
408
|
+
# If the object is not a FastMCP AccessToken, convert it to one if the
|
|
409
|
+
# fields are compatible (e.g. `claims` is not present in the SDK's AccessToken).
|
|
410
|
+
# This is a workaround for the case where the SDK or auth provider returns a different type
|
|
411
|
+
# If it fails, it will raise a TypeError
|
|
412
|
+
try:
|
|
413
|
+
access_token_as_dict = access_token.model_dump()
|
|
414
|
+
return AccessToken(
|
|
415
|
+
token=access_token_as_dict["token"],
|
|
416
|
+
client_id=access_token_as_dict["client_id"],
|
|
417
|
+
scopes=access_token_as_dict["scopes"],
|
|
418
|
+
# Optional fields
|
|
419
|
+
expires_at=access_token_as_dict.get("expires_at"),
|
|
420
|
+
resource=access_token_as_dict.get("resource"),
|
|
421
|
+
claims=access_token_as_dict.get("claims") or {},
|
|
422
|
+
)
|
|
423
|
+
except Exception as e:
|
|
424
|
+
raise TypeError(
|
|
425
|
+
f"Expected fastmcp.server.auth.auth.AccessToken, got {type(access_token).__name__}. "
|
|
426
|
+
"Ensure the SDK is using the correct AccessToken type."
|
|
427
|
+
) from e
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# --- Schema generation helper ---
|
|
83
431
|
|
|
84
432
|
|
|
85
433
|
@lru_cache(maxsize=5000)
|
|
@@ -91,6 +439,10 @@ def without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
91
439
|
validation. The wrapper internally handles all dependency resolution and
|
|
92
440
|
Context injection when called.
|
|
93
441
|
|
|
442
|
+
Handles:
|
|
443
|
+
- Legacy Context injection (always works)
|
|
444
|
+
- Depends() injection (always works - uses docket or vendored DI engine)
|
|
445
|
+
|
|
94
446
|
Args:
|
|
95
447
|
fn: Original function with Context and/or dependencies
|
|
96
448
|
|
|
@@ -100,7 +452,7 @@ def without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
100
452
|
from fastmcp.server.context import Context
|
|
101
453
|
|
|
102
454
|
# Identify parameters to exclude
|
|
103
|
-
context_kwarg =
|
|
455
|
+
context_kwarg = find_kwarg_by_type(fn, Context)
|
|
104
456
|
dependency_params = get_dependency_parameters(fn)
|
|
105
457
|
|
|
106
458
|
exclude = set()
|
|
@@ -120,15 +472,22 @@ def without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
120
472
|
new_sig = inspect.Signature(user_params)
|
|
121
473
|
|
|
122
474
|
# Create async wrapper that handles dependency resolution
|
|
475
|
+
fn_is_async = inspect.iscoroutinefunction(fn)
|
|
476
|
+
|
|
123
477
|
async def wrapper(**user_kwargs: Any) -> Any:
|
|
124
478
|
async with resolve_dependencies(fn, user_kwargs) as resolved_kwargs:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
479
|
+
if fn_is_async:
|
|
480
|
+
return await fn(**resolved_kwargs)
|
|
481
|
+
else:
|
|
482
|
+
# Run sync functions in threadpool to avoid blocking the event loop
|
|
483
|
+
result = await call_sync_fn_in_threadpool(fn, **resolved_kwargs)
|
|
484
|
+
# Handle sync wrappers that return awaitables (e.g., partial(async_fn))
|
|
485
|
+
if inspect.isawaitable(result):
|
|
486
|
+
result = await result
|
|
487
|
+
return result
|
|
129
488
|
|
|
130
489
|
# Set wrapper metadata (only parameter annotations, not return type)
|
|
131
|
-
wrapper.__signature__ = new_sig # type: ignore
|
|
490
|
+
wrapper.__signature__ = new_sig # type: ignore[attr-defined]
|
|
132
491
|
wrapper.__annotations__ = {
|
|
133
492
|
k: v
|
|
134
493
|
for k, v in getattr(fn, "__annotations__", {}).items()
|
|
@@ -140,6 +499,9 @@ def without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
140
499
|
return wrapper
|
|
141
500
|
|
|
142
501
|
|
|
502
|
+
# --- Dependency resolution ---
|
|
503
|
+
|
|
504
|
+
|
|
143
505
|
@asynccontextmanager
|
|
144
506
|
async def _resolve_fastmcp_dependencies(
|
|
145
507
|
fn: Callable[..., Any], arguments: dict[str, Any]
|
|
@@ -213,24 +575,26 @@ async def _resolve_fastmcp_dependencies(
|
|
|
213
575
|
async def resolve_dependencies(
|
|
214
576
|
fn: Callable[..., Any], arguments: dict[str, Any]
|
|
215
577
|
) -> AsyncGenerator[dict[str, Any], None]:
|
|
216
|
-
"""Resolve dependencies
|
|
578
|
+
"""Resolve dependencies for a FastMCP function.
|
|
217
579
|
|
|
218
580
|
This function:
|
|
219
581
|
1. Filters out any dependency parameter names from user arguments (security)
|
|
220
|
-
2. Resolves
|
|
221
|
-
3. Injects Context if needed
|
|
222
|
-
4. Merges everything together
|
|
582
|
+
2. Resolves Depends() parameters via the DI system
|
|
223
583
|
|
|
224
584
|
The filtering prevents external callers from overriding injected parameters by
|
|
225
585
|
providing values for dependency parameter names. This is a security feature.
|
|
226
586
|
|
|
587
|
+
Note: Context injection is handled via transform_context_annotations() which
|
|
588
|
+
converts `ctx: Context` to `ctx: Context = Depends(get_context)` at registration
|
|
589
|
+
time, so all injection goes through the unified DI system.
|
|
590
|
+
|
|
227
591
|
Args:
|
|
228
592
|
fn: The function to resolve dependencies for
|
|
229
593
|
arguments: User arguments (may contain keys that match dependency names,
|
|
230
594
|
which will be filtered out)
|
|
231
595
|
|
|
232
596
|
Yields:
|
|
233
|
-
Dictionary of filtered user args + resolved dependencies
|
|
597
|
+
Dictionary of filtered user args + resolved dependencies
|
|
234
598
|
|
|
235
599
|
Example:
|
|
236
600
|
```python
|
|
@@ -240,8 +604,6 @@ async def resolve_dependencies(
|
|
|
240
604
|
result = await result
|
|
241
605
|
```
|
|
242
606
|
"""
|
|
243
|
-
from fastmcp.server.context import Context
|
|
244
|
-
|
|
245
607
|
# Filter out dependency parameters from user arguments to prevent override
|
|
246
608
|
# This is a security measure - external callers should never be able to
|
|
247
609
|
# provide values for injected parameters
|
|
@@ -249,29 +611,23 @@ async def resolve_dependencies(
|
|
|
249
611
|
user_args = {k: v for k, v in arguments.items() if k not in dependency_params}
|
|
250
612
|
|
|
251
613
|
async with _resolve_fastmcp_dependencies(fn, user_args) as resolved_kwargs:
|
|
252
|
-
# Inject Context if needed
|
|
253
|
-
context_kwarg = _find_kwarg_by_type(fn, kwarg_type=Context)
|
|
254
|
-
if context_kwarg and context_kwarg not in resolved_kwargs:
|
|
255
|
-
resolved_kwargs[context_kwarg] = get_context()
|
|
256
|
-
|
|
257
614
|
yield resolved_kwargs
|
|
258
615
|
|
|
259
616
|
|
|
260
|
-
|
|
261
|
-
|
|
617
|
+
# --- Dependency classes ---
|
|
618
|
+
# These must inherit from docket.dependencies.Dependency when docket is available
|
|
619
|
+
# so that get_dependency_parameters can detect them.
|
|
262
620
|
|
|
263
|
-
context = _current_context.get()
|
|
264
|
-
if context is None:
|
|
265
|
-
raise RuntimeError("No active context found.")
|
|
266
|
-
return context
|
|
267
621
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
"""Internal dependency class for CurrentContext."""
|
|
622
|
+
class _CurrentContext(Dependency): # type: ignore[misc]
|
|
623
|
+
"""Async context manager for Context dependency."""
|
|
271
624
|
|
|
272
625
|
async def __aenter__(self) -> Context:
|
|
273
626
|
return get_context()
|
|
274
627
|
|
|
628
|
+
async def __aexit__(self, *args: object) -> None:
|
|
629
|
+
pass
|
|
630
|
+
|
|
275
631
|
|
|
276
632
|
def CurrentContext() -> Context:
|
|
277
633
|
"""Get the current FastMCP Context instance.
|
|
@@ -298,20 +654,23 @@ def CurrentContext() -> Context:
|
|
|
298
654
|
return cast("Context", _CurrentContext())
|
|
299
655
|
|
|
300
656
|
|
|
301
|
-
class _CurrentDocket(Dependency):
|
|
302
|
-
"""
|
|
657
|
+
class _CurrentDocket(Dependency): # type: ignore[misc]
|
|
658
|
+
"""Async context manager for Docket dependency."""
|
|
303
659
|
|
|
304
660
|
async def __aenter__(self) -> Docket:
|
|
305
|
-
|
|
661
|
+
require_docket("CurrentDocket()")
|
|
306
662
|
docket = _current_docket.get()
|
|
307
663
|
if docket is None:
|
|
308
664
|
raise RuntimeError(
|
|
309
|
-
"No Docket instance found. Docket is only
|
|
310
|
-
"
|
|
665
|
+
"No Docket instance found. Docket is only initialized when there are "
|
|
666
|
+
"task-enabled components (task=True). Add task=True to a component "
|
|
667
|
+
"to enable Docket infrastructure."
|
|
311
668
|
)
|
|
312
|
-
|
|
313
669
|
return docket
|
|
314
670
|
|
|
671
|
+
async def __aexit__(self, *args: object) -> None:
|
|
672
|
+
pass
|
|
673
|
+
|
|
315
674
|
|
|
316
675
|
def CurrentDocket() -> Docket:
|
|
317
676
|
"""Get the current Docket instance managed by FastMCP.
|
|
@@ -324,6 +683,7 @@ def CurrentDocket() -> Docket:
|
|
|
324
683
|
|
|
325
684
|
Raises:
|
|
326
685
|
RuntimeError: If not within a FastMCP server context
|
|
686
|
+
ImportError: If fastmcp[tasks] not installed
|
|
327
687
|
|
|
328
688
|
Example:
|
|
329
689
|
```python
|
|
@@ -335,22 +695,27 @@ def CurrentDocket() -> Docket:
|
|
|
335
695
|
return "Scheduled"
|
|
336
696
|
```
|
|
337
697
|
"""
|
|
698
|
+
require_docket("CurrentDocket()")
|
|
338
699
|
return cast("Docket", _CurrentDocket())
|
|
339
700
|
|
|
340
701
|
|
|
341
|
-
class _CurrentWorker(Dependency):
|
|
342
|
-
"""
|
|
702
|
+
class _CurrentWorker(Dependency): # type: ignore[misc]
|
|
703
|
+
"""Async context manager for Worker dependency."""
|
|
343
704
|
|
|
344
705
|
async def __aenter__(self) -> Worker:
|
|
706
|
+
require_docket("CurrentWorker()")
|
|
345
707
|
worker = _current_worker.get()
|
|
346
708
|
if worker is None:
|
|
347
709
|
raise RuntimeError(
|
|
348
|
-
"No Worker instance found. Worker is only
|
|
349
|
-
"
|
|
710
|
+
"No Worker instance found. Worker is only initialized when there are "
|
|
711
|
+
"task-enabled components (task=True). Add task=True to a component "
|
|
712
|
+
"to enable Docket infrastructure."
|
|
350
713
|
)
|
|
351
|
-
|
|
352
714
|
return worker
|
|
353
715
|
|
|
716
|
+
async def __aexit__(self, *args: object) -> None:
|
|
717
|
+
pass
|
|
718
|
+
|
|
354
719
|
|
|
355
720
|
def CurrentWorker() -> Worker:
|
|
356
721
|
"""Get the current Docket Worker instance managed by FastMCP.
|
|
@@ -363,6 +728,7 @@ def CurrentWorker() -> Worker:
|
|
|
363
728
|
|
|
364
729
|
Raises:
|
|
365
730
|
RuntimeError: If not within a FastMCP server context
|
|
731
|
+
ImportError: If fastmcp[tasks] not installed
|
|
366
732
|
|
|
367
733
|
Example:
|
|
368
734
|
```python
|
|
@@ -373,89 +739,14 @@ def CurrentWorker() -> Worker:
|
|
|
373
739
|
return f"Worker: {worker.name}"
|
|
374
740
|
```
|
|
375
741
|
"""
|
|
742
|
+
require_docket("CurrentWorker()")
|
|
376
743
|
return cast("Worker", _CurrentWorker())
|
|
377
744
|
|
|
378
745
|
|
|
379
|
-
class
|
|
380
|
-
"""
|
|
381
|
-
|
|
382
|
-
Provides the same interface as Progress but stores state in memory
|
|
383
|
-
instead of Redis. Useful for testing and immediate execution where
|
|
384
|
-
progress doesn't need to be observable across processes.
|
|
385
|
-
"""
|
|
386
|
-
|
|
387
|
-
def __init__(self) -> None:
|
|
388
|
-
super().__init__()
|
|
389
|
-
self._current: int | None = None
|
|
390
|
-
self._total: int = 1
|
|
391
|
-
self._message: str | None = None
|
|
392
|
-
|
|
393
|
-
async def __aenter__(self) -> DocketProgress:
|
|
394
|
-
return self
|
|
395
|
-
|
|
396
|
-
@property
|
|
397
|
-
def current(self) -> int | None:
|
|
398
|
-
return self._current
|
|
399
|
-
|
|
400
|
-
@property
|
|
401
|
-
def total(self) -> int:
|
|
402
|
-
return self._total
|
|
403
|
-
|
|
404
|
-
@property
|
|
405
|
-
def message(self) -> str | None:
|
|
406
|
-
return self._message
|
|
407
|
-
|
|
408
|
-
async def set_total(self, total: int) -> None:
|
|
409
|
-
"""Set the total/target value for progress tracking."""
|
|
410
|
-
if total < 1:
|
|
411
|
-
raise ValueError("Total must be at least 1")
|
|
412
|
-
self._total = total
|
|
746
|
+
class _CurrentFastMCP(Dependency): # type: ignore[misc]
|
|
747
|
+
"""Async context manager for FastMCP server dependency."""
|
|
413
748
|
|
|
414
|
-
async def
|
|
415
|
-
"""Atomically increment the current progress value."""
|
|
416
|
-
if amount < 1:
|
|
417
|
-
raise ValueError("Amount must be at least 1")
|
|
418
|
-
if self._current is None:
|
|
419
|
-
self._current = amount
|
|
420
|
-
else:
|
|
421
|
-
self._current += amount
|
|
422
|
-
|
|
423
|
-
async def set_message(self, message: str | None) -> None:
|
|
424
|
-
"""Update the progress status message."""
|
|
425
|
-
self._message = message
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
class Progress(DocketProgress):
|
|
429
|
-
"""FastMCP Progress dependency that works in both server and worker contexts.
|
|
430
|
-
|
|
431
|
-
Extends Docket's Progress to handle two execution modes:
|
|
432
|
-
- In Docket worker: Uses the execution's progress (standard Docket behavior)
|
|
433
|
-
- In FastMCP server: Uses in-memory progress (not observable remotely)
|
|
434
|
-
|
|
435
|
-
This allows tools to use Progress() regardless of whether they're called
|
|
436
|
-
immediately or as background tasks.
|
|
437
|
-
"""
|
|
438
|
-
|
|
439
|
-
async def __aenter__(self) -> DocketProgress:
|
|
440
|
-
# Try to get execution from Docket worker context
|
|
441
|
-
try:
|
|
442
|
-
return await super().__aenter__()
|
|
443
|
-
except LookupError:
|
|
444
|
-
# Not in worker context - return in-memory progress
|
|
445
|
-
docket = _current_docket.get()
|
|
446
|
-
if docket is None:
|
|
447
|
-
raise RuntimeError(
|
|
448
|
-
"Progress dependency requires a FastMCP server context."
|
|
449
|
-
) from None
|
|
450
|
-
|
|
451
|
-
# Return in-memory progress for immediate execution
|
|
452
|
-
return InMemoryProgress()
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
class _CurrentFastMCP(Dependency):
|
|
456
|
-
"""Internal dependency class for CurrentFastMCP."""
|
|
457
|
-
|
|
458
|
-
async def __aenter__(self):
|
|
749
|
+
async def __aenter__(self) -> FastMCP:
|
|
459
750
|
server_ref = _current_server.get()
|
|
460
751
|
if server_ref is None:
|
|
461
752
|
raise RuntimeError("No FastMCP server instance in context")
|
|
@@ -464,8 +755,11 @@ class _CurrentFastMCP(Dependency):
|
|
|
464
755
|
raise RuntimeError("FastMCP server instance is no longer available")
|
|
465
756
|
return server
|
|
466
757
|
|
|
758
|
+
async def __aexit__(self, *args: object) -> None:
|
|
759
|
+
pass
|
|
760
|
+
|
|
467
761
|
|
|
468
|
-
def CurrentFastMCP():
|
|
762
|
+
def CurrentFastMCP() -> FastMCP:
|
|
469
763
|
"""Get the current FastMCP server instance.
|
|
470
764
|
|
|
471
765
|
This dependency provides access to the active FastMCP server.
|
|
@@ -490,137 +784,133 @@ def CurrentFastMCP():
|
|
|
490
784
|
return cast(FastMCP, _CurrentFastMCP())
|
|
491
785
|
|
|
492
786
|
|
|
493
|
-
|
|
494
|
-
"""Get the current FastMCP server instance directly.
|
|
787
|
+
# --- Progress dependency ---
|
|
495
788
|
|
|
496
|
-
Returns:
|
|
497
|
-
The active FastMCP server
|
|
498
789
|
|
|
499
|
-
|
|
500
|
-
|
|
790
|
+
@runtime_checkable
|
|
791
|
+
class ProgressLike(Protocol):
|
|
792
|
+
"""Protocol for progress tracking interface.
|
|
793
|
+
|
|
794
|
+
Defines the common interface between InMemoryProgress (server context)
|
|
795
|
+
and Docket's Progress (worker context).
|
|
501
796
|
"""
|
|
502
797
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if server is None:
|
|
508
|
-
raise RuntimeError("FastMCP server instance is no longer available")
|
|
509
|
-
return server
|
|
798
|
+
@property
|
|
799
|
+
def current(self) -> int | None:
|
|
800
|
+
"""Current progress value."""
|
|
801
|
+
...
|
|
510
802
|
|
|
803
|
+
@property
|
|
804
|
+
def total(self) -> int:
|
|
805
|
+
"""Total/target progress value."""
|
|
806
|
+
...
|
|
511
807
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
request = request_ctx.get().request
|
|
808
|
+
@property
|
|
809
|
+
def message(self) -> str | None:
|
|
810
|
+
"""Current progress message."""
|
|
811
|
+
...
|
|
517
812
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
request = _current_http_request.get()
|
|
813
|
+
async def set_total(self, total: int) -> None:
|
|
814
|
+
"""Set the total/target value for progress tracking."""
|
|
815
|
+
...
|
|
522
816
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
817
|
+
async def increment(self, amount: int = 1) -> None:
|
|
818
|
+
"""Atomically increment the current progress value."""
|
|
819
|
+
...
|
|
526
820
|
|
|
821
|
+
async def set_message(self, message: str | None) -> None:
|
|
822
|
+
"""Update the progress status message."""
|
|
823
|
+
...
|
|
527
824
|
|
|
528
|
-
def get_http_headers(include_all: bool = False) -> dict[str, str]:
|
|
529
|
-
"""
|
|
530
|
-
Extract headers from the current HTTP request if available.
|
|
531
825
|
|
|
532
|
-
|
|
533
|
-
|
|
826
|
+
class InMemoryProgress:
|
|
827
|
+
"""In-memory progress tracker for immediate tool execution.
|
|
534
828
|
|
|
535
|
-
|
|
536
|
-
|
|
829
|
+
Provides the same interface as Docket's Progress but stores state in memory
|
|
830
|
+
instead of Redis. Useful for testing and immediate execution where
|
|
831
|
+
progress doesn't need to be observable across processes.
|
|
537
832
|
"""
|
|
538
|
-
if include_all:
|
|
539
|
-
exclude_headers = set()
|
|
540
|
-
else:
|
|
541
|
-
exclude_headers = {
|
|
542
|
-
"host",
|
|
543
|
-
"content-length",
|
|
544
|
-
"connection",
|
|
545
|
-
"transfer-encoding",
|
|
546
|
-
"upgrade",
|
|
547
|
-
"te",
|
|
548
|
-
"keep-alive",
|
|
549
|
-
"expect",
|
|
550
|
-
"accept",
|
|
551
|
-
# Proxy-related headers
|
|
552
|
-
"proxy-authenticate",
|
|
553
|
-
"proxy-authorization",
|
|
554
|
-
"proxy-connection",
|
|
555
|
-
# MCP-related headers
|
|
556
|
-
"mcp-session-id",
|
|
557
|
-
}
|
|
558
|
-
# (just in case)
|
|
559
|
-
if not all(h.lower() == h for h in exclude_headers):
|
|
560
|
-
raise ValueError("Excluded headers must be lowercase")
|
|
561
|
-
headers = {}
|
|
562
833
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if lower_name not in exclude_headers:
|
|
568
|
-
headers[lower_name] = str(value)
|
|
569
|
-
return headers
|
|
570
|
-
except RuntimeError:
|
|
571
|
-
return {}
|
|
834
|
+
def __init__(self) -> None:
|
|
835
|
+
self._current: int | None = None
|
|
836
|
+
self._total: int = 1
|
|
837
|
+
self._message: str | None = None
|
|
572
838
|
|
|
839
|
+
async def __aenter__(self) -> InMemoryProgress:
|
|
840
|
+
return self
|
|
573
841
|
|
|
574
|
-
def
|
|
575
|
-
|
|
576
|
-
Get the FastMCP access token from the current context.
|
|
842
|
+
async def __aexit__(self, *args: object) -> None:
|
|
843
|
+
pass
|
|
577
844
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
request is available.
|
|
845
|
+
@property
|
|
846
|
+
def current(self) -> int | None:
|
|
847
|
+
return self._current
|
|
582
848
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
access_token: _SDKAccessToken | None = None
|
|
849
|
+
@property
|
|
850
|
+
def total(self) -> int:
|
|
851
|
+
return self._total
|
|
587
852
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
try:
|
|
592
|
-
request = get_http_request()
|
|
593
|
-
user = request.scope.get("user")
|
|
594
|
-
if isinstance(user, AuthenticatedUser):
|
|
595
|
-
access_token = user.access_token
|
|
596
|
-
except RuntimeError:
|
|
597
|
-
# No HTTP request available, fall back to context var
|
|
598
|
-
pass
|
|
853
|
+
@property
|
|
854
|
+
def message(self) -> str | None:
|
|
855
|
+
return self._message
|
|
599
856
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
857
|
+
async def set_total(self, total: int) -> None:
|
|
858
|
+
"""Set the total/target value for progress tracking."""
|
|
859
|
+
if total < 1:
|
|
860
|
+
raise ValueError("Total must be at least 1")
|
|
861
|
+
self._total = total
|
|
603
862
|
|
|
604
|
-
|
|
605
|
-
|
|
863
|
+
async def increment(self, amount: int = 1) -> None:
|
|
864
|
+
"""Atomically increment the current progress value."""
|
|
865
|
+
if amount < 1:
|
|
866
|
+
raise ValueError("Amount must be at least 1")
|
|
867
|
+
if self._current is None:
|
|
868
|
+
self._current = amount
|
|
869
|
+
else:
|
|
870
|
+
self._current += amount
|
|
606
871
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
872
|
+
async def set_message(self, message: str | None) -> None:
|
|
873
|
+
"""Update the progress status message."""
|
|
874
|
+
self._message = message
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
class Progress(Dependency): # type: ignore[misc]
|
|
878
|
+
"""FastMCP Progress dependency that works in both server and worker contexts.
|
|
879
|
+
|
|
880
|
+
Handles three execution modes:
|
|
881
|
+
- In Docket worker: Uses the execution's progress (observable via Redis)
|
|
882
|
+
- In FastMCP server with Docket: Falls back to in-memory progress
|
|
883
|
+
- In FastMCP server without Docket: Uses in-memory progress
|
|
884
|
+
|
|
885
|
+
This allows tools to use Progress() regardless of whether they're called
|
|
886
|
+
immediately or as background tasks, and regardless of whether pydocket
|
|
887
|
+
is installed.
|
|
888
|
+
"""
|
|
889
|
+
|
|
890
|
+
async def __aenter__(self) -> ProgressLike:
|
|
891
|
+
# Check if we're in a FastMCP server context
|
|
892
|
+
server_ref = _current_server.get()
|
|
893
|
+
if server_ref is None or server_ref() is None:
|
|
894
|
+
raise RuntimeError("Progress dependency requires a FastMCP server context.")
|
|
895
|
+
|
|
896
|
+
# If pydocket is installed, try to use Docket's progress
|
|
897
|
+
if is_docket_available():
|
|
898
|
+
from docket.dependencies import Progress as DocketProgress
|
|
899
|
+
|
|
900
|
+
# Try to get execution from Docket worker context
|
|
901
|
+
try:
|
|
902
|
+
docket_progress = DocketProgress()
|
|
903
|
+
return await docket_progress.__aenter__()
|
|
904
|
+
except LookupError:
|
|
905
|
+
# Not in worker context - fall through to in-memory progress
|
|
906
|
+
pass
|
|
907
|
+
|
|
908
|
+
# Return in-memory progress for immediate execution
|
|
909
|
+
# This is used when:
|
|
910
|
+
# 1. pydocket is not installed
|
|
911
|
+
# 2. Docket is not running (no task-enabled components)
|
|
912
|
+
# 3. In server context (not worker context)
|
|
913
|
+
return InMemoryProgress()
|
|
914
|
+
|
|
915
|
+
async def __aexit__(self, *args: object) -> None:
|
|
916
|
+
pass
|