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回の結果で語ると、プロセス起動のばらつきに引きずられます。