pi-mcp-tasks0.1.0
pi extension: MCP Tasks Extension (SEP-2663) client over pi's registered MCP servers
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-mcp-tasks
Client for SEP-2663, the MCP Tasks extension (io.modelcontextprotocol/tasks), as a pi extension.
A tool call can return a task handle instead of a finished result. This extension tells the server that pi supports tasks, polls tasks/get until the task ends, and puts the result, error, or cancellation back into the conversation so the model sees it. It uses the MCP servers already registered with pi. It does not open a second connection.
The pinned spec is commit 02dd8f61. The extension follows the migration rules from the legacy 2025-11-25 tasks protocol: it never calls tasks/result, never sends the legacy task parameter, and never advertises tasks.requests, tasks.list, or tasks.cancel.
Install
Node 22. The peer dependency is @earendil-works/pi-coding-agent 1.0.1. The hook also patches the bundled connection class in dist/bundle/chunks/runtime-*.js, which is what pi 1.1 loads.
pi install npm:pi-mcp-tasks
Or add the package path to your pi settings. The package advertises itself with:
"pi": { "extensions": ["./index.ts"] }
Register MCP servers the usual way, in ~/.pi/agent/mcp.json or the project .pi/mcp.json. This extension does not register a slash command and does not replace pi's built-in MCP client.
{
"mcpServers": {
"jobs": {
"command": "my-tasks-server",
"args": []
}
}
}
How a task runs
- On connect, the client adds
extensions["io.modelcontextprotocol/tasks"] = {}to the initialize capabilities when pi'sMcpClient.connectcan be patched. - Every
tools/call,tasks/get,tasks/update,tasks/cancel, andsubscriptions/listenrequest carries_meta["io.modelcontextprotocol/clientCapabilities"].extensions["io.modelcontextprotocol/tasks"] = {}. That per-request object is the signal the spec requires. A server that does not see it must not return a task handle. - When
tools/callreturnsresultType: "task", the tool result in the conversation describes the running task. The tool itself returns immediately. The final payload arrives later. - The client calls
tasks/getat once, then again afterpollIntervalMs(or 1000 ms when the server omits it). A changed interval is honored on the next wait. - While the task is open, the status line shows
tasks Nand themcp-taskswidget lists each task's id, server, tool, status, status message, poll interval, and TTL. input_requiredis shown to the user for confirmation before anytasks/updatewhen approval mode isui(the default). In print mode, with no UI, the key stays pending and the model is told to callmcp_tasks_respond. Set--mcp-tasks-approvals toolorPI_MCP_TASKS_APPROVALS=toolto skip the confirm and input dialogs even when a UI exists. The environment variable wins.mcp_tasks_respondis then the only way to answer.- A terminal snapshot is appended as a custom message (
customType: "mcp-task"). When a model is selected, that message starts a follow-up turn. The text includes the status,statusMessage, and the tool result, JSON-RPC error, or cancellation. - Open tasks are stored as session entries (
customType: "pi-mcp-tasks") and polling resumes on the nextsession_start.
completed with isError: true stays completed. failed is only a JSON-RPC execution error, and that error is included in the message. A dropped connection is a client-local disconnected outcome. It is not relabeled as the spec status failed.
Codemode scripts see the task handle in structuredContent, not the final tool result. The final result is the later mcp-task message. The tool is not held open until completion, because pi's tool timeout would fire first.
Tools
| Tool | Effect |
|---|---|
mcp_tasks_list |
Every task from this session: status, status message, poll interval, TTL. |
mcp_tasks_show |
One task, including result or error. The id is a task id, or server/taskId when two servers share an id. |
mcp_tasks_cancel |
tasks/cancel. Polling stops. Does not send notifications/cancelled. |
mcp_tasks_respond |
tasks/update with inputResponses as a JSON string. Keys already answered are skipped. |
Compliance
Every client requirement below is implemented. The test column is the node:test name that covers it.
| ID | Level | Requirement | Status | Test |
|---|---|---|---|---|
| C-MUST-01 | MUST | Handle either CallToolResult or CreateTaskResult for a supported request (tools/call). |
implemented | C-MUST-01 |
| C-MUST-02 | MUST | Treat CreateTaskResult on an unsupported request type as an invalid response. | implemented | C-MUST-02 |
| C-MUST-03 | MUST | Inspect inputRequests when a task is input_required. |
implemented | C-MUST-03 |
| C-MUST-04 | MUST | Apply the same trust model to inputRequests as to a direct elicitation or sampling request. |
implemented | C-MUST-04 |
| C-MUST-05 | MUST | Cancel tasks with tasks/cancel, never notifications/cancelled. |
implemented | C-MUST-05 |
| C-MUST-06 | MUST | Set Mcp-Name to the task id on tasks/get, tasks/update, and tasks/cancel over Streamable HTTP. |
implemented | C-MUST-06 |
| C-MUST-07 | MUST | Do not call the removed tasks/result method. |
implemented | C-MUST-07 |
| C-MUST-08 | MUST | Do not send the legacy task parameter on tools/call. |
implemented | C-MUST-08 |
| C-MUST-09 | MUST | Do not advertise legacy tasks.requests, tasks.list, or tasks.cancel capabilities. |
implemented | C-MUST-09 |
| C-MUST-10 | MUST | Declare io.modelcontextprotocol/tasks on tools/call and on task protocol requests. |
implemented | C-MUST-10 |
| C-MUST-11 | MUST | Keep a tool result with isError: true on the completed status, not failed. |
implemented | C-MUST-11 |
| C-MUST-12 | MUST | Surface a failed task's JSON-RPC error to the user and the model. | implemented | C-MUST-12 |
| C-SHOULD-01 | SHOULD | Keep the set of task-augmented methods open so later revisions can add request types. | implemented | C-SHOULD-01 |
| C-SHOULD-02 | SHOULD | Honor pollIntervalMs, including a value that changes between polls. |
implemented | C-SHOULD-02 |
| C-SHOULD-03 | SHOULD | Keep polling until the task is terminal or the client cancels it. | implemented | C-SHOULD-03 |
| C-SHOULD-04 | SHOULD | Persist task ids so polling can resume after a restart. | implemented | C-SHOULD-04 |
| C-SHOULD-05 | SHOULD | Fulfill input_required by sending inputResponses on tasks/update. |
implemented | C-SHOULD-05 |
| C-SHOULD-06 | SHOULD | After tasks/update, keep observing the task until it is terminal. |
implemented | C-SHOULD-06 |
| C-SHOULD-07 | SHOULD | Deduplicate inputRequests keys across polls. |
implemented | C-SHOULD-07 |
| C-SHOULD-08 | SHOULD | Do not send inputResponses for a key that was already answered. |
implemented | C-SHOULD-08 |
| C-SHOULD-09 | SHOULD | Show statusMessage to the user and include it in what the model sees. |
implemented | C-SHOULD-09 |
| C-MAY-01 | MAY | Expose the task lifecycle with list, inspect, cancel, and respond tools, and a status widget. | implemented | C-MAY-01 |
| C-MAY-02 | MAY | Treat createdAt plus ttlMs as a backstop when the task never reaches a terminal status. |
implemented | C-MAY-02 |
| C-MAY-03 | MAY | Subscribe to notifications/tasks and keep polling. |
implemented | C-MAY-03 |
| C-MAY-04 | MAY | Drop answered input-request state after tasks/cancel. |
implemented | C-MAY-04 |
| C-MAY-05 | MAY | Accept a later terminal notification after a cooperative cancel. | implemented | C-MAY-05 |
tests/coverage.test.ts fails if a row in src/requirements.ts has no matching test name.
Integration tests against a local server cover completion, failure, cancellation, update, expiry, a tool result with isError: true, a legacy server that has no tasks methods, a dropped connection, and the Streamable HTTP Mcp-Name header. A pi session test calls a direct MCP tool through pi's own connection and waits for the mcp-task message.
Readings
The spec leaves these points open. This is the reading the code uses.
- Poll default. The spec does not define a default
pollIntervalMs. A missing or non-positive value waits 1000 ms. A positive value is rounded and honored, including a value that changes on a later poll. The firsttasks/getis sent immediately. - TTL backstop. When
ttlMsis a number and the status is still non-terminal atDate.parse(createdAt) + ttlMson the client clock, the task is expired locally and polling stops.ttlMs: nullor a missing field means unlimited. An unparseablecreatedAtskips the backstop.ttlMs: 0expires atcreatedAt. A later snapshot can changettlMs; the deadline uses the latest value and the originalcreatedAt. Clock skew between client and server can expire a task early or late. -32602. Atasks/getinvalid-params error whose message matches/expir/i, or that arrives after the TTL deadline, isexpired. Any other-32602isunavailable(not found). Other failures retry, then becomedisconnected.tasks/getwithoutresultType. A snapshot with a task id and a known status is accepted. The spec saystasks/getresults useresultType: "complete", which is not a create result, so it is not treated as a new task.- Elicitation content. The spec's example sends
{ "input": "Luca" }whilerequestedSchemaasks for{ name }. Submitted content matchesrequestedSchema. The user confirms before accept. Declining leaves the key pending and tells the user to callmcp_tasks_respond. URL elicitation shows the URL and confirms completion. Sampling wraps the user's text as an assistant text result (model: "user",stopReason: "endTurn"). - Cancel.
tasks/cancelis sent, polling stops, answered keys are cleared, and the conversation gets a cancellation-requested message. A later terminalnotifications/taskscan replace that outcome. - Capability placement. The normative client signal is the per-request
_metaobject. Initializecapabilities.extensionsis sent in addition whenMcpClient.connectis the class pi's unbundled runtime uses. Legacycapabilities.tasksis removed from that initialize payload. - HTTP headers.
Mcp-Nameis the task id andMcp-Methodis the JSON-RPC method, and only ontasks/get,tasks/update, andtasks/cancel. Stdio does not send them. - Unknown status. A status outside
working | input_required | completed | cancelled | failedis a protocol failure. It retries, then becomesdisconnected.
pi gaps
pi's public extension API cannot do four things this protocol needs. The workaround patches the MCP client pi already created: McpServerConnection.getClient in dist/extensions/mcp/runtime.js, and the same class re-exported from dist/bundle/chunks/runtime-*.js. It does not construct a second client.
| Gap | Workaround |
|---|---|
ClientCapabilities has no extensions field, and nothing lets an extension add one at initialize. |
Wrap McpClient.prototype.connect on the module resolved from runtime.js and add the tasks extension there. The bundled CLI inlines its own McpClient, so that initialize patch does not apply to pi's single-file binary. Per-request _meta still does, because the bundled connection class is patched. |
tools/call sends only name and arguments. There is no hook to add _meta. |
Wrap client.request after getClient(). |
There is no public pi.mcpRequest. |
tasks/get, tasks/update, tasks/cancel, server/discover, and subscriptions/listen go through that same client.request. The binding calls the original getClient so the wrap does not recurse. |
| Streamable HTTP builds headers inside the transport, with no per-request hook. | Replace transport.headers for the duration of those three methods, under AsyncLocalStorage, so concurrent calls do not share a task id. |
Other limits, left as they are:
tool_resultruns after pi has converted the MCP result. A directtool.execute()never emits it. Task detection therefore starts in the wrappedrequest, andtool_resultonly rewrites the text the model sees. The rewritten result still returnsstructuredContent.- pi does not implement elicitation or sampling. This extension confirms through
ctx.uiand does not accept on the user's behalf. McpServerConnectionis not in the package exports map, and the exports condition isimportonly. The patch loads it byimport.meta.resolveplus the file URL.- pi's negotiated protocol list stops at
2025-11-25. SEP-2663 says the tasks extension is not defined for that version and that servers must not treat the capability as enabling tasks on it. This client still sends the capability. A server that follows the spec on2025-11-25will answer with an immediate tool result, which this client delivers unchanged. The test server speaks2025-11-25because that is the version pi will negotiate, and it honors the extension anyway. session.dispose()does not emitsession_shutdown. pi closes MCP children only on that event. Quitting or reloading the session does emit it. Tests emit it themselves so the fixture process exits.
Development
npm ci
npm test
npm run typecheck
npm test runs the unit, integration, session, and coverage suites with Node's test runner (--experimental-strip-types). CI on GitHub Actions runs both commands on Node 22.
License
MIT. Copyright (c) 2026 Richard Anaya.