* 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>
* Implement MCP background tasks (SEP-1686) using Docket
Adds support for background task execution via the MCP task protocol,
powered by Docket for task queue management.
- Tools, resources, and prompts can be marked with `task=True` to run async
- Progress dependency for tracking task progress
- CurrentDocket and CurrentWorker dependencies for advanced use cases
- Client API with `.call_tool(..., task=True)` returns task handles
- Task status notifications via subscriptions
- CLI worker command for distributed task processing
Configuration via environment:
- FASTMCP_ENABLE_DOCKET=true
- FASTMCP_ENABLE_TASKS=true
- FASTMCP_DOCKET_URL=redis://... (or memory:// for single-process)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix tasks example import (TaskStatusResponse → GetTaskResult)
The example was using a non-existent TaskStatusResponse type.
Updated to use mcp.types.GetTaskResult which is what the
on_status_change callback actually receives.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix env var name in Docket error messages
The error messages referenced FASTMCP_EXPERIMENTAL_ENABLE_DOCKET but the
actual setting is FASTMCP_ENABLE_DOCKET.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove deprecated code re-added from pre-#2329 branch
- Remove ExtendedEnvSettingsSource (FASTMCP_SERVER_ prefix support)
- Remove dependencies parameter from FastMCP.__init__
* Replace fakeredis git pin with PyPI release
* Remove redundant fakeredis dev dep (pulled via pydocket)
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>