Skip to content

v2.23.0

Latest

Choose a tag to compare

@github-actions github-actions released this 28 Oct 05:58
· 2 commits to main since this release
58287b0

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/call stalled every other request on the process, initialize and server/discover
    included. On Cloud Run at concurrency 80, trivial calls measured p50 28.2 s behind a single 30 s
    call. Every Server entry point takes &self and guards its own state, so the outer mutex
    protected nothing.

Added

  • StreamableHttpServer::new_concurrent / with_config_concurrent and
    pmcp::axum::concurrent_router / concurrent_router_with_config, which take an Arc<Server>
    and dispatch requests in parallel. The existing Arc<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-server and pmcp-server use 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_message changes a working task's statusMessage without changing its
      status, so a worker can report progress. It is a defaulted trait method;
      InMemoryTaskStore implements 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 this tools/call declared the io.modelcontextprotocol/tasks
    extension, the trigger the dispatcher uses, instead of reading the 2025-11-25 task field.
    Before, it returned false for every conformant v2 task request, and true for 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_store rustdoc on both builders, the pmcp-book and pmcp-course Tasks chapters,
    docs/TASKS_WITH_POLLING.md and the pmcp-tasks crate docs:
    • pmcp-tasks' DynamoDB and Redis stores plug into the legacy TaskRouter path only.
      InMemoryTaskStore is the only store task_store(...) accepts out of the box, so
      multi-instance deployments need their own TaskStore implementation.
    • 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, as TASKS_WITH_POLLING.md had claimed.
    • The high-level Server::builder() does take task_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 the task_store(...) path. They aren't reachable there.