Repository navigation
Ships the fixes from a team's field report against pmcp 2.22.7 (MCP Tasks on Cloud Run). The
streamable-HTTP server no longer handles one request at a time, task handlers can hand work to a
background worker, and the Tasks docs now describe the task_store(...) path as it is. Minor
bump for the new API.
Crates published in this release: pmcp 2.23.0, pmcp-sql-server 0.4.1,
pmcp-openapi-server 0.4.2, pmcp-workbook-server 0.4.1 and pmcp-server 0.2.5
(each now requires pmcp 2.23 and uses the concurrent constructors), pmcp-workbook-compiler
0.4.1 (zip default features off, dropping zstd 0.13 and RUSTSEC-2026-0391/0398/0403),
pmcp-tasks 0.1.2 (docs only: which server path its stores serve) and cargo-pmcp
0.29.0 (see cargo-pmcp/CHANGELOG.md).
Fixed
- The streamable-HTTP server can now handle requests concurrently. Field report F-4 (pmcp
2.22.7): the transport held the server mutex for a tool handler's whole duration, so one long
tools/callstalled every other request on the process,initializeandserver/discover
included. On Cloud Run at concurrency 80, trivial calls measured p50 28.2 s behind a single 30 s
call. EveryServerentry point takes&selfand guards its own state, so the outer mutex
protected nothing.
Added
StreamableHttpServer::new_concurrent/with_config_concurrentand
pmcp::axum::concurrent_router/concurrent_router_with_config, which take anArc<Server>
and dispatch requests in parallel. The existingArc<Mutex<Server>>constructors are unchanged
and still serialize, because callers own that mutex; their docs now say so.pmcp-sql-server,
pmcp-openapi-server,pmcp-workbook-serverandpmcp-serveruse the concurrent constructors.- Task handlers can hand work to a background worker (field report F-1, F-2, F-6):
RequestHandlerExtra::will_create_task()reports the dispatcher's create gate before the
handler runs: the client asked for a task, a store is configured, and the tool declares task
support. A handler that branches on it never returns a task handle the client cannot poll.RequestHandlerExtra::on_task_created(hook)registers a callback that receives the
store-minted task and its owner (task_store::CreatedTask) once the task exists. Until now
the store minted the id after the handler returned, so a worker had no id to report against.TaskStore::set_status_messagechanges a working task'sstatusMessagewithout changing its
status, so a worker can report progress. It is a defaulted trait method;
InMemoryTaskStoreimplements it.- New example:
s58_task_background_worker.
Changed
RequestHandlerExtra::is_task_request()follows the protocol era (field report F-2). On
2026-07-28 it now reports whether thistools/calldeclared theio.modelcontextprotocol/tasks
extension, the trigger the dispatcher uses, instead of reading the 2025-11-25taskfield.
Before, it returnedfalsefor every conformant v2 task request, andtruefor a v2 request
that carried the v1 field even though the dispatcher ignores it there. 2025-11-25 behaviour is
unchanged.
Documentation
- Tasks docs now match the
task_store(...)path (field report F-5, F-7, F-8). Covered in the
task_storerustdoc on both builders, the pmcp-book and pmcp-course Tasks chapters,
docs/TASKS_WITH_POLLING.mdand thepmcp-taskscrate docs:pmcp-tasks' DynamoDB and Redis stores plug into the legacyTaskRouterpath only.
InMemoryTaskStoreis the only storetask_store(...)accepts out of the box, so
multi-instance deployments need their ownTaskStoreimplementation.- Without authentication, tasks share an owner bucket (
"local"on v1,""on v2), so any
caller holding a task id can read the task. - The owner is the OAuth subject, not the
client_id, asTASKS_WITH_POLLING.mdhad claimed. - The high-level
Server::builder()does taketask_store(...). Three pages said it did not. - Long work belongs in a task, because hosts don't reliably cancel an abandoned synchronous call.
- Corrected an internal comment that said the production stores refuse the shared anonymous owner
on thetask_store(...)path. They aren't reachable there.