Introduce extension SDK for TS - #21
LucaButBoring wants to merge 33 commits into
Conversation
- add provenance-checked V1 and V2 schema artifacts - implement generation-specific declarations and strict codecs - cover protocol invariants with property-based tests
- implement the connected session port and immediate executions - manage tool declarations with abortable ordered refreshes - add input routing, lifecycle guards, and V2 tool codecs
- add package-level format and format-check scripts - enforce formatting in the package check workflow - preserve immutable schema artifacts through ignore rules
- Add V1 and V2 task drivers with polling, notifications, and retries - Implement conflated updates, cancellation, and lifecycle cleanup - Cover protocol envelopes and adversarial races with property tests
- Add type-aware ESLint configuration for the ext-tasks package - Integrate linting into package checks and prepack verification - Fix type-safety, lifecycle, and test diagnostics without suppressions
- Correlate V1 task input with related-task metadata - Handle keyed V2 input through validated partial updates - Add fail-closed and lifecycle race coverage
- Bind serialized references to durable endpoint identities - Resume V1 and V2 tasks with generation-specific lifecycles - Adapt Client 2.0 through public request and handler seams
- Split client and core monoliths into cohesive internal modules - Consolidate generation-specific task drivers and remove pre-release aliases - Strengthen codec, export, packed-consumer, and CI contracts
- Share generation-neutral codec validation and path helpers - Preserve generation-specific request and wire decoding - Require and add JSDoc for exported functions
- Replace handwritten V1/V2 codecs and wire types with inferred Zod schemas - Accept caller result schemas directly and remove custom codec compatibility APIs - Harden schema exports, protocol defaults, and packed dependency contracts
- Enable the strict type-checked typescript-eslint preset - Resolve all unsafe, redundant, and confusing typed lint findings - Strengthen runtime boundary checks and race/property tests
- Extract input projection, correlation, and context utilities - Decompose V2 input resolution and schema construction - Clarify task update queues and close a notification startup race
Centralize snapshot arbitration and observation sequencing behind named driver contexts. Decompose V2 task and input processing, with regression coverage for terminal authority and one-shot input keys.
Replace forward-declared branch results with focused helpers and explicit outcomes. Keep timing-sensitive session locals and iterative cursor state unchanged.
- Finalize generation-tagged declarations and codec-neutral results - Correct task input routing, recovery ownership, and lifecycle cleanup - Add packed exports and SDK peer-range contract validation - Document the initial client integration API without naming consumers Signed-off-by: Luca Chang <lucalc@amazon.com>
- Add semantic task sessions, controllers, settlement, and input routing - Add SDK raw-dispatch integration and receiver task bindings - Add runtime boundary adapters and generation-neutral projections - Remove obsolete task-result bridging and session history APIs - Expand protocol, lifecycle, packaging, and consumer validation Signed-off-by: Luca Chang <lucalc@amazon.com>
- Add per-request timeout options to tool calls and task controllers - Preserve timeout context across initiating and follow-up requests - Forward timeout values through the SDK client adapter - Cover managed and detached task request propagation Signed-off-by: Luca Chang <lucalc@amazon.com>
There was a problem hiding this comment.
🟡 Changes recommended
Protocol routing, task preference enforcement, schema validation, timer handling, and lifecycle issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Introduces a publishable TypeScript SDK for MCP Tasks, supporting V1/V2 requester workflows and V1 receivers, with documentation, tests, packaging, and CI integration.
Changes:
- Adds generation-neutral task sessions, execution lifecycle, recovery, input routing, and receiver APIs.
- Adds runtime schemas, adapters, extensive tests, and package validation.
- Adds TypeScript guides and repository CI/documentation integration.
File summaries
| File | Description |
|---|---|
.github/workflows/ci.yml |
Expands package, documentation, and peer-range CI. |
.github/workflows/deploy.yml |
Validates the repository before documentation deployment. |
.gitignore |
Ignores package tarballs. |
.vitepress/config.mts |
Adds TypeScript SDK navigation. |
README.md |
Documents the TypeScript package and development commands. |
package-lock.json |
Locks workspace dependencies. |
package.json |
Configures workspaces and root scripts. |
packages/ext-tasks/.prettierignore |
Excludes generated/package artifacts. |
packages/ext-tasks/LICENSE |
Adds package licensing. |
packages/ext-tasks/README.md |
Documents package usage and entry points. |
packages/ext-tasks/eslint.config.mjs |
Configures package linting. |
packages/ext-tasks/package.json |
Defines package exports, dependencies, and scripts. |
packages/ext-tasks/schema/v1/schema.json |
Adds the pinned V1 JSON schema. |
packages/ext-tasks/schema/v1/schema.ts |
Adds pinned V1 TypeScript declarations. |
packages/ext-tasks/schema/v2/schema.json |
Adds the pinned V2 JSON schema. |
packages/ext-tasks/schema/v2/schema.ts |
Adds pinned V2 TypeScript declarations. |
packages/ext-tasks/scripts/check-exports.mjs |
Validates package and consumer contracts. |
packages/ext-tasks/scripts/check-peer-range.mjs |
Checks supported client peer versions. |
packages/ext-tasks/scripts/check-schema-provenance.mjs |
Verifies pinned schema provenance. |
packages/ext-tasks/src/client/api.ts |
Defines the public client API. |
packages/ext-tasks/src/client/client-adapter.test.ts |
Tests SDK client adaptation. |
packages/ext-tasks/src/client/declarations-capabilities.test.ts |
Tests declarations and negotiated capabilities. |
packages/ext-tasks/src/client/execution.ts |
Implements execution state, observation, and settlement. |
packages/ext-tasks/src/client/immediate-session-basics.test.ts |
Tests immediate execution and session basics. |
packages/ext-tasks/src/client/index.ts |
Exposes the client entry point. |
packages/ext-tasks/src/client/input-routing.ts |
Implements application-input routing. |
packages/ext-tasks/src/client/internal.ts |
Provides generation-neutral projections. |
packages/ext-tasks/src/client/port.ts |
Implements task RPC dispatch. |
packages/ext-tasks/src/client/protocol-matrix.test.ts |
Tests V1/V2 semantic parity. |
packages/ext-tasks/src/client/request-input-continuation.test.ts |
Tests request-scoped input continuation. |
packages/ext-tasks/src/client/sdk-client-adapter.ts |
Adapts SDK clients to task sessions. |
packages/ext-tasks/src/client/session-facade.test.ts |
Tests the public session facade. |
packages/ext-tasks/src/client/session.ts |
Implements task-enabled sessions. |
packages/ext-tasks/src/client/task-controller.test.ts |
Tests manual task controllers. |
packages/ext-tasks/src/client/task-controller.ts |
Implements manual task operations. |
packages/ext-tasks/src/client/task-lifecycle-races.test.ts |
Tests lifecycle and concurrency races. |
packages/ext-tasks/src/client/task-protocol-v1.ts |
Implements V1 task execution. |
packages/ext-tasks/src/client/task-protocol-v2.ts |
Implements V2 task execution and input. |
packages/ext-tasks/src/client/task-resumption.test.ts |
Tests task recovery and ownership. |
packages/ext-tasks/src/client/tool-declarations.ts |
Manages tool discovery and refreshes. |
packages/ext-tasks/src/client/v1-input-task.test.ts |
Tests V1 input routing. |
packages/ext-tasks/src/client/v2-input-task.test.ts |
Tests V2 task input handling. |
packages/ext-tasks/src/core/index.test.ts |
Tests core JSON and codec utilities. |
packages/ext-tasks/src/core/index.ts |
Adds common types, codecs, and errors. |
packages/ext-tasks/src/core/v1/index.test.ts |
Tests V1 schemas and negotiation. |
packages/ext-tasks/src/core/v1/index.ts |
Exposes V1 APIs. |
packages/ext-tasks/src/core/v1/negotiation.ts |
Implements V1 capability negotiation. |
packages/ext-tasks/src/core/v1/schemas.ts |
Defines V1 runtime schemas. |
packages/ext-tasks/src/core/v2/index.test.ts |
Tests V2 runtime contracts. |
packages/ext-tasks/src/core/v2/index.ts |
Exposes V2 APIs. |
packages/ext-tasks/src/core/v2/integration.ts |
Adds V2 capability and subscription helpers. |
packages/ext-tasks/src/core/v2/schemas.ts |
Defines V2 runtime schemas. |
packages/ext-tasks/src/receiver/index.ts |
Implements the V1 Tasks receiver. |
packages/ext-tasks/src/receiver/receiver.test.ts |
Tests receiver lifecycle behavior. |
packages/ext-tasks/test-support/client/fake-port.ts |
Adds a client test port. |
packages/ext-tasks/test-support/client/semantic.ts |
Adds semantic test adapters. |
packages/ext-tasks/tsconfig.eslint.json |
Configures lint type information. |
packages/ext-tasks/tsconfig.json |
Configures package compilation. |
packages/ext-tasks/tsconfig.test.json |
Configures test type-checking. |
packages/ext-tasks/vitest.config.ts |
Configures package tests. |
tsconfig.package.json |
Adds shared package compiler settings. |
typescript/adapters-and-schemas.md |
Documents advanced adapters and schemas. |
typescript/client/execution.md |
Documents execution control. |
typescript/client/input-and-recovery.md |
Documents input handling and recovery. |
typescript/getting-started.md |
Adds a requester quick start. |
typescript/index.md |
Adds the TypeScript SDK overview. |
typescript/migrating-from-the-sdk.md |
Documents SDK migration. |
typescript/receiver.md |
Documents V1 receiver usage. |
typescript/troubleshooting.md |
Documents common lifecycle failures. |
Review details
- Files reviewed: 65/69 changed files
- Comments generated: 9
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
V2 validation, capability merging, and documented signal behavior contain interoperability issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (4)
Previously missed (2) — in code that hasn't changed since the last review.
packages/ext-tasks/src/core/v2/integration.ts:126
- This overwrites an existing
io.modelcontextprotocol/clientCapabilitiesobject. For custom ports, request metadata that already advertises sampling, elicitation, or another extension loses those capabilities whenever Tasks framing is added. Merge the existing capability object and itsextensionsmap, then add the Tasks key, matching the merge performed byframeV2TaskRequest().
typescript/client/execution.md:93 - This states that
signalbounds the whole operation, but aftercallTool()returns a task execution the implementation disposes the linked call signal and constructs the execution with only the session lifecycle signal. Aborting the supplied signal later therefore does not stop task polling or input handling. Either retain that signal in the task execution lifecycle or document that it only bounds task creation.
packages/ext-tasks/src/core/v2/schemas.ts:256
- The wire schema accepts any JSON value as a root, so values such as
{ roots: [null, "x"] }pass validation and are sent intasks/update. The 2026-07-28 schema requires each root to be an object with a stringuri(plus optionalnameand_meta). Validate the actual Root shape here.
packages/ext-tasks/src/core/v2/schemas.ts:259 - This schema only validates
action; becauseopenObjecttreatscontentand_metaas undeclared keys, it accepts any JSON value for either. The protocol requirescontentto be an object whose values are strings, integers, booleans, or string arrays, and_metato be an object. Invalid handler output currently passes and is sent to the server.
- Files reviewed: 65/69 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Lifecycle cancellation, protocol decoding, polling, and schema-validation defects can cause invalid traffic or broken task executions.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (8)
Previously missed (7) — in code that hasn't changed since the last review.
packages/ext-tasks/src/client/execution.ts:74
- A missing server polling hint currently causes 100
tasks/getrequests per second per active task. For minute-long tasks or many concurrent executions this can overwhelm both client and server; use a production-safe default cadence (typically on the order of seconds) or make it configurable.
packages/ext-tasks/src/client/execution.ts:574 - A successful direct
execution.cancel()does not end the input lifetime, so an in-flight application input handler can continue and submittasks/updateafter cancellation was accepted. End input handling when the shared cancellation promise succeeds (but not when cancellation fails).
packages/ext-tasks/src/client/session.ts:260 - Input is aborted before remote cancellation is known to have succeeded. If the cancellation RPC fails, the still-running task permanently loses its input handler and can no longer progress. Abort the input lifetime only after
execution.cancel()resolves.
packages/ext-tasks/src/client/session.ts:425 - This disposes the link to
ToolCallOptions.signalas soon as task creation returns. Consequently aborting that signal later does not stop task polling or task-scoped input, despite the public contract documenting that it bounds the operation lifecycle. Keep the linked lifecycle alive for task executions and dispose it when that execution ends.
packages/ext-tasks/src/client/task-protocol-v2.ts:179 - Including the initial hint in every
Math.maxmakes a later lowerpollIntervalMsineffective, although the wire contract allows this value to change over a task's lifetime. Once a detailed snapshot exists, honor its current hint rather than permanently retaining the creation-time value.
packages/ext-tasks/src/core/v1/schemas.ts:37 - The V1 wire schema defines JSON-RPC request IDs as strings or integers, but
z.number()also accepts fractions. This lets the exported guards/types accept invalid request envelopes; usez.int()to match the pinned schema.
packages/ext-tasks/src/core/v2/schemas.ts:256 ListRootsResultrequires an array of Root objects with a stringuri, but this accepts any JSON values (including numbers and strings). Since this schema validates application input responses beforetasks/update, malformed roots can be emitted on the wire instead of being rejected locally.
This issue also appears on line 257 of the same file.
packages/ext-tasks/src/core/v2/schemas.ts:259
contentis a declared part of an elicitation result, but leaving it to the open-object catchall accepts arbitrary nested JSON. The protocol restricts each submitted field to string, number, boolean, or string array, so invalid handler output currently passes validation and is sent intasks/update. Add the declaredcontentschema.
- Files reviewed: 65/69 changed files
- Comments generated: 3
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
Public input types and runtime schemas accept invalid protocol data, JSON validation has an edge-case bypass, and declaration projection loses extension fields.
Review details
Suppressed comments (7)
Previously missed (6) — in code that hasn't changed since the last review.
packages/ext-tasks/src/client/api.ts:165
- The public callback type permits nested objects, nulls, and arbitrary arrays in elicitation content, although those values are not valid on the wire. This makes type-correct handlers fail only when their response is encoded; narrow the value type to the protocol's scalar/string-array union.
packages/ext-tasks/src/client/api.ts:177 - A root is typed as an arbitrary record, so TypeScript accepts
{ roots: [{}] }even though every protocol root requires a stringuri. Model the required root fields here so invalid handler implementations are rejected at compile time.
packages/ext-tasks/src/client/internal.ts:160 - When projecting a discovered V1 tool, only
taskSupportis retained fromexecution; every other nested execution field is silently discarded even thoughToolDeclaration.executionExtensionspromises to retain such data for inspection and projection.
This issue also appears on line 203 of the same file.
packages/ext-tasks/src/core/index.ts:166
- This sparse-array check can be bypassed by an enumerable non-index property: an array with one hole and one
extraproperty haslength === Object.keys(...).length, whileevery()skips the hole.isJsonValue()then returns true even though serialization changes the value to[null]. Verify that every index is an own property in addition to rejecting extra keys.
packages/ext-tasks/src/core/v2/schemas.ts:256 - This accepts any JSON value as a root, so callback output such as
{ roots: [null] }passes validation and is sent intasks/update. The protocol requires each root to have a stringuri(with optionalnameand_meta); validate that shape here.
packages/ext-tasks/src/core/v2/schemas.ts:259 - The elicitation result schema validates only
action; because the object is open, any JSON value is accepted forcontent. This lets invalid form responses reachtasks/update, while the protocol limits content values to strings, numbers, booleans, or string arrays.
packages/ext-tasks/src/client/internal.ts:209
executionExtensionsis part of the public neutral declaration specifically to retain nested execution fields, but V1 projection drops it and emits onlytaskSupport. A declaration created bytoolDeclarationFromMcpTool()therefore cannot round-trip vendor execution data.
return generation === "v1"
? {
...common,
...(declaration.taskSupport === undefined
? {}
: { execution: { taskSupport: declaration.taskSupport } }),
}
- Files reviewed: 66/70 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Task cancellation, V1 result retrieval, capability merging, and malformed input handling contain unresolved correctness issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
packages/ext-tasks/src/core/v2/integration.ts:127
- Installing the Tasks capability replaces any existing value under
io.modelcontextprotocol/clientCapabilities, including other capabilities and extensions supplied by a custom port caller. Merge the existing capability object and itsextensionsmap before adding the Tasks key, asframeV2TaskRequest()already does for SDK-backed ports.
packages/ext-tasks/src/receiver/index.ts:160 - A non-null scalar
taskvalue is treated as valid augmentation, so malformed requests such as{ task: true }bypass the SDK validator and create retained tasks. Require an object-shaped task metadata value (and validatettlwhen present); malformed augmentation can then continue through the SDK validation path instead of the receiver handler.
packages/ext-tasks/src/receiver/index.ts:451 - The 2025-11-25
tasks/resultoperation is a blocking result retrieval, but this rejects whenever work is still pending. A conforming client may call it immediately after task creation and should be allowed to awaitrecord.result; the current behavior breaks those clients even though this package's own requester happens to poll first.
- Files reviewed: 65/70 changed files
- Comments generated: 2
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
Task cancellation signals, recovery request context, and V1 manual cancellation outcomes are not preserved correctly.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
packages/ext-tasks/src/client/session.ts:433
- The call-scoped signal is disconnected as soon as the initial
tools/callreturns: thisfinallydisposes its linkage, and both task constructors below receive onlythis.lifecycleController.signal. Consequently, abortingoptions.signalaftercallTool()returns does not stop polling or abort task input, despite the documented contract that the signal bounds the operation's local lifecycle. Transfer the linked lifecycle to task-backed executions and dispose it when those executions settle/detach; only dispose it here for immediate/error paths.
packages/ext-tasks/src/client/session.ts:575 - Recovery dispatches
tasks/getwithout anyDispatchContext, and the recovered execution is likewise created without one. SinceTaskRecoveryOptionsexposes no headers or request timeout, a task originally called with per-request authentication cannot be resumed after reconnecting: every follow-up silently loses that required context. Add recovery request-context options and propagate them to the initial lookup and the recovered execution for both generations.
packages/ext-tasks/src/client/task-controller.ts:113 - The V1 branch calls
tasks/resultdirectly and wraps every rejection as afailedoutcome. Under the V1 protocol,tasks/resultreturns an error for any non-completed task, so a cancelled task can never produce thecancelledoutcome promised by this generation-neutral API (and a known failed task loses its task snapshot). Polltasks/getto a terminal state first, as the owned V1 execution does, then classify cancelled/failed and calltasks/resultonly for completed tasks.
- Files reviewed: 64/70 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Runtime schema, recovery lifecycle, fallback routing, receiver error handling, and release validation issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
packages/ext-tasks/src/core/v2/schemas.ts:273
ApplicationElicitContentValueand the pinned MCPElicitResult.contenttype both allow any JSON number, but this schema accepts only integers. A type-correct handler returning a decimal (for example, a form field with1.5) will fail response validation, so the task update is skipped and the task can remaininput_required. Usez.number()here.
packages/ext-tasks/src/receiver/index.ts:241report()runs from detached promise rejection handlers (notification failures and late callback failures). If the consumer'sonErrorcallback throws, those detached promises reject with no observer and can trigger an unhandled rejection. Guard the error sink, as the client session'sreportBackgroundErroralready does, so reporting cannot create a second background failure.
- Files reviewed: 62/70 changed files
- Comments generated: 3
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
Protocol validation, V1 task requirements, lifecycle cancellation, and unsafe operational defaults need correction before release.
Review details
Suppressed comments (6)
Previously missed (4) — in code that hasn't changed since the last review.
packages/ext-tasks/src/client/execution.ts:85
- A missing server polling hint currently produces one
tasks/getevery 10 ms per active task (100 requests/second/task). This default can overwhelm both client and server under ordinary concurrent workloads; use a production-safe cadence unless the server explicitly requests faster polling.
packages/ext-tasks/src/client/session.ts:237 - Unlike calls and task-controller operations,
listTasks()passes only the caller signal to the port. Closing or invalidating the session therefore does not abort an in-flight list request, so it can hang or resolve after the session is unusable. Link the caller signal withthis.lifecycleController.signal, dispose that link infinally, and re-check session usability after dispatch.
This issue also appears on line 320 of the same file.
packages/ext-tasks/src/core/v2/integration.ts:43
- This predicate reports Tasks support for any owned value, including
null,true, or a non-empty object, althoughTasksExtensionCapabilityV2Schemarequires an empty object. Callers can therefore enable V2 behavior from a malformed capability envelope. Validate the extension value as well as the key's presence.
packages/ext-tasks/src/receiver/index.ts:184 - This accepts
task.ttl: null, even though the package's own 2025-11-25TaskMetadataV1Schemadefinesttlas an optional integer and rejectsnull. A malformed task augmentation therefore reaches the receiver and creates a task; omission should represent an unspecified TTL.
This issue also appears on line 214 of the same file.
packages/ext-tasks/src/receiver/index.ts:217
- The default combination retains tasks forever (
ttlMs === null) but rejects every new task once 1,000 records have accumulated. Because there is no per-task deletion API, a long-lived receiver using defaults permanently stops accepting work after its first 1,000 requests. Please pair the finite capacity with a finite default retention period, provide terminal-record eviction, or otherwise make the defaults preserve availability.
packages/ext-tasks/src/client/session.ts:324 - For V1,
preference: "forbid"forcescallAsTaskV1false even when the declaration saystaskSupport: "required"; likewise, missing server task capability falls through to an ordinary call. Both paths violate the known tool contract and dispatch a request that cannot validly execute. Reject this conflict before dispatch instead.
- Files reviewed: 60/70 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Introduces an extension SDK for Tasks support, layered on top of the general TS SDK. Supports both the
2025-11-25and2026-07-28specifications. Original implementation comes from the Inspector and has been reshaped to make a cleaner public API.Motivation and Context
Unblocks support for Tasks for TS SDK consumers.
How Has This Been Tested?
Integrated into the MCP Inspector in a branch (modelcontextprotocol/inspector#2308).
Breaking Changes
N/A; new package.
Types of changes
Checklist
Additional context
We'll need to set up CI/CD to actually publish this separately. Need to figure out how to do this.