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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -1,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.types import is_class_member_of_type
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
- def _find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None:
61
- """Find the name of the kwarg that is of type kwarg_type.
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
- This is the legacy dependency injection approach, used specifically for
64
- injecting the Context object when a function parameter is typed as Context.
90
+ def require_docket(feature: str) -> None:
91
+ """Raise ImportError with install instructions if docket not available.
65
92
 
66
- Includes union types that contain the kwarg_type, as well as Annotated types.
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
- sig = inspect.signature(fn)
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, kwarg_type):
81
- return name
82
- return None
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 = _find_kwarg_by_type(fn, Context)
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
- result = fn(**resolved_kwargs)
126
- if inspect.isawaitable(result):
127
- result = await result
128
- return result
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 and inject Context for a FastMCP function.
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 Docket dependencies
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 + Context
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
- def get_context() -> Context:
261
- from fastmcp.server.context import _current_context
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
- class _CurrentContext(Dependency):
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
- """Internal dependency class for CurrentDocket."""
657
+ class _CurrentDocket(Dependency): # type: ignore[misc]
658
+ """Async context manager for Docket dependency."""
303
659
 
304
660
  async def __aenter__(self) -> Docket:
305
- # Get Docket from ContextVar (set by _docket_lifespan)
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 available within "
310
- "a running FastMCP server context."
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
- """Internal dependency class for CurrentWorker."""
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 available within "
349
- "a running FastMCP server context."
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 InMemoryProgress(DocketProgress):
380
- """In-memory progress tracker for immediate tool execution.
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 increment(self, amount: int = 1) -> None:
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
- def get_server():
494
- """Get the current FastMCP server instance directly.
787
+ # --- Progress dependency ---
495
788
 
496
- Returns:
497
- The active FastMCP server
498
789
 
499
- Raises:
500
- RuntimeError: If no server in context
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
- server_ref = _current_server.get()
504
- if server_ref is None:
505
- raise RuntimeError("No FastMCP server instance in context")
506
- server = server_ref()
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
- def get_http_request() -> Request:
513
- # Try MCP SDK's request_ctx first (set during normal MCP request handling)
514
- request = None
515
- with contextlib.suppress(LookupError):
516
- request = request_ctx.get().request
808
+ @property
809
+ def message(self) -> str | None:
810
+ """Current progress message."""
811
+ ...
517
812
 
518
- # Fallback to FastMCP's HTTP context variable
519
- # This is needed during `on_initialize` middleware where request_ctx isn't set yet
520
- if request is None:
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
- if request is None:
524
- raise RuntimeError("No active HTTP request found.")
525
- return request
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
- Never raises an exception, even if there is no active HTTP request (in which case
533
- an empty dict is returned).
826
+ class InMemoryProgress:
827
+ """In-memory progress tracker for immediate tool execution.
534
828
 
535
- By default, strips problematic headers like `content-length` that cause issues if forwarded to downstream clients.
536
- If `include_all` is True, all headers are returned.
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
- try:
564
- request = get_http_request()
565
- for name, value in request.headers.items():
566
- lower_name = name.lower()
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 get_access_token() -> AccessToken | None:
575
- """
576
- Get the FastMCP access token from the current context.
842
+ async def __aexit__(self, *args: object) -> None:
843
+ pass
577
844
 
578
- This function first tries to get the token from the current HTTP request's scope,
579
- which is more reliable for long-lived connections where the SDK's auth_context_var
580
- may become stale after token refresh. Falls back to the SDK's context var if no
581
- request is available.
845
+ @property
846
+ def current(self) -> int | None:
847
+ return self._current
582
848
 
583
- Returns:
584
- The access token if an authenticated user is available, None otherwise.
585
- """
586
- access_token: _SDKAccessToken | None = None
849
+ @property
850
+ def total(self) -> int:
851
+ return self._total
587
852
 
588
- # First, try to get from current HTTP request's scope (issue #1863)
589
- # This is more reliable than auth_context_var for Streamable HTTP sessions
590
- # where tokens may be refreshed between MCP messages
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
- # Fall back to SDK's context var if we didn't get a token from the request
601
- if access_token is None:
602
- access_token = _sdk_get_access_token()
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
- if access_token is None or isinstance(access_token, AccessToken):
605
- return access_token
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
- # If the object is not a FastMCP AccessToken, convert it to one if the
608
- # fields are compatible (e.g. `claims` is not present in the SDK's AccessToken).
609
- # This is a workaround for the case where the SDK or auth provider returns a different type
610
- # If it fails, it will raise a TypeError
611
- try:
612
- access_token_as_dict = access_token.model_dump()
613
- return AccessToken(
614
- token=access_token_as_dict["token"],
615
- client_id=access_token_as_dict["client_id"],
616
- scopes=access_token_as_dict["scopes"],
617
- # Optional fields
618
- expires_at=access_token_as_dict.get("expires_at"),
619
- resource_owner=access_token_as_dict.get("resource_owner"),
620
- claims=access_token_as_dict.get("claims"),
621
- )
622
- except Exception as e:
623
- raise TypeError(
624
- f"Expected fastmcp.server.auth.auth.AccessToken, got {type(access_token).__name__}. "
625
- "Ensure the SDK is using the correct AccessToken type."
626
- ) from e
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