MCPサーバーのヘルスチェックと死活監視:pingが廃止された2026-07-28仕様で何を確認すべきか【MCP Hosting】
MCPサーバーのヘルスチェックは何を叩けばいいのか。2026-07-28仕様でpingは廃止され、GETは400を返すため、一般的な死活監視はそのままでは誤検知します。正常なサーバーと上流APIキーが切れたサーバーに10種類のプローブを実際に送った結果、3層のチェック設計、Docker・Kubernetes・Cloud Runの設定例、アラート設計、スケールゼロとの費用計算までまとめました。
最終更新:2026年10月27日 / 対象:MCP仕様 2026-07-28、MCP Python SDK 2.3.0 のサーバー(
MCPServer)、2026-07-28版のMCP Inspector CLIドキュメント、2026年10月時点のDocker・Kubernetes・Google Cloud Runのヘルスチェック関連ドキュメント。検証は2026年10月27日、Linux・Python 3.13・curlで実施。
結論(30秒で読める要約)
MCPサーバーのヘルスチェックとは、ホスティングしているMCPサーバーがMCPのリクエストに応答できるか、さらにはツールが本来の仕事をこなせるかを、定期的なリクエストで確かめる仕組みです。MCP Hostingの現場では、ふつうの死活監視をそのまま使うと「正常なのに落ちている」と判定するか、「壊れているのに正常」と判定するかのどちらかになりがちです。
pingはもうありません。2026-07-28の変更履歴には「Removeping」と明記されています。検証では、2026-07-28のサーバーにpingを送るとHTTP404、JSON-RPCエラー-32601 Method not foundが返りました。監視ガイドでよく見る、プロトコル用ヘッダーなしのpingは400 "Missing session ID"でした。- GETによる監視も使えません。Python SDKのサーバーでは
GET /mcpが400を返し、仕様では2026-07-28専用サーバーはGETに405を返すことになっています。どちらも正常かどうかの判断材料になりません。 - HTTP 200は「動いている」ことを意味しません。上流APIのキーが切れた状態でも、
server/discover、tools/list、単純なツールはすべて200でした。失敗したのはキーを使うツールだけで、それも200のレスポンスの中に"isError": trueが入る形でした。存在しないツールを呼んでも200とisError: trueが返ります。 - 3つの層に分けて確認します。プロセスが動いているか(liveness)、MCPとして応答するか(
server/discoverと、期待するツールがtools/listにあるか)、ツールが働くか(読み取り専用のhealth_checkツールの出力を確認)です。 - 監視がスケールゼロを打ち消すことがあります。起動中の時間で課金されるプラットフォームでは、アイドルタイムアウトより短い間隔で監視すると、サーバーは1か月ずっと起きたままになります。512MBなら約365GB時間です。
検証結果は3章、層の設計は4章、費用計算は7章にあります。
目次
- MCPサーバーのヘルスチェックとは
- 2026-07-28仕様でヘルスチェックの何が変わったか
- 検証:よく使われる10種類のプローブの結果
- ヘルスチェックの3つの層
- プラットフォームのプローブ:Docker、Kubernetes、Cloud Run
- 外部の死活監視:リクエストとアサーション
- 監視間隔とスケールゼロ:費用の計算
- 設定すべきアラート
- よい
health_checkツールの書き方 - チェックリスト
- トラブルシューティング:症状→原因→対処
- NodeFlareならこう解決できる
- よくある質問
1. MCPサーバーのヘルスチェックとは
MCPサーバーのヘルスチェックとは、稼働中のMCPサーバーについて「プロセスに届くか」「MCPとして応答するか」「ツールが依存先に届くか」の3点を、決まった間隔の自動リクエストで確かめることです。問いごとに送るべきリクエストが違い、1つ目しか見ない監視では実際の障害の多くを見逃します。
| 確かめたいこと | 主に見つかる障害 | 送るリクエスト |
|---|---|---|
| プロセスに届くか | クラッシュ、メモリ不足での強制終了、DNSやTLSの不具合、基盤の障害 | TCP接続、または別に用意した /healthz へのGET |
| MCPとして応答するか | 起動コマンドの誤り、ブリッジの停止、プロトコルバージョンの不一致、認証層の故障 | server/discover、tools/list |
| ツールが働くか | APIキーの期限切れ、上流サービスの停止、環境変数の設定漏れ、DBに届かない | 読み取り専用のヘルスツールへの tools/call |
隣り合う2つのテーマはこの記事では扱いません。新しいリリースに切り替える前の1回限りの確認はデプロイ時のスモークテストで、無停止でのMCPサーバーデプロイにまとめています。誰がどのツールを呼んだかを後から追うための記録は監査ログで、MCPの監査ログで解説しています。この記事は、すでに本番で動いているサーバーを継続的に監視する話です。
2. 2026-07-28仕様でヘルスチェックの何が変わったか
2026-07-28版でMCPはステートレスになり、そのうち4つの変更が、以前のバージョン向けに作ったヘルスチェックを動かなくします。
| 2026-07-28の変更 | 仕様の記述 | 監視への影響 |
|---|---|---|
ping の廃止 |
「Remove ping, logging/setLevel, and notifications/roots/list_changed」 |
ping を使った監視は新しいサーバーで失敗する |
initialize ハンドシェイクとセッションの廃止 |
各リクエストが _meta でプロトコルバージョンとクライアント機能を運ぶ |
1回のリクエストで確認でき、開いたままのセッションも残らない |
server/discover の追加 |
サーバーはこのRPCを実装しなければならない(MUST) | ping の代わりに使える。軽く、副作用がなく、準拠したサーバーなら必ずある |
| GETエンドポイントの廃止 | MCPエンドポイントへのGETとDELETEには 405 Method Not Allowed を返す |
GETの監視では正常と故障を区別できない |
| 標準ヘッダーの必須化 | すべてのPOSTに MCP-Protocol-Version と Mcp-Method、tools/call には Mcp-Name |
ヘッダーのないプローブは 400(-32020 HeaderMismatch)になる |
2025-11-25以前のセッション方式のサーバーは今も ping に対応しています。ただし状態を持つサーバーでは、セッションの中でしか使えず、先に initialize を送る必要があります。Cronitorの監視ガイドは、定期的に initialize を送る監視はサーバー側にセッションがたまるとして、まさにこれを避けるよう書いています。
3. 検証:よく使われる10種類のプローブの結果
MCP Python SDK 2.3.0 の MCPServer(Streamable HTTP、JSONレスポンス)で同じサーバーを2つ起動しました。1つは上流APIのキーが有効、もう1つは期限切れです。APIを包むタイプのサーバーで最もよくある障害の1つです。ツールは echo、get_quote(上流のキーを使う)、health_check(キーが無効なら例外)、slow_report の4つ。それぞれにcurlでプローブを送りました。
| # | プローブ | 正常なサーバー | 上流キーの期限切れ | 実際に分かること |
|---|---|---|---|---|
| 1 | GET /mcp |
400 |
400 |
何も分からない。正常と故障が同じ結果 |
| 2 | GET /health |
404 |
404 |
SDKはヘルス用のルートを作らない。自分で足す必要がある |
| 3 | ヘッダーなしの ping(多くのガイドの例) |
400 "Missing session ID" |
400 |
新しいサーバーでは何も分からない |
| 4 | 2026-07-28ヘッダー付きの ping |
404、-32601 Method not found |
404 |
ping は廃止済み |
| 5 | server/discover |
200、supportedVersions: ["2026-07-28"] |
200 |
プロセスが動き、MCP層が応答している |
| 6 | tools/list |
200、ツール4個 |
200、ツール4個 |
ツールが登録されている |
| 7 | tools/call echo |
200、isError: false |
200、isError: false |
ツールの実行経路が動く |
| 8 | tools/call get_quote |
200、isError: false |
200、isError: true、「Error executing tool get_quote」 |
上流の依存先が動くか |
| 9 | tools/call health_check |
200、isError: false |
200、isError: true |
同上(監視用のツール経由) |
| 10 | 存在しないツールへの tools/call |
200、isError: true、「Unknown tool: nope」 |
200、isError: true |
ツール単位の確認ではHTTPステータスが役に立たない |
ここから分かることは4つです。
- 障害を捉えたのは8と9だけでした。プロトコル層の確認はすべて正常のまま、上流APIが必要な実際のリクエストはすべて失敗していたはずです。
- 失敗は
200の中にありました。HTTPステータスしか見ない監視は、壊れたサーバーでも8と9を合格にします。レスポンス本文を確認する必要があります。 - SDKは存在しないツールをプロトコルエラーではなくツールのエラーとして返しました。仕様では「Unknown tool」はプロトコルエラー(JSON-RPC
-32602)の例ですが、Python SDK 2.3.0 はisError: trueの通常の結果として返しました。デプロイでヘルスツールの名前が変わっても、JSON-RPCのerrorだけを見る監視は気づきません。期待する出力の文字列で確認してください。 - エラー文から原因が消えます。SDKは例外のメッセージ(「upstream returned 401: API key expired」)を「Error executing tool get_quote」に置き換えました。ヘルスツールは例外を投げず、通常の出力でどの依存先が失敗したかを返すのが得策です(9章)。
使ったプローブを、再利用できるスクリプトにしたものです。
#!/usr/bin/env bash
# mcp-probe.sh URL METHOD [TOOL] [ARGS_JSON] - 2026-07-28のリクエストを1回送り、ステータス・時間・本文を表示
URL=$1; M=$2; T=$3; A=${4:-{\}}
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"probe","version":"1"},"io.modelcontextprotocol/clientCapabilities":{}}'
if [ -n "$T" ]; then P="{\"name\":\"$T\",\"arguments\":$A,$META}"; NAMEH=(-H "Mcp-Name: $T"); else P="{$META}"; NAMEH=(); fi
curl -s -m 10 -w '\n%{http_code} %{time_total}s\n' -X POST "$URL" \
-H "Authorization: Bearer $MCP_API_KEY" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' -H "Mcp-Method: $M" "${NAMEH[@]}" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$M\",\"params\":$P}"
使い方:./mcp-probe.sh https://mcp.example.com/mcp server/discover、または ./mcp-probe.sh https://mcp.example.com/mcp tools/call health_check。
4. ヘルスチェックの3つの層
役に立つMCPの監視は、liveness・プロトコル・機能を別々に、別の頻度で、別の担当者に向けて確認します。混ぜると、上流の障害を再起動でさらに悪化させるか、本当の障害中にダッシュボードが緑のままになるか、のどちらかが起きます。
| 層 | リクエスト | 確認する内容 | 失敗時に動くもの | 目安の間隔 | やってはいけないこと |
|---|---|---|---|---|---|
| 1. liveness | TCP接続、または自分で足した GET /healthz |
接続できる、または 200 |
基盤がコンテナを再起動 | 10〜30秒 | 上流APIを呼ぶ(上流の障害で正常なコンテナまで再起動される) |
| 2. プロトコル | server/discover、続けて tools/list |
200、supportedVersions に 2026-07-28、期待するツール名がある |
人にアラート。デプロイを止める | 1〜5分(常時起動の場合) | ping や initialize を使う |
| 3. 機能 | tools/call health_check |
本文に HEALTH_OK(自分で決めた目印)、応答時間がしきい値以内 |
人にアラート | 5〜15分。スケールゼロではもっと間隔をあける(7章) | 書き込む、1回ごとに費用がかかる、管理者権限の認証情報を使う |
| 4. 受動監視(実トラフィック) | プローブなし。リクエストログを読む | ツールごとの isError: true と5xxの割合(10〜15分) |
人にアラート | 常時 | 利用の少ないサーバーで唯一の手段にする |
分ける理由は再起動にあります。KubernetesやCloud Runは、livenessが失敗したコンテナを停止して起動し直します。livenessに上流APIの確認を入れると、そのAPIが落ちたときに全レプリカが再起動を繰り返し、まだ動くはずのツールまで応答しなくなります。再起動はプロセスの状態に、アラートは機能の状態に結びつけます。
5. プラットフォームのプローブ:Docker、Kubernetes、Cloud Run
基盤のプローブはGETを前提にしていますが、MCPにはヘッダーとJSON-RPC本文つきのPOSTが必要です。選択肢は、TCPで確認する、別のGETルートを用意する、コンテナ内で小さなスクリプトを実行するexecプローブ、の3つです。
| 基盤 | プローブの種類 | JSON-RPCをPOSTできるか | 既定値 | 失敗したときの動き |
|---|---|---|---|---|
Docker HEALTHCHECK |
任意のコマンド(exec) | スクリプト経由で可能 | 間隔30秒、タイムアウト30秒、リトライ3回 | コンテナが unhealthy になる。素のDockerは再起動しない(再起動ポリシーは終了に反応し、ヘルス状態に反応するのはオーケストレーター) |
| Kubernetes | httpGet、tcpSocket、grpc、exec |
exec のときだけ |
間隔10秒、タイムアウト1秒、失敗しきい値3。httpGet は200〜399で成功 |
liveness:再起動。readiness:Serviceの宛先から外れる |
| Google Cloud Run | startup:HTTP・TCP・gRPC。liveness・readiness:HTTP・gRPC | できない。HTTPプローブはGET(独自ヘッダーは可) | startupプローブ未設定なら、タイムアウト240秒のTCP startupプローブが自動で付く。livenessとstartupは間隔10秒・タイムアウト1秒・失敗しきい値3 | liveness:失敗が続くとSIGKILL。処理中のリクエストは 503 で終わり、新しいインスタンスが起動 |
方法A:liveness用のGETルートを足す。SDKは用意しませんが、Starletteのアプリにはルートを追加できます。検証したコードです。
# server.py - MCP Python SDK 2.3.0
from starlette.responses import JSONResponse
from starlette.routing import Route
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("my-server")
# ... @mcp.tool() の定義 ...
async def healthz(request): # livenessのみ:プロセスとイベントループ。上流は呼ばない
return JSONResponse({"status": "alive"})
app = mcp.streamable_http_app(json_response=True)
app.router.routes.append(Route("/healthz", healthz))
# 起動:uvicorn server:app --host 0.0.0.0 --port 8000
GET /healthz は正常なサーバーでも壊れたサーバーでも 200 {"status":"alive"} を返しました。プロセス自体は正常だったので、livenessとしてはこれが正しい結果です。
方法B:MCPを話すexecプローブ。readinessの確認や、手を入れられないサーバー(ブリッジの裏のstdioサーバー、GitHubから見つけてそのまま動かしているサーバー)では、server/discover をPOSTするスクリプトを実行します。標準ライブラリだけで書いているので、イメージに追加の依存は入りません。
#!/usr/bin/env python3
# mcp_healthcheck.py URL - server/discover が 2026-07-28 で応答すれば終了コード0、それ以外は1
import json, sys, urllib.request
url = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8000/mcp"
meta = {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "healthcheck", "version": "1"},
"io.modelcontextprotocol/clientCapabilities": {}}
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": meta}}).encode()
req = urllib.request.Request(url, data=body, method="POST", headers={
"Content-Type": "application/json", "Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2026-07-28", "Mcp-Method": "server/discover"})
try:
with urllib.request.urlopen(req, timeout=3) as r:
raw = r.read().decode()
if raw.lstrip().startswith(("event:", "data:")): # SSEの場合は最後のdata行を使う
raw = [l[5:] for l in raw.splitlines() if l.startswith("data:")][-1]
ok = "2026-07-28" in json.loads(raw)["result"]["supportedVersions"]
except Exception as e:
print(f"unhealthy: {e}", file=sys.stderr); sys.exit(1)
sys.exit(0 if ok else 1)
検証では、動いているサーバー(JSON応答・SSE応答の両モード)に対して 0、停止したサーバーに対して「Connection refused」とともに 1 で終了しました。組み込み方は次のとおりです。
# Dockerfile
COPY mcp_healthcheck.py /app/
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD ["python3", "/app/mcp_healthcheck.py", "http://127.0.0.1:8000/mcp"]
# Kubernetes のコンテナ定義
livenessProbe:
httpGet: { path: /healthz, port: 8000 }
periodSeconds: 10
failureThreshold: 3
readinessProbe:
exec:
command: ["python3", "/app/mcp_healthcheck.py", "http://127.0.0.1:8000/mcp"]
periodSeconds: 15
timeoutSeconds: 5
サーバーがすべてのリクエストを認証する場合は、認証を前段のゲートウェイが担う構成で 127.0.0.1 のポートを叩くか、環境変数でプローブ専用のキーを渡します。Cloud RunのHTTPプローブはPOSTの本文を送れないため、livenessには /healthz のルートを使います。
6. 外部の死活監視:リクエストとアサーション
基盤のプローブは自分のインフラの内側から動くため、DNS・TLS・CDN・認証層を、クライアントと同じ見え方では確認できません。そこを埋めるのが外部の監視サービスです。JSON本文と独自ヘッダーつきのPOSTを送れて、レスポンス本文を条件にできるものを選びます。たとえばCronitorは、この方法でのMCPの監視手順を公開しています。
外部監視から送る機能チェックのリクエストです。
POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer <監視用キー>
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: health_check
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"health_check","arguments":{},
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"uptime-monitor","version":"1"},
"io.modelcontextprotocol/clientCapabilities":{}}}}
| 確認する条件 | 理由 |
|---|---|
ステータスコードが 200 |
停止、認証の失敗(401/403)、ヘッダーの不備(400)、基盤のエラー(5xx)を検出 |
本文に HEALTH_OK を含む |
isError: true、依存先の劣化、ヘルスツールの改名や削除を検出。どれも 200 で返るため |
本文に "isError":true を含まない |
ヘルスツールの出力形式が変わったときの保険 |
| 応答時間がしきい値以内(例:3秒) | タイムアウトになる前に、上流の遅延を検出 |
| TLS証明書の残り期間が14〜30日以上 | 証明書の期限切れは、すべてのリモートクライアントを同時に止める |
監視用の認証情報には3つのルールがあります。
- 監視専用のキーを発行する。単独で無効化でき、利用状況の集計からも除外できます。
- ヘルスツールだけに権限を絞る。ホスティングやゲートウェイがツール単位のスコープに対応していれば、監視用キーは
health_check以外を呼べないようにします。キーのスコープ設計はMCPセキュリティのカテゴリで扱っています。 - IP許可リストを使っている場合は、監視サービスの送信元IPを許可する。そうしないと、関係のない理由で監視が失敗します。
スクリプトで済ませるなら、MCP InspectorのCLIも使えます。cronやCIから実行でき、終了コードがそのままアラートの種類に対応します(2026-07-28版のInspectorドキュメントより)。
| 終了コード | 意味 | アラートの種類 |
|---|---|---|
0 |
成功 | 正常 |
3 |
認証が必要(HTTP 401/403) | 監視用キーの失効、または認証設定の誤り |
4 |
到達できない(DNS、接続拒否、タイムアウト) | 停止 |
5 |
tools/call が isError: true、またはツールが見つからない |
機能の障害 |
mcp-inspector --cli https://mcp.example.com/mcp --transport http \
--header "Authorization: Bearer $MCP_MONITOR_KEY" \
--method tools/call --tool-name health_check --format json
OAuthで保護されたサーバーをCIで確認する場合は --stored-auth-only を付けます。TTYがない環境では、ブラウザを待たずに auth_required ですぐ失敗します。
7. 監視間隔とスケールゼロ:費用の計算
インスタンスが起きている間メモリに課金されるプラットフォームでは、プローブは無料ではありません。プローブのたびにスケールゼロのサーバーが起き、アイドルタイムアウトの間は動き続けます。監視間隔がアイドルタイムアウトより短ければ、サーバーは一度も止まりません。
プローブだけで起きている時間 ≈ 730時間 ×(アイドルタイムアウト ÷ 監視間隔、上限1)。512MBのサーバーで、アイドルタイムアウトを5分とした例です(実際の値は利用中の基盤で確認してください)。
| 監視間隔 | プローブによる月間稼働時間 | 月間GB時間(512MB) | 影響 |
|---|---|---|---|
| 1分 | 約730時間 | 約365 | 常時起動と同じ |
| 5分 | 約730時間 | 約365 | 常時起動と同じ(止まる直前に次のプローブが来る) |
| 15分 | 約243時間 | 約122 | 月の3分の1は起きている |
| 30分 | 約122時間 | 約61 | |
| 60分 | 約61時間 | 約30 |
これが問題になるかどうかは課金方式で決まります。
| 課金方式 | 例 | プローブ1回の費用 |
|---|---|---|
| リクエスト単位 | Cloud Runの既定のリクエストベース課金:「リクエストを処理している間と、起動時・終了時だけ」課金 | 小さい。プローブの処理時間と、起こした場合の起動・終了分 |
| インスタンスの稼働時間単位 | Cloud Runのインスタンスベース課金(インスタンスの存続期間全体に課金)、起動中のGB時間で課金する多くのコンテナ・マイクロVM型ホスティング | 1回ごとに最大でアイドルタイムアウト分 |
スケールゼロのサーバーを監視すると、ほかにも2つの影響があります。
- 健康状態ではなくコールドスタートを測ってしまう。止まっているサーバーへのプローブには起動時間が含まれます。監視のタイムアウトをコールドスタートより長くするか、2回連続で失敗したときだけアラートにします。コールドスタートの測り方と短縮方法は最初のMCP呼び出しが遅いのはなぜかにまとめています。
- アイドルのインスタンスはいずれ止まる。Cloud Runは、インスタンスがリクエスト処理後15分を超えてアイドルのままでいることはないとしています。頻繁なプローブは、このカウントを毎回リセットします。
実用的な目安は次のとおりです。
| サーバーの種類 | おすすめの監視 |
|---|---|
| 常時起動、または最小インスタンス数1以上 | プロトコルの確認を1〜5分ごと、機能の確認を5〜15分ごと |
| スケールゼロで、業務時間に使う | 実トラフィックの受動監視に加え、機能の確認を30〜60分ごと、または業務時間だけ |
| スケールゼロで、たまにしか使わない | 受動監視、1日1回の機能確認、デプロイ後のスモークテスト |
8. 設定すべきアラート
アラートは1つの「ありそうな原因」に対応させます。呼び出された人がどこを見ればいいか、すぐ分かるようにするためです。検証と仕様から出てきたシグナルは次のとおりです。
| シグナル | しきい値 | 最も考えられる原因 |
|---|---|---|
接続拒否、DNSの失敗、TLSエラー(Inspectorの終了コード 4) |
2回連続 | サーバー停止・クラッシュ、証明書やDNSの不具合 |
プローブが 401 / 403(終了コード 3) |
1回 | 監視用キーの失効・期限切れ、認証層の設定ミス |
400 と -32020(HeaderMismatch) |
1回 | 監視側に Mcp-Method / Mcp-Name がない。監視を直す |
400 と -32022(UnsupportedProtocolVersion) |
1回 | サーバーか監視のどちらかが別のプロトコルバージョンに上がった |
ping に 404 と -32601 |
1回でも | セッション時代向けに作った監視。server/discover に切り替える |
tools/list に期待するツールがない |
1回 | デプロイでツールが改名・削除された |
ヘルスツールの出力に HEALTH_OK がない、または isError: true |
2回連続 | 上流の認証情報、上流の停止、設定漏れ |
実トラフィックで特定ツールの isError: true の割合が上がる |
15分間で通常の2〜3倍以上 | そのツールの依存先、またはクライアントからのおかしな入力 |
| ヘルスツールの応答時間p95 | 15分間しきい値超え | 上流の遅延、メモリ不足、同居する負荷 |
プローブが 429 |
1回でも | プローブがレート制限に数えられている。間隔をあけるか除外する |
402 や課金関連のエラー |
1回でも | 利用上限やクォータに達した |
9. よい health_check ツールの書き方
ヘルスツールは、安く、読み取り専用で、速く、依存先ごとの状態を通常の出力で名前つきで返すように作ります。そうすれば数分おきに呼んでも安全で、失敗したときにも役に立ちます。検証に使ったものです。
import os, time, asyncio
from mcp.server.mcpserver import MCPServer
from mcp.types import ToolAnnotations
mcp = MCPServer("my-server")
_cache = {"at": 0.0, "result": None}
async def check_upstream() -> str:
# 本物のツールと同じキーで、安い読み取りAPI(例:GET /v1/me)を呼ぶ処理に置き換える
...
return "ok"
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True))
async def health_check() -> str:
"""Read-only self test for monitoring. Reports each dependency; never writes."""
if time.monotonic() - _cache["at"] < 30 and _cache["result"]:
return _cache["result"] # 上流の確認は30秒に1回まで
checks = {}
try:
checks["upstream"] = await asyncio.wait_for(check_upstream(), timeout=3)
except asyncio.TimeoutError:
checks["upstream"] = "timeout"
except Exception as e:
checks["upstream"] = type(e).__name__
status = "OK" if all(v == "ok" for v in checks.values()) else "DEGRADED"
detail = " ".join(f"{k}={v}" for k, v in checks.items())
result = f"HEALTH_{status} {detail} version={os.environ.get('APP_VERSION', 'dev')}"
_cache.update(at=time.monotonic(), result=result)
return result
2つのテスト用サーバーでの結果:正常な方は HEALTH_OK upstream=ok version=dev を55ミリ秒で返し、30秒のキャッシュが効いている間は2〜3ミリ秒でした。壊れた方は HEALTH_DEGRADED upstream=401 from upstream version=dev を返したので、アラートの文面に「Error executing tool」ではなく、何が壊れたかがそのまま出ます。
設計のルールです。
- 本物のツールと同じ認証情報を使う。ヘルスチェックだけ別のAPIキーを使うと、本番のキーが切れたことに気づけません。
- 書き込まない。テスト用の行もメッセージもチケットも作りません。書き込みでしか確かめられない上流なら、同じサービスの読み取り用エンドポイントで確認します。
- 依存先ごとに時間を区切る(ここでは3秒)。上流が固まっても、詳細のない監視タイムアウトではなく
timeoutと返せます。 - 短時間キャッシュする。監視サービスが複数あったり、複数リージョンやレプリカから呼ばれたりしても、上流への呼び出しが増えません。
- バージョン文字列を返す。デプロイ後に、どのビルドが応答しているか確認できます。
- 出力は短く、変えない。監視は
HEALTH_OKで照合しているので、気軽に文言を変えないでください。 - モデルから隠すことも検討する。ほかのツールと同じく
tools/listに載り、少しだけコンテキストを使います。ホスティングやゲートウェイがスコープでツールを絞れるなら、health_checkは監視用キーにだけ見せます。
10. チェックリスト
- [ ]
ping、initialize、MCPエンドポイントへのGETを使う監視がない - [ ] livenessはプロセスだけを確認し(TCPか
/healthz)、上流APIを呼ばない - [ ] プロトコルの確認で、
MCP-Protocol-Version: 2026-07-28とMcp-Methodを付けてserver/discoverをPOSTしている - [ ] クライアントが頼っているツール名が
tools/listにあるか確認している - [ ] 読み取り専用の
health_checkツールが、本番と同じ認証情報で依存先ごとの状態を名前つきで返す - [ ] 外部監視がHTTP
200だけでなく本文(HEALTH_OK)を確認している - [ ] 監視用キーは専用で、単独で無効化でき、ヘルスツールだけに絞られている
- [ ] IP許可リストを使う場合、監視サービスのIPを許可している
- [ ] 監視間隔を、アイドルタイムアウトと課金方式を踏まえて決めた
- [ ] スケールゼロのサーバーでは、監視のタイムアウトがコールドスタートより長い
- [ ] 実トラフィックを受動監視している(ツールごとの
isErrorと5xxの割合) - [ ] アラートごとに、ありそうな原因と担当者が決まっている
11. トラブルシューティング:症状→原因→対処
| 症状 | 原因 | 対処 |
|---|---|---|
| 監視は「停止」なのに、クライアントは普通に使えている | 監視がGETか ping を送っている |
2026-07-28のヘッダー付きで server/discover をPOSTする |
プローブに 400 "Missing session ID" |
MCP-Protocol-Version のないリクエストが、セッション方式にも対応したサーバーに届いた |
ヘッダーと _meta を付ける |
プローブに 404 -32601 |
2026-07-28で ping が廃止された |
server/discover に切り替える |
400 -32020 HeaderMismatch |
Mcp-Method や Mcp-Name がない、または本文と食い違う |
method と params.name をヘッダーにも書く |
| 監視は緑なのに、利用者は失敗している | ステータスコードしか見ていない。失敗は 200 と isError: true で返る |
本文の HEALTH_OK を確認する |
| 上流の障害中に全レプリカが再起動する | livenessで上流を呼んでいる | livenessはプロセスだけにし、上流の確認は機能の層へ移す |
| Kubernetesのプローブでリクエストを送れない | httpGet はGETのみ |
mcp_healthcheck.py を使うexecプローブ、または /healthz ルート |
unhealthy のまま何も起きない |
素のDockerは unhealthy のコンテナを再起動しない | オーケストレーターを使う、またはプロセス管理側で対処する |
| 監視を入れたら請求が増えた | 稼働時間課金で、監視間隔がアイドルタイムアウトより短い | 間隔をあけるか、受動監視を主にする(7章) |
| スケールゼロのサーバーでときどきタイムアウト | プローブがコールドスタートを含んでいる | タイムアウトを延ばし、2回連続の失敗でアラートにする |
| ヘルスツールはOKなのに、ツールが失敗する | ヘルスツールが別の依存先や認証情報を確認している | 本物のツールと同じ認証情報・エンドポイントを確認する |
| デプロイ後にヘルスツールが消えたのに監視が緑 | 200 しか見ておらず、存在しないツールも 200 で返る |
出力の目印で確認する |
12. NodeFlareならこう解決できる
NodeFlareは、GitHubのリポジトリからMCPサーバー(stdio専用のものを含む)を、認証つきのHTTPSエンドポイントとしてデプロイするMCP Hostingサービスです。この記事で挙げた課題に、次のように対応します。
| この記事の課題 | NodeFlareでの対応 |
|---|---|
| ポートは開いているのにMCPを話さないサーバー | デプロイのたびに本物のMCP initialize リクエスト(バックオフつきで最大8回)で確認し、Verified・Broken・Inconclusiveのいずれかで結果を示す。クラッシュを繰り返すプロセスは公開されず、デプロイ失敗になる |
| 監視の合間にstdioサーバーやブリッジが落ちる | 同梱のstdioアダプターが、1秒から30秒までの指数バックオフでサーバーのプロセスを再起動する。再起動の上限に達するとアダプターの /health は 503 を返す |
| HTTP 200の中に隠れた失敗 | すべてのMCPリクエストを tool_name、http_status、outcome(ok、scope_denied、rate_limited、budget_exceeded など)、duration_ms、error_message、user_agent、client_ip つきで記録し、ツール名・エラー・メソッド・outcome・IPで検索できる。4章の受動監視(第4層)の材料になる |
| 監視用キーの権限が広すぎる | mcp_* のAPIキーに tools:call:health_check のようなスコープを付けられ、監視用キーはヘルスツールしか呼べない。tool_list_filter_by_scope で、呼べるツールだけを一覧に見せられる |
| プローブがスケールゼロを打ち消す | アイドル状態が数分続くとスナップショットを取って停止し、1秒未満で復元する。課金は起動中だけなので、間隔をあけたプローブなら費用は小さい。頻繁に監視したい場合は、Always-On(Pro以上)で1レプリカを常に起動しておける |
| 監視で請求が知らないうちに増える | 有料プランでは月の利用上限を設定でき、上限に達するとサーバーは 402 を返す。サーバーごとの月間上限も設定できる |
| 監視がネットワーク制限で弾かれる | IP許可リストはIPv4/IPv6アドレスとCIDRを受け付け、認証より前に適用される。監視サービスの送信元IPを追加する |
できないこと:NodeFlareのドキュメントには、組み込みの死活監視アラートや通知機能の記載はありません。第2層と第3層は外部の監視サービスで行い、第4層にリクエストログを使ってください。ログの検索範囲は1時間・24時間・7日・30日(Freeは1時間固定)、保存期間はFreeが7日、ProとTeamが30日です。
料金:Freeは0円で、MCPサーバー1つと月30GB時間の計算資源つき。Proは月2,980円で、サーバー数無制限、300GB時間の計算資源と10GBのストレージを含みます。超過分の計算資源は1GB時間あたり5円で、アイドル中のサーバーは0円です。
最小の始め方:
- nodeflare.tech にGitHubでログインし、MCPサーバーのリポジトリを選ぶ。公開前にNodeFlareが本物のMCPリクエストで動作を確認します。
- 読み取り専用の
health_checkツールを足し、監視用にtools:call:health_checkに絞ったAPIキーを作る。 - 外部の監視サービスに
/mcpのURLと6章のリクエストを設定し、実トラフィックはログのoutcomeで見る。
1サーバーまで無料で試せます → nodeflare.tech
13. よくある質問
MCPサーバーのヘルスチェックはどうやるの?
GETではなく、JSON-RPCのリクエストをPOSTします。手早くプロトコル層を確認するなら、MCP-Protocol-Version: 2026-07-28 と Mcp-Method のヘッダーを付けて server/discover を呼びます。ツールが動くことまで確かめるなら、読み取り専用の health_check ツールを呼んで出力を確認します。ツールの失敗はHTTP 200 と isError: true で返るためです。
MCPにpingはある?
今はありません。2026-07-28の仕様で、initialize ハンドシェイクやセッションとともに ping は廃止されました。検証では、2026-07-28のサーバーは ping に 404 と -32601 Method not found を返しました。2025-11-25以前のセッション方式のサーバーでは使えますが、セッションの中に限られます。
pingの代わりに何を使えばいい?
server/discover です。2026-07-28のサーバーは必ず実装しなければならず、対応するプロトコルバージョン、機能、サーバー情報を返します。副作用がなくセッションも不要なので、軽いlivenessやreadinessの確認に使えます。
動いているのに監視サービスが「停止」と判定するのはなぜ?
監視がGETか ping を送っている可能性が高いです。MCPエンドポイントはPOSTしか受け付けず、2026-07-28のサーバーはGETにエラー(検証したPython SDKのサーバーでは 400、仕様では 405)を、ping に 404 を返します。プロトコル用のヘッダーを付けた server/discover のPOSTに切り替えてください。
HTTP 200が返ればMCPサーバーは正常?
いいえ。検証では、上流APIのキーが切れたサーバーでも server/discover、tools/list、すべてのツール呼び出しが 200 でした。失敗したツールは、200 のレスポンスの中で isError: true を返していました。存在しないツールを呼んでも 200 です。レスポンス本文で判定してください。
ヘルスチェックでtools/callを呼ぶべき?
機能の層では呼ぶべきです。ただし、書き込みをせず、数秒で終わり、本物のツールと同じ認証情報を使う専用の読み取りツールに限ります。基盤のlivenessには入れないでください。上流が落ちたときに、正常なコンテナまで再起動されてしまいます。
KubernetesやCloud RunでMCPサーバーをプローブできる?
工夫すればできます。HTTPプローブはGETを送るため、livenessには /healthz のような小さなGETルートを足すか、Kubernetesなら server/discover をPOSTするスクリプトを exec プローブで動かします。Cloud Runのlivenessとreadinessが対応するのはHTTPとgRPCだけなので、ルートを足すのが最も簡単です。
ヘルスチェックでスケールゼロが効かなくなる?
なることがあります。起動中の時間で課金される基盤では、プローブのたびにサーバーが起き、アイドルタイムアウトの間動き続けます。監視間隔がアイドルタイムアウトより短ければ一度も止まらず、512MBで月約365GB時間になります。Cloud Runの既定のようなリクエスト単位の課金なら、費用はプローブの処理時間と起動分だけです。
どのくらいの間隔で監視すればいい?
常時起動のサーバーなら、プロトコルの確認を1〜5分ごと、機能の確認を5〜15分ごとが一般的です。スケールゼロのサーバーは、実際のリクエストの受動監視を主にし、機能の確認は30〜60分ごと、または業務時間だけにします。
stdioのMCPサーバーはどう監視する?
stdioサーバー自体にはネットワークの入り口がないので、それを公開している側を監視します。supergatewayやmcp-proxyのようなブリッジの裏で動かしているなら、ブリッジのHTTPエンドポイントに server/discover とツール呼び出しを送ります。クライアントの中でローカルに動かしているだけなら外から確認する手段はないため、クライアントのMCPログを見ます。
MCP Inspectorは監視に使える?
使えます。CLIモードは1回リクエストを送って終了し、失敗の種類ごとに終了コードを返します。認証が必要なら 3、到達できなければ 4、isError: true やツールが見つからない場合は 5 です。cronやCIから実行し、終了コードごとに別のアラートにできます。
まとめ
古いMCPサーバーや一般的なWeb API向けに作ったヘルスチェックは、2026-07-28のサーバーでは判断を誤らせます。ping はなくなり、GETは拒否され、本当の失敗はHTTP 200 と isError: true で返ってくるからです。役割を層に分けましょう。コンテナの再起動は、プロセスだけを見るlivenessに任せます。人へのアラートは、server/discover、tools/list に期待するツールがあるか、依存先ごとの状態を返す読み取り専用の health_check ツールで判断します。判定はステータスコードではなく本文で行います。監視間隔はアイドルタイムアウトと課金方式から決め、監視のせいでスケールゼロのサーバーが常時起動にならないようにします。4つ目のシグナルとして、実トラフィックの isError の割合も見てください。
参考資料
- MCP Specification 2026-07-28: Key Changes
- MCP Specification 2026-07-28: Streamable HTTP transport
- MCP Specification 2026-07-28: Discovery (server/discover)
- MCP Specification 2026-07-28: Tools(エラー処理)
- MCP Inspector: CLI client
- Google Cloud Run: コンテナのヘルスチェックの設定
- Google Cloud Run: 課金設定
- Kubernetes: Liveness, Readiness and Startup Probes
- Docker: Dockerfile reference(HEALTHCHECK)
- Cronitor: Monitor MCP servers
- MCP Python SDK(PyPI)
