ANTIGRAVITY LABEN
記事一覧/連携・プラグイン
連携・プラグイン/2026-08-20初級

MCP サーバーが1本も起動しない原因は、起動前の 40 行で特定できます

設定ファイルの1件の書き間違いで MCP サーバーが揃って起動しなくなる状況を、起動前に検査する小さなスクリプトで潰す手順です。実際に壊した設定へ通した出力と、JSON の同名キーが黙って消える落とし穴まで扱います。

MCP23Antigravity CLI28設定7トラブルシューティング28Node.js4

朝、Antigravity を開いたらツール一覧が空でした。前日まで動いていた MCP サーバーが、まとめて姿を消しています。

原因は、寝る前に足した最後のエントリで args を配列ではなく文字列で書いたことでした。1件の書き間違いが、無関係な他のサーバーを道連れにしていたわけです。ログを開いて原因の行にたどり着くまで、20分ほど溶かしました。

CLI 1.1.14(8月18日)で、この挙動は改善されています。不正な MCP サーバー設定が混ざっていても、そのエントリだけをログに記録してスキップし、残りは読み込まれるようになりました。ただ、更新が行き渡るまでの環境は道連れの挙動のままですし、設定ファイルが JSON として壊れている場合は、どのバージョンでも全滅します。パーサーが読めなければ、スキップすべき1件がどれなのかも決められないからです。

個人開発で複数のプロジェクトを行き来していると、MCP の設定ファイルは編集の頻度が高く、その分だけ壊す機会も増えます。私自身、この全部止まる状態を数えられる程度には踏みました。

起動してからログを読むより、起動する前に設定を読むほうが速い。そう考えて書いた 40 行ほどのスクリプトを置いておきます。

起動前に読めば足りること

MCP の設定ファイルで実際に踏んだ失敗を並べると、起動前の静的な検査で見つかるものがほとんどでした。

検査項目実際に踏んだ形起動前に分かるか
JSON として読めるか末尾のカンマ、閉じ括弧の抜け分かる
command が PATH 上にあるかバージョン管理ツールを入れ替えて uvx が消えた分かる
args が配列か1個しか渡さないときに文字列で書いた分かる
env の参照先が存在するか環境変数を参照する書き方にしたが、そのシェルには未設定だった分かる
url がスキームを持つかlocalhost:8931/mcp と書いてしまった分かる
サーバーが実際に応答するか起動はしたが握手が返らない分からない

最後の1行だけは、実際に起動しないと確かめられません。逆に言えば、それ以外は起動を待たずに落とせます。検査の目的は「全部を保証すること」ではなく、ログを読む必要がある問題だけを残すことです。

40 行のプリフライトスクリプト

Node.js があれば動きます。設定ファイルのパスを1つ渡すだけです。

#!/usr/bin/env node
import { readFileSync, accessSync, constants } from "node:fs";
import { delimiter, join } from "node:path";
 
const file = process.argv[2];
if (!file) { console.error("usage: node mcp-preflight.mjs <settings.json>"); process.exit(2); }
 
let raw, cfg;
try { raw = readFileSync(file, "utf8"); }
catch (e) { console.error(`FATAL 読み込み失敗: ${e.message}`); process.exit(2); }
try { cfg = JSON.parse(raw); }
catch (e) {
  console.error(`FATAL JSON として読めません: ${e.message}`);
  const m = /position (\d+)/.exec(e.message);
  if (m) console.error(`      → ${raw.slice(0, Number(m[1])).split("\n").length} 行目付近を確認してください`);
  process.exit(2);
}
 
const servers = cfg.mcpServers ?? {};
const names = Object.keys(servers);
if (names.length === 0) { console.error("FATAL mcpServers が空です"); process.exit(2); }
 
function which(cmd) {
  if (cmd.includes("/")) { try { accessSync(cmd, constants.X_OK); return cmd; } catch { return null; } }
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
    try { accessSync(join(dir, cmd), constants.X_OK); return join(dir, cmd); } catch {}
  }
  return null;
}
 
// JSON は同名キーを黙って上書きするため、生テキスト側で重複を数える
const declared = [...raw.matchAll(/^[\t ]*"([A-Za-z0-9_.-]+)"[\t ]*:[\t ]*\{/gm)].map((m) => m[1]);
const dupes = [...new Set(declared.filter((n) => names.includes(n) && declared.filter((x) => x === n).length > 1))];
for (const d of dupes) console.log(`WARN ${d} が複数回宣言されています(最後の1つだけが読まれます)`);
 
let bad = 0;
for (const name of names) {
  const s = servers[name] ?? {};
  const errs = [];
  if (!/^[A-Za-z0-9_-]+$/.test(name)) errs.push("サーバー名に使えない文字が含まれています");
  if (s.url) {
    if (!/^https?:\/\//.test(s.url)) errs.push(`url が http(s) で始まっていません: ${s.url}`);
  } else if (typeof s.command !== "string" || s.command.length === 0) {
    errs.push("command が未設定です(url も無い場合は必須)");
  } else if (!which(s.command)) {
    errs.push(`command が PATH 上に見つかりません: ${s.command}`);
  }
  if (s.args !== undefined && !Array.isArray(s.args)) errs.push("args が配列ではありません");
  for (const [k, v] of Object.entries(s.env ?? {})) {
    if (typeof v !== "string") { errs.push(`env.${k} が文字列ではありません`); continue; }
    const ref = /^\$\{?([A-Z0-9_]+)\}?$/.exec(v);
    if (ref && !process.env[ref[1]]) errs.push(`env.${k} が参照する ${ref[1]} が未設定です`);
    if (v === "") errs.push(`env.${k} が空文字です`);
  }
  if (errs.length) { bad++; console.log(`NG  ${name}`); for (const e of errs) console.log(`      - ${e}`); }
  else console.log(`OK  ${name}`);
}
console.log(`---\n${names.length} 件中 ${names.length - bad} 件が起動可能な設定です`);
process.exit(bad ? 1 : 0);

いくつか、なぜこう書いたかを補足させてください。

which コマンドを呼ばずに PATH を自分で歩いているのは、外部コマンドに依存させたくないからです。CI コンテナのような最小構成のイメージには which が入っていないことがあります。ただし、この実装は Windows では不十分ですnpx.cmd のような拡張子の補完がないため、PATHEXT を回して候補を作る分岐を足してください。

JSON のパースエラーで行番号を自分で数えているのは、位置(文字数)しか返さない処理系への保険です。手元の Node.js 22 では、エラーメッセージ自体に (line 6 column 5) まで含まれていました。新しい処理系なら、この数行は無くても困りません。

終了コードは、パースできない場合が 2、エントリ単位の不備が 1、問題なしが 0 です。シェルから条件分岐させたいので、警告と致命的な失敗を分けています。

わざと壊した設定に通す

5本のサーバーを書いた設定を用意し、そのうち3本にありがちな間違いを仕込んで通してみました。実際の出力です。

OK  filesystem
OK  sqlite
NG  internal-api
      - url が http(s) で始まっていません: localhost:8931/mcp
NG  notes
      - args が配列ではありません
      - env.NOTES_TOKEN が参照する NOTES_TOKEN が未設定です
NG  search
      - env.SEARCH_ENDPOINT が空文字です
---
5 件中 2 件が起動可能な設定です

notes のように、1つのエントリで2件の不備が同時に見つかることがあります。最初の1件で打ち切らず、エントリごとに全部の検査を通してから出力しているのはこのためです。1件直して起動して、また別の1件で落ちる往復が、地味に時間を食います。

末尾にカンマを1つ足したファイルを通すと、こうなりました。

FATAL JSON として読めません: Expected double-quoted property name in JSON at position 156 (line 6 column 5)
      → 6 行目付近を確認してください

エントリ単位の検査までたどり着けないので、ここで打ち切ります。パーサーが読めないファイルに対して「どのサーバーが悪いのか」を推測しても、当たらない答えを出すだけです。

command が見つからないケースは、環境そのものが原因のことが多いので、Antigravity の MCP サーバーが spawn npx ENOENT で起動しないときの原因と対処のほうが具体的です。GUI から起動したときだけ PATH が違う、という話が中心になります。

JSON は同名キーを黙って上書きします

このスクリプトを書いていて、いちばん驚いたのがここでした。

同じサーバー名を2回書いた設定を通したところ、出力はこうなりました。

WARN notes が複数回宣言されています(最後の1つだけが読まれます)
NG  notes
      - command が PATH 上に見つかりません: bunx
---
1 件中 0 件が起動可能な設定です

ファイルには2本書いてあるのに、「1 件中」と表示されています。JSON の仕様上、同じキーが2回現れたら後勝ちで上書きされ、前のほうは何の警告もなく消えます。エラーにはなりません。

これが厄介なのは、症状が「起動しない」ではなく「なぜかそのサーバーだけ古い設定で動いている」という形で出ることです。ログにも何も残りません。設定ファイルをコピー&ペーストで増やしていくと、名前を変え忘れた瞬間に静かに踏みます。

そのため、パース後のオブジェクトではなく、生のテキスト側で宣言の回数を数えています。整形の仕方によっては取りこぼす素朴な正規表現ですが、コピー&ペースト由来の重複はこれで拾えます。

続けられる置き場所にする

検査は、思い出したときに走らせるものにすると続きません。私はこのスクリプトを、次の3か所に置いています。

  1. package.jsonscripts"mcp:check": "node scripts/mcp-preflight.mjs .antigravity/settings.json" として登録する
  2. シェルの関数にして、設定を編集した直後の手癖で叩けるようにする
  3. リポジトリで設定ファイルを共有しているなら、CI の最初のステップに入れる

3番目が効くのは、チームで MCP の設定を共有している場合です。手元では uvx が入っているので通るけれど、同僚の環境では入っていない、という差は、pull request の段階で分かるほうが穏やかです。終了コードを分けてあるので、CI での扱いも素直に書けます。

起動前の検査で残るのは、「設定は正しいのにサーバーが応答しない」という本当に調べる価値のある問題だけになります。そこから先は、どのツールをエージェントに渡すかという設計の話に変わります。渡す範囲を絞る考え方はエージェントに渡すMCPツールを絞る — 最小権限の許可リスト設計にまとめました。読者の方に支えていただいているプレミアム記事です。

まずは今お使いの設定ファイルを、このスクリプトに一度通してみてください。全部 OK で返ってくれば、次に起動が失敗したときはログを読む価値がある、と判断できます。

お読みいただきありがとうございました。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

連携・プラグイン2026-06-01
Antigravity の MCP サーバーが spawn npx ENOENT で起動しないときの原因と対処
Antigravity に MCP サーバーを追加した直後、ログに spawn npx ENOENT と出てサーバーが起動しない症状の原因と直し方をまとめます。設定 JSON は正しいのに GUI からだけ失敗する PATH 非継承の問題に絞った診断手順です。
連携・プラグイン2026-08-04
起動 3 ミリ秒の裏で、最初のターンはツールを 0 本しか見ていませんでした
MCP の非ブロッキングロードで起動待ちは 2,907ms から 3ms になりました。けれど待ちは消えず、最初のターンへ移っていました。待ちの行き先を実測し、必要な分だけ待つレディネスゲートを設計します。
連携・プラグイン2026-07-19
無応答の MCP サーバーでエージェントが止まる — 接続・一覧・呼び出しに別々のタイムアウトを敷く
Antigravity CLI 1.1.3 は、無応答の MCP サーバーがエージェントを無限に止める問題を、接続・ツール一覧・ツール呼び出しごとのタイムアウトで塞ぎました。3つの境界で失敗の出方が違う理由を整理し、防御ラッパーとサーキットブレーカー、失敗時だけの通知を、動くコードと夜間運用の実測でまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →