−25%

Windows 年払い、10月31日まで。 プランを見る

EQVPS
始める

MCPゲートウェイ用VPS:複数のMCPサーバーを1つのアドレスと1つのトークンの背後に

チームで3つのセルフホストMCPサーバーを使っていて、各自が3つのURLと3つのシークレットを抱えている。小さなVPS上の1つのHTTPSアドレスの背後にまとめましょう。Caddyがbearerトークンを確認し、パスで振り分け、サーバー自体はインターネットから隠します。検証済みの構成です。

セルフホストのMCPサーバーは増えていきます。最初は社内ドキュメント用が1つ、次にデータベース用、そのうち誰かがデプロイパイプライン用の小さなものを書く。それぞれにポート、URL、シークレットがあり、チーム全員がその3つをクライアント設定に貼り付けます。トークンをローテーションするたびに全員へ連絡が必要です。

ゲートウェイを使えば、それが1つのアドレスと1つのトークンになります。クライアントはhttps://mcp.example.com/docs/mcpや.../db/mcpなどに接続します。ゲートウェイがトークンを確認し、各パスをlocalhost上の正しいサーバーに送り、サーバー自体は外に出ません。必要なものはごくわずかです。専用IPv4付きの小さなVPSとCaddyだけ。

以下の構成はテストサーバーで組み、実際のクライアントで確認してから書いています。

必要なもの

  • クライアントが443に届くよう、公開IPv4を持つVPS。MCPサーバー自体が重い処理をしない限り、Nano-IPで足ります。
  • サーバーを指すAレコードを持つドメイン。ここではmcp.example.com。
  • Ubuntu 24.04またはDebian 12/13。Caddyはどちらの標準リポジトリにもあります。

1. MCPサーバーをlocalhostで動かす

各サーバーはsystemdサービスとして動かします。ここでは例を自己完結させるため、リファレンスサーバーserver-everythingを2つ使います。実際には自分のサーバーに置き換えてください。Node.js 20以降が必要です。

mkdir -p /opt/mcp && cd /opt/mcp
npm install @modelcontextprotocol/server-everything
# /etc/systemd/system/mcp-everything-1.service
[Unit]
Description=MCP server everything #1
After=network.target

[Service]
WorkingDirectory=/opt/mcp
Environment=PORT=3101
ExecStart=/opt/mcp/node_modules/.bin/mcp-server-everything streamableHttp
Restart=always
User=nobody

[Install]
WantedBy=multi-user.target

2つ目のコピーはポート3102を使います。そのあと:

systemctl daemon-reload
systemctl enable --now mcp-everything-1 mcp-everything-2

テスト中に気づいた点:このサーバーは127.0.0.1だけでなくすべてのインターフェースで待ち受けます。ほかにも同じ挙動のサーバーはたくさんあります。手順3でこれに対処します。

2. Caddy:まずトークンを確認し、次にパスで振り分ける

トークンを生成し、環境ファイル経由でCaddyに渡します。

apt install -y caddy
echo "MCP_GATEWAY_TOKEN=$(openssl rand -hex 32)" > /etc/caddy/gateway.env
chmod 600 /etc/caddy/gateway.env
mkdir -p /etc/systemd/system/caddy.service.d
printf '[Service]\nEnvironmentFile=/etc/caddy/gateway.env\n' > /etc/systemd/system/caddy.service.d/env.conf
systemctl daemon-reload

Caddyfile:

mcp.example.com {
	route {
		@unauthorized not header Authorization "Bearer {$MCP_GATEWAY_TOKEN}"
		respond @unauthorized 401

		handle_path /everything/* {
			reverse_proxy 127.0.0.1:3101 {
				flush_interval -1
			}
		}
		handle_path /tools2/* {
			reverse_proxy 127.0.0.1:3102 {
				flush_interval -1
			}
		}
		respond 404
	}
}
caddy validate --config /etc/caddy/Caddyfile && systemctl restart caddy

mcp.example.comの証明書はCaddyが自動で取得します。

routeブロックは飾りではありません。最初の版では、トークン確認とhandle_pathブロックをトップレベルに並べて書いていて、トークンなしのリクエストが200で素通りしました。Caddyはディレクティブを独自の順序で並べ替え、handle_pathはrespondより先に実行されます。routeの中では書いた順に実行されます。この構成を応用するなら、まず「トークンなし」のケースを試してください。

handle_pathはプレフィックスを取り除くので、/everything/mcpはサーバーには/mcpとして届きます。flush_interval -1はストリーミングのイベントを届いた瞬間に流します。

3. SSHとHTTPS以外はすべて閉じる

apt install -y ufw
ufw default deny incoming
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw enable

このあと、私たちのテストではポート3101と3102にサーバーの外から届かなくなり、22と443は応答しました。ポート80は、Caddyが証明書の取得・更新とHTTPSへのリダイレクトをするためだけに開けておきます。

4. 確認する

TOKEN=$(cut -d= -f2 /etc/caddy/gateway.env)
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"1"}}}'
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/everything/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT"
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/everything/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT"

1つ目のリクエストは401、2つ目は200を表示するはずです。私たちの実行では、間違ったトークンと途中で切れたトークンはどちらも401、未知のパスは正しいトークンでも404でした。

次にクライアントから。たとえばClaude Code:

claude mcp add --transport http everything https://mcp.example.com/everything/mcp \
  --header "Authorization: Bearer $TOKEN"
claude mcp list

両方のサーバーが✔ Connectedと表示されました。同じクライアントで間違ったトークンを使うと✘ Failed to connectでした。クライアント設定の詳細はEQVPS MCPをClaude CodeとCodexに接続するを参照してください。

次のサーバーを追加する

新しいlocalhostポートで起動し、handle_pathブロックを1つ追加して、Caddyをリロードします。誰の設定も変わりません。同じホスト・同じトークンで新しいURLが1つ増えるだけです。

知っておくべき限界

  • 全員で1つのトークンは簡単で、ローテーションもファイル1つですが、全員分を変えずに1人だけのアクセスを取り消すことはできません。ユーザーごとのアクセスが必要なら、前段に認証プロキシを置くか、各自に専用のルートとトークンを与えてください。
  • ゲートウェイはサーバーを安全にはしません。 シェルコマンドを実行するMCPサーバーは、トークンを持つ人にとって依然としてシェルです。トークンを共有設定やgitに置かないでください。
  • サイズはゲートウェイではなくサーバーに合わせる。 Caddy自体は数十MB程度です。ローカルモデルを動かしたり大きなリポジトリをインデックスしたりするサーバーがあれば、プランを決めるのはプロキシではなくそのサーバーです。

MCPサーバーが1つだけなら、MCPサーバー向けVPSがその単純なケースを扱っています。エージェントに自分が動いているサーバーの管理もさせたいなら、EQVPSのMCPサーバーが対応します(スクリプトが好みならREST API)。専用IPv4付きプランは月$8からです。

FAQ

そもそもMCPサーバーをゲートウェイの背後に置く理由は?

サーバーごとではなくチームごとに1つのアドレスと1つのトークン、TLSは1か所で処理、サーバー自体はlocalhostだけで待ち受け。4台目を追加するのはゲートウェイ側の3行の変更で済み、全員に新しいシークレットを配る必要はありません。

どのプランが必要ですか?

専用IPv4付きのプランです。クライアントがポート80と443に届く必要があるためです。Nano-IP(月$8、2 vCPU、1 GB)でゲートウェイと軽いMCPサーバー数台は十分です。重い処理をするサーバーがあるなら、そのサーバーに合わせて選んでください。

Caddy経由でストリーミングは動きますか?

はい。Streamable HTTPは通常のPOSTリクエストとserver-sent eventsを使います。リバースプロキシでflush_interval -1を設定すれば、イベントはバッファされずすぐに流れます。

共有トークン1つで十分安全ですか?

小さなチームなら妥当な出発点です。トークンがHTTPSの外に出ることはなく、ファイル1つの変更でローテーションできます。個人ごとのアクセスや失効が必要になったら、前段に本格的な認証レイヤーを置いてください。共有トークンは最低ラインであって上限ではありません。

NATプランでも動きますか?

公開ゲートウェイとしては無理です。NATプランにはSSH用の転送ポートが1つあるだけで、ほかの受信はないため、クライアントは443に届きません。すべてのクライアントが同じサーバー上にある場合にだけ使えます。

コメント

まだコメントはありません。最初になりましょう。

コメントを残す

コメントは表示される前にモデレートされます。