Partial fulfillment means two in-flight updates can carry different answers,
so acknowledging the one that loses the update lock stranded the task on a key
the client had already sent.
Keep the final outstanding input marker until the next task leg is durable,
so a racing tasks/get cannot read a parked leg as complete. Let resources and
resource templates return InputRequiredResult like tools and prompts. Identify
parked requests by their question rather than sort order.
* Pin burner-redis below the Windows-crashing 0.1.7 release
burner-redis 0.1.7 crashes the interpreter (native fault, no Python traceback)
running the memory:// task backend under pytest-xdist on Windows — reproduced on
GitHub Actions windows-latest via the 'Upgrade checks' workflow, confirmed
absent on macOS/Linux with the identical dependency versions.
pydocket only floors burner-redis at >=0.1.6, so capping pydocket's own version
is not enough: a resolver remains free to pick the newest burner-redis
satisfying that floor. fastmcp-tasks previously pinned pydocket>=0.20.0 with no
upper bound, so a fresh 'pip install fastmcp[tasks]' today can resolve straight
into the broken combination for a real Windows user on the default backend.
Pin burner-redis<0.1.7 directly, which in turn caps pydocket to <0.20.2 (the
last release that doesn't itself require burner-redis>=0.1.7). Verified the pin
holds under both locked and --upgrade (highest) resolution.
* Scope the burner-redis pin to Windows only
burner-redis 0.1.7 is confirmed fine on macOS/Linux (full suite green there with
the identical upgraded dependencies) - only Windows crashes. The previous
unconditional pin blocked every platform from newer pydocket/burner-redis
releases unnecessarily. Add sys_platform == 'win32' to the burner-redis
constraint so only Windows installs are capped.
Verified via uv pip compile --python-platform: macOS/Linux resolve to
burner-redis 0.1.7 / pydocket 0.23.0 (unblocked); Windows resolves to
burner-redis 0.1.6 / pydocket 0.20.1 (still capped).
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.
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.
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.
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>
- 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>