mcpField
A
aimcpgate

aimcpgate

ローカル
github.com/akomyagin開発ツール02 週間前に更新

aimcpgate について

aimcpgateは、複数のMCPサーバーを一つのエンドポイントにまとめたい運用者向けのGo製ゲートウェイです。上流のツール、プロンプト、リソースを集約し、呼び出しを振り分けて記録します。

主な機能

  1. 01複数上流のカタログを統合
  2. 02stdioとHTTP/SSEに対応
  3. 03ツールのフィルタリングと改名
  4. 04呼び出しジャーナルとログ表示
  5. 05上流ごとの制限と再起動
  6. 06プロンプトとリソースの集約

活用シーン

  1. 01一つのクライアントから複数のMCPサーバーに接続
  2. 02転送呼び出しやゲートウェイイベントの確認
  3. 03上流のレートと同時実行数の制限

MCP 対応

クライアントには単一のMCPサーバーとして接続し、リクエストを複数の上流MCPサーバーへ振り分けます。ツール、プロンプト、リソースの集約と、stdioおよびHTTP/SSEトランスポートに対応します。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"akomyagin-aimcpgate": {
"command": "npx",
"args": [
"-y",
"aimcpgate"
]
}
}
}
起動コマンドnpx -y aimcpgate

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
ocighcr.io/akomyagin/aimcpgate:0.5.0
npmaimcpgate0.5.0

ツール

ツール一覧はまだ取得されていません

ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。

詳細

aiMCPGate

ロシア語版 — README_RU.md。

Go で書かれた MCP サーバー(Model Context Protocol)向けのゲートウェイ/プロキシです。 MCP クライアント(Claude Code、Cursor など)には 1 つの MCP サーバーとして認識されますが、内部では複数のアップストリーム MCP サーバーへの呼び出しを多重化し、それらのツール、プロンプト、リソースを 1 つのカタログに集約して、すべての呼び出しをログに記録します。

状態:MVP 完了(ステージ 0~6)+MVP 後のステージ 7~19 をリリース済み。最新リリースは v0.6.0 です。 フェーズ 1 — 呼び出しログ付きの stdio エンドポイントの背後で stdio アップストリームを多重化。フェーズ 2 — HTTP/SSE クライアント向けトランスポート、 HTTP アップストリーム、CLI ログビューアー(mcp-gate logs)、リリースパイプライン (goreleaser、linux/darwin/windows × amd64/arm64 向けにクロスコンパイル、CGO なし)。 MVP 後には、アップストリームの自動再起動、設定のホットリロード、ツールの フィルタリング/名前変更、doctor が追加されました。また v0.3.0 では、 prompts/resources/resources/templates/completion の完全な集約、 ping、進行状況の転送と実際のキャンセル、logging/setLevel のファンアウト、 アップストリームごとの呼び出し制限(レート制限/同時実行数/結果の切り詰め/タイムアウト)、 遅延カタログと tools/list のページネーション、クライアント側とアップストリーム側の 両方における SSE サーバー→クライアントストリームが追加されました。v0.4.0 では サーバー→クライアント方向を完成させました。サーバー起点の 3 つのメソッド (elicitation/create、sampling/createMessage、roots/list)すべてが、4 通りの トランスポートの組み合わせ(クライアント側が stdio または HTTP × アップストリーム側が stdio または HTTP)でプロキシされます。ゲートウェイは、包括的な {} ではなく、 クライアントが宣言した能力と同じものをアップストリームに宣言するようになりました。 HTTP トランスポートには、DELETE /mcp による終了に対応したサーバー側 Mcp-Session-Id セッションが追加されました。v0.5.0 では、運用時の可観測性 (ステージ 18)を追加しました。アップストリームの起動失敗とスーパーバイザーの 再試行断念、破棄された通知とサーバー→クライアントリクエスト、GET SSE のない HTTP アップストリーム、カタログの衝突と不正な URI テンプレート、max_result_bytes を 気付かれないまま回避した結果、という 8 種類のイベントが、通常 MCP クライアントが 管理する stderr ではなく、呼び出しジャーナル(mcp-gate logs)に記録されます。 設定の解析は厳格になり、不明なキーや誤記されたキーは致命的なエラーになります。 さらに、ガード/切り詰め処理のうちクライアント向けの部分も完成しました。レート制限 または同時実行数ガードによって拒否された tools/call は、区別できない -32603 の 代わりに、機械可読な data: {"retryable":true,"reason":...} を伴う専用の JSON-RPC エラーコード -32009 を返します。また、max_result_bytes を回避した非テキストの結果には result._meta マーカーが付与されます(content はバイト単位で変更されません)。 最後に、未設定の環境変数を参照する auth_token がある場合、HTTP 認証を暗黙に無効化する のではなく、ゲートウェイの起動を拒否するようになりました。v0.6.0 では、ゲートウェイが 2 つの時代の仕様に対応します。同じエンドポイントで両方の MCP プロトコル改訂版を提供し、 リクエストごとに選択します。2026-07-28 仕様に準拠したクライアントには、最新の ステートレスなインターフェイス(server/discover、resultType/ttlMs/cacheScope などの結果への装飾、HTTP Mcp-Method/Mcp-Name/MCP-Protocol-Version ヘッダーの 検証、削除されたメソッドに対する -32601 の返却)が提供されます。従来のクライアントは v0.5.0 の動作をバイト単位で維持します。MRTR ブリッジにより、最新のクライアントは アップストリームから呼び出し途中に寄せられた質問(elicitation/create、 sampling/createMessage、roots/list)に、inputResponses を付けて呼び出しを再試行する ことで回答できます。入力受付ウィンドウはアップストリームの call_timeout と同じ長さです (デフォルトは 30 s)。また、このリリースではセキュリティ関連の変更も加わりました。 stdio アップストリーム向けのオプトイン設定 env_isolation: strict、既知のシークレット値を クラッシュログとジャーナルレコード内で常に *** に置換する処理、serve/doctor における 平文 HTTP 経由のシークレットに関する警告、doctor における弱いファイル権限の警告、 HTTP セッションあたり最大 16 個の GET SSE ストリームです。ツールチェーンは Go 1.26.7 に 移行し、公開済みの v0.5.0 バイナリに含まれていた、到達可能な標準ライブラリの CVE 34 件を 解消しました。また、リリース成果物にアーカイブごとの SBOM(SPDX JSON)と cosign による 鍵なし署名が追加されました。

v0.6.0 へのアップグレード — 契約変更が 1 件、条件付きの変更が 2 件:

  • ゲートウェイビジーエラーのコードが -32029 → -32009 に変更されました。 レート制限/ 同時実行数ガードによる拒否では、機械可読な data: {"retryable":true,"reason":...} が引き続き返されますが、古い数値コードを ハードコードしている箇所はすべて更新が必要です。-32029 は、2026-07-28 仕様で プロトコルエラー用に予約された -32020..-32099 の範囲内でした。
  • HTTP セッションあたりの同時 GET SSE ストリーム数が 16 に制限されます。 17 回目の 試行には、レスポンスヘッダーが送信される前に HTTP 503 が返されます。開いている ストリームが強制的に終了されることはなく、1 つ閉じるとその枠が解放されます。ストリームを リークするクライアント(古いストリームを閉じずに再接続する場合など)でのみ確認できます。
  • 新しいオプトイン設定フラグ env_isolation: "strict"(グローバル)が追加されました。 デフォルトは変わりません。stdio アップストリームは引き続きゲートウェイの環境全体を 継承します。strict を指定すると、子プロセスに渡されるのは最小限のクロスプラットフォーム 共通環境(Unix では PATH、HOME、TMPDIR、Windows では SystemRoot、PATHEXT、 TEMP、TMP など)と、その子プロセス独自の env: ブロックのみです。そのため、継承された 変数に依存するアップストリームは、それらを明示的に指定する必要があります。
  • 追加のみで対応は不要です。既知のシークレット値は、クラッシュログの stderr の末尾、 ジャーナルの err フィールド、およびイベント詳細で *** に置換されます(常に有効。 8 バイト未満の値は置換されません)。serve と doctor は、平文 HTTP 経由で送信される シークレットについて警告します。doctor は、設定ファイルまたは --env-file がグループ または全ユーザーから読み取り可能な場合に警告します。従来仕様のクライアントでは、ほかに 動作変更はありません。バイト単位での安定性はゴールデンテストで固定されています。

v0.5.0 へのアップグレード — 動作変更が 3 件ありますが、設定ファイル形式自体への影響は ありません:

  • 設定の解析が厳格になりました。 これまで黙って無視されていた、不明なキーまたは 誤記されたトップレベルキーやアップストリームごとのキーがある設定は、読み込みに失敗します。 キー名を修正する(エラーにキー名が表示されます)か、削除してください。
  • 未設定の VAR を参照する auth_token: ${VAR} があると、起動を拒否します。 エラーメッセージには変数名が表示されます。以前は空のトークンに変換されていました。 その結果、HTTP ゲートウェイでは警告なしにベアラーチェックが完全に無効になっていました。 変数を設定する(または --env-file を渡す)か、意図的に認証なしで実行する場合は auth_token を削除してください。
  • 呼び出しジャーナル(log_file/calls.jsonl)に、既存の呼び出しレコードに加えて、 2 つ目のレコード種別 "kind":"event" が追加されました。 v0.4.0 以前のバイナリで v0.5.0 のジャーナルを読み込むと、イベント行はエラーにならず、情報の少ない ERR エントリとして表示されます。ジャーナルを書き込んだバイナリと同じバージョンか、それより 新しいバイナリで読み込んでください。

v0.4.0 へのアップグレード: 設定ファイルの変更はありませんが、HTTP モードでは 観測可能な動作変更が 2 件あります。initialize の後に POST /mcp を行うには、セッション ID が 必須になりました(ヘッダーは initialize のレスポンスで返されます)。また、アップストリーム レジストリの起動はプロセス開始時ではなく、最初の実際の MCP リクエスト時に遅延されます。

未実装: クライアントごとのアクセス制御ポリシー。

リリース

クロスプラットフォームバイナリは、goreleaser (.goreleaser.yaml)を使用してビルドされます。対象は linux/darwin/windows × amd64/arm64 で、CGO は使用せず、 -ldflags -X main.version=... によってバージョンが埋め込まれ、チェックサムは SHA256SUMS に保存されます。ローカルでのドライラン:goreleaser release --snapshot --clean。 v0.6.0 以降、各アーカイブには SBOM(SPDX 2.3 JSON)も含まれ、 SHA256SUMS には cosign のキーレス署名(.sig + .pem)が付属します。 ghcr.io のイメージマニフェストはレジストリ自体で署名されます(キーは保存されません。 ID は GitHub Actions OIDC を通じて発行され、Rekor に記録されます)。

MCP レジストリからインストールする

リリースバイナリに加えて、ゲートウェイは GitHub Container Registry 上の OCI イメージと npm ラッパーパッケージとしても提供されます。MCP レジストリからインストールする場合は、この 2 つの形式を使用します。

Docker:

docker run --rm -i -v $(pwd)/config.yaml:/config.yaml ghcr.io/akomyagin/aimcpgate serve

-i は必須です。ゲートウェイは stdio 経由で MCP 通信を行うため、クライアントは stdin を開いたままにする必要があります(指定しないと、コンテナーは EOF を受け取ってすぐに終了します)。イメージには独自の設定ファイルがないため、設定ファイルをマウントしてください。上記の例では デフォルトのパス /config.yaml にマウントしていますが、serve -c を使えば 別のパスも使用できます。

実際の上流サーバーを使わずに、レジストリのサンドボックスチェック(Glama.ai など)を 再現するには、イメージに組み込まれているデモ設定を使用してください。サンドボックスで実行するコマンドは次のとおりです。

docker run --rm -i ghcr.io/akomyagin/aimcpgate serve -c /demo.config.yaml

npx(初回インストール時にプラットフォーム用のビルド済みバイナリをダウンロードし、 SHA256 チェックサムを検証します):

npx aimcpgate serve -c ./config.yaml

イメージのポリシー: OCI イメージに含まれるのは mcp-gate バイナリのみで、stdio 上流サーバー用のランタイム(node/npx、python、シェル)は含まれません。設定ファイルで stdio 上流サーバーを起動する場合は、イメージを自分で拡張し、必要なものをインストールしてください。HTTP 上流サーバーはそのまま使用できます(CA 証明書は含まれています)。

デモ設定: demo.config.yaml と非公開の __demo-echo サブコマンドは、実際の上流サーバーを使わずにレジストリのサンドボックス(Glama.ai)がゲートウェイを 調査できるようにするためだけのものです。実際のデプロイでは決して使用しないでください。

コンテナー内での CLI コマンドの実行

doctor、catalog、call、logs は、運用担当者がデプロイ状況を確認するためのコマンドです。コンテナー内での実行方法を決める事実は、次の3つです。

  1. バイナリは /mcp-gate にあり、$PATH には含まれていません。 Dockerfile では COPY mcp-gate /mcp-gate と ENTRYPOINT ["/mcp-gate"] を実行しており、検索パスに追加する処理はありません(おかしいと思った場合は Dockerfile を確認してください)。そのため、次のような分かりやすい形式は失敗します。

    $ docker exec mcp-gate mcp-gate catalog -c /config.yaml
    OCI runtime exec failed: exec failed: unable to start container process: exec: "mcp-gate": executable file not found in $PATH
    

    代わりに絶対パスを使ってください。違いはそれだけです。

  2. イメージは distroless のため、シェルがまったくありません。 ベースイメージは gcr.io/distroless/static-debian12:nonroot で、バイナリと CA 証明書以外は何も含まれていません。docker exec mcp-gate sh -c '…' は、sh が存在しないため失敗します。中を調べるための ls や cat もありません。パイプ、グロブ、リダイレクトは、コマンドのホスト側に置いてください。

  3. docker exec は新しいプロセスを起動します。実行中の serve に問い合わせるわけではありません。 doctor、catalog、call は、それぞれ独自のレジストリを構築し、アップストリームへの独自の接続を開いて、結果を報告して終了します。したがって、出力が示すのは「現時点での」アップストリームへの到達可能性であり、稼働中のゲートウェイの状態ではありません。実行中のプロセスがアップストリームへの接続を失い、それをカタログから削除していても、これらのコマンドには表示されません。また、ジャーナリングを無効にして実行するため、呼び出しジャーナルを汚しません。この方法で実行した call は logs に表示されません。

docker exec mcp-gate /mcp-gate version
docker exec mcp-gate /mcp-gate doctor  -c /config.yaml
docker exec mcp-gate /mcp-gate catalog -c /config.yaml
docker exec mcp-gate /mcp-gate call demo__echo '{"text":"hi"}' -c /config.yaml
docker exec mcp-gate /mcp-gate logs    -c /config.yaml --tail 50

これらのコマンドは、たとえば docker run -d --name mcp-gate … のように、コンテナーがデタッチモードで名前付きで起動されていることを前提としています。上記のフォアグラウンドで実行する docker run --rm -i … の例とは異なり、こちらは標準入出力のクライアントが切断されると終了するため、docker exec の接続先が残りません。また、同じ例のように、設定ファイルがデフォルトのパス /config.yaml にマウントされていることを前提としています。demo__echo は、独自のカタログにあるツールの例です。注意点をいくつか示します。

  • logs は事実3の例外です。稼働中のゲートウェイが書き込むジャーナルファイルを読み取るため、実行中プロセスの状態を反映します。そのためには、マウントした設定ファイル内の log_file がコンテナー内から見えるパスを指し、その場所にボリュームがマウントされている必要があります。そうでない場合、ジャーナルはコンテナーの標準エラー出力(つまり docker logs)に出力され、mcp-gate logs で読み取るものはありません。ジャーナルの場所を指定するには -c を使います。--file で上書きできます。
  • これは実質的には HTTP モードについての説明です。 stdio モードでは、MCP クライアントがコンテナーを起動して管理するため、exec で接続できる長時間稼働のコンテナーが存在しないのが一般的です。調査可能なゲートウェイは、transport: http を指定して別途起動したもの(docker run -d --name mcp-gate …)です。
  • HTTP モードでは、デフォルト以外の listen_addr が必要です。 デフォルトは 127.0.0.1:28080 です。これはコンテナー内のループバックアドレスのため、-p を指定してもホストから接続できません。設定ファイルで listen_addr: 0.0.0.0:<port> を指定してください。するとゲートウェイは、意図的に auth_token がないと起動を拒否します(「HTTP エンドポイントが認証なしでネットワークからアクセス可能になるため」です)。

理由

MCP を利用するユーザーは通常、複数のサーバー(filesystem、GitHub、検索、カスタムサーバーなど)を設定しており、それぞれが各クライアントの設定に重複して記述されています。aiMCPGate が提供するのは次のとおりです。

  • 単一の入口 — クライアント設定に N 個のエントリーを並べる代わりに、単一の MCP エンドポイントを使用できます。
  • 単一のカタログ — すべてのアップストリームサーバーのツールとプロンプトを統合します(名前が重複しないよう <upstream>__<tool> の形式で名前空間を設定します)。さらに、それらのリソースとリソーステンプレートも含みます(URI で指定するため、名前が変更されることはありません)。
  • 呼び出しログ — どのアップストリームのどのツールを、いつ呼び出し、成功したか失敗したかを記録します。これが「単なるプロキシ」に付加された価値です。

個人の趣味プロジェクトです。優先事項は Go(並行処理、os/exec、JSON-RPC 2.0、stdio および HTTP/SSE トランスポート)の学習です。デフォルトでは費用は 月額 $0(ローカルプロセス)で、テレメトリもありません。

仕組み(概要)

MCP client ──stdio/HTTP──▶ aiMCPGate ──JSON-RPC──▶ upstream A (stdio)
                              │        ├─────────▶ upstream B (stdio)
                          call log     └─────────▶ upstream C (http, Phase 2)

MVP(2 つのフェーズ)

  • フェーズ 1 — 2 つ以上の stdio アップストリームを、1 つの stdio エンドポイント(Claude Code が使用するものと同じトランスポート)の背後で多重化し、基本的なログ記録を行います。
  • フェーズ 2 — HTTP/SSE トランスポート、HTTP アップストリームサーバー、ログビューアー (CLI 版は作成しましたが、Web ビューは意図的に廃止しました)、アクセス ポリシー(検討しましたが、採用しませんでした)。

ビルド

export PATH="$HOME/sdk/go/bin:$PATH"   # if go isn't already on PATH
go build ./...
go vet ./...
go test -race ./...

go run ./cmd version

使用方法

# stdio mode (the client launches the gateway as a subprocess):
mcp-gate serve --config ./config.yaml

# http mode (transport: http in the config) — endpoint at http://<listen_addr>/mcp;
# every request after initialize carries the issued Mcp-Session-Id (see below):
mcp-gate serve --config ./config-http.yaml

# check every enabled upstream once (launch → handshake → tools/list) and print
# a per-upstream OK/FAIL table; exit code is non-zero if any upstream failed
# (scriptable for CI/cron), no auto-restart, no call logging — one pass then exit.
# It also WARNs (POSIX only) if config.yaml or --env-file is readable by
# group/others — either may hold a secret literal, and nothing else checks:
mcp-gate doctor --config ./config.yaml

# call one aggregated tool once from the shell (single bring-up, no supervisor —
# the fastest way to debug a config, a filter or a rename without a live client):
mcp-gate call github__search_repositories '{"query":"mcp"}' --config ./config.yaml

# report the aggregated catalog size per upstream (tools / bytes / ~tokens) plus
# the heaviest individual tools — the data behind allow-list / strip decisions:
mcp-gate catalog --config ./config.yaml --top 20

# view the journal — tool calls AND operator events (last 50 lines; filter by
# upstream/tool/status):
mcp-gate logs --file ./logs/calls.jsonl --tail 50
mcp-gate logs --config ./config.yaml --upstream github --status err
# show ONLY the operator events (see "Operator events" below):
mcp-gate logs --config ./config.yaml --events
# keep watching the log as it grows, or aggregate it instead of listing records
# (--follow and --stats are mutually exclusive):
mcp-gate logs --config ./config.yaml --follow
mcp-gate logs --config ./config.yaml --stats

# generate a random auth token (for the HTTP transport) and see how to wire it in:
mcp-gate token --generate
# print the auth token currently set in the config:
mcp-gate token --config ./config-http.yaml

# print ready-to-paste MCP client config snippets (Claude Code / Cursor / Claude
# Desktop) for whichever transport the config uses: a launch command for stdio, or
# the endpoint URL plus the Bearer header (when auth_token is set) for http:
mcp-gate client-config --config ./config.yaml

# print a SKILL.md teaching an agent how to use the aggregated catalog
# (built-in text by default; overridable via skill_file in the config):
mcp-gate skill > .claude/skills/mcp-gate/SKILL.md

# shell completions (cobra's built-in command; the release archives also ship
# pre-generated ones):
mcp-gate completion bash > /etc/bash_completion.d/mcp-gate

token --generate、completion、および組み込みガイドにフォールバックする skill を除くすべてのコマンドは、設定を読み込みます。--config を渡すか、バイナリの隣に config.yaml を配置してください(「設定」を参照)。

serve、doctor、call、catalog では --env-file ./.env も使用できます。これは最小限の KEY=VALUE パーサーで、設定を読み込む前に適用されるため、設定内の ${VAR} 参照はそのファイルから解決されます。実際のプロセス環境変数は常にファイルより優先されます。

HTTP セッション(Mcp-Session-Id)

http モードでは、ゲートウェイは Streamable HTTP セッションを実行します。initialize への応答には Mcp-Session-Id ヘッダーが含まれ、その後のすべてのリクエスト(POST、GET SSE ストリーム、DELETE)でそのヘッダーを送り返す必要があります。ヘッダーがない場合は 400 が返されます。不明または期限切れの ID の場合は 404 が返され、クライアントに再度 initialize するよう通知します。セッションは DELETE /mcp(204)で解放されるか、リクエストがない状態が 30 分続くと解放されます。開いている SSE ストリームはアクティビティとして扱われ、セッションを維持します。

MCP クライアントがこれらすべてを処理します。手動で curl を実行する場合は、initialize の応答からヘッダーを取得して送り返してください。

SID=$(curl -sD - -o /dev/null -X POST http://127.0.0.1:28080/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
  | tr -d '\r' | awk -F': ' '/^[Mm]cp-[Ss]ession-[Ii]d/{print $2}')

curl -s -X POST http://127.0.0.1:28080/mcp \
  -H 'Content-Type: application/json' -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

curl -s -X DELETE http://127.0.0.1:28080/mcp -H "Mcp-Session-Id: $SID"

このセッションにより、呼び出しログも正確になります。すべての呼び出しは、その呼び出しを行ったセッションの clientInfo に基づいて監査されるため、複数の HTTP クライアントは calls.jsonl 内でそれぞれ識別され、空の client フィールドを共有することはありません。

HTTP 経由のサーバー→クライアントリクエスト(elicitation、sampling、roots)

上流サーバーから呼び出しの途中で elicitation/create、sampling/createMessage、roots/list のいずれかが要求されると、その質問はあるセッションの GET /mcp ストリーム上で SSE イベントとして送信されます。クライアントは、同じ ID と同じ Mcp-Session-Id を持つ JSON-RPC レスポンスを通常の POST で返答します。質問を受け取ったセッションだけが返答でき、ほかのセッションからの返答は無視されます。ストリームを開いた状態でその機能を宣言したクライアントがいない場合、上流サーバーはタイムアウトまで待たされず、仕様で定められた形式(エリシテーションの場合は {"action":"decline"}、残りの 2 つの場合は -32601)で直ちに拒否されます。質問が保留中の状態でセッションが終了した場合も同様です。

知っておくべき影響が 3 つあります。

  • 上流サーバーに通知される機能は、最初に初期化したクライアントのものです。その機能セットはプロセスの存続期間中固定されます。MCP 2025-06-18 には再ネゴシエーションの仕組みがないため、追加の機能を宣言する 2 つ目のクライアントがいても、すでに完了したハンドシェイクは変更できません。上流サーバーが、機能について通知されていないクライアントの代わりに、その機能を使えると約束されることはありません。
  • 上流サーバーは、ゲートウェイがポートをバインドしたときではなく、それを必要とする最初のリクエストがあったときに起動します。これによって、上記の宣言が可能になります。ハンドシェイクは、クライアントが対応機能を伝えた後に行う必要があるためです。上流サーバーを起動できない場合、クライアントには JSON-RPC -32603 が返され、ゲートウェイはエラーで終了します。上流サーバーを先行して起動していた場合と同様です。
  • 質問は、その機能を宣言したクライアントに送られます。呼び出しをきっかけにしたクライアントとは限りません。ルーティングは宣言された機能に基づいて行われ、一致するセッションの中では、最後にアクティブになったセッションが選ばれます。上流リクエストには、どの呼び出し元に属するかを示す情報がありません。クライアントが 1 つだけの場合(通常はこちら)、この違いは表面化しません。しかし、クライアントが 2 つあると、一方のクライアントの tools/call によって表示されたフォームが、もう一方のクライアントの UI に表示される場合があります。

同じやり取りの上流側でも HTTP が使われます。url: で接続されたリモート MCP サーバーは、SSE フレームとして質問を送ることができます。長時間接続の GET ストリーム上、またはゲートウェイ自身の POST に応答するストリームに割り込む形で送られます。SDK サーバーが tools/call の内部で発生した elicitation/create を送信するのは、後者です。ゲートウェイは同じパイプラインを介して中継し、クライアントの返答を、そのサーバー自身のリクエスト ID を使った JSON-RPC レスポンスを含む通常の POST として返します。このような上流サーバーに通知されるゲートウェイのクライアント機能についても、stdio の場合と同じく、実際の宣言に従う方針が適用されます。ゲートウェイ自身のクライアントが宣言した場合に限り、その機能が提示されます。クライアントを持たない doctor/call/catalog は、引き続き正確に {} を宣言します。返答の POST は再試行されません。返答を受け取れなかった上流サーバーは、独自のタイムアウト処理に移行します。

ジャーナル内のオペレーターイベント

log_file のジャーナルには、2 種類の行が記録されます。ツールの呼び出しごとの行と、オペレーターのイベントごとの行です。後者は、そうでなければ知ることのできないゲートウェイの状態を示します。stdio モードでは MCP クライアントがターミナルを使用するため、ゲートウェイの stderr は見えず、これらの状態のいくつかはこれまでデバッグレベルでしか記録されていませんでした。現在は、mcp-gate logs が読み取るのと同じファイルに書き込まれます。

イベント意味
upstream_start_failedアップストリームが起動しませんでした。そのツールはカタログに表示されません。
upstream_gave_upスーパーバイザーがアップストリームの再起動を停止し(試行回数の上限に達した、リロードによって再起動が無効になった、または死活確認用チャネルがない場合)、カタログから削除しました。
notification_droppedサブスクライバーのバッファーがいっぱいだったため、転送される通知が破棄されました。転送は設計上、ブロッキングを行いません。
server_request_droppedアップストリームが、クライアントだけが応答できる内容(elicitation/sampling/roots)を要求しましたが、どのトランスポートもその要求を受け取らなかったため、その代わりにツール呼び出しが拒否されました。
sse_stream_unavailableHTTP アップストリームが GET SSE ストリームを提供していないため、ゲートウェイを再起動するまで、そのアップストリームからの tools/list_changed は届きません。
catalog_collision2 つのエントリーが、クライアントに表示される同じツール名/プロンプト名、またはリソース URI を登録しました。先に登録された方が優先され、もう一方はクライアントに表示されません。
catalog_bad_templateリソース URI テンプレートをコンパイルできません。クライアントには表示されますが、読み取り時に一致することはありません。
result_truncation_skipped結果が max_result_bytes を超えましたが、切り詰められるテキストがなかったため(画像のみの場合など)、全体がそのまま渡されました。

イベントは呼び出しとともにインラインで表示され、EVT とマークされます。mcp-gate logs --events はイベントのみを表示し、--stats にはイベントごとのテーブルが追加されます。--tool と --status は呼び出しのみを対象とするフィルターなので、いずれかが設定されている間、イベントは除外されます(--upstream は呼び出しとイベントの両方に適用されます)。知っておくべき点として、notification_dropped にはアップストリーム名が含まれません。通知の破棄は、通知を送信した側ではなく、バッファーがいっぱいになったサブスクライバーに起因するためです。そのため、--upstream X を指定しても表示されません。このイベントを確認するときは、そのフィルターを付けずに検索してください。繰り返し発生する破棄はまとめられます。最初のイベントはただちに記録され、同じキーに対してその後1分以内に発生したイベントは、次の行の count= に集計され、残りはシャットダウン時に書き出されます。このような未記録分を含む行では、detail= に集計対象となる最も古い発生時刻が記載されます。行自体のタイムスタンプは最も新しい時刻なので、この2つから実際に発生した一連のイベントの時間範囲がわかります。

実運用にあたって、次の2点に注意してください。

  • log_file を設定してください。 空のままにすると、ジャーナルは stderr に出力されます。stdio モードでは stderr は MCP クライアントが使用するため、イベントを確認できない場所に書き込むことになります。
  • ジャーナルは、それを書き込んだものと同じ(またはそれより新しい)バイナリで読み取ってください。 イベントには古いバージョンでは認識されない "kind" フィールドが含まれるため、≤ v0.4.0 の mcp-gate logs では、ほとんど情報のない簡素なレコードとして表示されます。

この機能は MCP クライアントからは見えません。エラーコード、結果の内容、ケイパビリティに変更はなく、イベントはジャーナルにのみ記録されます。

ゲートウェイがルーティングできなかった呼び出しはイベントではなく、通常の失敗した CALL 行です。 どのアップストリームも提供していないツール名をクライアントが要求すると、他の呼び出しと同様の CallRecord が作成されます。ただし、upstream 列にはセンチネル値 (unrouted) が設定されます。mcp-gate logs --upstream '(unrouted)' を指定すると、そのような行だけが選択され、それ以外は表示されません。これとよく似ていますが別のケースとして、実際のアップストリーム名が記録される場合があります。ルートは存在します(そのツールはカタログにあります)が、アップストリームへの接続が切れています(再起動中、または削除済み)。その行には実際のアップストリーム名が記録されるため、センチネル値ではなく、通常どおり --upstream <name> で絞り込んでください。

設定の再読み込み(SIGHUP)

ゲートウェイは SIGHUP を受け取ると設定を稼働中に再読み込みします。再起動も、クライアント接続の切断もありません。config.yaml を編集して、シグナルを送信してください。

kill -HUP $(pgrep -f 'mcp-gate serve')

再読み込み時、ゲートウェイは新しい設定と実行中のアップストリームを比較し、必要最小限の変更を適用します。新たに追加されたアップストリームは起動し、削除されたアップストリーム(または enabled: false に設定されたアップストリーム)は停止します。起動フィールド(command/args/url/env/headers)が変更されたアップストリームは再起動し、ツールフィルター(allow/deny/rename、またはカタログの投影ルール strip_annotations/strip_output_schema/max_description/describe)だけが変更されたアップストリームは、再起動せずに再投影されます。呼び出し制限(グローバルまたはアップストリームごとの rate_limit、max_concurrent、max_result_bytes、call_timeout)も稼働中に適用されます。再起動は不要で、次の呼び出しから新しい値が使用されます。変更されていないアップストリームはそのまま実行されます。不正な編集(無効な YAML、検証の失敗)はログに記録され、無視されます。現在稼働中の設定はそのまま有効なので、入力ミスでゲートウェイが停止することはありません。

動作に関する注意: ゲートウェイは SIGHUP ハンドラーをインストールするため、SIGHUP を受け取っても OS のデフォルト動作のようにプロセスは終了しません。ゲートウェイを停止するには Ctrl-C、SIGINT、または SIGTERM を使用してください。

SIGHUP は Unix でのみ使用できます。Windows など、シグナルを送信したくない環境では、代わりにオプトイン方式のポーリングを使用してください。

mcp-gate serve --config ./config.yaml --watch-config        # bare flag = poll every 2s
mcp-gate serve --config ./config.yaml --watch-config=10s    # note the "=", not a space

指定した間隔で設定ファイルのフィンガープリントを確認し、SIGHUP と同じ再読み込み処理を適用します。SIGHUP ハンドラーと併用しても問題ありません。

ウォッチャーはファイルの mtime とサイズ を比較し、次のポーリングでも同じフィンガープリントが確認されてからファイルを読み込みます。これにより、ファイルを切り詰めてから書き込む二段階の保存も実際上安全になります。チェックをすり抜けるには、書き込み側がファイルを完全なポーリング間隔より長く中途半端な状態にしておく必要があるためです。その代わり、再読み込みには遅延が生じます。再読み込みが反映されるまで最大でポーリング間隔の 2 倍かかります(デフォルトの 2 秒では最大 4 秒)。

stdio では、クライアントから最初のリクエストが届くまでアップストリームは起動しません。そのため、クライアントが接続する前に行った編集は、まだ適用できません。ウォッチャーはその編集を保持し、ゲートウェイが起動するまでポーリングのたびに再試行してから適用します。反映させるためにファイルを再度保存する必要はありません。解析できない YAML や、下記の upstreams がない場合のガードなど、恒久的に拒否された編集は一度だけ報告され、再試行されません。

どちらのトリガーでも安全策として、新しい設定で upstreams が一つも宣言されていない場合、再読み込みは拒否され、ログに記録されます。これはファイルが書き込み途中であることを示すもので、適用すると実行中のすべてのアップストリームが停止してしまいます。意図的にすべてのアップストリームを削除するには、ゲートウェイを再起動してください。明示的に enabled: false を指定した場合には影響しません。最後のアップストリームを無効にする変更も適用されます。

設定

--config を指定しない場合、ゲートウェイは自身のバイナリと同じ場所にある config.yaml を探します(たとえば、mcp-gate が /etc/gate/ にインストールされている場合、起動時の作業ディレクトリにかかわらず /etc/gate/config.yaml を探します)。そのファイルが存在せず、--config も指定されていない場合、空のゲートウェイを起動せず、明示的にエラーになります。設定内の相対パス(log_file、skill_file、debug_payload_log)は、現在の作業ディレクトリではなく、設定ファイル自体があるディレクトリを基準に解決されます。

未知のキーがあると起動エラーになります。 設定は厳密に解析されます。スペルミスのあるキーや認識されないキーがあると、以前のように黙って無視されるのではなく、キー名と行番号を示してゲートウェイが停止します。具体的には、enabled の入力ミスによってアップストリームが気付かれないまま稼働し続けることはなくなります。カスタムの x- 一時キーも拒否されます。ブロックを共有するには、最初の実際のアップストリームに YAML アンカーを付け、それを他のブロックにマージしてください(<<: *anchor)。アンカーとマージキーは通常どおり機能します。 アップストリームはデフォルトで有効です。enabled:を完全に省略すると、ほかのアップストリームと同様に起動します。設定を削除せずにゲートウェイから除外するには、明示的に**enabled: false**を指定して無効にします。その場合、tools/listにもmcp-gate doctorの表にも表示されません。注意:値のないenabled:(またはenabled: null)は省略されたものとして扱われるため、値をコメントアウトしてもアップストリームは稼働したままです。無効にするには、リテラルのfalseだけを指定してください。

注:バイナリの「隣」を検索するときは、実行中の実行可能ファイルのパスを使います。 go run ./cmd ...では、その実行可能ファイルは一時ディレクトリに生成される使い捨てのビルド成果物なので、デフォルトの検索ではconfig.yamlが見つかりません。go runを使う場合は--configを明示的に指定するか、ビルド済みのバイナリを実行してください。

すべてのフィールドを含む完全な例は、config.example.yamlを参照してください。 アップストリームサーバーの一覧はYAMLで宣言します。シークレット(トークン)はenv/.envを介して渡します(読み込み時に${VAR}を展開)。設定ファイルにコミットすることはありません。 各アップストリームには、command(stdioサブプロセス)またはurl(HTTPサーバー、Streamable HTTP)のどちらか一方のみを指定します。接続の種類は自動的に判別されます。

未設定の${VAR}参照の扱いは、フィールドによって異なります。

  • 未設定の変数を参照する**auth_tokenがあると、変数名を示して起動に失敗します**。空のauth_tokenはHTTPベアラーチェックを無言で無効にするため、この状態が黙って発生することはありません。認証なしで実行するには、auth_tokenキー自体を削除してください。
  • アップストリームの**env/headers**にある未設定の変数はエラーになりません。値は空になり、不足しているシークレットは後でそのアップストリームから401が返されたときに判明します。ゲートウェイは事前にこれを報告します。ジャーナル(mcp-gate logs)にunresolved_secret_varイベントが記録され、mcp-gate doctorにWARN行が表示されます。
  • stdioモードでは、mcp-gate client-configが(stderrに)オペレーターの環境変数は継承されないと警告します。MCPクライアントは独自の環境でゲートウェイを起動するため、変数はクライアントの実行環境で設定してください。

デフォルトでは、すべてのstdioアップストリームがゲートウェイの完全なプロセス環境を継承します。これには、--env-fileで読み込まれた、ほかのアップストリーム用のシークレットやauth_tokenも含まれます。各stdioアップストリームが代わりに最小限の基本環境(PATH、HOME、TMPDIRとWindowsの同等の変数)と独自のenv:ブロックのみを受け取るようにするには、env_isolation: strict(グローバル)を設定してください。env:ブロックは常に子プロセスに渡されます。オープンレジストリからアップストリームを実行する場合(完全には信頼できないnpx起動のパッケージなど)に推奨されます。strict設定のアップストリームに渡したいゲートウェイ変数は、env:に明示的に記載してください(例:PYTHONPATH: ${PYTHONPATH})。値は起動時に読み込まれます。稼働中のアップストリームは、(再)起動されるまで、起動時の環境を保持します。

これとは別に、常に有効な機能があります。ゲートウェイが${VAR}シークレットを展開すると、その正確な値を把握し、オペレーター向けの自由記述(ジャーナル(mcp-gate logs)やクラッシュログ)では、その値を*****に置き換えます**。シークレットが設定されていなければ、コストはかかりません。

暗号化されていない平文HTTPで、ループバック以外のホストに送信されるシークレットについては、起動時にWARNが表示され、mcp-gate doctorにも行が表示されます(ハードエラーにはなりません。信頼されたインフラストラクチャ、たとえばVPNや隔離されたLANでの平文通信は意図的な選択である場合があり、設定だけからゲートウェイがそれを判断することはできません)。対象は、ループバックを超えてバインドされた空でないauth_token、またはhttps://ではないループバック以外のurlに送信されるアップストリームのheadersです。TLSはゲートウェイに組み込まれていません。必要な場合は、相互TLSで推奨しているのと同じ方法で、TLS終端を担うリバースプロキシを前段に配置してください。

transport: stdio            # stdio (Phase 1) | http (Phase 2)
listen_addr: "127.0.0.1:28080"  # only used for transport: http; loopback by default
# auth_token: ${AIMCPGATE_TOKEN}  # required if you widen listen_addr past loopback;
#                                 # the variable must be set or startup fails
log_file: ./logs/calls.jsonl
# debug_payload_log: ./logs/payloads.jsonl  # OPT-IN, off by default: logs raw
#                                   # arguments AND results — can contain secrets
# Optional global call limits (each can be overridden per upstream):
# rate_limit: { rps: 5, burst: 2 }  # token bucket per upstream for tools/call
#                                   # (refusal → client error -32009, retryable)
# max_result_bytes: 65536           # truncate oversized textual results (0 = off;
#                                   # non-text over-limit results get a _meta marker)
# call_timeout: 30s                 # bounds one upstream request
# How the catalog is presented to the client (both hot-reloadable):
# catalog_mode: lazy                # normal (default) | lazy: the client sees only
#                                   # gate_search_tools / gate_describe / gate_call
# page_size: 50                     # paginate tools/list (0/omitted = whole catalog;
#                                   # ignored in lazy mode)
# Auto-restart policy for crashed stdio upstreams (defaults: on, 1s→30s, 5 tries):
# restart: { enabled: true, initial_backoff: 1s, max_backoff: 30s, max_attempts: 5 }
# env_isolation: strict             # opt-in ("" default = inherit the gateway's full
#                                   # env): a stdio upstream then gets only a minimal
#                                   # base (PATH/HOME/...) plus its own env: block —
#                                   # NOT other upstreams' secrets or the auth token
upstreams:
  - name: filesystem        # stdio upstream
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"]
    enabled: true
  - name: github
    command: github-mcp-server
    env:
      GITHUB_TOKEN: ${GITHUB_TOKEN}   # from the environment, not hardcoded
    enabled: true
    # Optional per-upstream tool filter / catalog projection (keys are ORIGINAL
    # tool names; all editable live via SIGHUP with no upstream restart):
    # tools:
    #   allow: ["search_repositories"]  # if non-empty, only these survive
    #   deny: ["delete_repository"]     # always subtracted, even from allow
    #   rename: { search_repositories: "gh_search" }
    #   strip_annotations: true         # drop heavyweight catalog fields
    #   strip_output_schema: true
    #   max_description: 200            # truncate descriptions to N runes
    #   describe: { get_issue: "Fetch one issue." }   # replace wholesale
    # Optional per-upstream call limits (override the globals for this upstream):
    # rate_limit: { rps: 1, burst: 1 }  # rps: 0 disables the global limit here
    #                                   # (refusal → client error -32009, retryable)
    # max_concurrent: 4                 # cap on simultaneous in-flight calls
    #                                   # (refusal → client error -32009, retryable)
    # max_result_bytes: 32768           # 0 disables the global cap here
    # call_timeout: 120s                # this upstream is slow — give it longer
  - name: remote            # http upstream (Phase 2)
    url: https://mcp.example.com/mcp
    headers:
      Authorization: "Bearer ${REMOTE_MCP_TOKEN}"   # secret, never logged
    enabled: true
呼び出し制限に達したときにクライアントに表示される内容

上記の呼び出し制限のうち2つは、運用者のジャーナルだけでなく、MCP クライアント(エージェント)にも通知されます。

  • ガードによる拒否(rate_limit / max_concurrent)。 ゲートウェイが、アップストリームごとのレートリミッターまたは同時実行数の上限により呼び出しを受け付けられず、tools/call を拒否すると、クライアントにはゲートウェイ独自のコード -32009 と、機械可読な data: {"retryable": true, "reason": "rate_limit" | "concurrency_limit"} を含む JSON-RPC エラーが返されます。呼び出しはアップストリームに到達していないため、エージェントは二重実行のリスクなく待機して再試行できます。通常のトランスポート/ルーティングエラーは従来どおり -32603 です。また、アップストリーム自体が返したエラーは、コードとデータを変更せず、そのまま転送されます。アップストリームからの -32009 は、ゲートウェイからの通知ではありません。
  • 切り詰めできないサイズ超過の結果(max_result_bytes)。 テキストの結果は、コンテンツ内に [truncated by mcp-gate: …] マーカーを付けて短縮されます。サイズ制限を超えていても切り詰め可能なテキストがない非テキスト/標準外の結果(画像のみの場合など)は、全体をバイト単位でそのまま渡され、結果の content[] は一切変更されません。ただし、結果の _meta にはゲートウェイキー io.github.akomyagin.aimcpgate/result-over-limit と {"limitBytes": N, "resultBytes": M} が追加されるため、エージェントは制限が適用されなかったことを確認できます。このキーを知らないクライアントは無視します。運用者向けの result_truncation_skipped ジャーナルイベントは、これまでどおり発生します。

ライセンス

MIT — LICENSE をご覧ください。

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。