fastmcp 2.12.5__py3-none-any.whl → 2.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +2 -23
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +19 -33
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/run.py +13 -8
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +123 -225
- fastmcp/client/client.py +697 -95
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/logging.py +18 -14
- fastmcp/client/messages.py +7 -5
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +117 -30
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +10 -26
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +3 -3
- fastmcp/experimental/server/openapi/__init__.py +20 -21
- fastmcp/experimental/utilities/openapi/__init__.py +16 -47
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +54 -51
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +43 -21
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +161 -61
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -14
- fastmcp/server/auth/auth.py +197 -46
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1469 -298
- fastmcp/server/auth/oidc_proxy.py +91 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +29 -5
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +236 -116
- fastmcp/server/dependencies.py +503 -18
- fastmcp/server/elicitation.py +286 -48
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +71 -20
- fastmcp/server/low_level.py +165 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +15 -10
- fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +72 -48
- fastmcp/server/server.py +1415 -733
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +125 -113
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +138 -55
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -21
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +10 -5
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +8 -8
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
- fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
- fastmcp/utilities/tests.py +92 -5
- fastmcp/utilities/types.py +86 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
- fastmcp-2.14.0.dist-info/RECORD +156 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1083
- fastmcp/utilities/openapi.py +0 -1568
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
- fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/dependencies.py
CHANGED
|
@@ -1,30 +1,255 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
"
|
|
23
|
-
"
|
|
52
|
+
"get_http_request",
|
|
53
|
+
"get_server",
|
|
54
|
+
"resolve_dependencies",
|
|
55
|
+
"without_injected_parameters",
|
|
24
56
|
]
|
|
25
57
|
|
|
26
58
|
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
510
|
+
with contextlib.suppress(LookupError):
|
|
47
511
|
request = request_ctx.get().request
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
#
|
|
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()
|