- カテゴリー
- MCP
- 公開日
- 2026.09.26
Contents
はじめに
動画変換や大規模解析を通常のツール呼び出しで待ち続けると、HTTPタイムアウトや再接続への対応が難しくなります。MCP Tasksは、要求をタスクとして受理し、後から状態と結果を取得するための拡張です。
この記事は、JavaScriptの非同期処理を学んだ方が、長時間処理の状態管理を理解するための入門です。Tasks拡張の2026-07-28版を参照し、Node.jsだけで動く模擬ジョブを作ります。MCPクライアントへ接続する完成サーバーは作りません。
コア仕様・Tasks拡張・SDKの実装は別々に確認します。手元の@modelcontextprotocol/client 2.0.0で行った接続試験では、汎用のrequest()からのtasks/getがMETHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSIONで拒否されました。これはその構成の結果であり、すべてのSDKや将来版が未対応という意味ではありません。
参照する拡張ではtasks/getが最終結果も返し、追加の入力への応答にはtasks/update、取消要求にはtasks/cancelを使います。旧仕様のtasks/resultやexecution.taskSupportと混ぜないでください。実装時は、双方が同じ仕様版を扱うかを接続試験で確かめます。
TypeScript SDKの公式Roadmapでも、Tasksはコア仕様とは別の拡張として追跡され、v2が従来の実験的Tasksを提供しないことが説明されています。リポジトリのmainブランチに将来向けの説明やAPIが見えても、公開済みnpmパッケージで同じ機能を利用できるとは限りません。この記事のコードは独自のジョブ層を学ぶための例であり、SDK 2.0.0へTasksプロトコルを追加する完成コードではありません。
通常のツール呼び出しとの違い
通常の呼び出しは、処理が終わるまで同じ応答を待ちます。Tasksでは先に「受付番号」に相当するtask IDを返し、利用者は後から進捗や結果を確認します。宅配便で、荷物が届くまで窓口で待たず、追跡番号で状態を見るのに近い考え方です。
- ポーリング:一定時間ごとに状態を問い合わせること
- task ID:作成した処理を識別する番号
- 状態遷移:
workingからcompletedなどへ状態が変わること - 永続化:サーバーを再起動してもデータが残る場所へ保存すること
以下の模擬ジョブは外部通信・課金・実ファイルの変換を行いません。状態管理だけを切り出すので、MCPを導入する前でも試せます。
基本の流れ
- サーバーの能力と、要求ごとのクライアント能力で
io.modelcontextprotocol/tasksの対応を確認する tools/callを受けたサーバーが、通常の結果を返すか、タスク化するかを決める- タスク化した場合は
resultType: "task"とtask IDなどを返す - クライアントは
tasks/getで状態を照会し、完了時には同じ応答のresultから結果を読む input_requiredの場合は、提示された入力要求にtasks/updateで応答する- 不要になれば
tasks/cancelで取消の意図を伝える
クライアントが拡張対応を宣言しても、必ずタスクが作成されるわけではありません。通常結果とタスク受付の両方を扱う必要があります。
状態をアプリ内でも明示する
SDKへつなぐ前に、独自のジョブ層を作ります。以下のstateやgetJob()はアプリ内の名前で、MCPのメソッド・Task型ではありません。例えばこの例のfailedは単に模擬処理の失敗を表し、そのままプロトコルのエラー分類へ流用しません。
必要環境と完成コード
Node.js 24系を用意し、空の作業フォルダーへ次をjobs.mjsとして保存します。npmパッケージの追加は不要です。
import { setTimeout as delay } from "node:timers/promises";
const jobs = new Map();
export function getJob(id) {
const job = jobs.get(id);
if (!job) throw new Error("job not found");
// 内部のAbortControllerや変更可能なジョブ本体は返さない。
return { id: job.id, state: job.state, result: job.result, error: job.error };
}
export function createJob(id, { fail = false } = {}) {
if (typeof id !== "string" || !id.trim()) throw new Error("invalid id");
if (jobs.has(id)) throw new Error("job already exists");
const job = { id, state: "working", abortController: new AbortController() };
jobs.set(id, job);
job.done = (async () => {
try {
await delay(3000, undefined, { signal: job.abortController.signal });
job.abortController.signal.throwIfAborted();
if (fail) throw new Error("simulated failure");
job.result = 42;
job.state = "completed";
} catch (error) {
if (error.name === "AbortError") job.state = "cancelled";
else { job.error = error.message; job.state = "failed"; }
}
})();
return getJob(id);
}
export async function cancelJob(id) {
const job = jobs.get(id);
if (!job) throw new Error("job not found");
if (job.state !== "working") return getJob(id);
job.abortController.abort();
// この例では、待機処理が実際に終了してから取り消し結果を返す。
await job.done;
return getJob(id);
}
同じフォルダーにdemo.mjsを保存します。
import { setTimeout as delay } from "node:timers/promises";
import { createJob, getJob, cancelJob } from "./jobs.mjs";
console.log("開始:", createJob("success").state);
while (getJob("success").state === "working") await delay(250);
console.log("完了:", getJob("success").state, getJob("success").result);
console.log("結果再取得:", getJob("success").result);
console.log("完了後取消:", (await cancelJob("success")).state);
createJob("cancel");
console.log("処理中取消:", (await cancelJob("cancel")).state);
createJob("failure", { fail: true });
while (getJob("failure").state === "working") await delay(250);
console.log("失敗:", getJob("failure").state, getJob("failure").error);
try { getJob("missing"); } catch (error) { console.log(error.message); }
try { createJob("success"); } catch (error) { console.log(error.message); }
実行と動作確認
ターミナルで2ファイルのあるフォルダーを開き、次を実行します。
node demo.mjs
約6秒後までに次が表示され、プロセスが終了すれば正常です。処理時間は端末負荷により前後します。
開始: working
完了: completed 42
結果再取得: 42
完了後取消: completed
処理中取消: cancelled
失敗: failed simulated failure
job not found
job already exists
これは模擬ジョブの検証であり、MCPの通信試験ではありません。Mapは終了・再起動で消え、所有者の認可、保存期限、入力待ち、再送の冪等性を実装していません。同じIDの拒否だけでは、同じ依頼に別IDを付けた二重実行は防げません。公開サーバーへそのまま転用しないでください。
ポーリング設計
クライアントは間隔なしでtasks/getを連打してはいけません。サーバーが示すpollIntervalMsを尊重します。アプリ側の待機上限や再試行間隔も決めてください。上の250msはローカル模擬処理のための値で、サーバーへの推奨値ではありません。
結果には保存期限を設定します。期限切れ、存在しないID、別ユーザーのIDは区別しすぎると情報漏えいになる場合があるため、認可ポリシーを先に決めます。
キャンセルの意味
tasks/cancelの受理は、処理停止や最終状態cancelledを保証しません。完了と競合する場合もあります。上の独自cancelJob()は待機終了を待ちますが、これはサンプルの契約であり、MCPの取消応答と同じではありません。既に送ったメールなどの副作用は別途扱います。
処理を小さな単位に分け、区切りごとにAbortSignalを確認します。次は設計上の断片で、filesやconvertOneは実際のアプリで用意するものです。
for (const file of files) {
if (signal.aborted) throw new DOMException("Cancelled", "AbortError");
await convertOne(file, signal);
}
実装時の確認項目
- workingからcompletedへ進み、結果を1回以上取得できる
- 模擬ジョブは処理中の取消でcancelledになる。MCPへ接続する場合は取消受理と停止完了を区別する
- 完了済みタスクのキャンセルを安全に扱う
- 不明なIDと他ユーザーのIDを拒否する
- サーバー再起動後も必要なタスクが復元される
- 同じ作成要求の再送で二重実行しない
上の完成コードが確認するのは、完了・取消・失敗・不明ID・重複IDです。他ユーザーの認可や再起動復元は別の実装が必要です。Tasks対応ランタイムへ接続する際は、仕様上の能力宣言・応答形式・取消の意味も含めた通信試験を追加します。
よくあるトラブル
- task IDを受け取ったのに結果がない:
tasks/getの状態と、完了時のresultを確認する - 状態照会でサーバーが重くなる:待ち時間を入れずポーリングしていないか確認する
- 再起動ですべて消える:サンプルの
Mapはメモリ上にしか保存されない - キャンセル後も外部処理が続く:処理側が
AbortSignalを定期的に確認しているか調べる - 同じ処理が複数作られる:再送を識別するキーと冪等性の設計を追加する
向かない用途
数十ミリ秒で終わる計算をすべてTask化すると実装と通信が増えます。また、リアルタイムな連続データ配信をTasksだけで置き換えるものでもありません。短い処理は通常応答、進捗通知が必要ならsubscription等との組み合わせを検討します。
関連記事
まとめ
長時間処理では、受付と完了を分けるだけでなく、失敗・取消・再取得を設計します。模擬ジョブで状態管理を理解したら、参照するTasks仕様とSDKの対応を確認し、永続化・認可・冪等性を加えてから接続してください。


コメント