A task-augmented tools/call returns a CreateTaskResult up through the middleware
chain. Response caching and response limiting assumed a ToolResult and accessed
.content/.wrap(), crashing after the task was already enqueued (a client retry
could duplicate side effects). Both now pass any non-ToolResult through
untouched, alongside the existing InputRequiredToolResult bypass.
Five review fixes. ctx.session_id / get_state / set_state now work in a Docket
worker by falling back to the snapshotted session id. Task management wire calls
(submission, tasks/get/update/cancel) create client spans and propagate trace
context. TasksClientSettings loads .env like DocketSettings, and the docs use
its real env var name. A state-only guard round (request_state, no input
requests) fails with a clear error instead of silently completing wrong.
* Archive v3 docs under /v3 and publish v4 as the primary version
* Label primary docs version v4.0.0 (alpha 1)
* Add What's New in v4 page; fix upgrade-guide phrasing; point banner at What's New
* Rewrite What's New around v4's new capabilities, not the sampling deprecation
* Lead What's New with the SDK v2 engine swap and the SEPs it brings
* State ships now (link Session State); tasks arrive next alpha
* Exclude docs/v3 frozen snapshots from doc-example import validation
Lock in the tasks x stateless-session-state (#4604) integration: a
session: UserSession parameter resolves in a Docket worker via the task-aware
get_server() and the principal restored from the task snapshot, sharing state
across a principal's tasked calls and staying isolated between principals.
The example README pointed at github.com/PrefectHQ/docket (404); the canonical
repo is chrisguidry/docket. Point the docs' Docket-docs link at the canonical
docket.lol.
* Design doc: stateless session state
* Add stateless session-state primitives: Scope, SessionCodec, scoped state
* Add SessionProvider and Session() annotation for stateless session state
* Rewrite session-state design to final shape (Session object, two patterns, no seal)
* Rework stateless session state to final Session/SessionId design
Remove Scope, SessionCodec/sealing, and scoped ctx.get_state. Add the
Session object (get/set/delete/clear over one dict per (principal,
session_id) key), injected session: Session (keyed by principal, requires
auth), session_id: SessionId argument with auto-filled description, and
SessionProvider contributing create_session/end_session.
* Rename injected marker to UserSession; auto-wire SessionProvider on SessionId
* Document stateless session state as a v4 feature
* Require SessionProvider and make sessions create-then-validate
Remove the implicit SessionProvider auto-wiring; a SessionProvider must now be
registered explicitly. create_session records an owned session and get_session
validates the id, rejecting uncreated or foreign ids.
* Add Session.id (public id for session_id sessions, None for UserSession)
* Fix ty: narrow Tool | None and ToolResult.structured_content in session tests
* Fix session-provider enforcement gap for non-local tools; stop embedding raw principal in UserSession key
* Fix disabled session_id tools blocking listing; reject local tools shadowing SessionProvider lifecycle names
* Decouple SessionId description from lifecycle tool name so it survives namespaced mounts
* Remove SessionProvider enforcement; get_session validation is the guarantee
* Fix stale enforcement/key-format docs; document store-owned session TTL
* Dedup SessionId contract description; tighten context.mdx session-state lead
* Make session store/description resolution work in Docket task workers and for partial tools
* Expose get_session as a standalone task-safe function; drop foreground-only Context.get_session
* Move get_session to dependencies alongside the other request accessors
* Reframe context state docs as Request State; cross-request persistence points to Session State
* Address UserSession injection edge cases from review
- inject a UserSession instance (not bare Session) so isinstance holds
- support session: UserSession | None = None (inject None when unauth)
- detect SessionId params past a partial's positional binding
A resumed leg that runs longer than its pointer's wall-clock TTL stranded
_lookup_task on the base leg (false completion / not found). Each poll now
refreshes the routing keys' TTL (sliding expiration), so an actively-polled
task keeps them alive regardless of execution duration, and the resumed-leg
write uses the same buffered TTL as creation. Separately, remote-worker server
resolution now respects the requested tool version, so two versions of the same
mounted tool name resolve to their own child server.
asyncio.wait_for raises asyncio.TimeoutError, a distinct type from the builtin
before Python 3.11, so an elicitation-callback timeout leaked an uncaught type
on 3.10. Convert it to the builtin TimeoutError the rest of the drive raises.
DocketSettings now loads the same dotenv source as core settings, so a
FASTMCP_DOCKET_* value in .env configures the backend instead of silently
using memory://. The root fastmcp publish waits for the matching fastmcp-tasks
to appear on PyPI before uploading, so the [tasks] extra is never installable
but unresolvable. And the example README uses the real worker entry point
(python -m fastmcp_tasks.worker_cli worker).
Three review fixes. A Docket worker may reuse an asyncio context across tasks,
so snapshot restore now always resets auth and headers to the current task's
state — an anonymous task following an authenticated one no longer inherits the
prior caller's identity. A stalled in-task elicitation handler is now bounded by
the call's remaining timeout, like polling and sleeps. And call_tool_task takes
a version= to task a specific component version rather than the highest.
Two remote-worker fixes. A separate worker process cannot reach the submitting
process's server map, so a mounted task's ctx.fastmcp/CurrentFastMCP() fell back
to the root; the worker now re-resolves the owning child from the root using the
snapshotted tool name. And restoring headers no longer fabricates a live Request
— get_http_headers() reads a dedicated task-headers context var while
get_http_request()/CurrentRequest() correctly keep raising inside a task.
The fastmcp[tasks] extra pins fastmcp-tasks=={version}, but no workflow
published it — pip install "fastmcp[tasks]" would fail to resolve. Mirror the
fastmcp-remote workflow: build on release, wait for the matching fastmcp-slim to
appear on PyPI, then publish.
Three review fixes: transparent call_tool(timeout=N) now enforces one deadline
across the whole poll loop (not per-request), matching the sync timeout; the
tools/call interceptor resolves the client-requested component version instead
of the highest; tasks/cancel runs under the per-task update lock and re-resolves
the live leg, so it can't cancel a stale leg while an update enqueues the next.
Server runs over HTTP on the default memory:// backend (no Redis needed); the
client drives it transparently, via an explicit handle, and with a parallel
command that fires several tasks at once to show them overlap. A 1s poll
interval keeps the demo snappy.
A guard task parked on input has an already-COMPLETED Docket execution, so
docket.cancel on it was a no-op: tasks/get reported input_required forever and
tasks/update could still resume it. Record a durable logical-cancellation
marker that tasks/get reports as cancelled and tasks/update refuses to resume,
and clear the parked leg's outstanding requests on cancel.
A queued task can outlive its submitter's token expiry: install the snapshot
token only if still valid, matching the SDK bearer check, so a delayed task
never runs under credentials a live request would reject. ToolTask.wait now
bounds each tasks/get by the remaining deadline so a stalled poll cannot block
past the caller's timeout.
Resolve the error-masking policy via the worker-server resolver instead of
the active Context: a task tool that raises without requesting a ctx param
has no active context, so the old lookup leaked unmasked error text past
mask_error_details=True. Also route custom Tool subclasses through the same
error-conversion wrapper as FunctionTool.
- Client task support is opt-in via importing fastmcp_tasks (drop the core
auto-load of companion packages); a plain Client never advertises tasks.
- A worker restores the submitting caller's auth token and headers from the
task snapshot into the standard ambient context, so get_access_token() /
get_http_headers() work in a distributed worker with no new core hooks.
- worker_cli validates the loaded extension's resolved backend, not env defaults,
so a constructor-configured Redis worker starts.
- Thread the per-call read timeout through task polling; bound ToolTask.wait by
its deadline; set_elicitation_callback rebuilds internal extensions so a
later-set handler answers in-task input.
- README imports TaskConfig from fastmcp.utilities.tasks.
Co-Authored-By: Claude <noreply@anthropic.com>
Server (servers/tasks.mdx) and client (clients/tasks.mdx) docs rewritten for
the extension model: add_extension(TasksExtension()), the guard pattern for
in-task input (no imperative ctx.elicit()), tools-only, and the modern-protocol
requirement (the inverse of the old SEP-1686 legacy-only note). Mechanical
fixes elsewhere for the same reason: telemetry.mdx's tasks/{operation} method
list (get/update/cancel, not result/list), client.mdx's legacy-only feature
list (tasks moved to modern-only) and extension-composition paragraph
(describes the tasks ClientExtension, not the removed notification binding),
and stale SEP-1686 references in the FastMCP 2 upgrade guide. v4-notes status
lines updated to Shipped (#4602, #4603).
- tasks/get|update|cancel now return -32003 when the client did not declare the
tasks extension for the request (SEP-2663 MUST).
- A task tool that raises is a completed task with an is_error result, not a
failed task; failed is reserved for protocol faults, matching a live tools/call.
- A per-task lock serializes concurrent tasks/update so two racing answers cannot
each enqueue a next leg (double execution).
Co-Authored-By: Claude <noreply@anthropic.com>
A FastMCP client now transparently completes tasked tools/call: the tasks
ClientExtension advertises the capability and claims the CreateTaskResult, and
the resolver drives the tasks/get poll loop to completion, answering in-task
input through the client's elicitation handler and returning the tool's real
result. call_tool is transparent, call_tool_mcp exposes the raw result, and
call_tool_task yields a Task handle. The client half moves to fastmcp-tasks;
the [tasks] client extension auto-wires into Client (ProxyClient opts out).
Co-Authored-By: Claude <noreply@anthropic.com>
A task tool that returns InputRequiredResult now ends its leg (freeing the
worker) and stores the ask as durable state; tasks/update enqueues a fresh
Docket execution (the next leg) with accumulated request_state/input_responses
injected via ctx. No worker ever blocks on input, so a parked task no longer
holds up shutdown. Imperative ctx.elicit() inside a task is removed and raises
with guard-pattern guidance.
Co-Authored-By: Claude <noreply@anthropic.com>
Widen the tools/call result serialization (via a refcounted, modern-gated wrap
installed by TasksExtension) so a CreateTaskResult reaches the client instead of
being stripped by the CallToolResult|InputRequiredResult surface — the SDK ships
claim consumption but no production. Emit the resultType discriminator the
protocol requires (task on CreateTaskResult, complete on the tasks/* results);
the draft schema forbids it (additionalProperties:false), a contradiction
reported upstream. Closes compliance gaps G1/G4/G5.
Co-Authored-By: Claude <noreply@anthropic.com>
TasksExtension serves io.modelcontextprotocol/tasks on the extension API:
a decide-and-task tools/call interceptor (era-gated to modern connections),
tasks/get with inlined results and inputRequests, tasks/update delivering
poll-based in-task elicitation, tasks/cancel, durable creation, and
auth-scoped task isolation. Wire models validate against the vendored
ext-tasks schema. Worker-side Context hooks are refcounted so sibling
servers cannot strand each other's workers.
Co-Authored-By: Claude <noreply@anthropic.com>
Engine modules (keys, context snapshot, docket lifespan, worker CLI,
client handles) move intact; SEP-1686 wire modules park in _legacy_wire
for adaptation to SEP-2663. Core keeps task=True declaration on tools
only and raises at serve time until the tasks extension is registered.
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix percent-encoded skill file names unreadable in resources mode
Encode supporting-file paths explicitly (quote/unquote) when building
and resolving skill:// resource URIs, instead of relying on AnyUrl's
implicit encoding. This also closes the ambiguity where a file literally
named "setup%20guide.md" would collide with "setup guide.md" once both
were percent-encoded.
Fixes#4545
* Quote main_file_name when building its resource URI
Keeps the main-file URI on the same explicit quote/unquote round-trip
as supporting files, so a custom main_file_name containing a literal
'%' still resolves after the shared unquote() in _get_resource().
Preserve explicit suffixes when building data-backed File resource URIs, while keeping the inferred-extension fallback for names without a suffix.
Closes#4530
Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
* Add M2M client credentials auth providers
Wrap the SDK's client_credentials and private_key_jwt OAuth providers as
FastMCP-idiomatic ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider,
enabling browser-free client authentication via Client(auth=...).
* Fix M2M token cache collision and explicit-scope drop
Namespace the token cache by client_id so distinct clients sharing one store don't overwrite each other's tokens; pin caller-supplied scopes so the token request keeps them; fix CodeQL URL-substring check in tests; drop unused logger.
* Preserve step-up scope union, scope-aware token cache, restore token expiry
Only pin the caller's explicit scopes on initial authorization, leaving the SDK's step-up scope union intact; namespace the token cache by requested scopes as well as client_id; restore persisted absolute expiry on init so an expired stored token is re-fetched.
* Skip expiry restore for non-expiring reloaded tokens
* Distinguish expires_in=0 from omitted when restoring expiry
* Scope step-up flag to the flow via ContextVar; runnable JWT signing example