はじめに
MCP の 2026-07-28 仕様で、長時間処理を扱う Tasks が実験的コアから io.modelcontextprotocol/tasks 拡張へ移りました。ポーリング型の tasks/get と tasks/update を持ち、Tier 1 SDK 4種が新仕様に対応した、と公式ブログは書いています。
では、npm から入る TypeScript SDK で今日それを実装できるのか。@modelcontextprotocol/server 2.0.0 / @modelcontextprotocol/client 2.0.0 / @modelcontextprotocol/core 2.0.0 を実際にインストールし、Tasks に関係する 6 つのメソッドを 1 つずつ叩いて確かめました。結果は「4 つは動く、2 つは動かない、そして動かない理由は SDK のバグではない」でした。
対象読者は、MCP サーバーに長時間実行の仕組みを入れようとしていて、Tasks 拡張を採用するか判断したい開発者です。
検証結果の一覧
Node.js v22.22.2・@modelcontextprotocol/{server,client,core} 2.0.0・InMemoryTransport で server と client を直結し、それぞれのメソッドを実行した結果です。
| # | 対象 | 2026-07-28 仕様での位置づけ | SDK 2.0.0 での実測 | 判定 |
|---|---|---|---|---|
| 1 |
initialize のバージョン交渉 |
2026-07-28 | ネゴシエート結果は 2025-11-25
|
❌ 新仕様に未到達 |
| 2 |
tools/call をタスク化して { task } だけ返す |
タスク生成の入口 | -32602 Invalid tools/call result |
❌ 単独では返せない |
| 3 | tasks/get |
ポーリング取得 | 自前ハンドラで正常応答 | ✅ 動く |
| 4 | tasks/result |
結果ペイロード取得 | 自前ハンドラで正常応答 | ✅ 動く |
| 5 |
tasks/list / tasks/cancel
|
一覧・キャンセル | 自前ハンドラで正常応答 | ✅ 動く |
| 6 | tasks/update |
公式ブログが挙げた新メソッド | Method not found |
❌ SDK に不在 |
以下、1 件ずつ実測ログとあわせて見ていきます。
1. ネゴシエートされるのは 2025-11-25 で、2026-07-28 ではない
まず SDK が持つプロトコルバージョンの定数を読み出しました。
const s = require('@modelcontextprotocol/server');
console.log('LATEST:', s.LATEST_PROTOCOL_VERSION);
console.log('SUPPORTED:', JSON.stringify(s.SUPPORTED_PROTOCOL_VERSIONS));
LATEST: 2025-11-25
DEFAULT_NEGOTIATED: 2025-03-26
SUPPORTED: ["2025-11-25","2025-06-18","2025-03-26","2024-11-05","2024-10-07"]
SUPPORTED_PROTOCOL_VERSIONS に 2026-07-28 が入っていません。クライアント側で protocolVersion: '2026-07-28' を指定しても接続自体は成立しますが、実際に流れるワイヤーを覗くと交渉結果は 2025-11-25 に落ちています。
C->S {"method":"initialize","params":{"protocolVersion":"2025-11-25", ...
S->C {"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{},"tasks":{...}}, ...
つまり Tasks 拡張が前提とする 2026-07-28 のワイヤー規約(結果の resultType による判別など)には、既定では到達しません。
なぜこうなっているかは、リリース日を見ると腑に落ちます。@modelcontextprotocol/server 2.0.0 の公開は 2026-07-27 23:55 UTC で、仕様の公開日 2026-07-28 の 前日 でした。
"2.0.0-beta.5": "2026-07-21T13:39:33.625Z",
"2.0.0": "2026-07-27T23:55:22.239Z"
新仕様のコード自体は入っています。SDK のバンドルには 2026 era 用のコーデックがあり、node_modules/@modelcontextprotocol/server/dist/createMcpHandler-CLhGwQTn.d.mts の encodeResult のコメントには次のようにあります。
The 2026-era codec strictly enforces the 2026 wire shape for the known deleted-field set (
execution.taskSupport,capabilities.tasks— Q1-SD3 iii), stampsresultType...
2026 era では resultType を必須の判別子として付与し、execution.taskSupport と capabilities.tasks は「削除済みフィールド」として扱う、という意味です。実装は先に入っていて、交渉テーブルに載っていないだけ という状態です。
2. tools/call から { task } だけを返すと -32602 で落ちる
Tasks の入口は「タスク実行を要求された tools/call に対して、結果ではなくタスク記述子を返す」ことです。素直に書くとこうなります。
server.setRequestHandler('tools/call', async (req) => {
const t = {
taskId: 't1', status: 'working', ttl: 60000,
createdAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
pollInterval: 100,
};
return { task: t }; // ← タスク記述子だけを返す
});
これを呼ぶと、クライアントに届く前に サーバー側の送信時バリデーション で弾かれます。
S->C {"jsonrpc":"2.0","id":1,"error":{"code":-32602,
"message":"Invalid tools/call result: [{\"code\":\"custom\",
\"message\":\"content is required when the body carries 'task' —
another result family cannot default into an empty tools/call success\"}]"}}
SDK のバンドルを追うと、この規則は 2025 era のワイヤーシームに置かれた CallToolResultWireSchema にあります。task / inputRequests / requestState の 3 キーは「別の結果ファミリー」として扱われ、content のない tools/call 応答に紛れ込むことが禁止されています。2026 era なら resultType で結果ファミリーを判別できますが、交渉結果が 2025-11-25 である以上その経路には入りません。
なお、リクエスト側の task パラメータ自体はサーバーまで届いています。
[server] tools/call params.task = {"ttl":60000}
tools/list で execution: { taskSupport: 'required' } を宣言することもでき、クライアントの listTools() からそのまま読めます。入口の語彙は通るのに、出口の応答形式だけが 2025 era の検査に引っかかる という非対称な状態です。
3. tasks/get / tasks/result / tasks/list / tasks/cancel は動く
一方、タスクのライフサイクルを扱う 4 メソッドは、自分でハンドラを書けば普通に動きます。SDK 2.0.0 の setRequestHandler は第 1 引数がメソッド名の文字列である点に注意してください(1.x のスキーマオブジェクト渡しは '[object Object]' is not a spec request method で弾かれます)。
server.setRequestHandler('tasks/get', async (req) => store.get(req.params.taskId).task);
server.setRequestHandler('tasks/result', async (req) => store.get(req.params.taskId).result);
server.setRequestHandler('tasks/list', async () => ({ tasks: [...store.values()].map((e) => e.task) }));
server.setRequestHandler('tasks/cancel', async (req) => { /* status を cancelled に */ });
実行結果です。
OK C1 tasks/get: {"taskId":"t1","status":"completed","ttl":60000,"createdAt":"2026-08-21T07:17:53.882Z",...}
OK C2 tasks/result: {"content":[{"type":"text","text":"done"}]}
OK C3 tasks/list: {"tasks":[{"taskId":"t1","status":"completed",...}]}
OK C4 tasks/cancel: {"taskId":"t1","status":"cancelled",...}
メソッド名も結果スキーマも SDK 側に実装済みで、GetTaskResultSchema / GetTaskPayloadResultSchema / ListTasksResultSchema / CancelTaskResultSchema がそのまま使えます。Task の状態は working / input_required / completed / failed / cancelled の 5 値で、ttl・createdAt・lastUpdatedAt が必須、pollInterval・statusMessage が任意です。
タスクの保存や実行そのものは SDK の担当外でした。TaskStore に相当する型は見当たらず、McpServer のコンストラクタにもタスク関連のオプションはありません。ハンドラと保存先は自前で用意する前提です。
4. tasks/update は SDK に存在しない
公式ブログは「ポーリング型の tasks/get と新しい tasks/update」と書いていますが、SDK 2.0.0 のメソッドテーブルにあるのは次の 4 つでした。
"tasks/get": null,
"tasks/result": null,
"tasks/list": null,
"tasks/cancel": null,
実際に投げると Method not found が返ります。
FAIL D tasks/update: Method not found
型定義側も type TaskRequestMethod = 'tasks/get' | 'tasks/result' | 'tasks/list' | 'tasks/cancel' で、tasks/update は含まれていません。結果ペイロードの取得はブログ記載の tasks/update ではなく tasks/result が担っており、スキーマ名も GetTaskPayloadRequestSchema(method は tasks/result のリテラル)になっています。SDK を読む側としては、ブログの表記ではなく TaskRequestMethod を正とするのが安全です。
5. いま Tasks 的な応答を返す唯一の抜け道
2 で落ちた原因は「task を運ぶ本体に content がない」ことでした。逆に言えば、content を添えれば task を同梱して返せます。
server.setRequestHandler('tools/call', async () => ({
content: [{ type: 'text', text: 'accepted' }],
task: { taskId: 't9', status: 'working', ttl: 60000, /* ... */ },
}));
B2 OK: {"content":[{"type":"text","text":"accepted"}],
"task":{"taskId":"t9","status":"working","ttl":60000,...}}
クライアントまで task が透過し、以降は tasks/get でポーリングして tasks/result で本体を取る、という流れを自分で組めます。ただしこれは 2026-07-28 の正式なタスク生成応答ではなく、2025 era のスキーマ検査を通すために content を添えた形です。仕様準拠のクライアントが resultType を見て分岐する未来には、そのまま乗り換えられません。プロトタイプの当座しのぎとしては使えるが、本番の互換性を約束する書き方ではない と理解しておくのが妥当です。
共通していたパターン
4 つの「動く」と 2 つの「動かない」は、バラバラの不具合ではありませんでした。境界は 1 本の線で引けます。
「語彙は入っているが、交渉テーブルに載っていない」 が今回の全事象の説明になります。だから個別のメソッドは叩けるのに、プロトコルの era 判定を経由する tools/call の応答形式だけが通らない。裏を返すと、SDK 側で必要なのは SUPPORTED_PROTOCOL_VERSIONS への追加とその周辺であって、Tasks の実装をゼロから待つ状況ではありません。
採用判断としては、次のように整理できます。
-
タスクの状態管理を自前で持ち、
tasks/*をハンドラで実装する なら、今日から書けます。保存先・実行・TTL 管理はもともと自前実装が前提です -
tools/callを仕様どおりタスク応答へ切り替える のは、SDK が 2026-07-28 を交渉できるようになってからです。それまではcontent同梱の暫定形になります -
tasks/updateに依存した設計は今は書けません。結果取得はtasks/resultに寄せておけば、後の移行コストが小さくなります
まとめ
MCP 2026-07-28 の Tasks 拡張を TypeScript SDK 2.0.0 で実装しようとすると、tasks/get / tasks/result / tasks/list / tasks/cancel の 4 メソッドは自前ハンドラで動く一方、tools/call からのタスク生成応答は -32602 で止まり、tasks/update は Method not found になります。原因は SDK の作りではなく、2.0.0 が仕様公開の前日リリースで SUPPORTED_PROTOCOL_VERSIONS に 2026-07-28 を持たないことです。
長時間処理を扱いたい場合、タスクの保存とライフサイクルは今のうちに自前で組み、tools/call の応答形式だけを後で差し替えられるように分離しておくのが現実的な進め方でした。
関連記事
- MCP SDK v2のステートレス化、旧クライアントは無改造で動いた
- LangGraph 1.2のerror_handlerとtimeoutを手書きと比較した
- deepagentsのファイルシステムバックエンド、混ぜると更新時刻が9時間ズレた