feat(mcp): serve task-augmented tool calls — CreateTaskResult, tasks/get, tasks/result, tasks/cancel, tasks/list (#369) - #550
Conversation
🦋 Changeset detectedLatest commit: 0f5d628 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
commit: |
…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.
0bc50d1 to
7997f2a
Compare
Fixes #369 (the #96 acceptance remainder). Lifts the dated deferral recorded by #394.
What lands
A generated route server serves the MCP
2025-11-25Tasks utility for tool routes that opt in; a client that never asks for a task sees no change.Config shape
ToolConfig.executionmirrors the wireTool.executionblocktools/listadvertises (the same way_meta,annotations,titlemirrorTool), rather than a framework-only key.forbiddenis the wire default when absent. Compile-time validation isAB4836(routes/task-support.ts: object, onlytaskSupport, 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-25session)tools/call+params.task: { ttl?, pollInterval? }on an opted-in toolCreateTaskResultat 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/getTask:workingwithstatusMessage= latest progress message and_meta["agent-bundle/progress"] = { progress, total?, message? }(fed by the sameAgent.Progressprojection as #498'snotifications/progress);completed;failed(isError: trueresult → failed, per spec) with the error text;cancelled. Last progress stays readable on a settled task.tasks/resultCallToolResult(content,structuredContent, layout_meta) plus_meta["io.modelcontextprotocol/related-task"]; a JSON-RPC error is returned as that error.tasks/cancelcancelledbefore answering, aborts the render through itsAbortSignal(Effect interruption via the projector'sinterruptWhenAborted); the interrupted render settles as the SDK tool error, whichtasks/resultreturns. Terminal task →-32602.tasks/listrequiredtool / task call to aforbiddentool (capability declared)-32601.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 ownprogressToken, stamped with the related-task key. On a2026-07-28session the SDK's own registry answerstasks/*with-32601and stripsexecution.taskSupport/capabilities.tasks; the server gates on the negotiated version and serves the core shape only (theio.modelcontextprotocol/tasksextension is a follow-up).Render budget (#454) alignment
The budget bounds the render (per task), not the client's request: a task's
tools/callreturns immediately whatever the budget, and the render behind it is still bounded by the route'sconfig.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/callresult validation refuses ataskbody. The lifecycle lives inpackages/agent-bundle/src/mcp-tasks.ts: aServersubclass overriding_wrapHandler(the SDK's documented protected seam for role-specific handling) answers a task-augmented request with aCreateTaskResultand runs the SDK-validated handler behind the task; the task methods register through the SDK's documented custom-method formsetRequestHandler(method, { params }, handler); results are validated client-side with the SDK's publicspecTypeSchemas.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 theSchemasuffix);docs/mcp-conformance.mdrecords the re-audit and keeps the original deferral text as history.Storage decision
Task records live with the server instance (session-scoped
Map, bounded byttlafter 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.tasksrow in each capability JSON; rendered on the hosts page as a new column + details table)tools/call?/tasks,CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS) while the call stays an ordinarytools/call; notaskscapability orparams.task; stdio servers stay on the legacy handshake unlessMCP_PROTOCOL_NEGOTIATION=auto. Live 2.1.257 calls (host-lineage audit) carriedclaudecode/toolUseId+ progressToken, neverparams.task.mcp_2026_07_28adds paginated discovery, MRTR, non-blocking startup; tasks there are theio.modelcontextprotocol/tasksextension shape, not served by this SDK; default legacy handshake; recorded calls carryx-codex-turn-metadata, noparams.task.progressToken.2025-11-25client that sendsparams.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'spollInterval, and gains List tasks when the server declares the capability. The typed browser→dev-server operation vocabulary gainscallToolTask/getTask/getTaskResult/cancelTask/listTasksend to end (remote transport → session routes → devMcpSession, using the SDK client's schema-validatedrequest()).The
host-testexample gains a task-capableslowprobe (execution.taskSupport: 'optional', holds a call forholdMs, reports progress everytickMs) 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 overInMemoryTransport, hand-registered tools): capability +executionadvertisement, 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 + compiledexecution.taskSupport, create → result equals the ordinary call's result + related-task key, progress viatasks/getfromprogress.report()and a streamedAgent.Progressfallback, 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:AB4836shapes and messages,routeTaskSupportreader.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: theslowprobe.Removed:
packages/rsc-runtime/tests/mcp-tasks-deferral.test.tsand the@ts-expect-errorsentinel (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:buildgreen.Changeset
patchforagent-bundleand@agent-bundle/runtime: everything is additive —ToolConfiggains an optionalexecutionkey, 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.tsis emitted as its own rslib entry. When rslib concatenated it into themcp-server-runtimechunk, that chunk gained animport { __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-serverintegration, the new e2e in real Chrome,pnpm docs:site:build.Review status
3928a61(PR opened), three threads — each answered on its thread and resolved (the PR auto-merged on0f5d628a5while the follow-up test hardening was being gated; that hardening is test(mcp): negotiate the non-task revision for real in the required-tool unit test (#369 follow-up) #553):mcp-tasks.ts— required tools on non-task revisions. Valid. Fixed in7997f2a: the lifecycle (including therequiredrejection) applies only on a task-capable session —2025-11-25with the capability declared; on any other revision every call,requiredtools included, is the ordinary request and task metadata is ignored. Pinned by the unit test "serves every tool as an ordinary request on a revision without the core Tasks utility" intests/mcp-tasks.test.ts; follow-up test(mcp): negotiate the non-task revision for real in the required-tool unit test (#369 follow-up) #553 negotiates that session for real (the SDK client offers only2025-06-18and the server'sgetNegotiatedProtocolVersion()is asserted) rather than narrowing the server's view — a2026-07-28session is opened only by the SDK's serving entries, never by a hand-connected in-memory pair, and takes the same branch..changeset/454-route-render-budget.md— two changesets. That file was feat(routes): let a route declare its render budget with config.render.maxElapsedMs (#454) #526's changeset, present only because this branch was stacked on feat(routes): let a route declare its render budget with config.render.maxElapsedMs (#454) #526. Resolved by the rebase: feat(routes): let a route declare its render budget with config.render.maxElapsedMs (#454) #526 merged as43d787ff8, so the diff againstmaincarries exactly one changeset,.changeset/369-mcp-tasks.md.mcp-page.tsx— stale task error after a later success. Valid. Fixed in7997f2a: a successfultasks/get,tasks/list, ortasks/resultanswer clears an earlier error, so polling resumes; covered in the fold test (packages/workbench/tests/mcp-page.test.ts).mainafter feat(routes): let a route declare its render budget with config.render.maxElapsedMs (#454) #526 and feat(cli): serve a built MCP App standalone — agent-bundle serve-app and serveApp in agent-bundle/api (#514) #537 merged (same seven#369commits; conflicts re-resolved inmcp-server-runtime.ts,routes/{graph,index,public}.ts,wait.tsx,docs/diagnostics.md, andmcp.mdxen/zh, keeping both sides). Re-verified locally on the rebased head:pnpm build,pnpm typecheck,pnpm lint,pnpm test:unit,pnpm test:route-unit,pnpm test:projection,pnpm --filter @agent-bundle/runtime test,pnpm docs:site:build.3928a61are not re-reviewed by the connector.Landing notes (rebase over
mainafter #526, #532, #545, #540)43d787ff8; its three stacked commits are gone from this diff and.changeset/454-route-render-budget.mdno longer appears here (the second Codex P1 above is moot). Rebase conflicts were indocs/diagnostics.md(theAB4835/AB4836rows beside each other),mcp-server-runtime.ts(feat(runtime): expose the resolved plugin root as request.plugin and hand it to providers (#468) #532'spluginon the request identity beside the task registration), androutes/index.ts(export lists) — resolved additively;7997f2adacarries the identical tree.tasks/get, render budget (Routes cannot raise the 60 s render session limit (maxElapsedMs); long-polling tools must clamp their own waits #454) bounding the render per task,mcp.taskshost evidence in all four capability JSONs, Workbench task flow with a real-Chrome e2e, docs en + zh, one changeset — all present. One gap: Task-augmented MCP tool calls (CreateTaskResult / tasks/result) — #96 acceptance remainder #369 names thepackages/agent-bundle/README.mdtesting story alongsidedocs/entry-conventions.md, and the README had no mention;45d2e37adds a "Task-augmented calls (mcp-in-memory)" paragraph to the proof-level section, describing the lifecycle exactly astests/projection/mcp-in-memory.test.tsdrives it through the session's SDKclient.pnpm build,pnpm typecheck,pnpm lint,pnpm test:unit(3336 passed, including the Workbench page/controller fold tests),pnpm test:route-unit,pnpm test:projection,pnpm --filter @agent-bundle/runtime test(447),pnpm test:packed -- packed-stdio-projection(27),mcp-tasks.e2e.test.tsin real Chrome (1 passed),pnpm docs:site:build(language parity green).