08. 通知で伝える

処理の結果をサーバーから伝える方法と、その限界

📅 作成: 2026-09-07 / 更新: 2026-09-07

この資料の内容

  1. できることとできないこと
  2. 進捗を知らせる
  3. 利用者に尋ねる
  4. 画面に出す(枠の外)
  5. 呼ばれていないときは届かない

できることとできないこと

「処理が終わったら知らせたい」。よくある要望ですが、MCP ではできることとできないことがはっきり分かれます。先に線を引いておきます。

やりたいこと手立て可否
処理の途中経過を伝えたいnotifications/progressできる
途中で利用者に尋ねたいelicitation/createできる
人に確実に気づかせたいOS の通知(MCP の外)できる
長い処理を投げて後で結果を取りたいタスク(09 章相手次第
何も呼ばれていないのに割り込みたいできない
時間 tools/call を処理している間 呼ばれた 結果を返した 通知を送れる 送れない × 結果を返した瞬間に通り道が閉じる。「返した後で知らせる」はできない
通知には流し込む先が要る。その先はリクエストの処理中だけ開いている。

なぜこうなっているのか。仕様には Servers MUST NOT initiate JSON-RPC requests と書かれています。やりとりを始めるのは常にクライアント側で、サーバーは応答と通知だけを返します。01 章で触れた非対称が、ここで効いてきます。

進捗を知らせる

ホストが _meta.progressToken を付けて呼んできたとき、そのときだけ進捗を送れます。

実際に届くもの

Claude Code から tools/call が来たときの中身です。

{
  "method": "tools/call",
  "params": {
    "name": "add",
    "arguments": { "a": 2, "b": 3 },
    "_meta": {
      "claudecode/toolUseId": "toolu_018tCHaryTHbnYbduhcYUqZW",
      "progressToken": 2
    }
  },
  "jsonrpc": "2.0",
  "id": 2
}

progressToken: 2 が付いています。これを添えて通知を返します。

送る

server.registerTool(
	'countdown',
	{
		title: '数える',
		description: '指定した秒数を数え、その間に進捗を知らせる',
		inputSchema: { seconds: z.number().int().min(1).max(10).default(3) },
	},
	async ({ seconds }, extra) => {
		const token = extra._meta?.progressToken;

		for (let i = 1; i <= seconds; i++) {
			await sleep(1000);
			if (token === undefined) continue;
			await extra.sendNotification({
				method: 'notifications/progress',
				params: { progressToken: token, progress: i, total: seconds, message: `${i}/${seconds} 秒` },
			});
		}

		return { content: [{ type: 'text', text: `${seconds} 秒数えた` }] };
	},
);

流れる通知です。

{"method":"notifications/progress","params":{"progressToken":"t1","progress":1,"total":2,"message":"1/2 秒"},"jsonrpc":"2.0"}
{"method":"notifications/progress","params":{"progressToken":"t1","progress":2,"total":2,"message":"2/2 秒"},"jsonrpc":"2.0"}
progressToken がある 通知に添えて返せる ホストが進み具合を表示できる 値は必ず増やす(仕様の決まり) progressToken が無い 送っても宛先が無い 送らないのが正しい if (token === undefined) で守る 付けてくるかどうかはホスト次第。付いていないことを前提に書く
トークンの有無を必ず確かめる。無いのに送るのは、宛名の無い手紙を出すのと同じ。

進捗の値は必ず増やします。仕様に「progress の値は毎回増えなければならない」と書かれています。総数が分からないときは total を省き、progress だけを 1, 2, 3… と増やします。

利用者に尋ねる

サーバーからリクエストは送れない、と書きました。ただし例外があります。クライアントが「尋ねられる用意がある」と名乗っている場合です。

相手が名乗っているか確かめる

Claude Code は initialize でこう名乗ってきます。

"capabilities": { "roots": { "listChanged": true }, "elicitation": {} }

elicitation があるので、尋ねられます。

尋ねる

const answer = await extra.sendRequest(
	{
		method: 'elicitation/create',
		params: {
			mode: 'form',
			message: 'お名前を教えてください',
			requestedSchema: {
				type: 'object',
				properties: { name: { type: 'string', title: '名前' } },
				required: ['name'],
			},
		},
	},
	z.object({ action: z.string(), content: z.record(z.string(), z.unknown()).optional() }),
);

if (answer.action !== 'accept') {
	return { content: [{ type: 'text', text: `尋ねたが ${answer.action} だった` }] };
}
return { content: [{ type: 'text', text: `こんにちは、${answer.content?.name} さん` }] };

3 つの返事

action意味やること
accept答えてくれたcontent を使う
declineはっきり断られた別の手を出す
cancel閉じられた・答えられなかった後でまた尋ねてよい
サーバー クライアント 利用者 elicitation/create 画面に出す 入力する action と content ツールの処理を止めたまま待つ。利用者が答えるまで結果は返せない
ツールの中から利用者に問いかけられる。ただし待たせることになるので、多用しない。

実測

対話していない状態(claude -p での実行)で呼ぶと、こう返ります。

elicitation の答え: {"action":"cancel"}

画面に出せる相手がいないので cancel です。断られる前提で書いておく必要があります。

パスワードや API キーを form で尋ねてはいけません。仕様で禁じられています。入力した値がクライアント側を通るためです。そうした情報が要る場合は、URL を開いてもらう mode: 'url' を使います。

画面に出す(枠の外)

「ビルドが終わったら気づきたい」。この要望に最も確実に応えるのは、MCP の外側です。サーバーは普通のプロセスなので、OS の通知を出せます。

await execFileAsync('powershell', [
	'-NoProfile', '-ExecutionPolicy', 'Bypass',
	'-File', join(HERE, 'toast.ps1'),
	'-Title', title, '-Message', message,
]);

呼ばれる toast.ps1 の中身です。

[void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]

$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent(
	[Windows.UI.Notifications.ToastTemplateType]::ToastText02)

$texts = $xml.GetElementsByTagName('text')
[void]$texts.Item(0).AppendChild($xml.CreateTextNode($Title))
[void]$texts.Item(1).AppendChild($xml.CreateTextNode($Message))

# 送り主として登録済みのアプリ ID が要る。PowerShell のものを借りる
$appId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'

$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId)
$notifier.Show([Windows.UI.Notifications.ToastNotification]::new($xml))

Windows PowerShell 5.1 で呼びます。pwsh 7 で同じことをすると Unable to find type [Windows.UI.Notifications.ToastNotificationManager...] になります。WinRT の型が読み込まれないためです。powershell(5.1)を明示して呼んでください。

MCP サーバー ただのプロセス MCP の通り道 処理中しか使えない OS の通知 いつでも出せる 同じプロセスから、2 つの経路が使える メール・Slack・音を鳴らす、も同じ考え方
MCP の中で解こうとすると詰まる要望も、外から見れば普通のプロセスなので手がある。

AI に伝えるのと、人に伝えるのは別です。OS の通知はに届きますが、モデルは知りません。モデルにも伝えたいなら、次にツールが呼ばれたときに「前回の処理は終わっています」と返す形が確実です。

呼ばれていないときは届かない

最後に、はっきり線を引きます。

誰も何も呼んでいないときに、サーバーから会話へ割り込むことはできません。これは実装の都合ではなく、仕様がそう決めています。通知には流し込む先が要り、その先はクライアントが開いたときにしか存在しません。

外で何か起きた 監視が異常を検知した サーバーは知る プロセスは動いている × 経路が無い 会話に割り込む モデルに知らせる OS の通知・メール・Slack こちらは通る(人に届く)
横に抜ける道はある。真っ直ぐ会話に入る道が無い。

実際にはこう作る

やりたいこと作り方
ビルドの完了を知りたいツールの中で最後まで待ち、進捗を送る。終わったら OS の通知も出す
長すぎて待てないタスクにする(09 章)。ただし相手の対応が要る
溜まったものを伝えたいサーバー側に溜めておき、次に呼ばれたときに結果へ添える
今すぐ人に気づかせたいOS の通知。MCP の外

「次に呼ばれたときに伝える」は地味ですが確実です。サーバー側に未読の知らせを溜めておき、どのツールが呼ばれても結果の末尾に「※ 3 件の完了があります」と付ける。どのホストでも動き、対応状況にも左右されません。

次の章へ

「長い処理を投げて、後から結果を取る」仕組みが仕様にあります。次の章で作りますが、手元の Claude Code では使われませんでした。その実測も含めて扱います。