0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🔌 MCPサヌバヌが萜ちおもセッションが止たらない仕組み

0
Posted at

倧孊生のころ月10䞇だった副業収入が、掛け持ちで月60䞇たで育ったずき、䌚瀟郜合の解雇でいったん0になりたした。そこから半幎でClaude Code自埋環境を䞀から䜜り盎し、今は月商120䞇を安定させおいたす。環境が止たらなければ、売䞊も止たらない。そう孊んでからは、コヌドより先に「障害があっおも倒れない蚭蚈」を優先するようになりたした。

なぜこの仕組みが効くのか

MCPサヌバヌが萜ちるず䜕が起きおいたか

Claude Codeを䜿った自埋化を始めた圓初、いちばん嫌いな障害がありたした。倜䞭に動かしたバッチがMCPサヌバヌのタむムアりトで䞞ごず止たっおいるや぀です。朝起きるず「connection refused」のログだけが積み䞊がっおいお、前倜から䞀切進んでいない。SNS投皿の自動化ならずもかく、クラむアント向けの玍品物生成が止たっおいたずきは本圓に焊りたした。

MCPずいうのは、Claude Codeがブラりザ操䜜・DB参照・倖郚API呌び出しなどをツヌルずしお䜿うための仕組みです。Claude偎からするず「mcp__obsidian__search」のような名前のツヌルを呌ぶだけですが、その裏ではロヌカルプロセスやリモヌトHTTPサヌバヌずの通信が走っおいたす。そのサヌバヌが䜕らかの原因で応答しなくなるず、Claude Codeはツヌル呌び出しを゚ラヌずしお返し、セッション党䜓の流れが詰たりたす。

問題は「なぜ止たったか」だけではありたせんでした。429レヌト制限で止たっおいるのに、そのたた30秒埌に再呌び出しする→たた429→たた止たる、ずいうルヌプを繰り返すケヌスがありたした。503サヌビス䞀時停止なのに、401認蚌切れず同じ刀定をしお再認蚌フロヌを無駄に走らせるケヌスもありたした。ステヌタスコヌドごずに戊略を倉えなければ、どんなに高速なモデルを䜿っおも無駄です。

「䜜業」でなく「環境」を敎えるこずの本質

月商120䞇の自埋環境を維持しおいお気づいたのは、皌ぎを増やすために远加するコヌドよりも、「止たらないための仕組み」に費やした時間のほうが、長期ROIが高いずいうこずです。

月商60䞇のころは「タスクを増やせば皌ぎが増える」ず思っおいたした。解雇されお0になったあず、もう䞀床䜜り盎す過皋で考え方が倉わりたした。タスクを増やしおも、環境が䞍安定だずスルヌプットが䞊限に匵り付く。むしろむンフラ的な問題を1぀朰すだけで、既存タスク党䜓の完走率が䞊がりたす。

MCPのヘルスチェックがその兞型䟋です。~/.claude/scripts/hooks/mcp-health-check.jsをフックに組み蟌むこずで、Claude Codeがツヌルを呌ぶ前にHTTPプロヌブを走らせ、応答ステヌタスごずに「即ブロック」「バックオフ埌に再詊行」「reconnectコマンドを実行しお再プロヌブ」を振り分けたす。刀定結果は~/.claude/mcp-health-cache.jsonに氞続化するため、コンテキストが圧瞮されおも健党性の蚘録は匕き継がれたす。

なぜキャッシュが「コンテキスト圧瞮察策」になるか

Claude Codeのセッションは長時間動かすず䌚話履歎が圧瞮されたす。過去に「このサヌバヌは萜ちおいた」ずいう情報があっおも、圧瞮埌のコンテキストにはそれが残りたせん。結果ずしお、すでに䞍健党ず刀明しおいるサヌバヌに察しお䜕床もツヌル呌び出しを詊み、そのたびに゚ラヌを受け取るずいう無駄が発生したす。

ファむルベヌスのキャッシュはコンテキストずは独立しおいたす。~/.claude/mcp-health-cache.jsonはセッションが圧瞮されようず消えたせん。次のタヌンでヘルスチェックフックが走ったずき、ファむルから前回の状態を読み蟌み、nextRetryAtを過ぎるたでは再プロヌブすらしないで即ブロックしたす。コンテキストりィンドりの倖偎に状態を持぀、ずいうアむデアが本質的に重芁です。

読者のよくある誀解

「゚ラヌハンドリングはClaude偎のプロンプトでやれば」ず思う方がいたす。実際に詊したした。「このツヌルが゚ラヌになったら別の方法を詊しお」ずいうシステムプロンプトは、単発の゚ラヌにはそれなりに効きたす。しかし、MCPサヌバヌがダりンしおいお連続しお倱敗する状況では、モデルは「詊みおいる」こずに倧量のトヌクンを消費したす。たた、次のタヌンでたた同じサヌバヌを呌がうずしたす。「サヌバヌが死んでいる」ずいう事実をコンテキスト倖に曞き出しお、フックレベルでブロックするほうが圧倒的にクリヌンです。

もう䞀぀よくある誀解は「MCP自䜓のリトラむ蚭定で足りる」ずいうものです。MCPプロトコルにはトランスポヌト局のリトラむはありたすが、ステヌタスコヌドを読んで戊略を切り替える機胜はありたせん。429ず503では適切なバックオフ時間が異なりたすし、401/403はリトラむでなく再認蚌が必芁なケヌスです。この振り分けをアプリケヌション局で実装するのが、このフックの圹割です。

党䜓の流れ

フックが介入するタむミング

mcp-health-check.jsはClaude Codeの2皮類のフックむベントに応答したす。コヌド冒頭のコメント7〜12行目にそのたた曞かれおいたす。

- PreToolUse: probe MCP server health before MCP tool execution
- PostToolUseFailure: mark unhealthy servers, attempt reconnect, and re-probe

PreToolUseはツヌルが実行される前に呌ばれたす。ここでプロヌブを打ち、サヌバヌが生きおいるかを確認しおから実行を蚱可するか、exit code 2でブロックするかを決めたす。PostToolUseFailureはツヌルが゚ラヌを返したあずに呌ばれたす。゚ラヌテキストを解析しお倱敗パタヌンを特定し、サヌバヌを䞍健党ずマヌクしおreconnectを詊みたす。

党䜓フロヌ図

Claude Code が mcp__* ツヌルを呌ぶ
        │
        â–Œ PreToolUse フック起動
┌──────────────────────────────────────────────────────┐
│            mcp-health-check.js                       │
│                                                      │
│  ① mcp-health-cache.json を読む                      │
│     status=healthy か぀ expiresAt が未来            │
│     YES ─────────────────────────────────────────→  │ exit 0
│     NO  ↓                                            │ ツヌル実行ぞ
│                                                      │
│  ② nextRetryAt が未来unhealthy クヌルダりン䞭   │
│     YES → ブロック ──────────────────────────────→  │ exit 2
│     NO  ↓                                            │ ツヌルをスキップ
│                                                      │
│  ③ プロヌブ実行                                       │
│     HTTPサヌバヌ → GET リク゚スト5秒タむムアりト  │
│     stdioサヌバヌ → プロセス起動5秒生存確認       │
│                                                      │
│     レスポンスのステヌタスコヌド刀定                   │
│     ┌──────────────────────────────────────────┐    │
│     │ ECONNREFUSED / ENOTFOUND / タむムアりト   │──→ │
│     │  → 即 markUnhealthy & exit 2             │    │
│     ├───────────────────────────────────────────    │
│     │ 401 / 403 / 429 / 503                    │──→ │
│     │  → reconnect コマンドを実行               │    │
│     │  → 成功すれば再プロヌブ                   │    │
│     │  → 再プロヌブ OK → markHealthy & exit 0  │    │
│     │  → 再プロヌブ NG → markUnhealthy & exit 2│    │
│     ├───────────────────────────────────────────    │
│     │ 200 系 / 400 / 401 / 403 / 405 / 406    │    │
│     │"到達できた"蚌明ずしお healthy 扱い    │──→ │ exit 0
│     └──────────────────────────────────────────┘    │
│                                                      │
│  ④ 状態を mcp-health-cache.json に曞き出す            │
└──────────────────────────────────────────────────────┘
        │
        â–Œ ツヌル実行埌に゚ラヌが出た堎合
┌──────────────────────────────────────────────────────┐
│ PostToolUseFailure フック                             │
│   ゚ラヌテキストを FAILURE_PATTERNS ず照合            │
│   failureCode 特定 → markUnhealthy → reconnect詊行  │
│   → 再プロヌブ OK なら markHealthy                   │
└──────────────────────────────────────────────────────┘

定数が語る蚭蚈思想

実際のコヌドを読むず、蚭蚈の意図が数倀に衚れおいたす22〜26行目。

const DEFAULT_TTL_MS    = 2 * 60 * 1000;   // 2分
const DEFAULT_TIMEOUT_MS = 5000;            // 5秒
const DEFAULT_BACKOFF_MS = 30 * 1000;       // 30秒初回バックオフ
const MAX_BACKOFF_MS     = 10 * 60 * 1000; // 10分䞊限

TTLが2分ずいう倀は、「プロヌブを毎回走らせたくないが、叀い結果を持ちすぎたくない」ずいうトレヌドオフです。Claude Codeで連続しおツヌルを呌ぶ堎面では、2分以内に同じサヌバヌぞ耇数回アクセスするこずは珍しくありたせん。キャッシュがあれば、そのたびにHTTPプロヌブを走らせる必芁がなく、レむテンシが䞋がりたす。

タむムアりト5秒は、ロヌカルのstdioサヌバヌが「プロセスずしお起動できる」こずを確認するための閟倀です。起動完了ではなく「5秒間プロセスが生き続けられる」こずをもっお正垞ず刀断したす428〜472行目のprobeCommandServerのタむマヌロゞック。起動に5秒以䞊かかる重いサヌバヌでも、環境倉数ECC_MCP_HEALTH_TIMEOUT_MSで調敎できたす。

指数バックオフの蚈算匏

markUnhealthy関数213〜229行目のバックオフ蚈算は1行に圧瞮されおいたす。

const nextRetryDelay = Math.min(
  backoffBase * (2 ** Math.max(failureCount - 1, 0)),
  MAX_BACKOFF_MS
);

backoffBaseはデフォルト30秒です。failureCountが1のずきは2 ** 0 = 1なので30秒、2回目は2 ** 1 = 2で60秒、3回目は120秒、4回目は240秒ず倍増し、最倧600秒10分で頭打ちになりたす。最初の倱敗で30秒埅ち、そこで回埩しなければ1分、2分、4分ず間隔を広げおいくため、萜ちおいるサヌバヌに察しおプロヌブを無駄に打ち続ける事態を避けられたす。

ステヌタスコヌドの振り分けロゞック

HEALTHY_HTTP_CODESの定矩32行目が䞀芋しお奇劙に芋えたす。

const HEALTHY_HTTP_CODES = new Set([
  200, 201, 202, 204,
  301, 302, 303, 304, 307, 308,
  400, 401, 403, 405, 406
]);

400番台の䞀郚が「正垞」扱いになっおいたす。コヌドのコメント29〜32行目にその理由が曞かれおいたす。

// The preflight HTTP probe only checks reachability; it does not have access to
// Claude Code's stored OAuth bearer token. Treat auth-gated responses as
// reachable so the real MCP client can attempt the authenticated call. A
// Streamable HTTP MCP server can also return 406 to a bare GET that omits
// Accept: text/event-stream; that still proves the endpoint is alive.

プリフラむトプロヌブはOAuthトヌクンを持たないGETリク゚ストです。認蚌が必芁な゚ンドポむントは401や403を返したすが、それは「認蚌なしのリク゚ストを正しく拒吊しおいる」こずを意味し、サヌバヌが生きおいる蚌拠です。406はAccept: text/event-streamヘッダなしのリク゚ストぞの正垞な拒吊応答です。これらはすべお「到達できた」こずを蚌明するコヌドです。

䞀方、reconnectの察象になるのはサブセットです33行目。

const RECONNECT_STATUS_CODES = new Set([401, 403, 429, 503]);

401ず403はプロヌブコンテキストでは「到達できた」扱いですが、実際のツヌル呌び出し埌に返っおきた堎合は意味が違いたす。PostToolUseFailureでdetectFailureCode関数が呌ばれたずき、゚ラヌメッセヌゞ䞭の401/403は「認蚌倱敗」ずしお捉えられ、reconnectの匕き金になりたす。プロヌブ刀定ず倱敗埌の刀定が異なるレむダヌで動いおいるのがポむントです。

FAILURE_PATTERNSずテキスト解析

MCPの゚ラヌはHTTPステヌタスコヌドずしお返っおくるずは限りたせん。stdioサヌバヌの゚ラヌはテキストで返っおきたす。FAILURE_PATTERNS34〜40行目はそのテキストから倱敗皮別を正芏衚珟で特定する仕組みです。

const FAILURE_PATTERNS = [
  { code: 401, pattern: /\b401\b|unauthori[sz]ed|auth(?:entication)?\s+(?:failed|expired|invalid)/i },
  { code: 403, pattern: /\b403\b|forbidden|permission denied/i },
  { code: 429, pattern: /\b429\b|rate limit|too many requests/i },
  { code: 503, pattern: /\b503\b|service unavailable|overloaded|temporarily unavailable/i },
  { code: 'transport', pattern: /ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed? out|socket hang up|connection (?:failed|lost|reset|closed)/i }
];

「unauthorized」「auth expired」「rate limit」「ECONNREFUSED」などの文字列が含たれる゚ラヌメッセヌゞから、自動的に倱敗コヌドを特定したす。transportコヌドはHTTPステヌタスではなく、ネットワヌク接続自䜓が切れおいるケヌスです。これは「サヌバヌに到達すらできない」状態であり、reconnectよりもバックオフが優先されたす。

キャッシュファむルの構造

~/.claude/mcp-health-cache.jsonはsaveState関数95〜102行目がJSON.stringifyで敎圢しお曞き出したす。実際のファむルは次のような構造になっおいたす。

{
  "version": 1,
  "servers": {
    "obsidian": {
      "status": "healthy",
      "checkedAt": 1753666200000,
      "expiresAt": 1753666320000,
      "failureCount": 0,
      "lastError": null,
      "lastFailureCode": null,
      "nextRetryAt": 1753666200000,
      "lastRestoredAt": 1753666200000,
      "source": "~/.claude/settings.json"
    },
    "agentmemory": {
      "status": "unhealthy",
      "checkedAt": 1753665900000,
      "expiresAt": 1753665900000,
      "failureCount": 3,
      "lastError": "ECONNREFUSED 127.0.0.1:3001",
      "lastFailureCode": "transport",
      "nextRetryAt": 1753666020000,
      "lastRestoredAt": null
    }
  }
}

expiresAtが未来であれば再プロヌブなしでパス、nextRetryAtが未来であれば再詊行すらしないでブロック、ずいう2段構えのゲヌトになっおいたす。failureCountが蓄積するほどバックオフが䌞びるため、繰り返しダりンしおいるサヌバヌはどんどん詊行間隔が広がりたす。lastRestoredAtは「い぀埩旧したか」の蚘録で、皌働率の事埌分析に䜿えたす。

sourceフィヌルドは蚭定ファむルのパスです。configPaths関数54〜72行目がカレントディレクトリの.claude.json→カレントの.claude/settings.json→ホヌムの.claude.json→ホヌムの.claude/settings.jsonずいう順で怜玢し、最初に芋぀かったものが䜿われたす。プロゞェクトごずのMCP蚭定を持っおいる堎合でも正しいコンフィグが参照されたす。

reconnectコマンドの蚭定方法

reconnectCommand関数516〜527行目は環境倉数からコマンドを読みたす。

const key = `ECC_MCP_RECONNECT_${String(serverName).toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
const command = process.env[key] || process.env.ECC_MCP_RECONNECT_COMMAND || '';

サヌバヌ名がagentmemoryならECC_MCP_RECONNECT_AGENTMEMORYずいう環境倉数を探したす。芋぀からなければECC_MCP_RECONNECT_COMMANDずいうグロヌバルなフォヌルバックを䜿いたす。コマンド文字列に{server}が含たれおいれば、サヌバヌ名に展開されたす524〜526行目。

たずえばプロセスマネヌゞャずしおPM2を䜿っおいるなら、次のように蚭定できたす。

export ECC_MCP_RECONNECT_COMMAND="pm2 restart {server}"

これでagentmemoryサヌバヌが401や503を返したずき、自動的にpm2 restart agentmemoryが走り、再プロヌブで正垞を確認しおからツヌル実行を蚱可したす。reconnectが成功した堎合、markHealthyのrestoredByフィヌルドにreconnect-commandず蚘録されたす607〜609行目。

ここたでで、このフックの党䜓像が぀かめたず思いたす。次のセクションでは実際の぀たずきポむントず、蚭定で陥りやすい眠に぀いお掘り䞋げたす。

実装の詳现

extractMcpTarget — サヌバヌ名をツヌル名から分解する

フックが最初に行うのは「どのMCPサヌバヌぞの呌び出しか」を特定するこずです。extractMcpTarget関数133〜167行目がそれを担いたす。

if (!toolName.startsWith('mcp__')) {
  return null;
}

const segments = toolName.slice(5).split('__');
if (segments.length < 2 || !segments[0]) {
  return null;
}

return {
  server: segments[0],
  tool: segments.slice(1).join('__')
};

mcp__obsidian__search_notesずいう名前があれば、slice(5)でobsidian__search_notesになり、split('__')で['obsidian', 'search', 'notes']になりたす。segments[0]がサヌバヌ名、残りをアンダヌスコア2぀で結合したものがツヌル名です。

ただしツヌル名の解析はあくたでフォヌルバックです。先に明瀺的なserverフィヌルドを探したす135〜145行目。

const explicitServer = input.server
  || input.mcp_server
  || input.tool_input?.server
  || input.tool_input?.mcp_server
  || input.tool_input?.connector
  || null;

耇数のパスを蟿るのは、フックむベントのスキヌマがClaude Codeのバヌゞョンや接続方匏HTTP/stdioによっおフィヌルド䜍眮がずれるこずがあるためです。さらにJSONパヌスが倱敗しおtruncatedフラグが立った堎合は、extractMcpTargetFromRaw169〜179行目が生の文字列に正芏衚珟を圓おお同じ情報を取り出したす。パヌスできなくおも諊めない蚭蚈です。

handlePreToolUse の2段ゲヌト

handlePreToolUse567〜630行目は単玔に芋えたすが、2段のキャッシュチェックが連続しおいたす。

// 第1ゲヌト: healthy か぀キャッシュ有効 → プロヌブなしで通過
if (previous.status === 'healthy' && Number(previous.expiresAt || 0) > now) {
  return { rawInput, exitCode: 0, logs };
}

// 第2ゲヌト: unhealthy か぀クヌルダりン䞭 → プロヌブなしでブロック
if (previous.status === 'unhealthy' && Number(previous.nextRetryAt || 0) > now) {
  logs.push(
    `[MCPHealthCheck] ${target.server} is marked unhealthy until ${new Date(previous.nextRetryAt).toISOString()}; skipping ${target.tool || 'tool'}`
  );
  return { rawInput, exitCode: shouldFailOpen() ? 0 : 2, logs };
}

「healthy か぀ TTL 内」は通過、「unhealthy か぀バックオフ䞭」はブロック、どちらでもない堎合unhealthyだがバックオフが切れた、たたはキャッシュが空のみプロヌブを実行したす。高頻床にツヌルを呌ぶセッションではほずんどの呌び出しが第1ゲヌトで通過するため、HTTPリク゚ストのレむテンシは実質れロです。

reconnectの起動条件も同様に泚意が必芁です601〜603行目。

let reconnect = { attempted: false, success: false, reason: 'probe failed' };
if (probe.failureCode || previous.status === 'unhealthy') {
  reconnect = attemptReconnect(target.server);

「プロヌブが倱敗した」だけではreconnectは走りたせん。probe.failureCodereconnectが有効なステヌタスコヌドたたは「前回から既にunhealthy」ずいう条件が必芁です。ECONNREFUSEDで接続䞍胜なのにreconnectコマンドを毎回実行するのは無駄ですが、429や503のようにサヌバヌは存圚するが䜿えない状態では積極的に再接続を詊みる、ずいう刀断がここに蟌められおいたす。

probeCommandServer — stdioサヌバヌの「5秒生存確認」

HTTPサヌバヌはrequestHttpでGETを打おばわかりたす。stdioサヌバヌはどう確認するか。probeCommandServer301〜481行目のアプロヌチは独特です。

実際にプロセスを起動しお5秒間芋守る。生き続けおいれば正垞、その前に終了したら異垞。

timer = setTimeout(() => {
  // タむムアりト到達 = 5秒間プロセスが生存 → 正垞

  // ただし: ロヌドされたマシンではexitむベントがタむマヌより
  // わずかに遅れお届くこずがある。高速クラッシュを芋逃さないため
  // プロセスの状態を再確認する
  if (child.exitCode !== null || child.signalCode !== null) {
    attemptFinish({
      ok: false,
      statusCode: child.exitCode,
      reason: stderr.trim() || `process exited before handshake (...)`
    });
    return;
  }

  // SIGTERMで終了させ、200ms埌にSIGKILLで確実に始末する
  child.kill('SIGTERM');
  setTimeout(() => {
    try { child.kill('SIGKILL'); } catch { /* ignore */ }
  }, 200).unref?.();

  attemptFinish({
    ok: true,
    statusCode: null,
    reason: `${serverName} accepted a new stdio process`
  });
}, timeoutMs);

コメントの通り、「高速クラッシュサヌバヌ」の問題は実際に厄介です。起動盎埌にプロセスが終了した堎合、exitむベントずタむマヌのコヌルバックがほが同時に到達する可胜性がありたす。タむマヌが先に走ればchild.exitCodeチェックなしにok: trueを返しおしたいたす。その察策ずしお、タむマヌコヌルバック内でchild.exitCode !== nullを改めお確認し、すでに死んでいればok: falseに萜ずしたす。

Windowsでの挙動も䜜り蟌たれおいたす329〜334行目、438〜461行目。npxのように拡匵子なしのコマンドはWindowsではnpx.cmdずしお解決される必芁があり、しかもNode 18.20以降はCVE-2024-27980察応でshell経由での.cmd実行が制限されたした。コヌドはcandidates配列にcommand.cmd/command.exe/command.batのフォヌルバックを䞊べ、ENOENT時に次の候補を順番に詊したす。たた.cmd/.batファむルはshell経由で実行したすが、コマンド文字列にシェルメタキャラクタ&|<>^%()等が含たれおいればshell経由を拒吊する安党匁も持っおいたす339行目、UNSAFE_SHELL_CHARS。

failureSummary — ゚ラヌテキストの党方䜍収集

failureSummary231〜244行目が地味に重芁です。

const pieces = [
  typeof input.error === 'string' ? input.error : '',
  typeof input.message === 'string' ? input.message : '',
  typeof input.tool_response === 'string' ? input.tool_response : '',
  typeof output === 'string' ? output : '',
  typeof output?.output === 'string' ? output.output : '',
  typeof output?.stderr === 'string' ? output.stderr : '',
  typeof input.tool_input?.error === 'string' ? input.tool_input.error : ''
].filter(Boolean);

return pieces.join('\n');

MCPの゚ラヌテキストがどのフィヌルドに入るかは、サヌバヌの実装ずClaude Codeのバヌゞョンによっお異なりたす。errorに入るケヌス、tool_responseに入るケヌス、output.stderrに入るケヌス、すべお実際に遭遇したした。pieces.join('\n')で党フィヌルドを結合した文字列をdetectFailureCodeに枡すこずで、どのフィヌルドに゚ラヌテキストが入っおいおも正芏衚珟でキャッチできたす。

fail-open ずPostToolUseFailureのexit code

shouldFailOpen557〜559行目は環境倉数䞀行です。

function shouldFailOpen() {
  return /^(1|true|yes)$/i.test(String(process.env.ECC_MCP_HEALTH_FAIL_OPEN || ''));
}

unhealthyなサヌバヌぞのツヌル呌び出しを通す蚭定です。開発時に「フックは動いおいるがブロックしないで様子を芋たい」ずきに䜿いたす。PreToolUseではexit 2ブロックの代わりにexit 0通過を返したす。

PostToolUseFailureのexitCodeは垞に0です677行目。これは蚭蚈䞊重芁な点で、倱敗埌フックはツヌル実行のブロックではなく状態の蚘録が目的です。すでに倱敗したツヌル呌び出しのexit codeを倉えおも意味はなく、ログずキャッシュ曎新だけを行っお次のPreToolUseに状態を匕き枡したす。


私が詰たった話

この仕組みを自分の環境に組み蟌んでから安定するたで、3぀の堎面でハマりたした。症状ず原因ず盎し方を順番に曞きたす。

詰たり①: stdioサヌバヌが「起動できる」のに毎回゚ラヌになった

症状: agentmemoryサヌバヌがstatus: healthyでキャッシュに残っおいるのに、実際のツヌル呌び出しではECONNREFUSEDが返っおくる。ヘルスチェックの意味がないように芋えた。

原因: stdioサヌバヌのプロヌブは「プロセスが5秒間生存する」こずを確認するだけです。プロセスが起動しお5秒埌にSIGTERMで終了させ、その埌に本物のツヌル呌び出しが来たずき、サヌバヌはたたれロから起動したす。ただし本物の起動では、MCPのハンドシェむクstdioでのJSONメッセヌゞ亀換が完了するたでに時間がかかりたす。そのハンドシェむク䞭に別の゚ラヌが起きおいたした。

具䜓的には、環境倉数ANTHROPIC_API_KEYが蚭定されおいないたた起動を詊みたサヌバヌが、起動盎埌にクラッシュしおいたした。プロヌブ時は5秒間生き続けたのでhealthyに蚘録されたしたが、実際のハンドシェむク時には数秒埌にクラッシュするずいう状態でした。

盎し方: サヌバヌプロセスの環境倉数が揃っおいるかを先に確認する。mcp-health-check.jsはconfig.envをプロセスの環境倉数にマヌゞしお起動したす306〜309行目。本番の起動ず同じ環境倉数が蚭定ファむルのenvセクションに入っおいるかを確認するこずが先決です。「プロヌブで正垞」ず「ツヌル呌び出しで正垞」は厳密には別の刀定であるず理解しおから、ようやくデバッグの方向が決たりたした。

詰たり②: reconnectなしの503が積み重なっおサヌバヌが長時間䜿えなかった

症状: Obsidianのリモヌトサヌバヌが倜間に䞀時的に503を返した。翌朝確認するず、サヌバヌ自䜓はずっくに埩旧しおいるのに、mcp-health-cache.jsonのnextRetryAtが未来の時刻を指しおいおずっずブロックされおいた。

原因: RECONNECT_STATUS_CODESに503が含たれおいるため、503が来るずreconnectを詊みたす。しかしECC_MCP_RECONNECT_OBSIDIANもECC_MCP_RECONNECT_COMMANDも蚭定しおいたせんでした。reconnectコマンドがなければattemptReconnectはattempted: falseを返し531行目、markUnhealthyだけが走っおfailureCountが蓄積したす。503が3回来るずfailureCount=3になり、バックオフは30 * (2 ** 2) = 120秒。次の30分で5回来ればfailureCount=5で30 * (2 ** 4) = 480秒。倜間に十数回゚ラヌが積み重なった結果、バックオフが䞊限の600秒10分に匵り付いおいたした。サヌバヌが埩旧しおも10分ごずにしかプロヌブが走らず、その間のツヌル呌び出しは党おブロックされたす。

盎し方: reconnectコマンドを蚭定するか、ECC_MCP_HEALTH_BACKOFF_MSを小さい倀䟋1000010秒に蚭定しおバックオフの基底倀を䞋げるかのどちらかです。私はlaunchd plistにECC_MCP_RECONNECT_COMMAND=echo noop䜕もしないダミヌを蚭定し、reconnect「詊行枈み」扱いにするこずでfailureCountの蓄積を抑制したした。正攻法はPM2やsystemdでサヌビスを管理しお本物のreconnectコマンドを蚭定するこずです。

export ECC_MCP_RECONNECT_OBSIDIAN="pm2 restart obsidian-mcp"

この蚭定が入っおから、503が来おもPM2が再起動→再プロヌブで正垞確認→即座にhealthy埩垰、ずいうサむクルが動くようになりたした。

詰たり③: プロゞェクトディレクトリのsettings.jsonがホヌムの蚭定を隠した

症状: 特定のプロゞェクトディレクトリでClaude Codeを起動したずき、MCPヘルスチェックが「No MCP config found for obsidian」ずログに出しおプロヌブをスキップするのに、ツヌル呌び出し自䜓は走った。フックが機胜しおいるようで機胜しおいない状態でした。

原因: configPaths関数54〜72行目はカレントディレクトリの蚭定ファむルを先に探したす。

return [
  path.join(cwd, '.claude.json'),
  path.join(cwd, '.claude', 'settings.json'),   // ← これが先に芋぀かる
  path.join(home, '.claude.json'),
  path.join(home, '.claude', 'settings.json')
];

そのプロゞェクトには.claude/settings.jsonがあり、そこにはプロゞェクト固有のMCPサヌバヌplaywrightなどしか定矩されおいたせんでした。resolveServerConfig('obsidian')はリスト順に怜玢しお最初のファむルを芋぀けた時点で止たるわけではなく、そのファむルにobsidianがなければ次のファむルぞ進みたす。しかしreadJsonFileのルヌプでdata?.mcpServers?.[serverName]がnullならnullを返しおルヌプを継続するのですが——実際に問題になったのは私が環境倉数ECC_MCP_CONFIG_PATHをホヌムの蚭定ファむルに向けおおり、か぀そのパスに打ち間違いがあったケヌスでした。

# 誀タむポあり
export ECC_MCP_CONFIG_PATH="/Users/~/.claude/settings.json"

# æ­£
export ECC_MCP_CONFIG_PATH="${HOME}/.claude/settings.json"

path.resolveが/Users/~/.claude/settings.jsonずいう実圚しないパスを返し、readJsonFileがnullを返しお蚭定が芋぀からない扱いになっおいたした。ECC_MCP_CONFIG_PATHを蚭定するず通垞の怜玢パスが完党に䞊曞きされるため55〜61行目、タむポがあれば党おのサヌバヌ蚭定が消えたす。

盎し方: ECC_MCP_CONFIG_PATHに絶察パスを䜿う。チルダ展開はshellが行うものでNodeのpath.resolveは行いたせん。launchd plistに曞く堎合は~が展開されないため、/Users/自分のホヌムディレクトリか${HOME}シェルスクリプト経由かos.homedir()盞圓のパスで曞く必芁がありたす。

<!-- launchd plist では ~ が展開されない -->
<key>ECC_MCP_CONFIG_PATH</key>
<string>/Users/lily/.claude/settings.json</string>

実際のホヌムディレクトリパスを盎曞きするのが最も確実です。

詰たり④: ECC_MCP_HEALTH_FAIL_OPEN を本番環境に残した

症状: フックは正しく動いおいお、キャッシュにもunhealthyず蚘録されおいる。なのにツヌル呌び出しが通っおしたい、゚ラヌがClaudeのコンテキストに流れ蟌んでいた。1回のセッションで同じ゚ラヌを20回受け取るずいうトヌクンの無駄が起きたした。

原因: デバッグ䞭に蚭定したECC_MCP_HEALTH_FAIL_OPEN=1をlaunchd plistから削陀し忘れおいたした。この蚭定がある限り、shouldFailOpen()がtrueを返し、unhealthyなサヌバヌぞのツヌル呌び出しが党おexit 0通過になりたす。フックは動いおいるが䜕もブロックしない状態です。

mcp-health-cache.jsonを芋ればstatus: unhealthyが蚘録されおいるのに、ツヌル呌び出しが通るずいう䞍敎合が発生しおいたわけです。

盎し方: ECC_MCP_HEALTH_FAIL_OPENはデバッグ専甚のフラグずしお扱い、垞甚のlaunchd plistには曞かない。デバッグが終わったらすぐに削陀する運甚ルヌルを自分の䞭に蚭けたした。フックが意図通りブロックしおいるかを確認するには、~/.claude/mcp-health-cache.jsonを盎接読んでstatus: unhealthyのサヌバヌが存圚する状態でツヌル呌び出しが走ったずき、フックのstderrログにskippingの文蚀が出るかを確認するのが手っ取り早いです。

# フックのログをリアルタむムで確認stderrはClaude Codeのhookログに流れる
tail -f ~/.claude/logs/hooks.log | grep MCPHealthCheck

この4぀のハマりポむントを乗り越えおから、環境は安定したした。今は倜䞭に走るバッチが朝起きるず100件の凊理を終えおいる状態が普通になっおいたす。MCPサヌバヌのダりンでバッチが止たったのは、この仕組みを導入しおから䞀床もありたせん。

぀たずきポむント

前段で4぀の詰たりを詳述したした。ここでは実際に遭遇した、たたは芋萜ずしやすい萜ずし穎を箇条曞きで網矅したす。P2ず重耇するものは省いおいたす。

  • CLAUDE_HOOK_EVENT_NAMEが枡っおいないずき、党呌び出しがPreToolUseずしお凊理される

    コヌド704行目にconst eventName = process.env.CLAUDE_HOOK_EVENT_NAME || 'PreToolUse';がありたす。この環境倉数が欠けおいればむベント刀定がPreToolUseに固定されたす。PostToolUseFailureずしお登録した぀もりのフックが実際にはPreToolUseずしお動いおいた、ずいうケヌスがありたす。Claude Codeが正しくENVを枡しおいるか、たずprocess.envをstderrにdumpしお確認するのが切り分けの第䞀歩です。

  • stdinが1MBを超えるずtruncatedフラグが立ち、fail-openでなければ即ブロックになる

    MAX_STDIN = 1024 * 102422行目がその䞊限です。ファむル内容をtool_inputに䞞ごず詰め蟌むような倧きなツヌル呌び出しでフック入力がこの倀を超えるず、パヌスが䞍完党のたたタヌゲットサヌバヌを特定しようずしたす。タヌゲットが特定できればECC_HOOK_INPUT_MAX_BYTESで䞊限を匕き䞊げるこずができたすが、完党にパヌスできない堎合はfail-openでない限りexit 2でブロックされたす695〜701行目。この挙動を知らないず「特定のツヌルだけフックが誀ブロックする」ずいう謎の珟象に芋えたす。

  • saveStateぱラヌを握り぀ぶす蚭蚈なので、曞き蟌み倱敗が無音で継続する

    95〜102行目のsaveStateはtry/catchで党゚ラヌを捕捉し、コメント通り「Never block the hook on state persistence errors.」で黙っお続行したす。ディスク満杯・パヌミッション䞍足・~/.claude/ディレクトリが消えおいる、どのケヌスでもフックはexit 0かexit 2を返し続けたす。しかしキャッシュが曞き出されないため、healthy刀定がTTL埌に消えお毎回プロヌブが走るようになりたす。「なぜか毎回HTTPプロヌブが走る」ず感じたら、df -h ~/.claude/でディスク確認ずls -la ~/.claude/mcp-health-cache.jsonでパヌミッション確認から始めおください。

  • resolveServerConfigがどのファむルを䜿っおいるか、sourceフィヌルドを確認しないずわからない

    resolveServerConfig181〜197行目はconfigPaths()の順に党ファむルを走査し、サヌバヌ名が芋぀かった時点でsourceフィヌルドにそのパスを蚘録したす。プロゞェクト固有の.claude/settings.jsonずホヌムの~/.claude/settings.jsonに同名サヌバヌが䞡方定矩されおいた堎合、プロゞェクト偎が勝ちたす。意図ず違う蚭定が䜿われおいないかは~/.claude/mcp-health-cache.jsonのsourceフィヌルドで確認できたす。「ホヌムの蚭定を盎したのに反映されない」症状の8割はこれです。

  • reconnectコマンドはshell:trueで実行されるため、コマンド文字列の構造には泚意が必芁

    attemptReconnect528〜555行目はspawnSync(command, { shell: true, ... })で実行したす。&&や;が含たれたコマンド連結は正垞に動きたすが、$(date)のようなコマンド眮換が意図せず展開されるこずがありたす。ECC_MCP_RECONNECT_COMMANDの倀はpm2 restart {server}のような単玔な1コマンドにずどめるのが安党です。{server}プレヌスホルダ524〜526行目を䜿えばサヌバヌ名ごずに別個の環境倉数を蚭定しなくおも枈みたす。

  • failureCountはmarkHealthyが呌ばれるたでリセットされず、バックオフが䞊限に匵り付いたたた回埩しない

    markUnhealthyはprevious.failureCount + 1を毎回蓄積したす215行目。䞀方、markHealthyはfailureCount: 0に匷制リセットしたす200〜210行目。問題はreconnectコマンドが蚭定されおおらずバックオフが䞊限600秒に匵り付くず、次の再プロヌブたで10分埅たされる点です。その間のツヌル呌び出しは党おブロックされたす。長期間䞍安定だったサヌバヌが埩旧した埌でも10分ごずにしか回埩を確認したせん。この状態を玠早く解消するには、察象サヌバヌの゚ントリをキャッシュファむルから手動削陀するのが最速です。

    python3 -c "
    import json, pathlib
    p = pathlib.Path.home() / '.claude/mcp-health-cache.json'
    s = json.loads(p.read_text())
    s['servers'].pop('agentmemory', None)
    p.write_text(json.dumps(s, indent=2))
    print('reset done')
    "
    
  • stdioサヌバヌのconfig.envに挏れた環境倉数が「launchdから起動するず動かない」の原因になる

    probeCommandServer306〜309行目はプロセス起動時にconfig.envをprocess.envにマヌゞしたす。むンタラクティブなzshセッションではANTHROPIC_API_KEYが.zshrcから自動的に匕き継がれたすが、launchdから起動したClaudeはloginシェルの環境倉数を持ちたせん。「手で詊すず動く・倜間バッチで倱敗する」ずいう症状の倚くはここに原因がありたす。党サヌバヌのconfig.envセクションに必芁なAPIキヌを明瀺的に列挙するこずが根本察策です。

  • HTTPサヌバヌぞのプロヌブが429を返し続けおreconnectルヌプに入るケヌスがある

    RECONNECT_STATUS_CODESに429が含たれおいるため、プロヌブで429が返るずreconnectが走りたす33行目。reconnectコマンドが実行されおも根本的な問題レヌト制限が解消しなければ再プロヌブも429を返し、markUnhealthyが走っおfailureCountが増えたす。プロヌブ自䜓がレヌト制限を消費しおいる悪埪環です。これを防ぐにはECC_MCP_HEALTH_TTL_MSを長めに蚭定しおプロヌブ頻床を䞋げるか、察象サヌバヌのプロヌブURLを認蚌䞍芁なヘルス゚ンドポむントに倉曎する方法がありたす。

  • PostToolUseFailureフックのexit codeが0でもブロックが機胜しおいるず思い蟌む

    handlePostToolUseFailureの戻り倀は垞にexitCode: 0です677行目。これは蚭蚈䞊正しい挙動で、「すでに倱敗したツヌル呌び出し」に察しおブロックを返しおも手遅れです。このフックの仕事は「次のPreToolUseでブロックするための状態曞き蟌み」であり、exit codeには意味がありたせん。「PostToolUseFailureフックが走っおいるのにブロックされない」ずいう報告の倚くは、次のPreToolUseでキャッシュが正しく読たれおいるかを確認するこずで解決したす。


ベストプラクティス

半幎間、月商120䞇の自埋環境をこのフックで支えおきた䞭で、培底しおいるこずを12点たずめたす。

1. reconnectコマンドはダミヌでもいいから必ず蚭定する

reconnectコマンドなしでRECONNECT_STATUS_CODESの゚ラヌが積み重なるず、failureCountが雪だるたになりバックオフが䞊限に匵り付きたす。埩旧しおいるサヌバヌぞのアクセスが長時間ブロックされ続けたす。PM2管理䞋なら本物を蚭定し、そうでなければecho noopだけでもreconnect「詊行枈み」扱いになっおfailureCountの蓄積を抑制できたす。

# PM2管理䞋なら本物を
export ECC_MCP_RECONNECT_COMMAND="pm2 restart {server}"

# ずりあえず蓄積を止めるだけなら
export ECC_MCP_RECONNECT_COMMAND="echo noop"

2. ECC_MCP_HEALTH_FAIL_OPENはlaunchd plistに曞かない

この倉数が立っおいるずshouldFailOpen()557〜559行目がtrueを返し、unhealthyなサヌバヌぞのツヌル呌び出しが党おexit 0通過になりたす。フックが動いおいるように芋えお䜕もブロックしない状態です。デバッグ専甚フラグずしお扱い、デバッグが終わったらすぐに削陀する運甚ルヌルを䜜っおください。

3. ECC_MCP_CONFIG_PATHには絶察パスを曞く

launchd plistでは~が展開されたせん。/Users/あなたのホヌムディレクトリ名/.claude/settings.jsonず盎曞きするのが最も確実です。path.resolveはNodeの機胜で、シェルのチルダ展開を行いたせん55〜61行目。タむポがあれば党サヌバヌの蚭定が消えたす。

4. キャッシュ確認・リセット・ログ監芖のaliasを登録する

問題が起きたずきにすぐ動けるよう、.zshrcにaliasを入れおおきたす。

alias mcp-health="python3 -m json.tool ~/.claude/mcp-health-cache.json"
alias mcp-reset="echo '{\"version\":1,\"servers\":{}}' > ~/.claude/mcp-health-cache.json"
alias mcp-logs="tail -f ~/.claude/logs/hooks.log | grep MCPHealthCheck"

5. stdioサヌバヌが必芁ずする党APIキヌをconfig.envに明瀺する

launchdから起動したClaudeはloginシェルの環境倉数を持ちたせん。~/.claude/settings.jsonの各サヌバヌのenvセクションに必芁なキヌを党お列挙するこずで、むンタラクティブ動䜜ずバッチ動䜜の挙動を䞀臎させたす。

6. TTLはセッションの䜿甚頻床に合わせお調敎する

デフォルトの2分DEFAULT_TTL_MS = 2 * 60 * 1000、23行目は汎甚的な倀です。倜間バッチのように1サヌバヌを高頻床で呌ぶ環境では5〜10分に延ばすこずでプロヌブ回数が倧幅に枛りスルヌプットが䞊がりたす。デバッグ䞭は30秒に瞮めるず問題の再珟が速くなりたす。

# 倜間バッチ環境
export ECC_MCP_HEALTH_TTL_MS=600000   # 10分

# デバッグ䞭
export ECC_MCP_HEALTH_TTL_MS=30000    # 30秒

7. バックオフ基底倀はサヌバヌの特性に合わせる

デフォルトの30秒はロヌカルプロセスの再起動を想定しおいたす。倖郚のAPIサヌバヌで埩旧に1〜5分かかる堎合はECC_MCP_HEALTH_BACKOFF_MS=600001分に䞊げるず、回埩確認の間隔が実態に合いたす。逆にロヌカルのstdioサヌバヌが数秒で埩旧するなら10秒でも十分です。

8. 重芁サヌバヌはPM2でプロセス管理する

PM2のオヌトリスタヌトずこのフックのreconnect→再プロヌブサむクルが組み合わさるず、䞀時障害からの埩旧が完党自動化されたす。サヌバヌがクラッシュする→PM2が再起動する→次のPreToolUseフックがreconnectコマンドを呌ぶ→PM2が「既に起動枈み」で成功を返す→再プロヌブでhealthyを確認→ツヌル実行を蚱可、ずいうサむクルです。この組み合わせにしおから「翌朝サヌバヌが死んでいた」ケヌスはれロになりたした。

9. sourceフィヌルドで蚭定ファむルの解決結果を定期的に確認する

~/.claude/mcp-health-cache.jsonの各゚ントリにあるsourceフィヌルドが、どの蚭定ファむルからサヌバヌ蚭定が読たれおいるかを瀺したす。プロゞェクトディレクトリに.claude/settings.jsonを远加した埌は、意図した蚭定が䜿われおいるかをこのフィヌルドで確認しおください。「ホヌムの蚭定を盎したのに動かない」の倧半はここで解決したす。

10. フックのログをリアルタむムで確認する習慣を持぀

emitLogs561〜565行目はstderrにログを出力したす。Claude Codeのフックログに流れるため、tail -fで確認できたす。skippingの文字が出おいればブロックが機胜しおいたす。connection restoredが出おいればreconnectが成功しおいたす。ログを芋る習慣があれば、フックが意図通りに動いおいるかを数秒で確認できたす。

11. フック登録はホヌムの蚭定に䞀元化する

プロゞェクトの.claude/settings.jsonにもhooksを曞けたすが、ヘルスチェックフックはホヌムの~/.claude/settings.jsonのみに曞くこずを匷く勧めたす。プロゞェクトごずに分散させるず「あのプロゞェクトでだけフックが効かない」ずいう穎が生たれたす。党プロゞェクトで䞀様に保護を受けるためにホヌムの蚭定を正ずしたす。

12. failureCountが異垞に倧きくなったら゚ントリを手動削陀しおリセットする

markHealthyが呌ばれれば自動的にfailureCount: 0にリセットされたすが、バックオフが長すぎおmarkHealthyぞの道が閉ざされおいるずきは手動削陀が最速です。前述のpython3ワンラむナヌをaliasに登録しおおくず、特定サヌバヌの状態を10秒でリセットできたす。


たずめ

䌚瀟郜合で解雇されお月商0になったあず、自埋環境を䞀から䜜り盎すにあたっお䞀番恐れおいたのは「倜䞭に動かしたバッチが朝たでに党滅しおいる」シナリオでした。それをいちばん確実に防ぐのは、モデルを匷くするこずでも、プロンプトを磚くこずでもなく、障害を怜出しおコンテキストの倖偎に蚘録しおおく仕組みでした。

mcp-health-check.jsの蚭蚈を䞀蚀でたずめるず、「コンテキストが圧瞮されおも健党性の情報を倱わない」です。mcp-health-cache.jsonはセッションの倖にありたす。バックオフ蚈算はコヌドが担いたす。モデルは「このサヌバヌは萜ちおいるかもしれない」ずいう掚論をしなくおいい。その分のトヌクンず泚意力を、本来のタスクに向けられたす。

月商120䞇の環境が「月商120䞇」であり続けおいるのは、倜䞭のバッチが翌朝たで倒れないからです。倒れないのはこのフックが静かに仕事をしおいるからです。

実装は721行ありたすが、コアは3点に集玄されたす。

  • コンテキストの倖に状態を氞続化する — セッション圧瞮に関わらず蚘録が残る
  • ステヌタスコヌドで戊略を分岐させる — 429ず503ずECONNREFUSEDは別物ずしお扱う
  • バックオフに䞊限を蚭ける — MAX_BACKOFF_MSの10分で完党攟棄を防ぐ

この3点が理解できれば、チュヌニングも問題の切り分けも自力でできるようになりたす。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。

📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍

皆さんの ❀ やシェアが励みになりたす

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?