Claude Media
Claude CodeのMCP tasksを自作サーバーで返す — taskIdとpollIntervalMs

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の扱い
resultTypeClaude Codeの扱い"task" でタスクの返答と判断する。無いと通常の結果として読む
taskIdClaude Codeの扱い必須。128文字以内の印字可能なASCIIで、空白を含まない
statusClaude Codeの扱い必須。5値のどれか
createdAt / lastUpdatedAt / ttlMsClaude Codeの扱い必須。ttlMs だけ null でもよい
pollIntervalMsClaude Codeの扱い任意で、null は不可。250〜30000にクランプ。無いときは2000
statusMessageClaude 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の動きは次のように分かれます。

statusClaude Codeの動き
workingClaude Codeの動きポーリングを続ける
completedClaude Codeの動き最上位の result を通常の tools/call 結果としてClaudeに渡す。その中の isError: true は失敗として扱う
failedClaude Codeの動き最上位の error オブジェクト(code・message・data)の message を伝える。無ければ statusMessage、それも無ければ Task failed
cancelledClaude Codeの動きstatusMessage を伝える。無ければ Task was cancelled by the server
input_requiredClaude 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で動かす場合にこの前提を利用者に伝えておくと、終了時の混乱を減らせます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/mcp
  2. modelcontextprotocol.io/extensions/tasks/overview
この記事を共有:XはてブLinkedIn
MCP をもっと見る →