チュートリアル

Remote MCP Server を本番に載せるには?2026 無状態 MCP + HTTP ロードバランサ + JSON-RPC アーキテクチャ

プロトコルが無状態でも、普通の HTTP ロードバランサを使わなければ得は出ない。本番の Remote MCP が解くのはレプリカの伸ばし方、SSE を代理が溜めないこと、長流を切らない drain であり、新しい Session ではない。

2026-07-28 以降、Remote MCP は initialize と Mcp-Session-Id でクライアントを一台のプロセスに釘付けしない。各 JSON-RPC リクエストは _meta にプロトコル版とクライアント能力を持ち、Streamable HTTP は必要な欄を HTTP ヘッダへ写す。ロードバランサとゲートウェイは body を分解せずに経路、制限、計測ができる。本番で難しいのは tools/call の書き方ではない。レプリカの伸ばし方、均衡方針、nginx が SSE 進捗を溜めないこと、subscriptions/listen を公開時に切らない drain、応用状態の置き場である。プロトコル層は Stateless MCP、誰が呼べるかは OAuth 2.1 で書いた。本稿は「HTTP ロードバランサの後ろにどう載せるか」だけを足す。伝送は Streamable HTTP。封筒は JSON-RPC 2.0。ローカル stdio 子プロセスにこの公網トポロジを被せてはならない。

本番の落とし穴:セッション親和は水平拡張を単点に戻す

「無状態」を聞いてコンテナ一台で済ませる人が多い。得が出るのは、round-robin、least-conn、CPU 加重といった普通の HTTP ロードバランサを使い、セッション親和を使わないときだけだ。旧改定は接続スコープの Session に依存した。握手のあと、tools/call は Mcp-Session-Id を持つプロセスへ戻さねばならなかった。レプリカを増やすと ALB / nginx で cookie 粘着や IP ハッシュが要り、その箱が落ちると Agent セッションごと死ぬ。2026-07-28 はプロトコル Session を外した。任意のレプリカが任意の POST を単独で終えねばならない。

Cursor、Claude Desktop、OpenAI のリモート MCP、自前 Agent Host は、一つの端点へ行儀よく直列には来ない。同じ秒に tools/list、tools/call、長寿命の subscriptions/listen が並ぶ。この三跳びは同じインスタンスに着く必要がない。クライアント IP で親和すれば、水平拡張は単点に戻り、レート制限とカナリアもその箱に縛られる。入口は MCP とは。Agent ループは AI Agent とは。

層の対照:無状態化が外すのはプロトコル Session であり、業務データではない。請求下書き、カート、未完の多 Round ツール呼び出しは Redis か DB に入れ、arguments の draftId で戻す。プロセス内の map を本番状態にしてはならない。認証もこの層ではない。公網 Remote MCP の Bearer は HTTP ヘッダにあり、前稿の OAuth を見よ。本稿は三層が分かれている前提で、トポロジと運用契約だけを書く。

やり方 ロードバランサが見るもの 本番の帰結
cookie / IP 親和 + プロセス内 Session同じクライアントを同じ upstream に貼る拡張が難しい、公開で切れる、単一障害点
無状態レプリカ + 普通の HTTP LB任意のレプリカが任意の JSON-RPC POST を受ける水平拡張、カナリア、ツール単位制限
Serverless コールドスタート + 単発 POST握手を「覚える」常駐プロセスがない短い RPC 向き。長い SSE は別タイムアウト

目標構成:Client → HTTP LB → N 個の無状態レプリカ

トポロジは薄くする。公網 DNS → TLS 終端(ALB、NLB+sidecar、nginx、Caddy、クラウド LB)→ 同じイメージ、同じ設定の MCP レプリカ群。プロトコルを補う「MCP Session Store」を前段に置いてはならない。ヘルスチェックは独立した GET /healthz。プロセスが生き、依存に届くことだけを答える。/mcp へ空 body の POST をするな。生きた tools/list をプローブにするな。本物の登録表に当たり、401 でレプリカを誤って外す。

各レプリカは一跳びを単独で終える。Origin を検証(DNS リバインディングなら 403)、MCP-Protocol-Version / Mcp-Method / Mcp-Name を読み、必要なら Bearer を検証し、JSON-RPC 封筒を解析し、ツールを実行し、単一 JSON かリクエストスコープの SSE で返す。仕様は POST だけを受ける単一 MCP 端点を求める。例は https://mcp.example.com/mcp。GET 流とプロトコル Session は 2026-07-28 で消えた。「古い探活のため」に GET /sse を再開するな。

レプリカが共有するのは応用依存である。データベース、オブジェクトストレージ、第三者 API、任意の Redis。「今開いている MCP 接続」は共有しない。Cloud Run、Cloud Functions、Knative のようなリクエスト単位伸縮は、この模型に合う。残る調整は長い SSE のアイドルタイムアウトであり、Session 粘着ではない。プロセスは非特権ユーザで反代の後ろに置く。MCP を 0.0.0.0:80 で公網に直接聞かせてはならない。

JSON-RPC 2.0 がロードバランサをどう横切るか

MCP は JSON-RPC 2.0 でメッセージを符号化し、UTF-8 必須である。Streamable HTTP では、クライアントが出す各リクエストまたは通知が新しい HTTP POST であり、サーバは JSON-RPC リクエストを始めない。ロードバランサは method の業務意味を知る必要がない。バイトを転送するだけだ。2026 の伝送は method を Mcp-Method へ、ツール/資源/プロンプト名を Mcp-Name へ写す。中間装置は body を分解せずにツール制限、方法分割、版カナリアができる。

body が真実である。ヘッダの MCP-Protocol-Version は params._meta.io.modelcontextprotocol/protocolVersion とバイト一致しなければならない。違えばサーバは 400 と HeaderMismatch を返す。クライアントは Accept: application/json, text/event-stream も付ける。通知 POST が受理されれば 202 Accepted、body なし。リクエストは JSON オブジェクト一つ、または SSE。JSON-RPC の id はこの跳びの要求と応答を揃えるだけだ。セッション番号ではない。レプリカ間で id から「文脈を取り戻す」な。

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 42,
  "error": {
    "code": -32600,
    "message": "HeaderMismatch: MCP-Protocol-Version does not match params._meta"
  }
}

下が本番形の tools/call である。認証は HTTP ヘッダ、封筒は body、ゲートウェイが見る経路鍵もヘッダにある。Access Token を params や _meta に埋めるのは 2026 の Remote MCP ではない。引数の形はなお Schema 検証が要る。MCP と JSON Schema を見よ。

POST /mcp HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: searchInvoices
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "searchInvoices",
    "arguments": {
      "startDate": "2026-01-01",
      "endDate": "2026-01-31",
      "status": "paid"
    },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {},
      "io.modelcontextprotocol/clientInfo": {
        "name": "jsonvue-demo",
        "version": "1.0.0"
      }
    }
  }
}

Streamable HTTP:緩衝、タイムアウト、SSE

短いツール呼び出しは Content-Type: application/json を返すべきだ。長い仕事は text/event-stream でもよい。先にこの要求に関する notifications/progress を押し、最後の JSON-RPC 応答で流を閉じる。本番事故の大半は MCP SDK ではなく逆プロキシにある。nginx の既定 proxy_buffering は進捗イベントを溜めて一塊で流す。Agent は固まったように見える。仕様は仲介者向けに X-Accel-Buffering: no を応答へ付けるよう勧める。nginx proxy_buffering を見よ。

Streamable HTTP の取消し信号は、クライアントがその SSE を閉じることである。追加の notifications/cancelled POST ではない(それは stdio 束のやり方だ)。だから LB はバックエンド切断をクライアントへ、クライアント切断をレプリカへ、そのまま伝え、worker が手を止める必要がある。途中に「POST を再試行する」ゲートウェイを置くな。JSON-RPC 要求は既定で冪等ではなく、tools/call はすでに DB へ書いていることがある。

subscriptions/listen は別種の長流である。応答流は開いたまま、tools/list_changed のような変更を運ぶ。ある呼び出しの progress ではない。仕様は、コロンで始まる SSE コメント行を定期的に出し、アイドルの仲介者が切らないよう勧める。Last-Event-ID による再開可能 SSE は無い。LB の idle / read timeout は keep-alive 間隔より長くする。Cloudflare、ALB、nginx の既定 60 秒では足りないことが多い。下は最小の逆プロキシ草案であり、安全基準ではない。TLS、制限、WAF は別に組む。

upstream mcp_replicas {
  least_conn;
  server 10.0.1.11:8080;
  server 10.0.1.12:8080;
  server 10.0.1.13:8080;
}

server {
  listen 443 ssl;
  server_name mcp.example.com;

  location /healthz {
    proxy_pass http://mcp_replicas;
    proxy_connect_timeout 2s;
    proxy_read_timeout 3s;
  }

  location /mcp {
    proxy_pass http://mcp_replicas;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
  }
}

ローリング公開、drain、subscriptions/listen

無状態レプリカは公開を簡単にする。SSE はなお進行中の HTTP 要求である。順はこうだ。インスタンスをターゲットグループから外す → 新しい /mcp を止める → 開いている JSON 応答と SSE が終わるか、公表した drain 期限まで待つ → それから worker に SIGTERM。progress を押しているプロセスへ SIGKILL するな。ヘルスが緑になってから戻す。

新版は旧い要求形をなお扱えねばならない。「先に握手を上げ、それから流量を切る」プロトコル段階はない。カナリアは POST の何パーセントを新しいイメージへ送るかだ。MCP-Protocol-Version を経路鍵にしてよい。2026-07-28 を宣言したクライアントだけ新プールへ入れ、旧クライアントは互換プールに残す。版が合わなければ 400 と UnsupportedProtocolVersionError。黙って下げてツールを続けてはならない。

公開窓では subscriptions/listen はほぼ切れる。クライアントは listen を開き直し、イベントが残ったと思ってはならない。サーバは未配送の list_changed をプロセスメモリに積むな。確実配送が要るなら外部キューへ書く。listen は購読口に過ぎない。MRTR(多 Round 入力)も独立した POST である。中間結果は共有ストレージへ置き、次の跳びは別レプリカへ落ちてよい。

ゲートウェイ制限、認証、観測、JSONVue

ゲートウェイが Mcp-Method と Mcp-Name を見るなら、QPS はソース IP だけでなくツール単位で置け。高いツール(書き込み、支払い、長い SQL)は別枠。tools/list は緩くてよい。公網では Origin を検証し、OAuth 2.1 の Resource Server として動かす。401、Protected Resource Metadata、毎ホップの Bearer。詳細は MCP OAuth 2.1。認証は伝送、制限はゲートウェイ、Schema は業務。三つを一つのミドルウェアに混ぜるな。

観測欄 出所 使い道
MCP-Protocol-Version / Mcp-Method / Mcp-Name要求ヘッダ(body の _meta と揃える)ツール制限、カナリア、ダッシュボード
JSON-RPC idこの跳びの封筒クライアント再試行とレプリカログを結ぶ
HTTP 状態 + JSON-RPC error.code伝送層と方法層401 / HeaderMismatch / 業務 error を分ける

アクセスログには上の三組を少なくとも残す。レプリカログには jsonrpc が 2.0 か、副作用の前にツールが走ったかを足す。ヘッダと body の不一致、欠けた Accept、不正な Origin はゲートウェイかレプリカ入口で止め、ツール関数まで落とすな。失敗サンプルを一つずつ残す。HeaderMismatch、401、欠けた arguments、緩衝された SSE(クライアントが最後の一塊だけを見た)。

ブラウザで結合できる:JSON 整形で封筒が parse できるか見、JSON Schema 検証で params.arguments を確かめ、JSON Diffで二つの失敗応答を比べ、JWT デコードで公網の Bearer aud を見る。データはこの機械を出ない。

続き:MCP とは、Stateless MCP、OAuth 2.1、MCP と JSON Schema、A2A vs MCP。

よくある質問 FAQ

本番で sticky session はまだ要るか?

2026-07-28 のプロトコル層では要らない。sticky は「まだ Session がある」と錯覚させるだけだ。応用が同じ地域やデータ分片に張り付くなら、Mcp-Param-* か arguments のテナント鍵で応用層経路を取れ。cookie 親和で MCP プロセスに貼るな。

ロードバランサのヘルスに GET /mcp を使ってよいか?

使うな。現代の MCP 端点は POST だけを受け、GET 流は消えた。/mcp への行き当たり GET は 405 か、古い互換ロジックを誤らせる。プローブは独立した /healthz。プロセスと依存だけを見、ツールは走らせない。

SSE が切れたら Last-Event-ID で続けるべきか?

仕様は再開可能 SSE を支えない。クライアントは対応する要求を開き直す(listen なら新しい subscriptions/listen。長いツールなら業務が冪等なときだけ再試行)。keep-alive のコメント行と緩衝しないプロキシの方が、自前の event id キャッシュより契約に合う。

ローカル stdio MCP もロードバランサの後ろに置くか?

置くな。stdio はクライアントが起動した子プロセスであり、バイトは標準入出力に乗る。HTTP hop はない。負荷分散、Origin 検証、Bearer、X-Accel-Buffering は Streamable HTTP / Remote の話だ。同じツールが二つの伝送を出してよい。本番トポロジは HTTP 面にだけ被せる。

まとめと次の一歩

Remote MCP の本番構成は一文で書ける。無状態の JSON-RPC 要求が普通の HTTP ロードバランサを横切り、同じレプリカのどれかへ着く。長流はリクエストスコープで開き、接続スコープでクライアントを覚えない。ヘッダはゲートウェイ用、body が真実、応用状態は外部ストレージへ。

載せる順:まず任意のレプリカが単独で tools/call を終える。次に緩衝を閉じ、タイムアウトを伸ばし、drain を足す。最後にツール単位制限とカナリア。成功封筒、HeaderMismatch、401 を JSONVue でこの機械に残す。プロトコル意味は Stateless MCP。誰が呼ぶかは OAuth 2.1。