VPSでMCPサーバーをAIコーディングエージェント向けに構築
VPS上でMCP serverを運用する手順です。stdioとremote HTTP、systemd、nginxのTLS、認証を設定し、JSON-RPCの破損や未認証エンドポイント公開などの失敗を防ぎます。
構築するもの
1 台の VPS 上で動作する 2 つの MCP 構成です。最初は stdio サーバーです。これは、Claude Code が子プロセスとして起動し、パイプ経由で通信するファイルシステムまたはデータベース用のツールです。次に、systemd と TLS 対応の nginx リバースプロキシの背後で長時間稼働するネットワークサービスとして動作する remote HTTP サーバーを構築します。接続先を指定した MCP クライアントから利用できます。
どちらのインストールも小規模です。このガイドの大半では、実際に問題になりやすい 2 点を扱います。JSON-RPC ストリームを常に正常な状態に保つことと、認証のないツールエンドポイントをパブリックインターネットに公開しないことです。
MCP とは実際には何か
Model Context Protocol は、AI クライアント、Claude Code、Claude Desktop、VPS 上の Gemini CLI、または独自のスクリプトから、外部ツールを呼び出し、外部リソースを読み取るための標準的な方法です。モデル自体は何も実行しません。モデルはクライアントに要求を送り、クライアントが MCP server と JSON-RPC 2.0 で通信し、server がツールを実行して結果を返します。このクライアントが、一般に agent harness と呼ばれるものです。agent harness は、モデルを囲むループとして、ツール一覧、権限チェック、セッション状態を管理します。MCP は、そのツール部分を拡張するための仕組みにすぎません。プロトコルが 1 つなので、一度作成した server は、MCP に対応するすべてのクライアントで使用できます。この分離が初めてで、特にモデルがツールを使うかどうかをどのように判断するのかが分からない場合は、これらの server に実際の認証情報を渡す前に、agent の基礎を段階的に学ぶ手順に 1 時間かける価値があります。
トランスポートは 2 種類あり、このガイドの以降の内容はそれぞれに分かれます。
- stdio。 クライアントが server を子プロセスとして起動し、標準入力と標準出力を介して、改行区切りの JSON-RPC メッセージを交換します。ネットワーク、ポート、認証は不要で、信頼境界はプロセスそのものです。ローカルツールのほとんどはこの方式で提供されます。
- Streamable HTTP(および古い方式の HTTP+SSE)。server は長時間稼働する Web サービスです。クライアントは HTTP で接続し、server は Server-Sent Events として応答をストリーミングできます。1 つの server を複数のクライアントで共有する場合や、サーバー上で常時稼働させる必要があるツールに適しています。
ツールが 1 台のマシンと 1 人のユーザーに属する場合は stdio を選びます。共有サービスの場合は HTTP を選びます。
前提条件と、事前に把握しておくべき注意点
Ubuntu 24.04 の新規 KVM VPS で、root または sudo を使用できることを前提とします。それ以外の条件は次のとおりです。
- サーバーの実装言語に対応するランタイム。 参照サーバーの多くは Node または Python で実装されています。Ubuntu 24.04 には Node 18 が付属しますが、現在の MCP パッケージの中には Node 20 以降を必要とするものがあります。そのため、
aptをそのまま使用せず、NodeSource または nvm から現行の LTS をインストールしてください。Python 3.12 はすでにインストールされています。 - ドメインと DNS A レコード。 これはリモート HTTP サーバーを使用する場合に限り必要です。TLS では、この VPS を指す名前が名前解決できる必要があります。stdio の例では DNS は一切必要ありません。
- 512 MB の RAM で十分です。 MCP サーバーは軽量な JSON-RPC プロセスです。メモリ使用量を左右するのはプロトコルではなく、ツールが扱う対象(データベースドライバーやファイルキャッシュなど)です。
- 仕様は新しく、変更が続いています。 2025-03-26 の改訂では、HTTP+SSE が Streamable HTTP に置き換えられ、SSE は非推奨になりました。SSE は現在も動作し、多くのサーバーが引き続き対応しています。そのため、トランスポートの固定指定は絶対的な前提とせず、サーバーのリリースノートで再確認してください。
Step 1: stdio サーバーを Claude Code に接続する
まずファイルシステムサーバーから始めます。公式に提供され、現在も保守されており、Node 以外は必要ありません。次の1つのコマンドで Claude Code に登録し、現在のプロジェクトに限定するため、コミット可能なファイルに保存されます。
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/api-- 区切り文字は重要です。これ以降は Claude Code が実行するコマンドであり、Claude Code のフラグではありません。これにより、プロジェクトのルートに .mcp.json が作成されます。
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}この時点ではまだ何も実行されていません。次回このディレクトリで Claude Code を起動すると、エージェントは .mcp.json を読み込み、npx -y @modelcontextprotocol/server-filesystem ... を子プロセスとして起動し、そのプロセスの標準入力と標準出力を通じて MCP ハンドシェイクを行います。正しく登録されたことを確認します。
claude mcp list正常なサーバーでは、コマンドと緑色のチェックマーク filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected が表示されます。セッション内で /mcp スラッシュコマンドを実行すると、サーバーが公開するツール(read_file、write_file、list_directory)が一覧表示されます。これでエージェントは、許可したパスに対してそれらのツールを呼び出せます。データベースツールも同じ形式です。パッケージを置き換え、最後の引数として接続文字列を渡します。ただし、現在のパッケージ名はサーバー自身のリポジトリで確認してください。公式の Postgres サーバーは、これまでに何度も管理元が変わっているためです。
エージェントをこのホスト上で実行する目的はここにあります。Claude Code セッションは VPS 内の tmux で実行され、stdio サーバーもそのすぐ隣で動作し、ネットワーク往復なしでプロジェクトファイルやローカルサービスに直接アクセスできます。エージェントが write_file と read_file の両方を扱えるようになったら、動作する最小限の変更を選ぶよう促す skill と組み合わせる価値があります。ファイルシステムツールを使うと、大規模な書き換えも2行の修正と同じ程度の手間で実行できてしまうためです。同じ接続方法はローカルファイル以外にも拡張できます。VPS 上ですでに検索エンジンを運用している場合は、自分で運用する SearXNG インスタンスを検索ツールとしてエージェントに渡せます。これにより検索クエリは自分のホスト内に保持されますが、信頼できないページのテキストがそのままエージェントのコンテキストに取り込まれ、その後の処理に使われます。
ステップ 2: リモート HTTP サーバーを構築する
stdio サーバーは親プロセスとともに終了し、クライアントごとに 1 回起動されます。そのため、このマシン上で相互に処理を渡す 2 つの Claude Code セッションを実行すると、それぞれがツールの専用コピーを持つことになります。すべてのクライアントで稼働し続けるツール、共有運用ツール、データベースゲートウェイ、ノート PC と CI の両方から呼び出すツールが必要な場合は、HTTP トランスポートと実際のサービスが必要です。以下は、公式 SDK を使用して 1 つのツールを公開する、最小構成の Python サーバーです。
# /opt/mcp-ops/server.py
from mcp.server.fastmcp import FastMCP
import subprocess
mcp = FastMCP("ops-tools", host="127.0.0.1", port=8000)
@mcp.tool()
def disk_free() -> str:
"""Return `df -h` for the server."""
out = subprocess.run(["df", "-h"], capture_output=True, text=True)
return out.stdout
if __name__ == "__main__":
# Serves Streamable HTTP at /mcp on 127.0.0.1:8000
mcp.run(transport="streamable-http")host="127.0.0.1" に注意してください。サーバーは localhost のみで待ち受けるため、マシンの外部から直接接続できません。これは、認証がまだない段階では適切な構成です。systemd が安定したインタープリターのパスを使用できるよう、専用の virtualenv にインストールします。
sudo useradd --system --home /opt/mcp-ops --shell /usr/sbin/nologin mcp
sudo install -d -o mcp -g mcp /opt/mcp-ops
sudo -H -u mcp python3 -m venv /opt/mcp-ops/.venv
sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install "mcp[cli]"ステップ 3: systemd でプロセスを常時稼働させる
エージェントが必要とした時点で停止しているツールは、ツールがない場合よりも役に立ちません。クライアント自体が長時間稼働するプロセスである場合は、特に重要です。再起動後もメモリとスケジュールを維持して常時稼働するエージェントは、誰も監視していない状態でスケジュールに従ってこれらのツールを呼び出します。そのため、サーバーも自動的に復旧する必要があります。/etc/systemd/system/mcp-ops.service を作成します。
[Unit]
Description=MCP ops-tools server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp-ops
ExecStart=/opt/mcp-ops/.venv/bin/python /opt/mcp-ops/server.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetExecStart にある venv の Python の絶対パスは必須です。これを /usr/bin/python3 に指定すると、プロセスは ModuleNotFoundError: No module named 'mcp' で起動します。システムインタープリターは pip install を認識していないためです。有効化して確認します。
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-ops
sudo systemctl status mcp-ops
curl -si -H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-X POST http://127.0.0.1:8000/mcpstatus は active (running) になるはずです。curl は本文に JSON-RPC エラーを含めて HTTP/1.1 400 Bad Request を返します。リクエストにはセッションも有効な JSON ペイロードも含まれていませんが、これは想定どおりです。ポートが応答し、プロトコルを処理していることを確認できます。Connection refused または空の応答が返る場合、プロセスは想定した場所で待ち受けていません。journalctl -u mcp-ops -n 50 を確認してください。
ステップ 4: TLS とリバースプロキシを前段に配置する
サーバーは localhost で待ち受けています。どこからでもアクセスできるようにするには、nginx で TLS を終端し、内部へプロキシします。nginx をインストールし、nginx で Certbot と Let's Encrypt を使用するで証明書を取得してから、location ブロックを記述します。重要なのはバッファリングを無効にすることです。nginx のデフォルト動作では、レスポンスが完全に生成されるまで保持されるため、SSE ストリームが停止したままになります。
server {
listen 443 ssl;
server_name mcp.example.com;
# ssl_certificate lines managed by Certbot
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# The four lines that make SSE work through nginx:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding off;
}
}sudo nginx -t && sudo systemctl reload nginxでリロードします。すでに多数のコンテナを運用している場合は、自動 TLS 対応の Traefik リバースプロキシが同じ処理を代行します。Traefik が証明書を発行し、ホスト名に基づいてルーティングするため、MCP コンテナにラベルを追加するだけで済みます。いずれの場合も、公開ポートで待ち受けるのはリバースプロキシだけになり、まだ保護していないサービスへ転送されます。どこかに URL を登録する前に、そのサービスを保護してください。
このトピックで最も重要なセキュリティルール
認証なしの MCP エンドポイントは絶対に公開しないでください。 MCP サーバーは読み取り専用 API ではありません。ファイル、データベース、場合によっては shell へのツールアクセスを許可します。パブリックインターネット上で公開された /mcp は、AI エージェントと同じ到達範囲を持つ第三者です。第三者は利用可能なツールを列挙し、それらを呼び出せます。認証なしの管理用ソケットとまったく同じものとして扱ってください。実際にそのものだからです。盗まれた token で何ができるかは、その背後にあるサーバーにも依存します。openGym workout tracker に付属する読み取り専用 MCP サーバーはトレーニングデータしか返せません。一方、filesystem や shell のツールがあれば、サーバー自体へのアクセスを渡すことになります。
優先順位の高い順に、3 つの防御策があります。
- 公開しないでください。 サーバーを
127.0.0.1に置き、SSH tunnel で laptop から接続します。ssh -L 8000:127.0.0.1:8000 matt@vpsを実行し、client の接続先をhttp://127.0.0.1:8000/mcpに設定します。外部に公開されるものはありません。 - private network に配置してください。 self-hosted WireGuard VPN の tunnel address に bind し、VPN peer だけが接続できるようにします。パブリックインターネットからは port が閉じて見えます。
- 公開が必要な場合は、token を必須にしてください。 適切な方法は、HTTP transport がネイティブにサポートする MCP OAuth flow です。実用上の最低限の対策は、proxy で共有 bearer token を検証することです。導入が容易で、無差別な接続を完全に防げます。
location /mcp {
if ($http_authorization != "Bearer REPLACE_WITH_LONG_RANDOM") {
return 401;
}
proxy_pass http://127.0.0.1:8000;
# ...buffering-off block from above...
}openssl rand -hex 32 で token を生成し、これらのいずれかを前段に置かずに、server 自体を 0.0.0.0 に bind しないでください。client は token を header として送信します。Claude Code では次のようにします。
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'shell で MCP_TOKEN を設定すると、secret が平文のまま .mcp.json に書き込まれません。Claude Code は読み取り時に環境変数から ${MCP_TOKEN} を展開します。
上記の防御策はいずれも、すでに token を保持している agent ではなく endpoint を保護します。これは問題のもう一方の側面です。client が DeepSeek Harness の場合は、agent が呼び出せるツールを制限し、ツール出力に埋め込まれた命令を検査する plugins がその側を保護します。
手順 6: MCP Inspector でデバッグする
サーバーが正しく動作しない場合は、agent 内で推測せず、公式の Web ベースのテストクライアントである Inspector から直接操作します。stdio サーバーには、agent が実行するのと同じコマンドを渡します。
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmphttp://localhost:6274 で UI が起動し、6277 でプロキシが起動します(最近のバージョンでは MCP_PROXY_AUTH_TOKEN クエリ文字列を含む URL が表示されます。UI に拒否されないよう、そのリンクをそのまま使用してください)。Connect、List Tools、Call Tool の順にクリックし、実際の引数を指定して呼び出します。Inspector では動作するのに agent では失敗する場合、問題はサーバーではなくクライアント設定にあります。リモート HTTP サーバーでは、Streamable HTTP トランスポートを選択し、https://mcp.example.com/mcp を入力して Authorization ヘッダーを追加し、接続します。これにより、agent を関与させる前に認証とプロキシが正しいことを最短で確認できます。
サーバーを最新の状態に保つ
MCP は頻繁に更新されるため、計画的にパッチを適用します。npx -y で起動する Node サーバーは、起動するたびに最新バージョンを取得します。便利ですが、再現性はありません。テストした正確なバージョンを固定し、npm view @modelcontextprotocol/server-filesystem version から読み取ったうえで、重要なサーバーでは .mcp.json(@modelcontextprotocol/server-filesystem@<version>)のパッケージ名に追加します。その後は意図的にバージョンを更新します。systemd で稼働する Python サーバーは、sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" に続けて sudo systemctl restart mcp-ops を実行して更新します。アップグレード時は、SDK が対象とする仕様のリビジョンを確認してください。SSE から Streamable-HTTP への境界をまたぐ更新では、クライアントが要求するトランスポートが変わる場合があります。
失敗するパターンと表示される文字列
エージェントにサーバーが失敗したと表示される。 claude mcp list には ✗ Failed to connect が出力され、TUI には MCP server 'filesystem' failed to start と表示されます。claude --debug を実行すると、通常は Error: spawn npx ENOENT が表示されます。これはコマンドがエージェントの PATH に存在しないことを示します。ランタイムがインストールされていないか、エージェントが探す場所にありません。Node が未インストール、npx が存在しない、または仮想環境の Python を bare name で指定しているケースがあります。コマンドを絶対パスに修正するか、ランタイムをインストールしてから再接続します。
stdio サーバーが接続直後に切断される。 クライアントのログには、Unexpected token 'S', "Server sta"... is not valid JSON や Failed to parse message のような JSON parse error が記録されます。原因は常に同じです。サーバーがログ行を stdout に書き込んでいます。stdio では stdout が JSON-RPC の通信チャネルです。そのため、余分な文字列がストリームを壊し、ハンドシェイクが失敗します。Node では console.log が stdout に出力されるため、console.error を使用します。Python では bare print() が stdout に出力されます。logging を sys.stderr に設定してログを書き込むか、file=sys.stderr を渡します。原則は明確です。stdio では stdout に JSON-RPC だけを出力し、人間向けの情報はすべて stderr に出力します。
リモートサーバーがタイムアウトするか、ハンドシェイクの途中で切断される。 クライアントは MCP error -32000: Connection closed で失敗するか、Inspector が Connect で停止し、ツールの一覧を表示しません。nginx の背後では、これはバッファリングが原因です。プロキシが SSE ストリームをフラッシュせずに保持するため、クライアントは返ってこない応答を待ち続けます。proxy_buffering off;(および Step 4 の残りのブロック)を location に追加します。公開 URL に対して curl -N を実行して確認します。イベントデータが最後に一括で届くのではなく、段階的に到着することを確認してください。
認証が拒否される。 クライアントには Error POSTing to endpoint (HTTP 401) が表示されるか、単に 401 Unauthorized と表示されます。原因は、ヘッダーがない、トークンが誤っている、またはクライアントが設定を読み込んだ時点でシェル変数が空だったことです。これはよくある落とし穴です。変数が未設定の場合、${MCP_TOKEN} は何も展開せず、nginx には値のない Bearer が渡されます。変数の内容を出力し、ヘッダーを再追加して、実際のバイト列が nginx の if に設定したトークンと完全に一致することを確認します。
systemd でサービスを起動できない。 journalctl -u mcp-ops には ModuleNotFoundError: No module named 'mcp' が表示され、ExecStart では venv のインタープリターではなくシステムの Python が指定されています。または Address already in use が表示されます。別のプロセスが 8000 番ポートを使用しています。sudo ss -ltnp | grep 8000 でそのプロセスを特定します。
FAQ
MCP serverとは何ですか?
Model Context Protocol と JSON-RPC 2.0 を使用して、AI client に tools と resources を公開するプログラムです。AI model が tool 自体を実行するのではありません。AI model が client に依頼し、client が MCP server を呼び出し、server が実行して結果を返します。protocol が標準化されているため、Claude Code、Claude Desktop、Gemini CLI など、準拠する任意の client で1台の server を利用できます。
stdio transport と HTTP transport の違いは何ですか?
stdio server は client に child process として起動され、stdin/stdout 経由で通信します。そのため、1台のマシン上の1つの client とともに起動・終了し、network や auth は必要ありません。HTTP server は、複数の client が同時に接続できる長時間稼働の network service です。そのため、TLS と authentication が必要です。local の単一ユーザー向け tool には stdio を使用します。共有または永続的に使用するものには HTTP(現在の server では Streamable HTTP)を使用します。
remote MCP server を保護するにはどうすればよいですか?
ファイル、database、shell への tool access を許可するものとして扱い、authentication なしで公開しないでください。localhost に bind し、SSH tunnel または private VPN 経由で接続する方法が最も安全です。public にする必要がある場合は、bearer token または MCP OAuth flow を適用する reverse proxy の背後に配置します。openssl rand -hex 32 で token を生成し、前段にこれらのいずれもない状態で server を 0.0.0.0 に bind しないでください。
起動しない server をデバッグするにはどうすればよいですか?
まず claude mcp list を確認します。✗ Failed to connect が spawn ... ENOENT の場合、command または runtime が存在しないため、path を修正するかインストールします。接続後に JSON parse error で切断される場合、server が stdout にログを出力して JSON-RPC stream を壊しています。すべてのログを stderr に移してください。それ以外の場合は、MCP Inspector で正確な command を実行します。MCP Inspector は server を分離して操作するため、server の bug と client 設定の bug を切り分けられます。