fastmcp 2.12.5__py3-none-any.whl → 2.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. fastmcp/__init__.py +2 -23
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +19 -33
  5. fastmcp/cli/install/claude_code.py +6 -6
  6. fastmcp/cli/install/claude_desktop.py +3 -3
  7. fastmcp/cli/install/cursor.py +18 -12
  8. fastmcp/cli/install/gemini_cli.py +3 -3
  9. fastmcp/cli/install/mcp_json.py +3 -3
  10. fastmcp/cli/install/shared.py +0 -15
  11. fastmcp/cli/run.py +13 -8
  12. fastmcp/cli/tasks.py +110 -0
  13. fastmcp/client/__init__.py +9 -9
  14. fastmcp/client/auth/oauth.py +123 -225
  15. fastmcp/client/client.py +697 -95
  16. fastmcp/client/elicitation.py +11 -5
  17. fastmcp/client/logging.py +18 -14
  18. fastmcp/client/messages.py +7 -5
  19. fastmcp/client/oauth_callback.py +85 -171
  20. fastmcp/client/roots.py +2 -1
  21. fastmcp/client/sampling.py +1 -1
  22. fastmcp/client/tasks.py +614 -0
  23. fastmcp/client/transports.py +117 -30
  24. fastmcp/contrib/component_manager/__init__.py +1 -1
  25. fastmcp/contrib/component_manager/component_manager.py +2 -2
  26. fastmcp/contrib/component_manager/component_service.py +10 -26
  27. fastmcp/contrib/mcp_mixin/README.md +32 -1
  28. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  29. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  30. fastmcp/dependencies.py +25 -0
  31. fastmcp/experimental/sampling/handlers/openai.py +3 -3
  32. fastmcp/experimental/server/openapi/__init__.py +20 -21
  33. fastmcp/experimental/utilities/openapi/__init__.py +16 -47
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +54 -51
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +43 -21
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +161 -61
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -14
  45. fastmcp/server/auth/auth.py +197 -46
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1469 -298
  50. fastmcp/server/auth/oidc_proxy.py +91 -20
  51. fastmcp/server/auth/providers/auth0.py +40 -21
  52. fastmcp/server/auth/providers/aws.py +29 -3
  53. fastmcp/server/auth/providers/azure.py +312 -131
  54. fastmcp/server/auth/providers/debug.py +114 -0
  55. fastmcp/server/auth/providers/descope.py +86 -29
  56. fastmcp/server/auth/providers/discord.py +308 -0
  57. fastmcp/server/auth/providers/github.py +29 -8
  58. fastmcp/server/auth/providers/google.py +48 -9
  59. fastmcp/server/auth/providers/in_memory.py +29 -5
  60. fastmcp/server/auth/providers/introspection.py +281 -0
  61. fastmcp/server/auth/providers/jwt.py +48 -31
  62. fastmcp/server/auth/providers/oci.py +233 -0
  63. fastmcp/server/auth/providers/scalekit.py +238 -0
  64. fastmcp/server/auth/providers/supabase.py +188 -0
  65. fastmcp/server/auth/providers/workos.py +35 -17
  66. fastmcp/server/context.py +236 -116
  67. fastmcp/server/dependencies.py +503 -18
  68. fastmcp/server/elicitation.py +286 -48
  69. fastmcp/server/event_store.py +177 -0
  70. fastmcp/server/http.py +71 -20
  71. fastmcp/server/low_level.py +165 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +50 -39
  76. fastmcp/server/middleware/middleware.py +29 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi/__init__.py +35 -0
  80. fastmcp/{experimental/server → server}/openapi/components.py +15 -10
  81. fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
  82. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  83. fastmcp/server/proxy.py +72 -48
  84. fastmcp/server/server.py +1415 -733
  85. fastmcp/server/tasks/__init__.py +21 -0
  86. fastmcp/server/tasks/capabilities.py +22 -0
  87. fastmcp/server/tasks/config.py +89 -0
  88. fastmcp/server/tasks/converters.py +205 -0
  89. fastmcp/server/tasks/handlers.py +356 -0
  90. fastmcp/server/tasks/keys.py +93 -0
  91. fastmcp/server/tasks/protocol.py +355 -0
  92. fastmcp/server/tasks/subscriptions.py +205 -0
  93. fastmcp/settings.py +125 -113
  94. fastmcp/tools/__init__.py +1 -1
  95. fastmcp/tools/tool.py +138 -55
  96. fastmcp/tools/tool_manager.py +30 -112
  97. fastmcp/tools/tool_transform.py +12 -21
  98. fastmcp/utilities/cli.py +67 -28
  99. fastmcp/utilities/components.py +10 -5
  100. fastmcp/utilities/inspect.py +79 -23
  101. fastmcp/utilities/json_schema.py +4 -4
  102. fastmcp/utilities/json_schema_type.py +8 -8
  103. fastmcp/utilities/logging.py +118 -8
  104. fastmcp/utilities/mcp_config.py +1 -2
  105. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  106. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  107. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  108. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
  109. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  110. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  111. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  112. fastmcp/utilities/openapi/__init__.py +63 -0
  113. fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
  114. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  115. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
  116. fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
  117. fastmcp/utilities/tests.py +92 -5
  118. fastmcp/utilities/types.py +86 -16
  119. fastmcp/utilities/ui.py +626 -0
  120. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
  121. fastmcp-2.14.0.dist-info/RECORD +156 -0
  122. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
  123. fastmcp/cli/claude.py +0 -135
  124. fastmcp/server/auth/providers/bearer.py +0 -25
  125. fastmcp/server/openapi.py +0 -1083
  126. fastmcp/utilities/openapi.py +0 -1568
  127. fastmcp/utilities/storage.py +0 -204
  128. fastmcp-2.12.5.dist-info/RECORD +0 -134
  129. fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  130. fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
  131. fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
  132. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  133. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,30 +1,255 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ import contextlib
4
+ import inspect
5
+ import weakref
6
+ from collections.abc import AsyncGenerator, Callable
7
+ from contextlib import AsyncExitStack, asynccontextmanager
8
+ from contextvars import ContextVar
9
+ from functools import lru_cache
10
+ from typing import TYPE_CHECKING, Any, cast, get_type_hints
4
11
 
12
+ from docket.dependencies import Dependency, _Depends, get_dependency_parameters
13
+ from docket.dependencies import Progress as DocketProgress
5
14
  from mcp.server.auth.middleware.auth_context import (
6
15
  get_access_token as _sdk_get_access_token,
7
16
  )
17
+ from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser
8
18
  from mcp.server.auth.provider import (
9
19
  AccessToken as _SDKAccessToken,
10
20
  )
21
+ from mcp.server.lowlevel.server import request_ctx
11
22
  from starlette.requests import Request
12
23
 
13
24
  from fastmcp.server.auth import AccessToken
25
+ from fastmcp.server.http import _current_http_request
26
+ from fastmcp.utilities.types import is_class_member_of_type
14
27
 
15
28
  if TYPE_CHECKING:
29
+ from docket import Docket
30
+ from docket.worker import Worker
31
+
16
32
  from fastmcp.server.context import Context
33
+ from fastmcp.server.server import FastMCP
34
+
35
+ # ContextVars for tracking Docket infrastructure
36
+ _current_docket: ContextVar[Docket | None] = ContextVar("docket", default=None) # type: ignore[assignment]
37
+ _current_worker: ContextVar[Worker | None] = ContextVar("worker", default=None) # type: ignore[assignment]
38
+ _current_server: ContextVar[weakref.ref[FastMCP] | None] = ContextVar( # type: ignore[invalid-assignment]
39
+ "server", default=None
40
+ )
17
41
 
18
42
  __all__ = [
43
+ "AccessToken",
44
+ "CurrentContext",
45
+ "CurrentDocket",
46
+ "CurrentFastMCP",
47
+ "CurrentWorker",
48
+ "Progress",
49
+ "get_access_token",
19
50
  "get_context",
20
- "get_http_request",
21
51
  "get_http_headers",
22
- "get_access_token",
23
- "AccessToken",
52
+ "get_http_request",
53
+ "get_server",
54
+ "resolve_dependencies",
55
+ "without_injected_parameters",
24
56
  ]
25
57
 
26
58
 
27
- # --- Context ---
59
+ def _find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None:
60
+ """Find the name of the kwarg that is of type kwarg_type.
61
+
62
+ This is the legacy dependency injection approach, used specifically for
63
+ injecting the Context object when a function parameter is typed as Context.
64
+
65
+ Includes union types that contain the kwarg_type, as well as Annotated types.
66
+ """
67
+
68
+ if inspect.ismethod(fn) and hasattr(fn, "__func__"):
69
+ fn = fn.__func__
70
+
71
+ try:
72
+ type_hints = get_type_hints(fn, include_extras=True)
73
+ except Exception:
74
+ type_hints = getattr(fn, "__annotations__", {})
75
+
76
+ sig = inspect.signature(fn)
77
+ for name, param in sig.parameters.items():
78
+ annotation = type_hints.get(name, param.annotation)
79
+ if is_class_member_of_type(annotation, kwarg_type):
80
+ return name
81
+ return None
82
+
83
+
84
+ @lru_cache(maxsize=5000)
85
+ def without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]:
86
+ """Create a wrapper function without injected parameters.
87
+
88
+ Returns a wrapper that excludes Context and Docket dependency parameters,
89
+ making it safe to use with Pydantic TypeAdapter for schema generation and
90
+ validation. The wrapper internally handles all dependency resolution and
91
+ Context injection when called.
92
+
93
+ Args:
94
+ fn: Original function with Context and/or dependencies
95
+
96
+ Returns:
97
+ Async wrapper function without injected parameters
98
+ """
99
+ from fastmcp.server.context import Context
100
+
101
+ # Identify parameters to exclude
102
+ context_kwarg = _find_kwarg_by_type(fn, Context)
103
+ dependency_params = get_dependency_parameters(fn)
104
+
105
+ exclude = set()
106
+ if context_kwarg:
107
+ exclude.add(context_kwarg)
108
+ if dependency_params:
109
+ exclude.update(dependency_params.keys())
110
+
111
+ if not exclude:
112
+ return fn
113
+
114
+ # Build new signature with only user parameters
115
+ sig = inspect.signature(fn)
116
+ user_params = [
117
+ param for name, param in sig.parameters.items() if name not in exclude
118
+ ]
119
+ new_sig = inspect.Signature(user_params)
120
+
121
+ # Create async wrapper that handles dependency resolution
122
+ async def wrapper(**user_kwargs: Any) -> Any:
123
+ async with resolve_dependencies(fn, user_kwargs) as resolved_kwargs:
124
+ result = fn(**resolved_kwargs)
125
+ if inspect.isawaitable(result):
126
+ result = await result
127
+ return result
128
+
129
+ # Set wrapper metadata (only parameter annotations, not return type)
130
+ wrapper.__signature__ = new_sig # type: ignore
131
+ wrapper.__annotations__ = {
132
+ k: v
133
+ for k, v in getattr(fn, "__annotations__", {}).items()
134
+ if k not in exclude and k != "return"
135
+ }
136
+ wrapper.__name__ = getattr(fn, "__name__", "wrapper")
137
+ wrapper.__doc__ = getattr(fn, "__doc__", None)
138
+
139
+ return wrapper
140
+
141
+
142
+ @asynccontextmanager
143
+ async def _resolve_fastmcp_dependencies(
144
+ fn: Callable[..., Any], arguments: dict[str, Any]
145
+ ) -> AsyncGenerator[dict[str, Any], None]:
146
+ """Resolve Docket dependencies for a FastMCP function.
147
+
148
+ Sets up the minimal context needed for Docket's Depends() to work:
149
+ - A cache for resolved dependencies
150
+ - An AsyncExitStack for managing context manager lifetimes
151
+
152
+ The Docket instance (for CurrentDocket dependency) is managed separately
153
+ by the server's lifespan and made available via ContextVar.
154
+
155
+ Note: This does NOT set up Docket's Execution context. If user code needs
156
+ Docket-specific dependencies like TaskArgument(), TaskKey(), etc., those
157
+ will fail with clear errors about missing context.
158
+
159
+ Args:
160
+ fn: The function to resolve dependencies for
161
+ arguments: The arguments passed to the function
162
+
163
+ Yields:
164
+ Dictionary of resolved dependencies merged with provided arguments
165
+ """
166
+ dependency_params = get_dependency_parameters(fn)
167
+
168
+ if not dependency_params:
169
+ yield arguments
170
+ return
171
+
172
+ # Initialize dependency cache and exit stack
173
+ cache_token = _Depends.cache.set({})
174
+ try:
175
+ async with AsyncExitStack() as stack:
176
+ stack_token = _Depends.stack.set(stack)
177
+ try:
178
+ resolved: dict[str, Any] = {}
179
+
180
+ for parameter, dependency in dependency_params.items():
181
+ # If argument was explicitly provided, use that instead
182
+ if parameter in arguments:
183
+ resolved[parameter] = arguments[parameter]
184
+ continue
185
+
186
+ # Resolve the dependency
187
+ try:
188
+ resolved[parameter] = await stack.enter_async_context(
189
+ dependency
190
+ )
191
+ except Exception as error:
192
+ fn_name = getattr(fn, "__name__", repr(fn))
193
+ raise RuntimeError(
194
+ f"Failed to resolve dependency '{parameter}' for {fn_name}"
195
+ ) from error
196
+
197
+ # Merge resolved dependencies with provided arguments
198
+ final_arguments = {**arguments, **resolved}
199
+
200
+ yield final_arguments
201
+ finally:
202
+ _Depends.stack.reset(stack_token)
203
+ finally:
204
+ _Depends.cache.reset(cache_token)
205
+
206
+
207
+ @asynccontextmanager
208
+ async def resolve_dependencies(
209
+ fn: Callable[..., Any], arguments: dict[str, Any]
210
+ ) -> AsyncGenerator[dict[str, Any], None]:
211
+ """Resolve dependencies and inject Context for a FastMCP function.
212
+
213
+ This function:
214
+ 1. Filters out any dependency parameter names from user arguments (security)
215
+ 2. Resolves Docket dependencies
216
+ 3. Injects Context if needed
217
+ 4. Merges everything together
218
+
219
+ The filtering prevents external callers from overriding injected parameters by
220
+ providing values for dependency parameter names. This is a security feature.
221
+
222
+ Args:
223
+ fn: The function to resolve dependencies for
224
+ arguments: User arguments (may contain keys that match dependency names,
225
+ which will be filtered out)
226
+
227
+ Yields:
228
+ Dictionary of filtered user args + resolved dependencies + Context
229
+
230
+ Example:
231
+ ```python
232
+ async with resolve_dependencies(my_tool, {"name": "Alice"}) as kwargs:
233
+ result = my_tool(**kwargs)
234
+ if inspect.isawaitable(result):
235
+ result = await result
236
+ ```
237
+ """
238
+ from fastmcp.server.context import Context
239
+
240
+ # Filter out dependency parameters from user arguments to prevent override
241
+ # This is a security measure - external callers should never be able to
242
+ # provide values for injected parameters
243
+ dependency_params = get_dependency_parameters(fn)
244
+ user_args = {k: v for k, v in arguments.items() if k not in dependency_params}
245
+
246
+ async with _resolve_fastmcp_dependencies(fn, user_args) as resolved_kwargs:
247
+ # Inject Context if needed
248
+ context_kwarg = _find_kwarg_by_type(fn, kwarg_type=Context)
249
+ if context_kwarg and context_kwarg not in resolved_kwargs:
250
+ resolved_kwargs[context_kwarg] = get_context()
251
+
252
+ yield resolved_kwargs
28
253
 
29
254
 
30
255
  def get_context() -> Context:
@@ -36,17 +261,259 @@ def get_context() -> Context:
36
261
  return context
37
262
 
38
263
 
39
- # --- HTTP Request ---
264
+ class _CurrentContext(Dependency):
265
+ """Internal dependency class for CurrentContext."""
40
266
 
267
+ async def __aenter__(self) -> Context:
268
+ return get_context()
41
269
 
42
- def get_http_request() -> Request:
43
- from mcp.server.lowlevel.server import request_ctx
44
270
 
271
+ def CurrentContext() -> Context:
272
+ """Get the current FastMCP Context instance.
273
+
274
+ This dependency provides access to the active FastMCP Context for the
275
+ current MCP operation (tool/resource/prompt call).
276
+
277
+ Returns:
278
+ A dependency that resolves to the active Context instance
279
+
280
+ Raises:
281
+ RuntimeError: If no active context found (during resolution)
282
+
283
+ Example:
284
+ ```python
285
+ from fastmcp.dependencies import CurrentContext
286
+
287
+ @mcp.tool()
288
+ async def log_progress(ctx: Context = CurrentContext()) -> str:
289
+ ctx.report_progress(50, 100, "Halfway done")
290
+ return "Working"
291
+ ```
292
+ """
293
+ return cast("Context", _CurrentContext())
294
+
295
+
296
+ class _CurrentDocket(Dependency):
297
+ """Internal dependency class for CurrentDocket."""
298
+
299
+ async def __aenter__(self) -> Docket:
300
+ # Get Docket from ContextVar (set by _docket_lifespan)
301
+ docket = _current_docket.get()
302
+ if docket is None:
303
+ raise RuntimeError(
304
+ "No Docket instance found. Docket is only available within "
305
+ "a running FastMCP server context."
306
+ )
307
+
308
+ return docket
309
+
310
+
311
+ def CurrentDocket() -> Docket:
312
+ """Get the current Docket instance managed by FastMCP.
313
+
314
+ This dependency provides access to the Docket instance that FastMCP
315
+ automatically creates for background task scheduling.
316
+
317
+ Returns:
318
+ A dependency that resolves to the active Docket instance
319
+
320
+ Raises:
321
+ RuntimeError: If not within a FastMCP server context
322
+
323
+ Example:
324
+ ```python
325
+ from fastmcp.dependencies import CurrentDocket
326
+
327
+ @mcp.tool()
328
+ async def schedule_task(docket: Docket = CurrentDocket()) -> str:
329
+ await docket.add(some_function)(arg1, arg2)
330
+ return "Scheduled"
331
+ ```
332
+ """
333
+ return cast("Docket", _CurrentDocket())
334
+
335
+
336
+ class _CurrentWorker(Dependency):
337
+ """Internal dependency class for CurrentWorker."""
338
+
339
+ async def __aenter__(self) -> Worker:
340
+ worker = _current_worker.get()
341
+ if worker is None:
342
+ raise RuntimeError(
343
+ "No Worker instance found. Worker is only available within "
344
+ "a running FastMCP server context."
345
+ )
346
+
347
+ return worker
348
+
349
+
350
+ def CurrentWorker() -> Worker:
351
+ """Get the current Docket Worker instance managed by FastMCP.
352
+
353
+ This dependency provides access to the Worker instance that FastMCP
354
+ automatically creates for background task processing.
355
+
356
+ Returns:
357
+ A dependency that resolves to the active Worker instance
358
+
359
+ Raises:
360
+ RuntimeError: If not within a FastMCP server context
361
+
362
+ Example:
363
+ ```python
364
+ from fastmcp.dependencies import CurrentWorker
365
+
366
+ @mcp.tool()
367
+ async def check_worker_status(worker: Worker = CurrentWorker()) -> str:
368
+ return f"Worker: {worker.name}"
369
+ ```
370
+ """
371
+ return cast("Worker", _CurrentWorker())
372
+
373
+
374
+ class InMemoryProgress(DocketProgress):
375
+ """In-memory progress tracker for immediate tool execution.
376
+
377
+ Provides the same interface as Progress but stores state in memory
378
+ instead of Redis. Useful for testing and immediate execution where
379
+ progress doesn't need to be observable across processes.
380
+ """
381
+
382
+ def __init__(self) -> None:
383
+ super().__init__()
384
+ self._current: int | None = None
385
+ self._total: int = 1
386
+ self._message: str | None = None
387
+
388
+ async def __aenter__(self) -> DocketProgress:
389
+ return self
390
+
391
+ @property
392
+ def current(self) -> int | None:
393
+ return self._current
394
+
395
+ @property
396
+ def total(self) -> int:
397
+ return self._total
398
+
399
+ @property
400
+ def message(self) -> str | None:
401
+ return self._message
402
+
403
+ async def set_total(self, total: int) -> None:
404
+ """Set the total/target value for progress tracking."""
405
+ if total < 1:
406
+ raise ValueError("Total must be at least 1")
407
+ self._total = total
408
+
409
+ async def increment(self, amount: int = 1) -> None:
410
+ """Atomically increment the current progress value."""
411
+ if amount < 1:
412
+ raise ValueError("Amount must be at least 1")
413
+ if self._current is None:
414
+ self._current = amount
415
+ else:
416
+ self._current += amount
417
+
418
+ async def set_message(self, message: str | None) -> None:
419
+ """Update the progress status message."""
420
+ self._message = message
421
+
422
+
423
+ class Progress(DocketProgress):
424
+ """FastMCP Progress dependency that works in both server and worker contexts.
425
+
426
+ Extends Docket's Progress to handle two execution modes:
427
+ - In Docket worker: Uses the execution's progress (standard Docket behavior)
428
+ - In FastMCP server: Uses in-memory progress (not observable remotely)
429
+
430
+ This allows tools to use Progress() regardless of whether they're called
431
+ immediately or as background tasks.
432
+ """
433
+
434
+ async def __aenter__(self) -> DocketProgress:
435
+ # Try to get execution from Docket worker context
436
+ try:
437
+ return await super().__aenter__()
438
+ except LookupError:
439
+ # Not in worker context - return in-memory progress
440
+ docket = _current_docket.get()
441
+ if docket is None:
442
+ raise RuntimeError(
443
+ "Progress dependency requires a FastMCP server context."
444
+ ) from None
445
+
446
+ # Return in-memory progress for immediate execution
447
+ return InMemoryProgress()
448
+
449
+
450
+ class _CurrentFastMCP(Dependency):
451
+ """Internal dependency class for CurrentFastMCP."""
452
+
453
+ async def __aenter__(self):
454
+ server_ref = _current_server.get()
455
+ if server_ref is None:
456
+ raise RuntimeError("No FastMCP server instance in context")
457
+ server = server_ref()
458
+ if server is None:
459
+ raise RuntimeError("FastMCP server instance is no longer available")
460
+ return server
461
+
462
+
463
+ def CurrentFastMCP():
464
+ """Get the current FastMCP server instance.
465
+
466
+ This dependency provides access to the active FastMCP server.
467
+
468
+ Returns:
469
+ A dependency that resolves to the active FastMCP server
470
+
471
+ Raises:
472
+ RuntimeError: If no server in context (during resolution)
473
+
474
+ Example:
475
+ ```python
476
+ from fastmcp.dependencies import CurrentFastMCP
477
+
478
+ @mcp.tool()
479
+ async def introspect(server: FastMCP = CurrentFastMCP()) -> str:
480
+ return f"Server: {server.name}"
481
+ ```
482
+ """
483
+ from fastmcp.server.server import FastMCP
484
+
485
+ return cast(FastMCP, _CurrentFastMCP())
486
+
487
+
488
+ def get_server():
489
+ """Get the current FastMCP server instance directly.
490
+
491
+ Returns:
492
+ The active FastMCP server
493
+
494
+ Raises:
495
+ RuntimeError: If no server in context
496
+ """
497
+
498
+ server_ref = _current_server.get()
499
+ if server_ref is None:
500
+ raise RuntimeError("No FastMCP server instance in context")
501
+ server = server_ref()
502
+ if server is None:
503
+ raise RuntimeError("FastMCP server instance is no longer available")
504
+ return server
505
+
506
+
507
+ def get_http_request() -> Request:
508
+ # Try MCP SDK's request_ctx first (set during normal MCP request handling)
45
509
  request = None
46
- try:
510
+ with contextlib.suppress(LookupError):
47
511
  request = request_ctx.get().request
48
- except LookupError:
49
- pass
512
+
513
+ # Fallback to FastMCP's HTTP context variable
514
+ # This is needed during `on_initialize` middleware where request_ctx isn't set yet
515
+ if request is None:
516
+ request = _current_http_request.get()
50
517
 
51
518
  if request is None:
52
519
  raise RuntimeError("No active HTTP request found.")
@@ -99,24 +566,42 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
99
566
  return {}
100
567
 
101
568
 
102
- # --- Access Token ---
103
-
104
-
105
569
  def get_access_token() -> AccessToken | None:
106
570
  """
107
571
  Get the FastMCP access token from the current context.
108
572
 
573
+ This function first tries to get the token from the current HTTP request's scope,
574
+ which is more reliable for long-lived connections where the SDK's auth_context_var
575
+ may become stale after token refresh. Falls back to the SDK's context var if no
576
+ request is available.
577
+
109
578
  Returns:
110
579
  The access token if an authenticated user is available, None otherwise.
111
580
  """
112
- #
113
- access_token: _SDKAccessToken | None = _sdk_get_access_token()
581
+ access_token: _SDKAccessToken | None = None
582
+
583
+ # First, try to get from current HTTP request's scope (issue #1863)
584
+ # This is more reliable than auth_context_var for Streamable HTTP sessions
585
+ # where tokens may be refreshed between MCP messages
586
+ try:
587
+ request = get_http_request()
588
+ user = request.scope.get("user")
589
+ if isinstance(user, AuthenticatedUser):
590
+ access_token = user.access_token
591
+ except RuntimeError:
592
+ # No HTTP request available, fall back to context var
593
+ pass
594
+
595
+ # Fall back to SDK's context var if we didn't get a token from the request
596
+ if access_token is None:
597
+ access_token = _sdk_get_access_token()
114
598
 
115
599
  if access_token is None or isinstance(access_token, AccessToken):
116
600
  return access_token
117
601
 
118
- # If the object is not a FastMCP AccessToken, convert it to one if the fields are compatible
119
- # This is a workaround for the case where the SDK returns a different type
602
+ # If the object is not a FastMCP AccessToken, convert it to one if the
603
+ # fields are compatible (e.g. `claims` is not present in the SDK's AccessToken).
604
+ # This is a workaround for the case where the SDK or auth provider returns a different type
120
605
  # If it fails, it will raise a TypeError
121
606
  try:
122
607
  access_token_as_dict = access_token.model_dump()