VPSでMCPサーバーを構築する方法:stdioとHTTPの運用ガイド
AIエージェントのツールを拡張するModel Context ProtocolサーバーをVPSで運用する手順を解説します。stdio接続の基本から、systemdとnginxを用いたTLS・認証付きHTTPサーバーの構築まで、実運用で直面する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 は現在も動作し、多くのサーバーが引き続き対応しています。そのため、トランスポートの固定指定は絶対的な前提とせず、サーバーのリリースノートで再確認してください。
手順 1: Claude Code に stdio サーバーを接続する
まず filesystem server から始めます。これは公式で継続的に保守されており、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 ... を子プロセスとして起動し、そのプロセスの stdin/stdout 経由で MCP handshake を実行します。正しく反映されたことを確認します。
claude mcp list正常なサーバーはコマンドと緑色のチェックマーク filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✔ Connected を表示します。セッション内で /mcp slash command を実行すると、サーバーが公開するツール(read_text_file、write_file、list_directory)が一覧表示されます。これでエージェントは、許可したパスに対してそれらのツールを呼び出せます。データベースツールも同じ構成で、パッケージを置き換え、最後の引数として接続文字列を渡します。ただし、現在のパッケージ名はサーバー自身のリポジトリで確認してください。reference Postgres server はこれまでに何度も管理元が変わっているためです。
これが、サーバー上でエージェントを実行する本来の目的です。Claude Code のセッションは VPS 上の tmux 内で動作し、stdio サーバーもエージェントのすぐ隣で動作するため、ネットワーク往復なしでプロジェクトファイルやローカルサービスに直接アクセスできます。エージェントが write_file と read_text_file の両方を保持するようになったら、最小限の変更で動作させる方向に導く skill と組み合わせる価値があります。filesystem tool を使うと、大規模な書き換えも 2 行の修正と同じ程度に容易になるためです。この接続方法はローカルファイル以外にも拡張できます。すでに VPS 上で検索エンジンを運用している場合は、独自の SearXNG インスタンスを検索ツールとしてエージェントに渡すことができます。これにより検索クエリはサーバー内にとどまりますが、信頼できないページのテキストがコンテキストに直接取り込まれ、その後エージェントが処理することになります。
ステップ 2: リモート HTTP サーバーを構築する
stdio サーバーは親プロセスが終了すると停止します。また、クライアントごとに 1 回ずつ起動されます。そのため、同じマシン上で相互に処理を渡す 2 つの Claude Code セッションを実行すると、それぞれがツールの独立したコピーを使用します。すべてのクライアントで常時稼働するツール、共有運用ツール、データベースゲートウェイ、ノート PC と CI の両方から呼び出すツールが必要な場合は、HTTP トランスポートと実際のサービスが必要です。以下は、公式 SDK を使用し、1 つのツールを公開する最小構成の Python サーバーです。
# /opt/mcp-ops/server.py
from mcp.server.mcpserver import MCPServer
import subprocess
mcp = MCPServer("ops-tools")
@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", port=8000)host="127.0.0.1"に注意してください。サーバーは localhost のみで待ち受けるため、マシン外から直接アクセスすることはできません。認証がまだない段階では、これが適切な構成です。これは pip install "mcp[cli]" が現在取得する 2.x SDK です(2026-10-20 時点で 2.3.0)。FastMCP は MCPServer に名前が変更され、host と port はコンストラクターから run() に移動しました。また、以前の mcp.server.fastmcp の import は ModuleNotFoundError で失敗します。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 でラップトップから接続します。ssh -L 8000:127.0.0.1:8000 matt@vpsを実行し、クライアントの接続先を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 を生成し、これらのいずれかを前段に置かずにサーバー自体を 0.0.0.0 に bind しないでください。クライアントは 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} を展開します。変数が未設定の場合、Claude Code は空文字列に置換しません。claude mcp list で未設定の変数を示し、${MCP_TOKEN} という文字列をそのまま使用します。変数名は独自のものを選んでください。remote server の header では、Claude Code は ANTHROPIC_API_KEY など、自身および cloud provider の credential 変数を意図的に空として扱います。環境に存在する NPM_TOKEN など、その他の credential も同様です。
上記の防御策はいずれも、token をすでに保持している agent ではなく endpoint を保護します。もう一方の問題はそこにあります。クライアントが DeepSeek Harness の場合は、agent が呼び出せるツールを制限し、ツール出力に含まれる注入命令を検査する pluginがその側を保護します。
手順 6: MCP Inspector でデバッグする
サーバーが想定どおりに動作しない場合、エージェント内で推測してはいけません。公式の Web ベーステストクライアントである Inspector から直接操作します。stdio サーバーの場合は、エージェントが実行するのと同じコマンドを渡します。
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpInspector は http://localhost:6274 上の 1 つの Web サーバーから UI を提供し、MCP_INSPECTOR_API_TOKEN クエリ文字列を含む URL を表示します。アドレスを記憶で入力せず、そのリンクを開いてください。v2 以降(2026-10-20 時点の最新バージョンは 2.11.0)では、バックエンドが読み込みのたびに提供するページへトークンも埋め込むため、再読み込み後も動作します。このトークンは、プロセスを起動できる /api/* ルートを保護します。6277 の独立した v1 プロキシは廃止されました。ただし、Web サーバーだけがリスナーではありません。v2 は MCP Apps 用のサンドボックスも MCP_SANDBOX_PORT(デフォルトでは 6275)で起動し、専用の app-origin サーバーを MCP_APP_ORIGIN_PORT(デフォルトでは 6278)で起動します。このサーバーは、UI リソースで _meta.ui.domain を宣言する MCP App を提供します。SSH トンネル経由で Inspector に接続する場合や、コンテナ内で実行して Apps タブを使用する場合は、6274 とともに 6275 および 6278 も転送してください。接続し、Tools タブを開いてツールを選択し、実際の引数を入力して呼び出します。Inspector では動作するのにエージェントでは失敗する場合、問題はサーバーではなくクライアント設定にあります。リモート HTTP サーバーの場合は、コマンドを指定せずに Inspector を起動し、Streamable HTTP トランスポートと https://mcp.example.com/mcp を使用してサーバーを追加し、設定に Authorization ヘッダーを設定して接続します。これにより、エージェントを介在させる前に認証とプロキシが正しいことを最も迅速に確認できます。Inspector はサーバーカタログ内のヘッダーを記述どおりに保存するため、そのトークンは ~/.mcp-inspector/mcp.json にも保存されます。
サーバーを常に更新する
MCP は頻繁に更新されるため、スケジュールを決めてパッチを適用します。ピン留めされていない npx -y で起動した Node サーバーは、npx が解決したバージョンで動作します。ローカルプロジェクトにインストール済みのバージョンがあればそれを使い、なければ npm キャッシュにコピーをインストールして使います。-y はインストール確認を省略するだけで、更新を強制するものではありません。そのため、最新リリースを追跡できず、実行中のバージョンも確実には分かりません。テスト済みの正確なバージョンを固定し、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 を実行して更新します。メジャーバージョンを更新する前にリリースノートを確認します。Python SDK を 1.x から 2.x に移行すると、import 時に 1.x のコードが失敗します。SDK 自身の移行ガイドでも、移行が完了するまでは <2 の上限を "mcp[cli]<2" のように指定することを推奨しています。アップグレード時は、SDK が対象とする仕様リビジョンを確認します。SSE から Streamable HTTP への境界をまたぐ更新では、クライアントが要求すべきトランスポートが変わることがあります。
障害パターンと表示される文字列
エージェントにサーバーが失敗したと表示される。 claude mcp list は ✘ Failed to connect を出力し、現在のバージョンではその行に失敗の詳細も追加されます。claude --debug を実行すると、通常は Error: spawn npx ENOENT が表示されます。これは、コマンドがエージェントの PATH に存在しないことを示します。ランタイムが不足しているか、エージェントが探す場所にありません。Node がインストールされていない、npx が存在しない、または bare name で指定した virtualenv の Python が見つからない、といった原因があります。コマンドを絶対パスに修正するか、ランタイムをインストールしてから再接続します。
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 が unset の場合、Claude Code は claude mcp list で変数がないことを警告し、リテラル文字列を送信します。そのため nginx には Bearer ${MCP_TOKEN} が渡されます。Claude Code、クラウドプロバイダー、または環境にある別の認証情報と同じ名前の変数は、代わりに空として読み込まれることがあります。その場合、nginx には値のない Bearer が渡されます。変数を echo し、ヘッダーを再追加して、実際のバイト列が 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クライアントにツールとリソースを公開するプログラムです。AIモデルがツール自体を実行することはありません。AIモデルがクライアントに依頼し、クライアントがMCP serverを呼び出し、MCP serverが実行結果を返します。プロトコルが標準化されているため、Claude Code、Claude Desktop、Gemini CLIなど、準拠する任意のクライアントで1台のserverを利用できます。
stdio transportとHTTP transportの違いは何ですか?
stdio serverはクライアントが子プロセスとして起動し、stdin/stdout経由で通信します。そのため、1台のマシン上の1つのクライアントとともに動作し、終了します。ネットワークも認証も必要ありません。HTTP serverは長時間稼働するnetwork serviceで、複数のクライアントから同時に接続できます。そのため、TLSと認証が必要です。ローカルで単一ユーザーが使うツールにはstdioを使用し、共有または永続的に使用するものにはHTTP(現在のserverではStreamable HTTP)を使用します。
リモートMCP serverを保護するにはどうすればよいですか?
ファイル、データベース、shellへのtool accessを許可するものとして扱い、認証なしで公開してはいけません。localhostにbindし、SSH tunnelまたはprivate VPN経由で接続する方法が最も安全です。公開する必要がある場合は、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を修正するか、必要なものをinstallします。接続後にJSON parse errorで切断される場合、serverがstdoutへloggingしてJSON-RPC streamを破壊しています。すべてのloggingをstderrへ移してください。それ以外の場合は、MCP Inspectorで正確なcommandを実行します。MCP Inspectorはserverを分離した状態で動かすため、serverのbugとclient-configのbugを切り分けられます。