1. はじめに / この記事でやること#
MCP(Model Context Protocol)サーバーは増えてきましたが、複数をまとめたり、認証・可観測性を一箇所で挟んだりする「ゲートウェイ」を自分で動かしたことはありませんでした。
そこで agentgateway を Docker で最小構成から立て、認証不要のリモート MCP(Langfuse docs MCP)を1つ中継させて、組み込み UI の Playground からツールを実行するところまでを試しました。

やってみると、UI が開けない問題と Playground の初期化エラーの2つに引っかかりました。原因と対処は6章にまとめています。
2. そもそも agentgateway とは / MCPゲートウェイの位置づけ#
agentgateway は、AIエージェントと、その接続先(MCPサーバー・他エージェント・LLM)の間に立つプロキシです。Solo.io 発で、現在は Linux Foundation ホストの OSS(Apache 2.0)。データプレーンは Rust 製です。
機能としては MCPゲートウェイ(今回触るのはこれ)、A2Aゲートウェイ(エージェント間通信)、LLMゲートウェイ(OpenAI互換API・フェイルオーバー・レート制限など)の3つを持ちます。
Solo の製品ファミリの中では、中継役のデータプレーンが agentgateway、ツールを一元管理するカタログが agentregistry、という役割分担になっています。周辺には kagent / Istio / kgateway もいます。
今回はライセンス不要の OSS standalone を使います。Solo Enterprise 版のクイックスタート(AgentgatewayPolicy などの CRD、Helm、license key)とは別物で、そちらの手順は使いません。OSS のデータプレーン本体はキー不要で動きます。
今回中継する https://langfuse.com/api/mcp は、認証不要で Langfuse のドキュメントを検索・取得できる Langfuse Docs MCP です。Langfuse には認証付きでプロジェクトのトレースなどを扱う Cloud MCP もありますが、別物です。本記事で「Langfuse MCP」と書いているのは前者を指します。詳細は公式 Introduction
を参照してください。
3. なぜMCPに専用ゲートウェイが必要なのか#
手を動かす前に、なぜ MCP に専用ゲートウェイという話が出るのかを整理しておきます。
従来の API ゲートウェイや Envoy 系は、短命な HTTP リクエストをさばくのに最適化されています。リクエストが来て、処理して、返して終わり。
MCP はそこに、1往復では終わらない要素が入ってきます。まず JSON-RPC ベースで、初期化・能力交渉・セッション管理を含むプロトコルです。HTTP では各メッセージが POST で送られますが、initialize で確立したセッション(Mcp-Session-Id)を後続のリクエストが引き継ぐ前提で設計されています。加えて、サーバーからクライアントへ非同期にメッセージを送る経路(SSE によるサーバー起点イベント)も想定されています。SSE やセッションIDが実際にどう使われるかは実装次第です。
誤解しやすいのは、Envoy や一般的な API ゲートウェイでも長寿命接続・HTTP/2・ストリーミング自体は扱える点です。違いは中身を読めるかどうかにあります。従来のゲートウェイは MCP の通信を「ただの HTTP リクエスト」としてしか扱えず、「どのツールが呼ばれたか」を見てツールごとに許可やログを分ける、といったことができません。そこまで踏み込めるのが MCP 向けゲートウェイ、というわけです。
flowchart TB
subgraph A["従来型:短命リクエストをさばく(API GW / Envoy 系)"]
direction LR
ca["Client"] -->|"リクエスト①(独立・短命)"| ga["Gateway"] -->|"さばいて返す"| sa["Server"]
ca -->|"リクエスト②(①とは無関係)"| ga
end
subgraph B["MCP:初期化とセッション管理が入る"]
direction LR
cb["Client"] <==>|"Mcp-Session-Id を引き継ぐ"| gb["agentgateway"] <==> sb["MCP Server"]
sb -.->|"サーバー起点イベント / SSE"| cb
end
図:従来型は短命リクエストを1回さばいて終わり。MCP では initialize で確立したセッション(Mcp-Session-Id)を後続リクエストが引き継ぎ、サーバーからのイベント経路も想定される。
ゲートウェイを挟むと、複数の MCP サーバーを1エンドポイントに束ねられる(federation / multiplex)、認証・認可・可観測性を1箇所で効かせられる、といった利点も出てきます。
このあとの検証では、initialize の応答で Mcp-Session-Id が返るところが実際に見えます。上の説明はそこで確認できます。
4. アーキテクチャ / 設定の考え方#
agentgateway の設定は、Bind / Listener / Route / Backend の順に読むと追いやすいです。
| 階層 | 役割 |
|---|---|
| Bind | リッスンする TCP ポート |
| Listener | ホスト名 / プロトコル(HTTP・MCP・A2A)/ TLS |
| Route | マッチ条件とポリシー |
| Backend | 上流ターゲット(MCPサーバー / A2Aエージェント / HTTPサービス / LLM) |
今回書いた config.yaml をこの4階層に対応づけると、こうなります。
# yaml-language-server: $schema=https://agentgateway.dev/schema/config
config:
adminAddr: 0.0.0.0:15000 # ← UIをホストに露出(詳細は6章のハマりどころ)
binds:
- port: 3001 # ← Bind: リッスンするポート
listeners:
- routes: # ← Listener / Route
- policies:
cors: # ← Route のポリシー(ブラウザからの接続用にCORS許可)
allowOrigins: ["*"]
allowHeaders: [mcp-protocol-version, content-type, cache-control, mcp-session-id]
exposeHeaders: ["Mcp-Session-Id"]
backends:
- mcp: # ← Backend: MCP種別
targets:
- name: langfuse-docs
mcp:
host: https://langfuse.com/api/mcp # ← リモート Streamable HTTP の MCP今回の Backend はリモートの Streamable HTTP MCP(Langfuse docs MCP)です。ローカルで別プロセスを起動する stdio 型ではなく、既に存在するリモート HTTP エンドポイントに中継します。ここが公式サンプル(npx で stdio 起動する例)との違いです。
なお、上は4階層を素直に表す binds 形式(config ファイルで書く legacy スタイル)ですが、後述するように UI の Playground を使うとトップレベルの mcp: セクション(新形式)に落ち着きます(6章②)。最終的に動かした config はそちらなので、再現するなら6章の最終 config を使うのが早いです。
5. 実際に触ってみる(ハンズオン本体)#
5-1. 環境準備#
mkdir -p ~/agentgateway-lab && cd ~/agentgateway-lab
docker version # 筆者環境: Client 29.1.4-rd / Server 29.1.3 (Rancher Desktop の dockerd)5-2. 設定ファイル作成#
まず公式の basic 設定を落として構造を把握します(これは stdio(npx) 版のサンプルです)。
curl -L https://agentgateway.dev/examples/basic/config.yaml -o config.basic.yamlこれを土台に、リモート MCP 向けの config.yaml を作ります(全文は4章のとおり)。リモート MCP の target は mcp.host にエンドポイント URL を書くだけです。キー名はバージョンで変わることがあるので、迷ったら公式の JSON スキーマ
で確認してください。ただし UI Playground から試す場合は、後述のとおり mcp.port の公開と CORS 設定も必要になります(→ 6章②)。
5-3. Docker で起動#
docker run -d --name agentgateway-lab \
-p 127.0.0.1:15000:15000 -p 127.0.0.1:3001:3001 \
-v "$HOME/agentgateway-lab/config.yaml:/config.yaml" \
cr.agentgateway.dev/agentgateway:1.3.1 --file /config.yamlポート公開はホストの 127.0.0.1 に絞り、LAN の他端末へ露出しないようにしています(意図して全公開する場合だけ -p 15000:15000 にします)。
本記事の検証は v1.3.1(イメージ digest sha256:c3ce7b75…)です。再現性のためタグを固定しています。最新版や別タグは GitHub リリース
で確認して差し替えてください。
起動ログは以下です。
state_manager loaded config from File("/config.yaml")
app serving UI at http://0.0.0.0:15000/ui
proxy::gateway started bind bind="bind/3001"5-4. UI / Playground で MCP に接続する#
- ブラウザで
http://localhost:15000/uiを開きます。Gateway Overview が表示されます。この時点では MCP は “Not enabled”、config で入れた MCP は Routes に “legacy MCP backend” として並んでいます(UI 管理の新形式とは別です)。


MCP の「Get started」→「Enable MCP」を押します。UI が config.yaml を自動更新し、トップレベルに
mcp:セクションが追加されます。「MCP Servers」→「Add server」を開きます。Transport = Streamable HTTP、URL =
https://langfuse.com/api/mcp、name =langfuse-docsで保存します。一覧に State: ready で並びます。

- 「Tool Playground」を開き、「Initialize」でセッションを張ります。“3 tools discovered” となり、Session ID が発行されます。

5-5. ツール実行#
searchLangfuseDocs を選び、query に How do I set up tracing in Python? を入れて「Call tool」を押します。Result カードに HTTP 200 と、Langfuse docs 由来の応答(LangChain Tracing & LangGraph Integration など)が返ってきました。

CLI でも同じ流れを確認できます。MCP のライフサイクルでは、initialize の成功後にクライアントが notifications/initialized を送り、以降のリクエストでは initialize で得た Mcp-Session-Id と MCP-Protocol-Version ヘッダを引き継ぎます。これを渡さないとセッションが成立しません。
# 1) initialize してレスポンスヘッダの mcp-session-id を控える
curl -sS -X POST http://localhost:3001/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' -D -
# 2) 初期化完了を通知(同じ Mcp-Session-Id / MCP-Protocol-Version を付ける)
curl -sS -X POST http://localhost:3001/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: <1で返った値>' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# 3) 後続リクエスト(tools/list)も同じヘッダを引き継ぐ
curl -sS -X POST http://localhost:3001/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Session-Id: <1で返った値>' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'返ってくる tools/list の3ツールは次のとおりです。
searchLangfuseDocs— Langfuse docs 全体へのセマンティック検索(RAG)getLangfuseDocsPage— 指定パスの docs ページの生 Markdown を取得getLangfuseOverview— 高レベルの機械可読インデックスを取得
ここまでで、Langfuse Docs MCP への中継と、ゲートウェイ越しのツール実行を確認できました。
6. ハマったところ / トラブルシュート#
今回詰まったのは主に2点です。
① UIが開かない(HTTP 000)→ adminAddr で解決#
-p 127.0.0.1:15000:15000 で公開したのに、http://localhost:15000/ui が HTTP 000(接続失敗)でした。
$ curl -o /dev/null -w "HTTP %{http_code}\n" http://localhost:15000/ui
HTTP 000ログを見ると原因が分かりました。
listener established address=127.0.0.1:15000 component="admin"UI/admin がコンテナ内の 127.0.0.1(localhost)にバインドされていたのです。Docker のポートフォワードはコンテナ内 localhost には届かないので、ホストのブラウザからは見えません。
--help を見ても UI アドレスを変えるフラグはありません(config 指定は -f/--file で正しかった)。そこで再び JSON スキーマを確認したところ、config.adminAddr("ip:port" / "localhost:port" / "off" を取る)というフィールドがありました。デフォルトが localhost だったわけです(ソースの config.rs でも Address::Localhost(.., 15000)、Helm の standalone チャートは 0.0.0.0:15000 を明示していました)。
対処は config トップに1ブロック追加するだけです。
config:
adminAddr: 0.0.0.0:15000再起動すると serving UI at http://0.0.0.0:15000/ui に変わり、UI が HTTP 200(<title>agentgateway</title>)で開けました。
この記事では安全側に寄せて、docker のポート公開をホストの 127.0.0.1 に絞り、CORS の allowHeaders も必要なヘッダだけに限定しています。ただし UI の「Apply CORS」は実際には allowHeaders: ["*"] のようなワイルドカードを入れるので、UI 任せにするときは注意してください。共有環境・本番では、allowOrigins を実際のオリジンに絞り、adminAddr も必要に応じて 127.0.0.1 にするなど、公開面をさらに絞ってください。この localhost バインド問題は、後編(k8s)での露出設計(LB / port-forward)にも繋がります。
② Playground の Initialize が Method Not Allowed(405)で失敗#
UI で Enable MCP → Add server まで進め、いざ Playground で「Initialize」を押すと失敗しました。
{"detail":"Method Not Allowed"}
# ブラウザ console:
POST http://localhost:3000/mcp 405 (Method Not Allowed)ブラウザが localhost:3000/mcp を叩いています。ここで2つの事実が噛み合っていました。
- Enable MCP を押したとき、UI が config に
mcp: { port: 3000, ... }を自動で足していました。しかもこの 3000 は Docker で公開していないポートです(何もなければブラウザからの接続そのものが失敗します。筆者環境ではたまたま別サービスが 3000 を使っていたため、その応答として 405 が返っていました)。 - Playground はブラウザから
mcp.portのリスナーを直接叩く設計です。つまりmcp.portは「Docker で公開済み」かつ「他サービスと衝突しない」ポートでなければなりません。
さらに Tool Playground を開いた時点で “Browser access is not allowed” の警告も出ます。これは「Apply CORS」ボタン一発で、mcp セクションの CORS に http://localhost:15000 許可と Mcp-Session-Id の expose が入ります。
最終的に、config を UI 新形式の mcp: 一本に整理し、mcp.port を Docker 公開済みの 3001 に変更(legacy な binds は削除)して解決しました。これが動作確認済みの最終形です。
# yaml-language-server: $schema=https://agentgateway.dev/schema/config
config:
adminAddr: 0.0.0.0:15000 # UIをホストへ露出(検証用。本番は 127.0.0.1 を検討)
mcp:
port: 3001 # docker 公開済みポート(未公開だと Playground から届かない)
targets:
- name: langfuse-docs
mcp:
host: https://langfuse.com/api/mcp
policies:
cors: # Playground(ブラウザ)からの接続用。必要なヘッダだけ許可
allowOrigins: [http://localhost:15000]
allowHeaders: [content-type, accept, mcp-protocol-version, mcp-session-id]
allowMethods: [GET, POST]
exposeHeaders: [Mcp-Session-Id]この検証で注意が必要だったのは、UI 操作が config.yaml を直接書き換える点です(file watch で即反映されます)。手編集と UI 編集を混ぜると混乱するので、どちらかに寄せるのが安全です。また config ファイル方式の “legacy backend” と UI 新形式の mcp: は別管理で、Playground は後者しか見ません。再現する場合は、最初から本節の最終形(mcp: セクション)だけを使い、4章の binds 形式は試行錯誤の参考として読むのがおすすめです。
原因→対処 早見表
| 症状 | 原因 | 対処 |
|---|---|---|
:15000/ui が HTTP 000 | UI/admin が localhost バインド | config.adminAddr: 0.0.0.0:15000 |
| Playground Initialize が 405/接続失敗 | mcp.port が未公開/衝突ポート | mcp.port を公開済みの空きポートへ |
| Playground “Browser access not allowed” | mcp の CORS 未設定 | UI の「Apply CORS」/ config に cors ポリシー |
| config parse エラー | target のキー名がバージョン不一致 | JSON スキーマで正しいキーを確認 |
unknown flag | CLIフラグ違い | --help(config は -f/--file) |
7. わかったこと / 知見まとめ#
一番の収穫は、initialize 後にセッションID(Mcp-Session-Id)を引き継ぐ挙動を実際に目で見られたことです。3章で書いた「MCP を単発の HTTP リクエストだけで扱いにくい理由」が、机上の説明ではなく実測として腑に落ちました。
動かした感触としては、OSS standalone はかなり手軽でした。イメージ 81MB・数秒起動、config.yaml 一枚でリモート MCP を中継できます。UI の Playground もツール一覧・セッションID・レスポンスをその場で確認でき、curl だけで追うより挙動を把握しやすかったです。
引っかかったのは、config のキー名がバージョンで変わる点です。公式 docs だけを見ていると古い記述に当たることがあり、JSON スキーマやソースを一次情報として当たる場面が何度かありました。ドキュメントとコードの乖離は、使うなら覚悟しておいたほうがよさそうです。
複数の MCP をまとめたい、認証・ログをゲートウェイ側で挟みたい、といった用途なら十分使えそうです。ただ本番投入となると、バージョン差分や設定変更への追従はそれなりに手間がかかります。
8. 次にやりたいこと#
- 複数MCPの集約(multiplex):
mcp.targetsに target を足すだけで、1エンドポイントに複数MCPを束ねられます。ツール名はサーバー名でプレフィックスされ衝突回避されます。 - 認証付き(OAuth/JWT)MCP をゲートウェイで保護する。
- LLMゲートウェイ機能(OpenAI互換API・フェイルオーバー・レート制限)を試す。
- 次回は、ゲートウェイ層を通る MCP / LLM 呼び出しを Langfuse でどこまで観測できるかを確認します(後編に続きます)。
9. 参考リンク#
- agentgateway 公式 Introduction: https://agentgateway.dev/docs/standalone/main/about/introduction/
- 公式 standalone MCP クイックスタート: https://agentgateway.dev/docs/standalone/latest/quickstart/mcp/
- 設定 JSON スキーマ(キー名の一次情報): https://agentgateway.dev/schema/config
- GitHub リポジトリ: https://github.com/agentgateway/agentgateway
- Solo.io ブログ「Five Minutes to Your First MCP Server Tool」: https://www.solo.io/blog/five-minutes-to-your-first-mcp-server-tool-a-quickstart-with-agentgateway
- Langfuse Docs MCP: https://langfuse.com/docs/docs-mcp
(
https://langfuse.com/api/mcp、Streamable HTTP、認証なし)