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

サーバーごとに秒数を書く前に測ったこと — MCP の接続と呼び出しは 486 倍違っていました

2.4.3 で MCP サーバーごとにタイムアウトの秒数を指定できるようになりました。何秒が妥当かを決めるために stdio サーバーを自作して境界別に実測し、単一の値では覆えない理由と、いま設定に書いている値の決め方をまとめます。

Antigravity348MCP21タイムアウト4計測9設計判断個人開発93

プレミアム記事

設定ファイルを開いて、timeout_seconds と打ったところで手が止まりました。

2.4.3 で MCP サーバーごとにタイムアウトの秒数を指定できるようになったので、自分の環境にも入れておこうと思ったのです。ところが、次に打つべき数字が浮かびませんでした。

30 でしょうか。60 でしょうか。

深夜のジョブが無応答のサーバーを待ち続けて朝を迎えた経験があるので、短めにしたい気持ちはあります。一方で、短くしすぎて健全なサーバーを切ってしまえば、朝には別の種類の失敗ログが並びます。

個人開発で回している夜間のジョブには、見ている人がいません。異常に気づくのは、翌朝の私です。だからこの数字は、失敗を防ぐためというより、失敗を早く知るために置くものだと考えています。

どちらの数字にも根拠がありませんでした。根拠のない数字を設定ファイルへ書き込むのは、あとで自分を困らせるだけです。それで、書く前に測ることにしました。

何を測ればいいのか、が最初の壁でした

測ろうと決めたところで、すぐに次の問題にぶつかりました。「MCP サーバーの応答時間」という一つの量が存在しないのです。

エージェントがサーバーに触れる場面は、少なくとも三つに分かれます。

プロセスを起こして初期化を終える接続。使えるツールの一覧を取る問い合わせ。そして実際のツール呼び出し。

この三つを一つの平均値へ潰してしまうと、どの数字も現実のどこにも対応しなくなります。私が知りたいのは平均ではなく、「何秒待ったら異常と判断してよいか」という上限のほうでした。

そこで、境界ごとに分けて測る計測ハーネスを書きました。

計測に使った stdio サーバー

実在のサーバーで測ると、そのサーバー固有の事情が混ざります。起動時に何をしているのか外から分からないため、数字の解釈もできません。

なので、中身が全部見えるサーバーを自分で書きました。JSON-RPC を行区切りで受け、initializetools/listtools/call に応答するだけの最小構成です。

違いを作るのは起動時の作業量だけにしました。環境変数 MCP_WEIGHT で、何もしない light と、指定ディレクトリ配下のファイルを全部読んでハッシュを取る heavy を切り替えます。

#!/usr/bin/env node
// 最小の MCP 風 stdio サーバー(JSON-RPC 2.0 / 行区切り)
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
 
const WEIGHT = process.env.MCP_WEIGHT || 'light';
const ROOT = process.env.MCP_ROOT || process.cwd();
 
// 起動時の初期化。実サーバーが起動時にやりがちな走査を模す
let index = new Map();
function buildIndex(dir, depth) {
  if (depth < 0) return;
  let entries = [];
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return; }
  for (const e of entries) {
    const p = path.join(dir, e.name);
    if (e.isDirectory()) buildIndex(p, depth - 1);
    else {
      try {
        const b = fs.readFileSync(p);
        index.set(p, crypto.createHash('sha1').update(b).digest('hex'));
      } catch (e) {}
    }
  }
}
if (WEIGHT === 'medium') buildIndex(ROOT, 1);
if (WEIGHT === 'heavy') buildIndex(ROOT, 3);
 
const TOOLS = [
  { name: 'hash_file', description: 'ファイルの sha256 を返す' },
  { name: 'count_lines', description: '行数を返す' },
];
 
function handle(req) {
  const { id, method, params } = req;
  if (method === 'initialize') {
    // indexed を返すのが後で効きます(後述)
    return { jsonrpc: '2.0', id, result: {
      protocolVersion: '2026-03-26',
      serverInfo: { name: 'demo', weight: WEIGHT, indexed: index.size } } };
  }
  if (method === 'tools/list') {
    return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
  }
  if (method === 'tools/call') {
    const name = params && params.name;
    const target = (params && params.arguments && params.arguments.path) || __filename;
    if (name === 'hash_file') {
      const b = fs.readFileSync(target);
      return { jsonrpc: '2.0', id, result: { content: [
        { type: 'text', text: crypto.createHash('sha256').update(b).digest('hex') }] } };
    }
    if (name === 'count_lines') {
      const b = fs.readFileSync(target, 'utf8');
      return { jsonrpc: '2.0', id, result: { content: [
        { type: 'text', text: String(b.split('\n').length) }] } };
    }
    return { jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown tool: ' + name } };
  }
  return { jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown method: ' + method } };
}
 
let buf = '';
process.stdin.on('data', (chunk) => {
  buf += chunk;
  let nl;
  while ((nl = buf.indexOf('\n')) >= 0) {
    const line = buf.slice(0, nl); buf = buf.slice(nl + 1);
    if (!line.trim()) continue;
    let res;
    try { res = handle(JSON.parse(line)); }
    catch (e) { res = { jsonrpc: '2.0', id: null, error: { code: -32700, message: String(e) } }; }
    process.stdout.write(JSON.stringify(res) + '\n');
  }
});

クライアント側は Python です。プロセスの起動から initialize の応答が返るまでを接続として一つの数字にまとめ、以降は往復ごとに計ります。

#!/usr/bin/env python3
"""stdio MCP サーバーの境界別レイテンシを実測する。"""
import json, os, subprocess, time
 
HERE = os.path.dirname(os.path.abspath(__file__))
SERVER = os.path.join(HERE, "server.js")
 
def pct(xs, p):
    if not xs:
        return float("nan")
    xs = sorted(xs)
    k = (len(xs) - 1) * p / 100.0
    lo, hi = int(k), min(int(k) + 1, len(xs) - 1)
    return xs[lo] + (xs[hi] - xs[lo]) * (k - lo)
 
class Session:
    def __init__(self, weight, root):
        env = dict(os.environ, MCP_WEIGHT=weight, MCP_ROOT=root)
        t0 = time.perf_counter()
        self.p = subprocess.Popen(
            ["node", SERVER], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
            env=env, text=True, bufsize=1)
        # 起動そのものにかかった時間を接続へ含めます
        self.spawn_ms = (time.perf_counter() - t0) * 1000
        self._id = 0
 
    def call(self, method, params=None):
        self._id += 1
        req = {"jsonrpc": "2.0", "id": self._id, "method": method}
        if params is not None:
            req["params"] = params
        t0 = time.perf_counter()
        self.p.stdin.write(json.dumps(req) + "\n")
        self.p.stdin.flush()
        line = self.p.stdout.readline()
        ms = (time.perf_counter() - t0) * 1000
        if not line:
            # 無応答ではなく終了。区別できないと原因の切り分けで詰まります
            raise RuntimeError("server closed stdout")
        return json.loads(line), ms
 
    def close(self):
        try:
            self.p.stdin.close()
            self.p.wait(timeout=5)
        except Exception:
            self.p.kill()
 
def collect(weight, root, target, sessions, calls):
    conn, listing, call = [], [], []
    indexed = None
    for _ in range(sessions):
        s = Session(weight, root)
        t0 = time.perf_counter()
        res, _ = s.call("initialize")
        conn.append(s.spawn_ms + (time.perf_counter() - t0) * 1000)
        indexed = res["result"]["serverInfo"]["indexed"]
        _, ms = s.call("tools/list")
        listing.append(ms)
        for _ in range(calls):
            _, ms = s.call("tools/call",
                           {"name": "hash_file", "arguments": {"path": target}})
            call.append(ms)
        s.close()
    return {"connect": conn, "list": listing, "call": call, "indexed": indexed}
 
def stats(xs):
    return {"n": len(xs), "p50": round(pct(xs, 50), 3), "p95": round(pct(xs, 95), 3),
            "p99": round(pct(xs, 99), 3), "p999": round(pct(xs, 99.9), 3),
            "max": round(max(xs), 3),
            "max_over_p50": round(max(xs) / pct(xs, 50), 2)}

実行環境は Linux のコンテナで、4 vCPU・メモリ 3.8 GB、Node v22.22.3、Python 3.10.12 です。走査対象には手元にある実ファイル群(MDX 2,002 個・合計 32 MB)を使いました。合成データではなく、日々触っているディレクトリです。

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

この記事の続きを読む

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

この記事で得られること
stdio の MCP サーバーを自作して接続・一覧・呼び出しを別々に測る計測ハーネス(Node + Python・そのまま動く完全版)
接続 127.96 ms に対して呼び出し 0.263 ms。境界間の 486 倍という差が、サーバーの重さの差 3.59 倍を 2 桁上回っていた実測
余裕率を「遅いほう」ではなく「裾の長いほう」に厚く取る根拠と、単一の秒数にした場合の待ち過ぎ 1,034 倍という具体的な代償
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

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

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

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

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

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

関連記事

Antigravity 基本2026-08-02
同じ agent.md を別のリポジトリへ置いたら、静かに別の仕事をしていました — 移植先で能力契約を解決してから走らせる
CLI 1.1.6 で agent.md をファイルとして持ち回れるようになりました。同じ定義を8つのリポジトリへ置き、走らせる前に前提が揃っているかを解決する仕組みを実装し、素朴な確認器との差を実測しています。
連携・プラグイン2026-07-28
MCP を束ねると read_file が 3 つになる — ツール名の衝突を起動前に落とす
複数の MCP サーバーを束ねるとツール名は静かに衝突します。5 サーバー 21 ツールの構成で 43% が衝突していた実測と、起動前に検出して決定的な別名を割り当てる Python プリフライトの設計をまとめました。
連携・プラグイン2026-07-19
無応答の MCP サーバーでエージェントが止まる — 接続・一覧・呼び出しに別々のタイムアウトを敷く
Antigravity CLI 1.1.3 は、無応答の MCP サーバーがエージェントを無限に止める問題を、接続・ツール一覧・ツール呼び出しごとのタイムアウトで塞ぎました。3つの境界で失敗の出方が違う理由を整理し、防御ラッパーとサーキットブレーカー、失敗時だけの通知を、動くコードと夜間運用の実測でまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →