目次

  1. なぜ2026年のMCPにレート制限が必須なのか
  2. エラーフォーマット — 429とJSON-RPC両対応
  3. サーバー側パターン1: トークンバケット
  4. サーバー側パターン2: スライディングウィンドウ
  5. エージェント側: Full Jitter付き指数バックオフ
  6. サーキットブレーカーとの併用
  7. 監視と運用 — 失敗から学ぶメトリクス
  8. 本番チェックリスト
  9. FAQ

なぜ2026年のMCPにレート制限が必須なのか

MCPサーバーが「ただAPIをラップするだけ」だった2025年と違い、2026年のMCPは 複数エージェントが並列に呼び出す共有インフラ になった。1つのMCPインスタンスにClaude・GPT・Gemini・社内エージェントが同時接続する事例が珍しくなくなり、1台のエージェントの暴走が他エージェントの体験を破壊するリスクが現実化している。

業界レポートでは、ループに陥ったエージェント1台が毎分1,000を超える呼び出しを撃ち込む事例が報告されている。レート制限なしのMCPは、(1) 上流SaaSのクォータを瞬時に食い潰し、(2) クラウドコストが想定の数倍に膨張し、(3) 上流APIから一時的にBANされ全エージェントが停止する、という三重苦に陥る。

MCPの新しい役割 — 2026年5月

MCPサーバーは「エージェントの暴走を吸収する膜」になった。レート制限・タイムアウト・サーキットブレーカーはオプションではなく、共有インフラとしての最低要件。✅ Splunk MCP Server v1.1.0(2026年4月時点でβ提供)もグローバル+ツール単位のレート制限を実装している。

エラーフォーマット — 429とJSON-RPC両対応

レート制限を実装する前に、「どう返すか」の出口を決める必要がある。MCPは Streamable HTTP / stdio / WebSocket を許容するため、トランスポート別に分岐する。

HTTPトランスポート: 429 + Retry-After

業界推奨は「HTTP 429 Too Many Requests」+「Retry-Afterヘッダ」の組み合わせ。Retry-Afterは秒数(整数)かHTTP-date形式を受け取り、エージェント側はこの値に基づいてバックオフを起動する。

HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Per-tool rate limit hit on 'create_invoice'.",
    "scope": "tool",
    "tool": "create_invoice",
    "retry_after_seconds": 12,
    "limit": "10 requests / 60s"
  }
}

stdio / 非HTTPトランスポート: JSON-RPC error

stdio経由のMCPでは、JSON-RPCのerrorオブジェクトにretry_afterを含める。エージェント側ライブラリ(MCP SDK)はこの値を読み取り、自動再試行に渡す。

{
  "jsonrpc": "2.0",
  "id": 42,
  "error": {
    "code": -32029,
    "message": "Rate limit exceeded",
    "data": {
      "scope": "global",
      "retry_after_seconds": 8,
      "limit": "100 requests / 60s",
      "current_usage": 100
    }
  }
}

ポイントは 「scope」 をエージェントに伝えること。グローバル制限なら「ツールを切り替えても無駄」、ツール固有なら「他ツールに切り替えて続行可能」と判断できる。

サーバー側パターン1: トークンバケット

「普段は緩く、急なバーストにも一定範囲で対応したい」場合の標準解。バケット容量がバースト許容量、補充レートが平均レートを表現する。エージェントの自然な使用パターン(数秒間に集中して呼び出し → アイドル)と相性が良い。

// TypeScript / Node.js (簡易版・本番は分散ロック付きRedis推奨)
class TokenBucket {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private capacity: number,    // バースト最大値
    private refillRate: number,  // 1秒あたり補充トークン数
  ) {
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }

  tryConsume(): { ok: boolean; retryAfter?: number } {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(
      this.capacity,
      this.tokens + elapsed * this.refillRate
    );
    this.lastRefill = now;

    if (this.tokens >= 1) {
      this.tokens -= 1;
      return { ok: true };
    }
    const needed = 1 - this.tokens;
    const retryAfter = Math.ceil(needed / this.refillRate);
    return { ok: false, retryAfter };
  }
}

// 使用例: 1分100リクエスト、最大バースト20
const bucket = new TokenBucket(20, 100 / 60);

マルチインスタンス本番では、tokensをRedisの単一キーに置き、SET key value NX PXINCRBYで原子的に更新するのが定石。Cloudflare WorkersならDurable Objects、AWSならElastiCache + Lua scriptが現実解だ。

サーバー側パターン2: スライディングウィンドウ

「直近N秒で必ずM回以下」を厳格に守りたい場合。上流APIのハードリミットを絶対に超えてはいけないユースケース(請求発行・送金など)で使う。

// Redis Sorted Set ベース (擬似コード)
async function checkSlidingWindow(
  key: string,
  windowSeconds: number,
  maxRequests: number,
): Promise<{ ok: boolean; retryAfter?: number }> {
  const now = Date.now();
  const windowStart = now - windowSeconds * 1000;

  // 1. 古いエントリを削除
  await redis.zremrangebyscore(key, 0, windowStart);
  // 2. 現在のカウントを取得
  const count = await redis.zcard(key);

  if (count >= maxRequests) {
    // 最古エントリの再試行可能時刻を計算
    const oldest = await redis.zrange(key, 0, 0, "WITHSCORES");
    const retryAfter = Math.ceil(
      (parseInt(oldest[1]) + windowSeconds * 1000 - now) / 1000
    );
    return { ok: false, retryAfter };
  }

  // 3. 現在のリクエストを追加
  await redis.zadd(key, now, `${now}-${crypto.randomUUID()}`);
  await redis.expire(key, windowSeconds);
  return { ok: true };
}

2026年のMCPでは 「ツール単位はトークンバケット、グローバルはスライディングウィンドウ」のハイブリッド構成 が典型解。前者はバースト対応の柔軟性、後者は上流API保護の厳格性を担う。

エージェント側: Full Jitter付き指数バックオフ

サーバー側で429を返しても、エージェントが愚直に再試行を撃ち続ければ意味がない。エージェント側に ジッター付き指数バックオフ を実装するのが必須だ。

なぜ単純な指数バックオフではダメか

10台のエージェントが同時に429を受けたとする。単純な指数バックオフ(1s, 2s, 4s, 8s...)だと、10台全員が同じスケジュールで再試行するため、ピークが同期して再発する。これが thundering herd問題

Full Jitter方式 — AWS推奨

AWS Architecture Blogが古くから推奨する sleep = random(0, base * 2^attempt) 方式。乱数で揺らぎを入れ、再試行を時間軸に分散させる。

// TypeScript (Anthropic SDK 等にも採用されているパターン)
async function callWithBackoff<T>(
  fn: () => Promise<T>,
  options: { maxRetries?: number; baseMs?: number; capMs?: number } = {}
): Promise<T> {
  const maxRetries = options.maxRetries ?? 5;
  const baseMs = options.baseMs ?? 500;
  const capMs = options.capMs ?? 30_000;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err: any) {
      // 429 か 5xx のみ再試行対象
      const status = err.status ?? err.response?.status;
      if (status !== 429 && (status < 500 || status > 599)) {
        throw err;  // 4xx (except 429) は即失敗
      }
      if (attempt === maxRetries) throw err;

      // サーバー指定 Retry-After を優先
      const retryAfter = err.headers?.["retry-after"];
      let delay: number;
      if (retryAfter) {
        delay = parseInt(retryAfter) * 1000;
      } else {
        // Full Jitter: random(0, base * 2^attempt) を cap で頭打ち
        const exp = Math.min(capMs, baseMs * Math.pow(2, attempt));
        delay = Math.floor(Math.random() * exp);
      }
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error("unreachable");
}
✅ 重要なルール

4xx(429以外)は即座に失敗させる。サーバーが「リクエスト自体が間違っている」と返したエラーを再試行しても成功しない。再試行で意味があるのは 429・5xx・ネットワークタイムアウトのみ

サーキットブレーカーとの併用

レート制限とサーキットブレーカーは守る対象が違う。

仕組み 守る対象 発動条件 主な目的
レート制限 自分(MCPサーバー / 上流API) 単位時間のリクエスト数超過 撃ち込みすぎを防ぐ
サーキットブレーカー 上流API 連続失敗が閾値超え 落ちている相手に追い討ちしない

典型的なサーキットブレーカーは open / half-open / closed の3状態を持つ。MCP本番運用の出発点としては「5xx連続5回でopen、30秒後にhalf-openでサンプル1〜2件試行、成功すればclosed」が現実的。

class CircuitBreaker {
  private state: "closed" | "open" | "half-open" = "closed";
  private failures = 0;
  private openedAt = 0;

  constructor(
    private threshold: number = 5,
    private resetMs: number = 30_000,
  ) {}

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === "open") {
      if (Date.now() - this.openedAt > this.resetMs) {
        this.state = "half-open";
      } else {
        throw new Error("circuit_open");
      }
    }
    try {
      const r = await fn();
      this.failures = 0;
      this.state = "closed";
      return r;
    } catch (err: any) {
      const status = err.status ?? err.response?.status;
      if (status >= 500 && status <= 599) {
        this.failures += 1;
        if (this.failures >= this.threshold) {
          this.state = "open";
          this.openedAt = Date.now();
        }
      }
      throw err;
    }
  }
}

監視と運用 — 失敗から学ぶメトリクス

「何が拒否されているか」が見えなければレート制限は調整不能になる。最低限取るべきメトリクスは以下。

Prometheus + Grafana / Cloudflare Analytics / AWS CloudWatch どれでもよいが、「ツール×エージェントID」で2軸切り出せることが運用の必須条件だ。「freee MCPのcreate_invoiceがエージェントXに対して頻繁に拒否されている」が見えて初めて、設定ミスかエージェントの実装バグかの切り分けができる。

本番チェックリスト

レート制限を本番投入する前の最低限チェックリスト。

225+サービスのレート制限実態をデータで把握する

KanseiLinkは日本SaaSと主要グローバルAPIの実測レート制限・成功率・タイムアウトデータをMCP経由で提供します。「どのSaaSが429を返しがちか」「Retry-Afterを正しく出しているか」を実エージェント挙動から可視化できます。

レート制限データで実装判断を補強する

FAQ

Q1. なぜMCPサーバーにレート制限が必要か?

2026年のMCPは複数エージェントが共有するインフラに進化した。1台のエージェントがループに陥ると毎分1,000リクエストを超える事例があり、放置すれば(1)他エージェントの体験崩壊、(2)コスト急増、(3)上流APIからのBAN、の三重苦になる。

Q2. レート制限エラーはどう返すべきか?

HTTPトランスポートでは「HTTP 429 Too Many Requests + Retry-Afterヘッダ」が業界推奨。stdio/非HTTPではJSON-RPCのerror.dataにretry_after_secondsとscope(globalかtoolか)を入れる。エージェント側はこの値で次の再試行タイミングと戦略を決める。

Q3. なぜジッター(乱数揺らぎ)が必要か?

ジッターなしの指数バックオフは「thundering herd問題」を起こす。複数エージェントが同時に429を受けると、全員が同じスケジュール(1s→2s→4s)で再試行するためピークが同期して再発する。AWS推奨のFull Jitter方式 sleep = random(0, base * 2^attempt) で時間軸に分散させる。

Q4. トークンバケットとスライディングウィンドウ、どちらを使うか?

用途で使い分け。トークンバケットは「普段緩く、バーストにも対応」したい場合。スライディングウィンドウは「直近N秒で必ずM回以下」を厳格に守る必要がある場合。MCPでは「ツール単位はトークンバケット、グローバルはスライディングウィンドウ」のハイブリッドが2026年の典型構成。

Q5. サーキットブレーカーとの違いは?

守る対象が違う。レート制限は「自分が撃ち込みすぎないため」、サーキットブレーカーは「落ちている上流APIに追い討ちしないため」。両方を組み合わせるのが正解で、開発の出発点としては「5xx連続5回でopen、30秒後にhalf-open」が現実的。

Q6. in-memoryのトークンバケットでも本番で使えるか?

単一インスタンスの開発・PoCなら可。本番で複数インスタンスにスケールする場合は、Redis(Durable Objects/ElastiCache)経由の原子操作が必須。in-memoryのままだと、各インスタンスで上限が独立カウントされるため、全体の制限が3倍・5倍に膨らんでしまう。

データ開示・免責事項

本記事に含まれる「ループに陥ったエージェントが毎分1,000リクエスト超」の数値はMintMCP Blog(mintmcp.com/blog/rate-limiting-with-mcp)が公表した業界推定値。MCPレート制限実装パターン(token bucket / sliding window / 429 + Retry-After / JSON-RPC retry-after)はFast.io資料(fast.io/resources/mcp-server-rate-limiting/)、WebScraping.AI FAQ、Splunk MCP Server v1.1.0公式ドキュメント(2026年4月時点β機能)に基づく。Full Jitter方式はAWS Architecture Blog "Exponential Backoff And Jitter"が古典的な参照元。コード例は実装の概念示唆を目的とした擬似コードで、本番投入時はマルチインスタンス整合・タイムゾーン・モニタリング統合を含めた検証が必要。価格・仕様は予告なく変更される可能性があるため、本番運用時は最新の公式ドキュメントをご確認ください。