* Scope tasks to authorization context, not session
Tasks were keyed by the transport-layer Mcp-Session-Id, which is
server-assigned and changes on reconnect — so clients lost access to
their running tasks after any connection interruption.
The MCP spec says tasks should be bound to authorization context, not
session. This replaces session_id with task_scope (derived from
AccessToken.client_id, URL-encoded) in all task data Redis keys and
Docket task keys. When no auth is configured, a "_" sentinel is used
and security comes from UUID task ID entropy per the spec.
Session ID is still used for transport-level concerns (notification
queues, subscriber registration) and is now stored in the
TaskContextSnapshot payload so background workers can still deliver
notifications.
Also extracts all the task context infrastructure (TaskContextInfo,
TaskContextSnapshot, snapshot loading, session/server registries) from
server/dependencies.py into a new server/tasks/context.py to keep the
DI module from sprawling further.
Closes#3758🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Rename _redis_key to _snapshot_redis_key
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Document in-process session registry as an optimization
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Tidy imports and docstrings
Hoist imports where safe, keep subscriptions/notifications deferred in
handlers.py since they pull in docket at module level. Sharpen docstrings
on keys.py and context.py so each module owns its lane. Clean up the
re-export block in dependencies.py.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix misleading comment on re-export block
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Tighten task scope: include sub claim, partition keyspaces
Addresses review feedback on #3800:
- Compose task scope from client_id and the JWT sub claim (when present)
so fixed-OAuth deployments isolate per user, not just per client.
- Replace the "_" anonymous sentinel with a tagged keyspace partition.
Docket keys are now auth:{enc_scope}:... or anon:..., and Redis keys
use fastmcp:task:auth:{enc_scope}:... or fastmcp:task:anon:...,
routed through a single task_redis_prefix() helper.
- get_task_scope() returns the raw scope (or None); encoding happens
once at the keys.py boundary, collapsing the previous double-quote
invariant.
- Drop the dormant fallback in notifications.py that routed
input_required relays into the anon keyspace when task_scope was
missing -- log and skip instead.
- Add comprehensive parser/encoder tests in test_task_keys.py covering
round-trips, malformed keys, and adversarial scopes ("anon", "_", and
scopes containing : / | %).
- Add cross-scope rejection tests: distinct client_ids, distinct sub
claims under a shared client_id, and authenticated vs anonymous.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete entirely commented-out test_run_server.py (99 lines dead code)
- Fix test_pydantic_model_with_stringified_json_no_strict: replace
try/except-both-branches-pass with clear pytest.raises assertion
- Fix test_path_traversal_blocked: remove dead assertions after
pytest.raises (lines after raise never execute)
Error handling middleware test fixes are in a separate PR (#3858)
which also fixes the underlying RetryMiddleware bug.
🤖 Generated with Claude Code
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Retry when LLM returns text instead of calling final_response tool
Instead of raising RuntimeError immediately when the LLM returns a text
response instead of calling the `final_response` tool for structured
output, retry up to 3 times with an explicit nudge message asking the
model to use the tool. This mirrors the existing retry behavior for
validation errors but with a separate, smaller cap.
Fixes#3847
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add tests for text response retry logic
Tests cover:
- Text response followed by successful final_response (retry works)
- Text response exceeding max retries (raises RuntimeError)
- Nudge message appended to history on retry
- No retry when result_type is None (text is valid)
Addresses review feedback from PR review tool (v1 flagged missing tests as high severity).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Slim down text response retry tests
Remove test_nudge_message_in_history (implementation detail).
Reduce boilerplate in remaining 3 tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix ruff format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the ___ separator for FastMCPApp backend tool routing with a
deterministic hash(app_name, tool_name) prefix, and replaces the shared
singleton prefab renderer resource with per-tool resources synthesized
on demand.
Backend tools are now callable via <hash>_<local_name> instead of
<app_name>___<local_name>. The dispatcher walks the provider tree
recursively via get_tool_by_hash (same pattern as get_app_tool).
Each prefab tool gets its own renderer resource at
ui://prefab/tool/<hash>/renderer.html with per-tool CSP — fixing the
bug where PrefabAppConfig(csp=...) never actually applied.
Closes#3735, closes#3805
- AggregateProvider._collect_list_results detects duplicate component
names across providers, respecting the server's on_duplicate setting
- Provider errors logged at WARNING instead of DEBUG
- Parent server re-masks ToolErrors from mounted children at the
FastMCPError catch boundary instead of mutating the child server
Fixes#3825
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enables stricter type checking by promoting rules that default to
ignore: division-by-zero, possibly-missing-attribute,
possibly-missing-import, possibly-unresolved-reference,
unsupported-dynamic-base, unsupported-operator, unused-ignore-comment.
6 of the 7 rules had zero violations. possibly-unresolved-reference
had 9 (5 in src/, 3 in tests/, 1 walrus-operator false positive
suppressed with ty: ignore).
🤖 Generated with Claude Code
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Detect async/sync generators after tool execution and materialize
into lists before the result conversion pipeline processes them
- Generator materialization runs inside timeout scope so slow generators
respect the configured timeout
- Handle bytes return types: UTF-8 bytes as text, non-UTF-8 as base64
- Suppress output_schema for bytes return types (can't be structured JSON)
- Catch UnicodeDecodeError alongside PydanticSerializationError in
convert_result for robustness
Fixes#3829
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pydocket 0.19.0 fixes the fakeredis 2.35.0 FakeConnection rename
internally, so we no longer need to carry the fakeredis ceiling
ourselves. Removes the direct fakeredis[lua]<2.35.0 dependency from the
tasks extra entirely — it's just a transitive of pydocket now.
🤖 Generated with Claude Code
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve OpenAPI 3.x server variables in _create_default_client
When an OpenAPI spec defines server variables (e.g. `https://{region}.api.example.com/v1`),
the default values are now substituted before constructing the httpx client base URL.
Previously, the URL was used as-is, causing all requests to fail for specs that use
server variable templating.
Fixes#1681
* fix: use str.replace instead of format_map for server variable substitution
format_map applies Python string formatting rules, so variable names
like {api.version} would be treated as attribute access and raise errors.
Literal token replacement handles all valid OpenAPI variable names safely.
* Unify background task context forwarding and fix concurrent dependency bugs
We've been getting a steady trickle of edge-case reports around background tasks
and contextual dependencies over the last few months (#3654, #3656, #3569). Each
one pointed at a different symptom, but they all traced back to the same area:
the way context is negotiated between the "frontend" server and Docket workers
was grown piecemeal, with each new piece of context (access tokens, HTTP headers,
origin request IDs) getting its own Redis key, its own restore function, and its
own ContextVar. This made it hard to reason about what state was available where,
and the shared-instance Dependency pattern made concurrent tasks stomp on each
other's cleanup state.
This takes a step back and reworks the whole thing as a single unified system:
- Dependency subclasses (_CurrentContext, Progress, _CurrentAccessToken, etc.)
are now stateless factories — __aenter__ returns a fresh per-invocation
object, so concurrent tasks never share mutable state. Fixes#3654, #3656.
- The three individual context-snapshot Redis keys (access_token, http_headers,
origin_request_id) are collapsed into a single TaskContextSnapshot stored as
one JSON key per task. The three _restore_task_* functions and two ContextVars
they populated are gone.
- Sync functions like get_http_request() and get_access_token() now find the
snapshot transparently in background tasks via a 3-tier sync fallback:
ContextVar (set by _CurrentContext for functions with deps) → in-memory dict
(same-process workers) → sync Redis GET (out-of-process workers). No function
wrapping needed.
- The _wrap_for_task_http_headers hack is deleted. FunctionTool registers its
raw function with Docket so Docket sees and resolves ALL dependencies,
including Docket-native ones like Retry and Timeout.
- ProxyTool.from_mcp_tool() now propagates execution.taskSupport metadata from
remote tools. Fixes#3569.
- Removed redundant _current_docket/_current_worker ContextVar management from
Context.__aenter__/__aexit__ (they're only set in the lifespan now).
Closes#3654Closes#3656Closes#3569🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address code review feedback
- _OptionalCurrentContext: guard __aexit__ against cleaning up contexts it
didn't create (check is_background_task before delegating)
- Narrow except clauses in snapshot loading (OSError, JSONDecodeError, etc.
instead of bare Exception)
- Fix docstrings on register_with_docket for resources/prompts/templates
- Simplify Progress: read ExecutionProgress directly from current_execution
instead of creating and manually entering a DocketProgress wrapper
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use pop-on-access transfer buffer instead of bounded LRU cache for snapshots
The in-memory snapshot dict is a transfer mechanism, not a cache. Entries go
in at submission and come out at the worker's first access. Using pop instead
of get means the dict only holds entries during the brief submission-to-execution
window, bounded by task concurrency (~10) rather than a 10,000-entry LRU limit.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Drop in-memory transfer buffer, use sync Redis for all backends
Instead of maintaining an in-memory dict to bridge the async/sync gap, use
a sync Redis client directly. For memory:// backends (fakeredis), shares the
same FakeServer instance via docket._redis.get_memory_server() so data written
by the async Docket client is visible to sync reads. For real Redis, creates a
standard sync connection. No in-process state to manage at all.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Move snapshot operations to TaskContextSnapshot methods
capture(), from_json(), to_json(), save() are now classmethod/instance methods
on the dataclass instead of free functions. Deduplicates JSON parsing that was
copy-pasted between the async and sync load paths.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Trim implementation details from register_with_docket docstrings
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clarify docket lookup comment in submit_to_docket
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Restore docket/worker ContextVar bridge in Context.__aenter__
Servers that own the Docket (the parent) re-set _current_docket/_current_worker
from their instance attributes when entering a Context. Mounted children skip
this (their _docket is None), so they inherit the parent's value. This is needed
for ASGI deployments where ContextVars set during the lifespan don't propagate
to request handlers.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Key snapshot cache by task_id to prevent cross-task context leakage
Docket workers may reuse the same asyncio context for sequential tasks.
The ContextVar cache now stores (task_id, snapshot) tuples so stale entries
from previous tasks are automatically ignored.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Transparently refresh upstream token in OAuthProxy.load_access_token()
When upstream token validation fails during load_access_token, attempt
to refresh using the stored refresh token before returning None. This
prevents premature 401s that force clients into expensive full re-auth
flows when the upstream token expires.
Co-authored-by: Claude <noreply@anthropic.com>
* Gate transparent refresh on token expiry, add advisory lock
Only attempt upstream refresh when the token is actually expired, not
on any validation failure (scope mismatch, revocation, etc.). Add
per-token advisory lock to prevent concurrent async tasks from racing
to refresh the same upstream token.
* Re-check expiry inside lock, reload from storage after refresh failure
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add TokenCache utility and caching to GitHubTokenVerifier
Extract the caching machinery from IntrospectionTokenVerifier into a
shared TokenCache class in fastmcp.utilities.token_cache, then wire
it into both IntrospectionTokenVerifier and GitHubTokenVerifier.
* Remove dead constant, validate negative cache params
* Fix overwrite eviction bug, skip cache on scope lookup failure
* pin pydantic-monty to 0.0.8
* rename tool/prompt/resource base modules to avoid decorator name shadow
* add sys.modules shims for old submodule import paths
* preserve original module paths in deprecation warnings
* clarify when sys.modules shims can be removed
* fix: enforce auth/visibility in ResourcesAsTools and PromptsAsTools for non-FastMCP providers
🤖 Co-authored-by: Claude <noreply@anthropic.com>
* fix: honor stdio auth bypass and correct transform ordering in provider wrappers
Co-authored-by: Claude <noreply@anthropic.com>
* fix: move context/dependencies imports into function to break circular import
* fix: route ResourcesAsTools/PromptsAsTools through ctx.fastmcp
Instead of manually reimplementing auth, visibility, and session
transforms in the transform layer, tool functions now call
ctx.fastmcp.read_resource() / ctx.fastmcp.render_prompt() which
routes through the server's full middleware chain. This matches
the pattern CodeMode uses with ctx.fastmcp.call_tool().
The isinstance(provider, FastMCP) branching is removed entirely.
* feat: add _scope parameter for provider-scoped listing
AggregateProvider can now filter which child providers to query when
listing components. ResourcesAsTools and PromptsAsTools use this to
scope listings to their configured provider while still routing
through ctx.fastmcp for full middleware coverage.
The scope matching walks wrapped providers, so a
WrappedProvider(Namespace, inner=MyProvider) matches if MyProvider
is in the scope list.
* test: add coverage for ResourcesAsTools scoped to a sub-server
* fix: delegate to super() when _scope is None, add AggregateProvider to scope matching
* simplify: remove _scope machinery, route everything through ctx.fastmcp
Reverts the _scope parameter from Provider/AggregateProvider/Server.
ResourcesAsTools and PromptsAsTools now simply route through
ctx.fastmcp for all operations. Apply to a FastMCP server instance
for proper auth/visibility/middleware coverage.
Tests rewritten to use FastMCP server directly instead of raw providers.
* warn when ResourcesAsTools/PromptsAsTools is applied to a non-FastMCP provider
* docs: explain that ResourcesAsTools/PromptsAsTools should wrap a FastMCP server
* raise TypeError instead of warning when applied to non-FastMCP provider
---------
Co-authored-by: Claude <noreply@anthropic.com>
Keycloak returns refresh_expires_in=0 for offline tokens (offline_access scope),
meaning "no fixed time-based expiry". The truthiness check on this value caused
the proxy to skip issuing a PROXY_RT, forcing browser re-auth every hour.
Closes#3509🤖 Generated with Claude Code
Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
* feat: make upstream_client_secret optional in OAuthProxy
Extract _create_upstream_oauth_client() factory method for subclass
override. Cookie signing falls back to JWT key material when no secret.
* fix: include client_id in revocation requests for public clients
* fix: use factory method for revocation auth
- Use 10 PBKDF2 iterations in test_mode (vs 1M in production) for
JWT key derivation — cuts auth test setup from ~2.5s to <0.1s
- Add timeout(15) to subprocess-spawning tests (TestKeepAlive,
test_mcp_config) that exceed 5s under parallel CI load
- Remove pytestmark filterwarnings overrides in tests/deprecated/
that were leaking DeprecationWarning to test output
- Fix deprecated add_tool_transformation() usage in test_authorization
- Document new settings in settings.mdx
* perf: expose minimum_check_interval, reduce task pickup latency
The Docket Worker polls for new tasks every minimum_check_interval
(previously hardcoded to 250ms in pydocket). Expose this setting so
users can tune it, default to 50ms, and override to 10ms in tests.
This cuts average task pickup latency from ~125ms to ~5ms per task.
* perf: reduce task test overhead and eliminate cross-test contamination
- Expose minimum_check_interval setting (default 50ms, 10ms in tests)
to reduce Docket Worker task pickup latency
- Isolate fakeredis per test via unique memory:// URLs to prevent
stale _async_blocking tasks from contaminating subsequent tests
- Make client disconnect timeout configurable (default 5s, 1s in tests)
- Add --durations=50 to CI for passive performance regression detection
- Remove 15s timeout band-aids from task test conftest files
- Add explicit @pytest.mark.timeout(10) to cancellation tests
- Fix deprecated FastMCP.as_proxy() usage in test_task_proxy.py