modelcontextprotocol / modelcontextprotocol/typescript-sdk

[v2] Tasks extension: tools/call rejects CreateTaskResult but accepts an omitted discriminator as complete

Open
#2,637 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement spec-2026-07-28 v2
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

What happened?

On a 2026-07-28 connection, after the client declares io.modelcontextprotocol/tasks in its per-request capabilities and the server advertises the same extension, the SDK has no end-to-end path for the CreateTaskResult that the extension permits in place of a CallToolResult:

  • McpServer.registerTool() types the callback as CallToolResult | InputRequiredResult, so returning CreateTaskResult requires a cast.
  • If the handler returns an explicit resultType: "task", the server preserves it on the wire, but Client.callTool() rejects it with SdkErrorCode.UnsupportedResultType: Unsupported result type 'task' for tools/call.
  • If a dynamically typed handler returns the same task fields but accidentally omits resultType, the server stamps resultType: "complete", adds content: [], and Client.callTool() resolves successfully. The task fields survive only as unknown extra keys, so application code sees an empty final tool result instead of a task handle or an error.

The last case is not a conforming handler result; it is included because the current encode path converts that server error into a valid-looking success rather than surfacing it.

Measured behavior
Handler return Wire result Client.callTool()
Task handle with resultType: "task" resultType: "task", plus content: [] Rejects with UNSUPPORTED_RESULT_TYPE
Same task fields without resultType resultType: "complete", plus content: [] Resolves as an empty result
Ordinary { content: [] } control resultType: "complete", content: [] Resolves normally

Every request in the reproduction declares extensions: { "io.modelcontextprotocol/tasks": {} }; the server advertises the same capability. This matters because the extension forbids returning a task handle to a client that did not declare support.

Code to reproduce

The standalone script pins @modelcontextprotocol/server, /client, and /node to 2.0.0, starts a real HTTP server and SDK client, records the exact wire result, and asserts all three outcomes:

https://github.com/AndresSaa/mcp-durable-tasks/blob/faf4943ddc8fda43cb7b559a6505d0cee9b857f0/examples/conformance-reproductions/result-type.mts

git clone https://github.com/AndresSaa/mcp-durable-tasks.git
cd mcp-durable-tasks
git checkout faf4943ddc8fda43cb7b559a6505d0cee9b857f0
corepack pnpm install --frozen-lockfile
corepack pnpm --dir examples/conformance-reproductions run result-type

The observed output for the conforming case is:

wireResultType: task
client: rejected
clientCode: UNSUPPORTED_RESULT_TYPE
clientMessage: Unsupported result type 'task' for tools/call

For the missing-discriminator case:

wireResultType: complete
wireContent: []
client: resolved
clientKeys: [_meta, content, createdAt, lastUpdatedAt, status, taskId, ttlMs]
Why this appears inconsistent

The Tasks extension says that, once the extension is negotiated, a server may return CreateTaskResult instead of CallToolResult, and the client must be prepared to handle either: https://github.com/modelcontextprotocol/ext-tasks/blob/2c1425d9a288b9b1f489430fe1e00bb392b47e48/specification/draft/tasks.md#L59-L61. It also requires the task discriminator to be explicit — servers MUST set resultType to "task" when returning a CreateTaskResult: https://github.com/modelcontextprotocol/ext-tasks/blob/2c1425d9a288b9b1f489430fe1e00bb392b47e48/specification/draft/tasks.md#L95-L102.

On current SDK main, the server-side encode contract stamps every absent discriminator as "complete" while allowing an explicit non-complete value through for tools/call: https://github.com/modelcontextprotocol/typescript-sdk/blob/cc4b41617ce3601b1290d67216ea0b194a3cd9ac/packages/core-internal/src/wire/rev2026-07-28/encodeContract.ts#L45-L85.

The client codec recognizes input_required, but treats every other non-complete discriminator, including task, as unsupported: https://github.com/modelcontextprotocol/typescript-sdk/blob/cc4b41617ce3601b1290d67216ea0b194a3cd9ac/packages/core-internal/src/wire/rev2026-07-28/codec.ts#L177-L234.

The public tool callback types likewise include CallToolResult | InputRequiredResult, but not an extension task result: https://github.com/modelcontextprotocol/typescript-sdk/blob/cc4b41617ce3601b1290d67216ea0b194a3cd9ac/packages/server/src/server/mcp.ts#L1218-L1252.

Expected

After both peers declare io.modelcontextprotocol/tasks, there should be a typed server path for returning CreateTaskResult and a client path that exposes that result instead of rejecting it. A handler that intends to return a task should not be silently projected to a successful final result when its required discriminator is missing.

This looks like a focused acceptance gap under #2189. The API-shape question is whether extension support should widen callTool() and ToolCallback to discriminated unions directly, or install an extension-specific result handler through the planned extension framework. Either shape would resolve the measured wire/client mismatch; I do not want to assume which one fits the v2 design.

SDK version

@modelcontextprotocol/server@2.0.0, @modelcontextprotocol/client@2.0.0, @modelcontextprotocol/node@2.0.0, Node.js v24.18.0.

Area

Server and Client

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked conformance reproduction, then read packages/core-internal/src/wire/rev2026-07-28/encodeContract.ts, codec.ts, and packages/server/src/server/mcp.ts. Trace the negotiated tasks path for tools/call and compare the typed callback, wire discriminator, and client result handling. Done means a negotiated CreateTaskResult is type-safe and exposed by the client, while a missing required discriminator is not converted into a successful empty result.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.