◉ANTIGRAVITY LABEN
記事一覧/連携・プラグイン
⬡ 連携・プラグイン/2026-08-04上級

起動 3 ミリ秒の裏で、最初のターンはツールを 0 本しか見ていませんでした

MCP の非ブロッキングロードで起動待ちは 2,907ms から 3ms になりました。けれど待ちは消えず、最初のターンへ移っていました。待ちの行き先を実測し、必要な分だけ待つレディネスゲートを設計します。

MCP29Antigravity CLI42起動時間レディネス設計14

✦ プレミアム記事

Antigravity CLI 1.1.9 の変更点に、対話起動時の MCP ロードが非ブロッキングになった、という一行があります。

起動が速くなる。それだけの話だと思っておりました。

手元の構成を上げ直して、体感で明らかに速くなったことを確認しました。そこまでは期待どおりです。

引っかかったのはその直後でした。起動してすぐ、いつもの調子で「このディレクトリを検索して」と打ったところ、エージェントが検索ツールを使わずに、自前で推測した答えを返してきたのです。

もう一度同じことを打つと、今度はきちんと検索ツールを呼びました。

同じプロンプト、同じ設定、違う結果。差は打鍵のタイミングだけでした。

起動の速さしか見ておらず、最初のターンに何が起きるかまでは考えていなかったのだと気づいたのは、同じ質問を二度打ったあとでした。

個人開発で回している環境なので、MCP サーバーは必要になるたびに自分で足してきたものばかりです。何本繋がっていて、どれが重いのかは把握しているつもりでおりました。

待ちは消えたのか、移動しただけなのか

非ブロッキングロードは、起動処理からサーバー接続を外して裏に回す変更です。表側の待ちは減ります。

けれど、サーバーが応答を返すのにかかる時間そのものが短くなったわけではありません。ハンドシェイクの実コストは変わらず存在します。

だとすると、その時間はどこへ行ったのか。

体感で語るには曖昧すぎる話です。計測できる形に落としました。

MCP サーバーは stdio 上の JSON-RPC で喋ります。ならば、起動コストと初期化コストを指定できる最小のサーバーを書いて、接続の組み立て方だけを差し替えれば、待ちの行き先は測れます。

計測ハーネスを組む

まずモックサーバーです。プロセス起動(インポートやランタイム立ち上げ)のコストと、initialize に対する応答遅延を別々に指定できるようにしました。この2つは実運用でも性質が違うためです。

#!/usr/bin/env python3
"""stdio 上の最小 MCP 風サーバー。改行区切りの JSON-RPC。
実サーバーのコールドスタートを模す: 起動コスト -> initialize コスト。
usage: mock_server.py <name> <boot_ms> <init_ms> <tool_ms>
"""
import sys, json, time
 
name, boot_ms, init_ms, tool_ms = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
time.sleep(boot_ms / 1000.0)  # プロセス起動 / モジュールインポートのコスト
 
for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    req = json.loads(line)
    m = req.get("method")
    if m == "initialize":
        time.sleep(init_ms / 1000.0)
        res = {"protocolVersion": "2025-06-18", "serverInfo": {"name": name}}
    elif m == "tools/list":
        res = {"tools": [{"name": f"{name}_query"}]}
    elif m == "tools/call":
        time.sleep(tool_ms / 1000.0)
        res = {"content": [{"type": "text", "text": "ok"}]}
    else:
        res = {}
    sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": req.get("id"), "result": res}) + "\n")
    sys.stdout.flush()

次にハーネス側です。測る地点を3つに分けました。ここを分けないと、そもそも「待ちが移動した」という現象が観測できません。

  • t_prompt — 利用者が最初のターンを打てるようになるまで
  • t_ready — すべてのサーバーが initialize と tools/list を終えるまで
  • t_first — 最初のツール呼び出しが返るまで

サーバー構成は、自分の mcp_config.json の顔ぶれに寄せました。速いものが数本、重いものが1〜2本、という現実的な偏りを持たせています。

#!/usr/bin/env python3
import json, subprocess, sys, threading, time
 
SERVERS = [
    # (name, boot_ms, init_ms, tool_ms)
    ("fs",       120,  40,  15),
    ("github",   180, 320,  60),
    ("db",       150, 210,  35),
    ("search",   140, 890,  45),   # 重い1本
    ("chrome",   260, 480, 110),
]
 
def spawn(spec):
    n, b, i, t = spec
    return subprocess.Popen(
        [sys.executable, "-u", "mock_server.py", n, str(b), str(i), str(t)],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)
 
def rpc(p, method, _id):
    p.stdin.write(json.dumps({"jsonrpc": "2.0", "id": _id, "method": method}) + "\n")
    p.stdin.flush()
    return json.loads(p.stdout.readline())
 
def handshake(spec, procs, done, lock):
    p = spawn(spec)
    rpc(p, "initialize", 1)
    rpc(p, "tools/list", 2)
    with lock:
        procs[spec[0]] = p
        done.append((spec[0], time.perf_counter()))
 
def run(strategy):
    t0 = time.perf_counter()
    procs, done, lock = {}, [], threading.Lock()
 
    if strategy == "blocking":
        for s in SERVERS:
            handshake(s, procs, done, lock)
        t_prompt = time.perf_counter() - t0
    else:
        ths = [threading.Thread(target=handshake, args=(s, procs, done, lock)) for s in SERVERS]
        for t in ths: t.start()
        t_prompt = time.perf_counter() - t0     # 入力欄は即座に使える
        for t in ths: t.join()
 
    t_ready = time.perf_counter() - t0
 
    # 最初のツール呼び出しは最も遅いサーバーへ向ける(現実的な最悪ケース)
    t_call0 = time.perf_counter()
    rpc(procs["search"], "tools/call", 3)
    t_first = time.perf_counter() - t0
    call_wall = time.perf_counter() - t_call0
 
    for p in procs.values():
        p.stdin.close(); p.terminate()
    return dict(strategy=strategy, t_prompt=t_prompt, t_ready=t_ready,
                t_first=t_first, call_wall=call_wall)
 
if __name__ == "__main__":
    for s in ("blocking", "nonblocking"):
        rows = [run(s) for _ in range(3)]
        med = lambda k: sorted(r[k] for r in rows)[1]
        print(f"{s:12s} t_prompt={med('t_prompt')*1000:7.1f}ms  "
              f"t_ready={med('t_ready')*1000:7.1f}ms  "
              f"t_first={med('t_first')*1000:7.1f}ms  "
              f"call_wall={med('call_wall')*1000:6.1f}ms")

3回走らせて中央値を取っています。1回の結果で語ると、プロセス起動のばらつきに引きずられます。

✦

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

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦ブロッキングと非ブロッキングの待ち時間を3つの地点で実測し、待ちが消えたのではなく移動していることを数字で示します
✦「起動が速いほど最初のターンのツールカタログが空になる」という取り違えやすい関係を、到達時刻別の可視率で明らかにします
✦全サーバーではなく必要集合だけを待つレディネスゲートの実装(デッドライン付き・失敗時も待たせない)を丸ごと掲載します
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

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

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

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

関連記事

⬡ 連携・プラグイン2026-10-22
プロジェクト直下の mcp_config.json が読まれているのに MCP が立ち上がらない — プローブ 1 本で確かめる
Antigravity CLI のログには project-local の設定ファイルを使うと出るのに、MCP サーバーは起動しない。公式リポジトリの Issue 報告をもとに、ログの文言が指すものと、起動の有無を自分で確かめる最小のプローブサーバーをまとめます。
⬡ 連携・プラグイン2026-10-19
Antigravity CLI に GitHub MCP を繋ぐ最初の 1 本 — 書き換えるのは url の 1 語だけでした
Antigravity CLI で GitHub MCP を使うには、設定の url を serverUrl に書き換える必要があります。公式の MCP ドキュメントを根拠に、リモート接続と Docker 接続の書き方、読み込む前の検査スクリプト、トークンの置き場所と切り分けの順番をまとめます。
⬡ 連携・プラグイン2026-10-10
Figma MCP と Stitch MCP、どちらを繋ぐか — 「画面がすでにあるか」で決める線引き
画面をコードに起こすとき、Figma MCP と Stitch MCP のどちらを Antigravity に繋ぐか。判断の軸は「正解の画面がすでにあるか」の一点です。両方を常時つなぐと何が起きるか、切り替えの手順、迷ったときの小さな判定スクリプトまでをまとめます。