Claude CodeのMCP tasksを自作サーバーで返す — taskIdとpollIntervalMs
MCPサーバーがtools/callにタスクで答えるとき、Claude Codeが受け付けるtaskId・status・pollIntervalMsの条件をまとめます。違反時のエラー文言と、tasks/getの応答の返し方も載せます。
自作のMCPサーバーで、ビルドやレポート出力のように数分かかる処理を tools/call の応答として返すとき、結果の代わりにタスクを返せます。Claude Codeは、そのタスクを受け付ける条件を taskId・status・pollIntervalMs の各フィールドごとに決めています。条件を外すと、Claudeにはツールエラーが返ります。この記事では、Claude Code側の受け入れ条件を軸に、サーバー側で何を返せばよいかを順に確かめます。
タスク拡張そのものの仕様変更はMCP Tasksとはにあります。ここではClaude Codeがその仕様のどこまでを実際に使うかを扱います。
Claude Codeはタスクを受け付けられるときだけ宣言を送る
タスクで答えてよいかは、リクエストを見て判断します。プロトコルリビジョン2026-07-28の接続では、Claude Codeが各リクエストの params._meta["io.modelcontextprotocol/clientCapabilities"] にクライアントの能力を載せます。その extensions に "io.modelcontextprotocol/tasks": {} が入っていれば、タスクで答えられます。
{
"params": {
"_meta": {
"io.modelcontextprotocol/clientCapabilities": {
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
}
}
}タスクにできない tools/call では、この拡張が省かれます。サーバーは毎回この有無を見て、省かれていれば通常の結果で返します。拡張が無いのにタスクで答えると、Claudeには Unsupported result type 'task' for tools/call というツールエラーが返ります。
宣言が省かれる条件は多めです。Claude Codeのドキュメントは、タスクになる呼び出しの条件を次のように並べています。
- サーバーがタスクに対応している
- HTTP・stdio・claude.aiコネクタのサーバーで、プロトコルリビジョン2026-07-28で接続している
- セッションがフィーチャーフラグを取得している(Amazon Bedrockなどのサードパーティプロバイダーや、テレメトリー・フラグ取得を切った環境は外れる)
- Anthropic側でその種類のセッションにMCPタスクが有効化されている。v2.1.304以降では端末のCLI・Agent SDK・GitHub Actionsが対象で、VS Code拡張とデスクトップアプリには無い
- 呼び出しがサブエージェントではなくメイン会話から来ている
- 対話セッションであるか、Agent SDKや
--input-format stream-jsonのclaude -pで所定の環境変数が揃っている
ワンショットの claude -p "<prompt>" はタスクを受けません。手元の開発では、動かしている面がどれに当たるかを先に確かめておくと、「タスクで返したのに普通の呼び出しになる」という食い違いを避けられます。待たせる側の環境変数はCLAUDE_CODE_DISABLE_MCP_TASK_BACKGROUNDの記事にまとまっています。
最初の応答で返すタスクオブジェクト
宣言を確認できたら、tools/call の result としてタスクオブジェクトを返します。content 配列は要りません。公式ドキュメントの例は次の形です。
{
"resultType": "task",
"taskId": "build-7f3a9c2e",
"status": "working",
"statusMessage": "Compiling, 40 of 212 files done",
"createdAt": "2026-10-22T14:03:07Z",
"lastUpdatedAt": "2026-10-22T14:03:07Z",
"ttlMs": null,
"pollIntervalMs": 5000
}各フィールドの扱いは次のとおりです。
| フィールド | Claude Codeの扱い |
|---|---|
resultType | Claude Codeの扱い"task" でタスクの返答と判断する。無いと通常の結果として読む |
taskId | Claude Codeの扱い必須。128文字以内の印字可能なASCIIで、空白を含まない |
status | Claude Codeの扱い必須。5値のどれか |
createdAt / lastUpdatedAt / ttlMs | Claude Codeの扱い必須。ttlMs だけ null でもよい |
pollIntervalMs | Claude Codeの扱い任意で、null は不可。250〜30000にクランプ。無いときは2000 |
statusMessage | Claude Codeの扱い任意。/tasks には直近の tasks/get の値が出る |
taskId・status・createdAt・lastUpdatedAt・ttlMs のどれかが欠けると、Unsupported result type 'task' のエラーになります。必須の5項目は、まとめて組み立てる関数に閉じ込めておくと漏れにくくなります。
taskIdの制約を送る前に検査する
taskId が長すぎたり空白を含んだりすると、MCP server "<name>" returned a malformed task handle for tool "<tool>" で始まるツールエラーになります。この検査は応答を返す側で先に済ませられます。次はTypeScriptでの一例で、公式の条件(128文字以内・印字可能なASCII・空白なし)をそのまま書いたものです。
// 0x21〜0x7E は印字可能なASCIIのうち空白を除いた範囲
const TASK_ID = /^[\x21-\x7e]{1,128}$/;
export function assertTaskId(id: string): string {
if (!TASK_ID.test(id)) {
throw new Error(`taskId が Claude Code の条件に合いません: ${id}`);
}
return id;
}
// 例: UUIDをそのまま使えば36文字で収まる
assertTaskId(crypto.randomUUID());日本語やスペースを含むジョブ名をそのまま taskId に使うと、この検査で落ちます。ジョブ名は statusMessage に入れ、IDは英数字とハイフンの生成値にする分け方が安全です。
pollIntervalMsとttlMsの扱い
pollIntervalMs は tasks/get を送る間隔で、Claude Codeは250〜30000の範囲に丸めます。たとえば100と書いても250で動き、60000と書いても30000で動きます。省略すれば2000です。値は各 tasks/get の応答から読み直すので、進みが遅い処理は途中で間隔を広げられます。
ttlMs は必須ですが、期限が過ぎてもClaude Codeはタスクを破棄しません。有効期限をクライアント側に強制させる設計は取れないため、期限切れの扱い(IDを知らないものとして -32602 を返す、など)はサーバーが決めます。
tasks/getで進捗と結果を返す
タスクの返答の直後から、Claude Codeは { "taskId": ... } を付けた tasks/get を送り、以後は pollIntervalMs ごとに繰り返します。応答には "resultType": "complete" を付け、taskId・status・createdAt・lastUpdatedAt・ttlMs を揃えます。完了時の例です。
{
"resultType": "complete",
"taskId": "build-7f3a9c2e",
"status": "completed",
"createdAt": "2026-10-22T14:03:07Z",
"lastUpdatedAt": "2026-10-22T14:09:41Z",
"ttlMs": null,
"result": {
"content": [{ "type": "text", "text": "Build succeeded: 212 files, 0 warnings" }]
}
}status ごとに、Claude Codeの動きは次のように分かれます。
| status | Claude Codeの動き |
|---|---|
working | Claude Codeの動きポーリングを続ける |
completed | Claude Codeの動き最上位の result を通常の tools/call 結果としてClaudeに渡す。その中の isError: true は失敗として扱う |
failed | Claude Codeの動き最上位の error オブジェクト(code・message・data)の message を伝える。無ければ statusMessage、それも無ければ Task failed |
cancelled | Claude Codeの動きstatusMessage を伝える。無ければ Task was cancelled by the server |
input_required | Claude Codeの動きinputRequests を読んで答える(次節) |
5値以外の status を返すと、Claude Codeはサーバーにキャンセルを依頼し、Claudeには失敗として伝えます。failed の理由は error.message に入れておくと、Claudeがそのまま原因を読めます。
使われないメソッドもあります。tasks/result・tasks/list・notifications/tasks は呼ばれないため、結果は必ず tasks/get の応答に載せてください。tasks/cancel はClaudeや利用者がタスクを止めたときに、サーバーが接続中であれば送られます。終了時には送られません。
input_requiredでユーザーの入力を挟む
処理の途中で確認が要る場合は、status を input_required にして inputRequests を返します。inputRequests は、サーバーが決めたキーを { method, params } に対応づけたオブジェクトです。Claude Codeは各キーに一度だけ tasks/update で答えます。
{
"resultType": "complete",
"taskId": "deploy-91c0",
"status": "input_required",
"createdAt": "2026-10-22T14:03:07Z",
"lastUpdatedAt": "2026-10-22T14:05:12Z",
"ttlMs": null,
"inputRequests": {
"confirm-region": {
"method": "elicitation/create",
"params": { "message": "本番リージョンに配信しますか", "requestedSchema": { "type": "object", "properties": {} } }
}
}
}答える側は、{ taskId, inputResponses } を tasks/update に載せます。inputResponses は同じキーに、その要求の結果を対応づけたものです。ここには制約が3つあります。
- 1タスクで答えるのは16キーまでで、それを超えたキーは無視される
elicitation/createは対話の端末セッションでだけ処理される。roots/listはどこでも処理される- 処理できない要求が来ると、Claude Codeはサーバーにタスクのキャンセルを依頼する
非対話セッションで入力を求めるタスクは、ユーザーの Elicitation フックも通らずにキャンセルされます。CIから呼ばれる可能性のあるサーバーでは、input_required に頼らず、入力が無くても進む既定値を持たせる設計が現実的です。
応答を返す順番と時間の制約
HTTPサーバーとclaude.aiコネクタでは、タスクの返答が最初の応答バイトとして、リクエストごとのタイマーに収まる必要があります。このタイマーはClaude Codeが、60秒・適用されるツールのタイムアウト・MCP_TIMEOUT のうち最大の値に設定します。stdioとWebSocketには、このタイマーはありません。実装の順序は「ジョブを受理し、IDを返してから、実処理を始める」にします。
// 擬似コード: 受理だけして即返し、重い処理はあとで走らせる
async function handleToolCall(args: BuildArgs) {
const taskId = assertTaskId(crypto.randomUUID());
const now = new Date().toISOString();
jobs.set(taskId, { status: "working", createdAt: now, updatedAt: now });
void runBuild(taskId, args); // 待たない
return {
resultType: "task",
taskId,
status: "working",
createdAt: now,
lastUpdatedAt: now,
ttlMs: null,
pollIntervalMs: 5000,
};
}もう一つ、再接続をまたぐ点があります。再開したセッションでは、Claude Codeが新しい接続でタスクIDについて問い合わせます。taskId をプロセスのメモリだけに持つと、サーバーの再起動後は答えられません。IDに対する状態は、再起動後も引ける場所に置いてください。IDを知らない場合に返す -32602 は、バックグラウンドのタスクでは最長5分の再確認を経て、結果が「不明」としてClaudeに報告されます。
tasks/cancelを受けたサーバーの止め方
tasks/cancel は、Claudeや利用者がタスクを止めたときに届きます。Claude Codeを終了したときは送られないので、終了後もジョブが走り続ける前提でサーバーを作ることになります。
サーバーが受けたら、まず該当ジョブの status を cancelled にして tasks/get から読めるようにします。理由は statusMessage に入れてください。Claude Codeは cancelled のとき statusMessage をそのままClaudeに伝えるので、書いておけば「なぜ止まったか」がClaudeに届きます。次に、実処理を中断できる作りなら中断し、中断できない処理は終わらせたうえで結果を捨てます。
キャンセルは、利用者の操作だけで起きるわけではありません。Claude Codeがサーバーに依頼する場面が2つあります。inputRequests に処理できない要求が入っていたとき(非対話セッションでの elicitation/create など)と、5値以外の status を返されたときです。サーバーのログに tasks/cancel が出ていても、利用者が止めたとは限りません。
タスクで返すか、2分の自動バックグラウンド化に任せるか
数分かかる処理でも、サーバーがタスクを実装しなければ、呼び出しが2分を超えた時点でClaude Code側が裏に回す仕組みがあります。どちらを選ぶかで、サーバー側の実装量と、Claude Codeを閉じたあとの扱いが変わります。
| 観点 | タスクで返す | 自動バックグラウンド化 |
|---|---|---|
| サーバー側の実装 | タスクで返すtasks/get などの状態管理が要る | 自動バックグラウンド化不要(通常の tools/call のまま) |
| 裏に回る契機 | タスクで返すサーバーが最初の応答でタスクを返したとき | 自動バックグラウンド化呼び出しが2分を超えたとき(閾値は CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS で変更できる) |
| セッションを終えたあと | タスクで返すサーバー上で動き続ける | 自動バックグラウンド化続かない |
| 途中の入力要求 | タスクで返すinput_required で挟める | 自動バックグラウンド化挟めない |
| 実行時間の上限 | タスクで返すサーバーの設計次第 | 自動バックグラウンド化サーバーごとの timeout などの上限が裏でも効く |
処理が2分に収まるなら、タスクを実装する理由は薄くなります。数分以上かかり、Claude Codeを閉じても続けたい処理や、途中で確認を挟みたい処理に、タスクを使う価値が出ます。
つまずきの早見表
| 症状 | 起きていること | 直し方 |
|---|---|---|
Unsupported result type 'task' | 起きていること拡張の宣言が無いのにタスクで返した。または必須フィールドの欠け、5値以外の status | 直し方リクエストの _meta を確認して通常結果に切り替える。必須5項目を揃える |
returned a malformed task handle | 起きていることtaskId が129文字以上、空白入り、印字不能文字入り | 直し方生成IDを使い、送る前に検査する |
| 間隔が思った通りにならない | 起きていることpollIntervalMs が250〜30000に丸められた | 直し方範囲内の値にする。null は不可 |
| タスクにならず普通の呼び出しになる | 起きていることVS Code拡張・デスクトップアプリ・ワンショットの claude -p などタスク非対応の面 | 直し方端末のCLIか対応する面で試す |
| 再開後に結果が「不明」になる | 起きていること再接続後にサーバーがIDを引けず -32602 を返した | 直し方タスクの状態を再起動後も引ける場所に置く |
接続自体が通らない場合の切り分けはMCPサーバーに接続できないときの手順で足ります。長い呼び出しを2分で自動的に裏へ回す仕組みは、タスクとは別物で、MCPの長時間ツール呼び出しの自動バックグラウンド化に整理しています。サーバー自体を最初から作る手順はMCPサーバー自作ガイドが受けます。
利用者の画面に出るステータスメッセージと、サーバー側の原因
タスクを返したあと、/tasks の項目やトランスクリプトの最終行には、Claude Codeがサーバーやタスクに届かないときのメッセージが出ます。メッセージが出ていてもジョブは動いている場合があり、Claude Codeはタスクに手を出さずに確認を続けます。サーバー作者の側から見ると、次のように原因を切り分けられます。
| メッセージ | サーバー側で疑う点 |
|---|---|
status requests come back with errors; retrying… | サーバー側で疑う点tasks/get がエラーを返している。または手前のプロキシが失敗させている |
status requests to server '<name>' came back with errors, and none succeeded in 300s | サーバー側で疑う点5分間エラーが続いた。サーバーが正常に動いているかとログを確認する |
the server rejected this task's id; asking again… | サーバー側で疑う点サーバーがIDを知らないと答えた。再起動で状態が消えていないか確かめる |
expired on the server: outcome unknown. | サーバー側で疑う点IDの拒否や恒久的なエラーで追跡を終えた。同じ呼び出しを繰り返すと処理が重複することがある |
最後の行は特に影響が大きく、利用者は同じツールをもう一度呼ぶ前に、サーバーの別ツールで実際の実行状況を確かめるよう案内されています。状況を引けるツール(ジョブ一覧や状態確認)を同じサーバーに用意しておくと、この場面で役に立ちます。
Claude Code側で動作を確かめる
タスクが通ると、端末のフッターに 1 MCP task のような件数が出ます。/tasks を開くと、タスクの詳細に runs on the server; survives exiting this session と表示されます。この表示が出れば、サーバー側が返したタスクを受け付けて追っています。終了すると、トランスクリプトに MCP task <server task id> (<server>/<tool>) completed. の行が出ます。結果そのものはトランスクリプトに出ず、Claudeに渡されるので、Claudeに結果を尋ねてください。
stdioサーバーは、Claude Codeを終了するとプロセスが止まります。サーバーのドキュメントに「処理はプロセスより長く生きる」と書かれていないかぎり、終了前にタスクの完了を待つか /tasks で止めます。サーバー作者は、stdioで動かす場合にこの前提を利用者に伝えておくと、終了時の混乱を減らせます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。