MCPサーバーの構築記事は100本読んだ。でも「落ちたときどうするか」を書いた記事は1本もなかった。
結論:MCPサーバーは「動かすまで」より「動かし続けること」が10倍難しい
この記事では、Notion MCPサーバーが本番環境で無応答になり、Claude Codeが無限リトライを繰り返した実際の障害体験をもとに、MCPサーバーの運用で本当に必要な5つの教訓をまとめます。
構築のハウツーではなく、落ちたときに何が起きるか、どう備えるかに焦点を当てた内容です。
環境・前提条件
- Claude Code: 最新版(CLI)
- MCPサーバー: Notion MCP Server(stdio / リモート両方の知見を含む)
- OS: macOS / Linux
- チーム規模: 3〜10名の開発チーム
- 前提知識: MCPサーバーの基本的な構築・設定ができる方
障害発生の経緯──Notion MCPが無応答になった夜
金曜日の夜22時。締め切り直前のドキュメント整理をClaude Codeに任せていた。Notion MCPサーバー経由でページの取得・更新を自動化していた、そのときだった。
Notion APIのレートリミットに到達し、MCPサーバーが応答を返せなくなった。
普通なら「エラーが返ってきて終わり」のはず。しかし実際に起きたのは、こうだ。
- Notion APIが
429 Too Many Requestsを返す - MCPサーバーがこのエラーを適切にハンドリングせず、レスポンスを返さないまま固まった
- Claude Codeはレスポンスを待ち続け、タイムアウト後に同じリクエストをリトライ
- リトライがさらにレートリミットを悪化させる負のスパイラル
- 気づいたときには、Notion APIの呼び出し回数が通常の50倍に膨れ上がっていた
この障害から学んだ5つの教訓を、順に解説します。
教訓1: ヘルスチェックとタイムアウト設計──MCPプロトコルの落とし穴
MCPには「標準的なヘルスチェック」がない
HTTPサーバーなら /health エンドポイントを叩くのが定石です。しかしMCPプロトコル(特にstdioトランスポート)には、サーバーが生きているか確認する標準的な仕組みが明確に規定されていません。
MCPの仕様にはpingメソッドが存在しますが、すべてのMCPサーバー実装がこれを適切にサポートしているとは限りません。
やるべきこと
① タイムアウトを明示的に設定する
claude_desktop_config.jsonや環境変数でタイムアウトを設定します。
{
"mcpServers": {
"notion": {
"command": "npx",
"args": ["-y", "@notionhq/notion-mcp-server"],
"env": {
"OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer ntn_xxx\"}",
"MCP_TIMEOUT": "30000"
}
}
}
}
② MCPサーバー側にヘルスチェック機構を実装する
自作のMCPサーバーであれば、定期的に外部APIの疎通確認を行うツールを追加します。
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "health_check") {
try {
const response = await fetch("https://api.notion.com/v1/users/me", {
headers: { "Authorization": `Bearer ${token}` },
signal: AbortSignal.timeout(5000) // 5秒でタイムアウト
});
return {
content: [{
type: "text",
text: JSON.stringify({
status: response.ok ? "healthy" : "degraded",
statusCode: response.status,
timestamp: new Date().toISOString()
})
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: JSON.stringify({ status: "unhealthy", error: error.message })
}],
isError: true
};
}
}
});
③ 外部APIのレートリミットを意識した設計にする
// リトライ時にexponential backoffを入れる
async function callWithBackoff(fn: () => Promise<any>, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error: any) {
if (error.status === 429 && i < maxRetries - 1) {
const waitTime = Math.pow(2, i) * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
throw error;
}
}
}
教訓2: ログ設計──何を記録すれば障害原因を30秒で特定できるか
障害が起きた夜、最初にやったのは「何が起きているか」を把握することでした。しかし、MCPサーバーのログがほぼ空だった。
MCPサーバーで記録すべき4つの情報
| レイヤー | 記録すべき内容 | 理由 |
|---|---|---|
| リクエスト | ツール名、引数(機密情報はマスク)、タイムスタンプ | 「何をしようとしたか」の把握 |
| 外部API | ステータスコード、レスポンスタイム、レートリミットヘッダー | ボトルネック・障害原因の特定 |
| エラー | エラー種別、スタックトレース、リトライ回数 | 根本原因の特定 |
| メトリクス | リクエスト数/分、平均レスポンスタイム、エラー率 | 異常検知と傾向分析 |
実装例:構造化ログ
import { stderr } from "process";
function mcpLog(level: string, event: string, data: Record<string, any>) {
const log = {
timestamp: new Date().toISOString(),
level,
event,
...data,
// トークンなどの機密情報をマスク
...(data.token && { token: data.token.slice(0, 8) + "..." })
};
// MCPのstdioトランスポートではstdoutがプロトコル通信用
// ログは必ずstderrに出力する
stderr.write(JSON.stringify(log) + "\n");
}
// 使用例
mcpLog("info", "tool_called", { tool: "search_pages", args: { query: "設計書" } });
mcpLog("error", "api_error", { statusCode: 429, retryAfter: 30 });
重要: stdioトランスポートのMCPサーバーでは、
console.log(stdout)を使うとプロトコル通信を破壊します。ログは必ずstderrに出力してください。
Claude Code側のログ確認
Claude Codeのデバッグ情報は以下のコマンドで確認できます。
# MCPサーバーの接続状態を確認
claude mcp list
# Claude Codeのログディレクトリ
ls ~/.claude/logs/
教訓3: フォールバック戦略──MCPが死んでもエージェントが暴走しない仕組み
今回の障害で最も怖かったのは、MCPサーバーが死んでいるのにClaude Codeがリトライし続けたことです。
フォールバックの3段階設計
第1段階:リトライ上限の設定
Claude Codeのシステムプロンプト(CLAUDE.md)にリトライポリシーを明記します。
# CLAUDE.md に追記するMCPルール
## MCP利用ルール
- MCPツールの呼び出しが2回連続で失敗した場合、同じツールの再呼び出しを停止すること
- タイムアウトエラーが発生した場合、ユーザーに報告して指示を仰ぐこと
- 429エラー(レートリミット)が返された場合、最低30秒は待機すること
第2段階:代替手段の用意
## フォールバック手段
- Notion MCPが使えない場合 → Notion URLを直接提示し、手動確認を依頼
- GitHub MCPが使えない場合 → git CLIコマンドで代替
- データベース検索MCPが使えない場合 → ローカルのキャッシュファイルを参照
第3段階:安全な停止
最も重要なのは「わからなければ止まる」ことです。Claude Codeに対して「MCPが応答しない場合は作業を中断してユーザーに報告する」というルールを明示しておきます。
教訓4: セキュリティ──トークンのスコープ最小化と定期ローテーション
障害対応中に気づいた、もう1つの恐怖。
MCPサーバーに渡していたNotionトークンが、ワークスペース全体の読み書き権限を持っていた。
もしMCPサーバーが外部に露出していたら、全ドキュメントが危険に晒されていたことになります。
トークン管理の原則
1. 最小権限の原則
- Notion: 必要なページ/データベースのみにアクセスを限定
- GitHub: リポジトリ単位 + 必要な権限のみ(read-only が基本)
- Slack: 特定チャンネルのみ
2. トークンのローテーション
- 90日ごとにトークンを再発行
- CI/CDパイプラインでローテーションを自動化
3. トークンの保管
- 設定ファイルに直接書かない
- 環境変数 or シークレットマネージャーを使う
環境変数での安全な設定例:
# .envファイル(.gitignoreに必ず追加)
NOTION_MCP_TOKEN=ntn_xxxxxxxxxxxx
# claude_desktop_config.json からは環境変数を参照
{
"mcpServers": {
"notion": {
"command": "npx",
"args": ["-y", "@notionhq/notion-mcp-server"],
"env": {
"OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer ${NOTION_MCP_TOKEN}\"}"
}
}
}
}
定期的なアクセス監査
# Notion APIの利用状況を確認するスクリプト例
curl -s https://api.notion.com/v1/users/me \
-H "Authorization: Bearer ${NOTION_MCP_TOKEN}" \
-H "Notion-Version: 2022-06-28" | jq '.bot.owner'
教訓5: チームでの共有MCPサーバー運用時の権限分離パターン
チームで同じMCPサーバーを使う場合、**「誰がどのツールを、どの範囲で使えるか」**を設計する必要があります。
権限分離の3パターン
| パターン | 方法 | 適するケース |
|---|---|---|
| トークン分離 | 権限の異なるトークンで別々のMCPサーバーを立てる | 小規模チーム |
| プロキシ型 | 認証プロキシの背後にMCPサーバーを置く | 中規模チーム |
| マルチテナント | MCPサーバー内でユーザーごとの権限制御を実装 | 大規模チーム |
小規模チームの現実的な設定例
// 開発者A用(フルアクセス)
{
"mcpServers": {
"notion": {
"command": "npx",
"args": ["-y", "@notionhq/notion-mcp-server"],
"env": {
"OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer ${NOTION_FULL_TOKEN}\"}"
}
}
}
}
// 開発者B用(読み取り専用 → トークンの権限で制御)
{
"mcpServers": {
"notion": {
"command": "npx",
"args": ["-y", "@notionhq/notion-mcp-server"],
"env": {
"OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer ${NOTION_READONLY_TOKEN}\"}"
}
}
}
}
チーム内でCLAUDE.mdを共有し、MCPの利用ルール(どのツールをどう使うか、エラー時の対応方針)を統一しておくことも重要です。
運用チェックリストテンプレート(コピペ可)
MCPサーバーを本番で運用するとき、以下のチェックリストを使ってください。
# MCP サーバー運用チェックリスト
## 初期構築時
- [ ] タイムアウト値を明示的に設定した
- [ ] MCPサーバーのログが stderr に出力される設計になっている
- [ ] ログに機密情報(トークン、個人情報)が含まれないことを確認した
- [ ] CLAUDE.md にリトライポリシーとフォールバック手順を記載した
- [ ] トークンのスコープを必要最小限に絞った
- [ ] トークンを環境変数 or シークレットマネージャーで管理している
- [ ] .gitignore にトークン関連ファイルを追加した
## 定期メンテナンス(月次)
- [ ] トークンの有効期限とスコープを確認した
- [ ] MCPサーバーの依存パッケージを更新した
- [ ] ログを確認し、エラー率の異常がないか確認した
- [ ] 外部APIのレートリミット消費状況を確認した
## 障害発生時
- [ ] MCPサーバーのプロセスが生存しているか確認(`claude mcp list`)
- [ ] stderr のログで直近のエラーを確認
- [ ] 外部APIの障害情報(ステータスページ)を確認
- [ ] Claude Code のリトライが止まっていることを確認
- [ ] 必要に応じてMCPサーバーを再起動(`claude mcp reset`)
- [ ] 障害の原因と対策をチームに共有
## チーム運用
- [ ] メンバーごとに適切な権限のトークンを発行した
- [ ] CLAUDE.md のMCPルールをチーム全員が確認した
- [ ] MCPサーバーの設定ファイルの管理方法を統一した
まとめ
- MCPサーバーは「外部APIとの接続点」であり、ネットワーク障害・レートリミット・認証切れなど、従来のAPI連携と同じ障害が発生する。 ヘルスチェック・タイムアウト・構造化ログを最初から組み込むことが必要です。
- MCPが死んだとき、本当に怖いのは「エージェントが暴走すること」。 CLAUDE.mdでリトライ上限とフォールバック手順を明示し、「わからなければ止まる」設計にしましょう。
- トークンの最小権限・定期ローテーション・チームでの権限分離は、MCPだからといって省略してよい話ではない。 むしろAIエージェントが自律的に操作する分、従来以上に厳格な管理が求められます。