Skip to content

wire-schema validation rejects SEP-2663 task envelopes - no resultType "task" branch #424

Description

@panyam

The wire-schema validator introduced in #399 discriminates resultType === 'input_required' before falling back to the request method's result definition, but has no branch for resultType === 'task'. A well-formed SEP-2663 task envelope answering tools/call is therefore validated against CallToolResult and fails with must have required property 'content'.

The envelope shape cannot be present in the core spec schema, since tasks moved to an extension as of SEP-2663. So any SDK that implements the tasks extension and passes wire validation on everything else still fails these checks. In our runs (mcpkit) all 8 tasks scenarios fail exactly one check each (wire-schema-valid) for this reason but every other check in those scenarios passes.

Sample violation from tasks-lifecycle. here is the response that the validator rejected:

{
 "origin": "implementation",
 "specVersion": "2026-07-28",
 "context": "response to 'tools/call'",
 "errors": [
  "CallToolResult: must have required property 'content' (result of 'tools/call')"
 ],
 "message": {
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
   "_meta": {
    "io.modelcontextprotocol/serverInfo": {
     "name": "tasks-v2-demo",
     "version": "0.1.0"
    }
   },
   "createdAt": "2026-07-29T20:36:13Z",
   "lastUpdatedAt": "2026-07-29T20:36:13Z",
   "pollIntervalMs": 1000,
   "resultType": "task",
   "status": "working",
   "taskId": "task-42451aab21bbd828f4db0c0d",
   "ttlMs": 300000
  }
 }
}

The result carries resultType: "task" and the SEP-2663 envelope fields. There is nothing for CallToolResult.content to match because it is not a CallToolResult.

Possible fixes, in rough order of preference:

  1. Treat an unrecognized resultType value as an extension result and skip typed validation for it (fall back to the generic JSONRPCResultResponse envelope check), since resultType is exactly the discriminator the final revision added for this purpose.
  2. Vendor the tasks extension's envelope schema alongside the core schema and add a task branch, if extension-aware validation is wanted.

Happy to contribute a PR for either direction if maintainers have a preference.

Activity

  1. paulbalandan commented on Aug 10, 2026

    @paulbalandan

    Confirming this independently from a second SDK. Still reproduces at 0.2.0-alpha.11.

    We hit exactly the pattern you describe in nexusphp/mcp (PHP, spec 2026-07-28): 8 of our 10 server-side tasks scenarios fail exactly one check each, wire-schema-valid, with CallToolResult: must have required property 'content'. Every other check in those scenarios passes. The two that don't fail (tasks-required-task-error, tasks-status-notifications) are the two that never emit a CreateTaskResult on a tools/call. Our envelope is field-identical to the one in your report.

    Two things from the referee's own artefacts that may help when weighing this:

    The suite contradicts itself on the same message. In tasks-capability-negotiation, tasks-per-request-meta-opt-in passes because the response is a CreateTaskResult, and wire-schema-valid fails on that same response for not being a CallToolResult. Both checks, same envelope, same run.

    Reference-fixture CI cannot catch this. requirements/2026-07-28.yaml marks every tasks scenario not_scored with note: io.modelcontextprotocol/tasks (SEP-2663); pending against the reference fixture, run for visibility, and typescript-sdk's spec.types.2026-07-28.ts contains no CreateTaskResult at all (versus 2 occurrences in its spec.types.2025-11-25.ts). So the only implementations that trip this are ones that have already shipped the extension.

    On the proposed fixes, option 1 looks right on spec grounds, beyond just being cheaper. SEP-2663 §"Polymorphic Results" defines the discriminator as open:

    type ResultType = "complete" | "input_required" | "task" | string;

    and the core schema's ResultType is a bare "type": "string" describing only complete and input_required. So an unrecognised resultType is precisely the "this is an extension result" signal, and falling back to the generic JSONRPCResultResponse envelope check generalises to the next extension without vendoring anything. Option 2 would fix tasks and leave the same hole for the next extension result type.

    Happy to test a patch against our suite if that's useful.

  2. added a commit that references this issue on Sep 6, 2026
    cf022cb
  3. panyam commented on Sep 7, 2026

    @panyam
    ContributorAuthor

    Awesome. Confirming the fix. Ran the tasks scenarios against mcpkit's SEP-2663 server (examples/tasks-v2, Go, spec 2026-07-28) on both sides of the merge:

    before (74edef3) after (a983ba9)
    scenarios with a wire-schema-valid failure 8 0
    tasks checks 36 pass / 8 fail 44 pass / 0 fail

    The 8 were one wire-schema-valid each, all CallToolResult: must have required property 'content', with every other check in those scenarios passing. Same pattern reported for nexusphp/mcp, down to which two scenarios never failed: tasks-required-task-error and tasks-status-notifications, neither of which emits a CreateTaskResult on a tools/call.

    Thanks for taking option 1. Just a note - the merged version is a bit broader than what we originally for. The x-acme/streamed case means a private extension result that happens to satisfy the method's own schema passes too, so the next extension needs a code change here only if it also changes the result shape.

    @paulbalandan since you offered to test a patch against your suite, and this is on main now if you want to re-run it.

  4. paulbalandan commented on Sep 8, 2026

    @paulbalandan

    Re-ran against a983ba9.

    0.2.0-alpha.11 a983ba9
    scenarios with a wire-schema-valid failure 8 0
    tasks checks 36 pass / 8 fail 44 pass / 0 fail

    All ten server-side tasks scenarios, --spec-version 2026-07-28 --force. The eight per-check entries in our expected-failures.yaml now report as stale. The rest of the server suite is unchanged at 145 passed. The one new failure elsewhere was resource-parameter-matches-prm from #488, a real bug on our side, fixed.

    On the breadth: a private extension result that satisfies the method's own schema is wire-valid by construction, so passing it fits the check's name. The case it misses is an extension result that is both unrecognised and malformed. That belongs to the extension's own schema once one is versioned.

    We will drop the eight baseline entries when this ships in a release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions