Skip to content

feat(mcp): serve task-augmented tool calls — CreateTaskResult, tasks/get, tasks/result, tasks/cancel, tasks/list (#369) - #550

Merged
ScriptedAlchemy merged 9 commits into
mainfrom
feat/369-mcp-tasks
Sep 4, 2026
Merged

feat(mcp): serve task-augmented tool calls — CreateTaskResult, tasks/get, tasks/result, tasks/cancel, tasks/list (#369)#550
ScriptedAlchemy merged 9 commits into
mainfrom
feat/369-mcp-tasks

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Fixes #369 (the #96 acceptance remainder). Lifts the dated deferral recorded by #394.

What lands

A generated route server serves the MCP 2025-11-25 Tasks utility for tool routes that opt in; a client that never asks for a task sees no change.

Config shape

export const config = {
  execution: { taskSupport: 'optional' }, // 'forbidden' | 'optional' | 'required'
} satisfies ToolConfig;

ToolConfig.execution mirrors the wire Tool.execution block tools/list advertises (the same way _meta, annotations, title mirror Tool), rather than a framework-only key. forbidden is the wire default when absent. Compile-time validation is AB4836 (routes/task-support.ts: object, only taskSupport, one of the three values, tool routes only — resources and prompts have no task augmentation in the spec). Reported once per route with its server; typegen is unaffected.

Request/response sequences (2025-11-25 session)

Request Response
tools/call + params.task: { ttl?, pollInterval? } on an opted-in tool CreateTaskResult at once — task: { taskId, status: "working", createdAt, lastUpdatedAt, ttl (client's, ≤ 24 h; 5 min default), pollInterval (≥ 100 ms; 1 s default) }, _meta["io.modelcontextprotocol/model-immediate-response"]. The Flight render starts under the task.
tasks/get The Task: working with statusMessage = latest progress message and _meta["agent-bundle/progress"] = { progress, total?, message? } (fed by the same Agent.Progress projection as #498's notifications/progress); completed; failed (isError: true result → failed, per spec) with the error text; cancelled. Last progress stays readable on a settled task.
tasks/result Blocks until settled, returns exactly the ordinary CallToolResult (content, structuredContent, layout _meta) plus _meta["io.modelcontextprotocol/related-task"]; a JSON-RPC error is returned as that error.
tasks/cancel Transitions to cancelled before answering, aborts the render through its AbortSignal (Effect interruption via the projector's interruptWhenAborted); the interrupted render settles as the SDK tool error, which tasks/result returns. Terminal task → -32602.
tasks/list The session's tasks, oldest first, cursor-paged.
Ordinary call to a required tool / task call to a forbidden tool (capability declared) -32601.
Unknown taskId -32602.

Capability: tasks: { list: {}, cancel: {}, requests: { tools: { call: {} } } }, declared only when at least one tool opted in; a server with none processes a task-augmented request as an ordinary one (spec fallback, unchanged behaviour). Progress notifications keep flowing only under the client's own progressToken, stamped with the related-task key. On a 2026-07-28 session the SDK's own registry answers tasks/* with -32601 and strips execution.taskSupport/capabilities.tasks; the server gates on the negotiated version and serves the core shape only (the io.modelcontextprotocol/tasks extension is a follow-up).

Render budget (#454) alignment

The budget bounds the render (per task), not the client's request: a task's tools/call returns immediately whatever the budget, and the render behind it is still bounded by the route's config.render (or the 60 s default) through the same dispatcher — no task-specific plumbing. Documented as such: raise the budget only when the render itself needs longer, not to outlast a host deadline.

How it sits on the SDK (@modelcontextprotocol/server@2.0.0, unchanged pin)

The SDK ships the task wire vocabulary without a runtime and its tools/call result validation refuses a task body. The lifecycle lives in packages/agent-bundle/src/mcp-tasks.ts: a Server subclass overriding _wrapHandler (the SDK's documented protected seam for role-specific handling) answers a task-augmented request with a CreateTaskResult and runs the SDK-validated handler behind the task; the task methods register through the SDK's documented custom-method form setRequestHandler(method, { params }, handler); results are validated client-side with the SDK's public specTypeSchemas.CreateTaskResult / GetTaskResult / CancelTaskResult / ListTasksResult. Nothing reaches past public/protected SDK surface. The deferral note's claim that the result schemas were not exported publicly was wrong (they are keyed without the Schema suffix); docs/mcp-conformance.md records the re-audit and keeps the original deferral text as history.

Storage decision

Task records live with the server instance (session-scoped Map, bounded by ttl after settling and 256 records; all working tasks cancelled on session close), not in the runtime's state driver. Reasons: the Tasks utility scopes a task to the session that created it (a spawned stdio process is one session); the render is bound to the process, so a durable record no later session could read back would claim more than the runtime can honour; and in a generated artifact the state driver is mounted in the Flight worker — the server process opens its own handle only for workspace-durable projects. The live render handle (AbortController, settled promise) is inherently in-memory either way.

Host evidence (mcp.tasks row in each capability JSON; rendered on the hosts page as a new column + details table)

Host Issues task-augmented tools/call? Evidence
Claude Code 2.1.250 No code.claude.com/docs/en/mcp (2026-09-04): a call past 2 min moves to a client-side background task (/tasks, CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS) while the call stays an ordinary tools/call; no tasks capability or params.task; stdio servers stay on the legacy handshake unless MCP_PROTOCOL_NEGOTIATION=auto. Live 2.1.257 calls (host-lineage audit) carried claudecode/toolUseId + progressToken, never params.task.
Codex 0.147.0 No Opt-in mcp_2026_07_28 adds paginated discovery, MRTR, non-blocking startup; tasks there are the io.modelcontextprotocol/tasks extension shape, not served by this SDK; default legacy handshake; recorded calls carry x-codex-turn-metadata, no params.task.
Cursor 2026-08-28 No cursor.com/docs/context/mcp "Protocol and extension support" lists Tools, Prompts, Resources, Roots, Elicitation, Apps — no Tasks; recorded calls carry only progressToken.
portable n/a No pinned host client; served to any 2025-11-25 client that sends params.task (SDK client, Workbench).

None does yet — the feature is spec-conformant and Workbench-testable, recorded honestly.

Workbench

The MCP page offers Run as task for tools advertising task support (forced for required), folds every task answer from the invocation history into a Tasks panel (status, statusMessage, progress, poll/fetch/cancel), auto-polls working tasks at the server's pollInterval, and gains List tasks when the server declares the capability. The typed browser→dev-server operation vocabulary gains callToolTask/getTask/getTaskResult/cancelTask/listTasks end to end (remote transport → session routes → dev McpSession, using the SDK client's schema-validated request()).

The host-test example gains a task-capable slow probe (execution.taskSupport: 'optional', holds a call for holdMs, reports progress every tickMs) so a host's handling of long tools can be observed in its capture log.

Tests

  • tests/mcp-tasks.test.ts (unit, real SDK client over InMemoryTransport, hand-registered tools): capability + execution advertisement, create → get (mid-render progress) → result, related-task meta, progress only under the client's token, cancel (status before response, signal aborted, terminal re-cancel -32602), isError → failed, list + bad cursor, required/forbidden -32601, ordinary calls untouched, unknown task -32602, ttl/pollInterval clamps, session close cancels.
  • tests/projection/mcp-in-memory.test.ts (generated server): capability + compiled execution.taskSupport, create → result equals the ordinary call's result + related-task key, progress via tasks/get from progress.report() and a streamed Agent.Progress fallback, cancel through the render's signal, list, non-opted tool refused, non-task client unchanged. Replaces the docs(runtime): record the dated deferral of task-augmented MCP tool calls (#369) #394 sentinels.
  • tests/route-task-support.test.ts: AB4836 shapes and messages, routeTaskSupport reader.
  • tests/packed-stdio-projection.test.ts: the task journey over real stdio framing inside the existing packed session (no new build/pack/spawn).
  • packages/workbench/tests/mcp-tasks.e2e.test.ts (real Chrome, 1440×900, host-test example through the dev server): run as task → working → polled to completed with progress → fetch result → cancel a second task → list. Plus controller/page/routes unit tests for the new operations.
  • examples/host-test/tests: the slow probe.

Removed: packages/rsc-runtime/tests/mcp-tasks-deferral.test.ts and the @ts-expect-error sentinel (the deferral is lifted).

Docs

website/docs/{en,zh}/guide/authoring/mcp.mdx — new "Long-running tools: tasks"; hosts page column + "Task-augmented calls by host" (generated from the capability JSON, en + zh); docs/diagnostics.md (AB4836); docs/mcp-conformance.md (re-audit, deferral kept as history); docs/entry-conventions.md; packages/rsc-runtime/README.md; examples/host-test/README.md. pnpm docs:site:build green.

Changeset

patch for agent-bundle and @agent-bundle/runtime: everything is additive — ToolConfig gains an optional execution key, the server gains request handlers and a capability only when a route opts in, the dev session vocabulary gains operations, the runtime README changes. No existing export, flag, config key, diagnostic, or default changes shape.

Packaging note

mcp-tasks.ts is emitted as its own rslib entry. When rslib concatenated it into the mcp-server-runtime chunk, that chunk gained an import { __webpack_require__ } from './rslib-runtime.js' whose identifiers shadow the artifact bundler's own runtime, and the packed stdio entry failed at load (__webpack_modules__[moduleId] is not a function). The packed proof caught it; it now passes.

Verification

pnpm typecheck, pnpm lint, pnpm test:unit (3268 passed), pnpm test:route-unit, pnpm test:projection (164 passed), pnpm --filter @agent-bundle/runtime test, pnpm test:packed -- packed-stdio-projection, generated-route-server integration, the new e2e in real Chrome, pnpm docs:site:build.

Review status

Landing notes (rebase over main after #526, #532, #545, #540)

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0f5d628

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agent-bundle Patch
@agent-bundle/runtime Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T08:22:05.967964Z 3928a61 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3928a61676

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/agent-bundle/src/mcp-tasks.ts
Comment thread .changeset/454-route-render-budget.md
Comment thread packages/workbench/src/mcp/mcp-page.tsx Outdated
@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@550
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@550
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@550

commit: 0f5d628

…get, tasks/result, tasks/cancel, tasks/list (#369)

Tool routes opt in with config.execution.taskSupport ('optional' | 'required',
validated as AB4836 and advertised in tools/list). The generated server answers
a task-augmented call with a CreateTaskResult at once and keeps the Flight
render behind the task: tasks/get reports status and the last render progress,
tasks/result blocks for the same CallToolResult the ordinary call returns,
tasks/cancel interrupts the render through its AbortSignal, tasks/list lists the
session's tasks. Clients that never ask for a task see no change; a server whose
tools never opted in advertises no tasks capability.

Replaces the dated #394 deferral sentinels with the lifecycle proofs at the
unit, mcp-in-memory, and packed-stdio levels.
…run as task, poll tasks/get, fetch tasks/result, cancel, list (#369)

The browser session controller, the dev session routes, and the dev McpSession
gain the typed task operations (callToolTask, getTask, getTaskResult,
cancelTask, listTasks) over the same epoch-bound operation vocabulary; the MCP
page offers a Run-as-task toggle for tools that advertise
execution.taskSupport, folds every task answer from the invocation history into
a Tasks panel, and polls working tasks at the server's suggested interval.

The host-test example gains a task-capable slow probe (execution.taskSupport
optional) so a host's handling of long-running tools can be observed; the new
desktop browser acceptance drives it through the real generated stdio server.
…dled mcp-server-runtime chunk stays free of the rslib runtime import

A generated artifact bundles dist/mcp-server-runtime.js; when rslib
concatenated the task module into that chunk it added an
`import { __webpack_require__ } from './rslib-runtime.js'` whose identifiers
shadow the artifact bundler's own runtime, and the packed stdio entry failed
at load (`__webpack_modules__[moduleId] is not a function`). Proven by the
packed-stdio proof, which now also drives the task journey.
…Tasks utility; clear a task's stale error on a later successful answer

Codex review of 3928a61: the required-tool rejection now applies only on a
task-capable session (2025-11-25 with the capability declared) — elsewhere
every call, required tools included, is the ordinary request and task metadata
is ignored; the Workbench task fold drops a prior error when a later tasks/get,
tasks/list, or tasks/result succeeds, so polling resumes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Task-augmented MCP tool calls (CreateTaskResult / tasks/result) — #96 acceptance remainder

1 participant