SSD Nodes Learn
ガイド Matt Connor著者 Matt Connor ・更新日 2026-07-24

VPSでMCPサーバーを運用する方法|stdioとHTTPの構築手順

VPS上でModel Context Protocolサーバーを動かし、AIエージェントに強力なツール機能を持たせる方法を解説します。stdioによるローカル実行から、systemdとnginxを用いたTLS対応のremote HTTPサーバー構築まで、認証や通信の注意点を含めて具体的に説明します。

作成するもの

1つの VPS 上に2つの MCP セットアップを構築します。1つ目は stdio サーバーです。これは filesystem や database のツールであり、Claude Code が child process として起動し、pipe を介して通信します。2つ目は remote HTTP サーバーです。これは systemd の配下で常駐する network service として動作し、TLS を備えた nginx reverse proxy を介して、任意の MCP client からアクセス可能です。どちらのインストール手順も簡潔です。本ガイドの主要な内容は、JSON-RPC stream の整合性を保つこと、および認証のない tool endpoint を公開インターネットに公開しないことです。

MCPの概要

Model Context Protocolは、AIクライアント(Claude Code、Claude Desktop、VPS上のGemini CLI、または自作スクリプト)が、外部ツールを呼び出し、外部リソースを読み取るための標準的な方法です。モデル自体は何も実行しません。モデルがクライアントに要求を出し、クライアントがMCP serverに対してJSON-RPC 2.0で通信します。その後、serverがツールを実行し、結果を返します。プロトコルが統一されているため、一度作成したserverは、MCPに対応しているすべてのクライアントで使用可能です。

本ガイドでは、以下の2つのtransportに基づいて解説を進めます。

  • stdio. クライアントがserverを子プロセスとして起動します。標準入力(stdin)と標準出力(stdout)を介して、改行区切りのJSON-RPCメッセージをやり取りします。ネットワーク、ポート、認証は不要です。信頼境界はプロセスそのものとなります。ほとんどのローカルツールがこの方式を採用しています。
  • Streamable HTTP(および旧式のHTTP+SSE)。serverは常駐するWebサービスです。クライアントはHTTP経由で接続し、serverはServer-Sent Eventsとしてレスポンスをストリーム送信できます。この方式により、1つのserverを複数のクライアントで共有したり、サーバー上に常駐させる必要があるツールを実行したりできます。

ツールが単一のマシンとユーザーに紐付く場合はstdioを選択してください。共有サービスとして利用する場合はHTTPを選択してください。

前提条件と注意点

rootまたはsudo権限を持つ、新規インストール済みのUbuntu 24.04 KVM VPSを想定します。それ以外に以下の条件が必要です。

  • サーバーが記述されているランタイム。 多くのリファレンスサーバーはNodeまたはPythonを使用します。Ubuntu 24.04にはNode 18が搭載されていますが、現在の多くのMCPパッケージはNode 20以降を必要とします。aptに頼らず、NodeSourceまたはnvmから最新のLTSをインストールしてください。Python 3.12は既にインストール済みです。
  • ドメインとDNSのA record。 リモートHTTPサーバーに対してのみ必要です。TLSには、このVPSに解決される名前が必要です。stdioの例ではDNSは一切不要です。
  • 512 MBのRAMがあれば十分です。 MCPサーバーは軽量なJSON-RPCプロセスです。メモリ消費量は、プロトコルではなく、使用するツール(データベースドライバ、ファイルキャッシュなど)に依存します。
  • 仕様は新しく、更新され続けています。 2025-03-26の改訂により、HTTP+SSEがStreamable HTTPに置き換わり、SSEは非推奨となりました。SSEはまだ動作し、多くのサーバーが引き続き対応しています。そのため、トランスポート層の仕様は絶対的なものと考えず、サーバーのリリースノートで再確認してください。

Step 1: Claude Code に stdio server を組み込む

まずは filesystem server から開始します。これは公式のツールであり、継続的にメンテナンスされています。Node があれば動作します。以下のコマンドを実行すると、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 ハンドシェイクを実行します。正しく設定されたか確認してください。

claude mcp list

正常なサーバーは、コマンドと緑色のチェックマーク(filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected)を表示します。セッション内で /mcp スラッシュコマンドを使用すると、サーバーが提供するツール(read_file, write_file, list_directory)が一覧表示されます。エージェントは、許可されたパスに対してこれらのツールを呼び出せるようになります。データベースツールも同様の構成です。パッケージを入れ替え、最後の引数に接続文字列を渡してください。ただし、Postgres サーバーのパッケージ名は変更されている可能性があるため、必ずサーバーのリポジトリで最新のパッケージ名を確認してください。

エージェントをローカルマシンで実行する最大の利点はこれです。Claude Code セッションは tmux 内の VPS 上で動作し、stdio server はプロジェクトファイルやローカルサービスに直接アクセスできる状態で、ネットワークの遅延なしにエージェントのすぐ隣で動作します。

Step 2: リモート HTTP server を構築する

stdio server は親プロセスが終了すると停止します。すべての client に対して稼働し続けるツール(共有 ops tool、database gateway、自身の laptop や CI から呼び出すものなど)が必要な場合は、HTTP transport と実体となる service が必要です。以下は、公式 SDK を使用して 1 つの tool を公開する最小構成の Python server です。

# /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" に注意してください。この server は localhost にのみ bind されます。外部から直接アクセスすることはできません。これは、auth が実装される前の状態として適切な設定です。systemd が stable な interpreter path を使用できるよう、独自の 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]"

Step 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.target

ExecStart 内の venv Python への絶対パスの指定は必須です。/usr/bin/python3 を指定してください。指定しない場合、システム側の interpreter は pip install を認識できないため、プロセスは ModuleNotFoundError: No module named 'mcp' で開始されます。有効化して確認を行います:

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/mcp

status の出力は active (running) である必要があります。curl は、ボディに JSON-RPC エラーを含む HTTP/1.1 400 Bad Request を返します。これは、リクエストに session と有効な JSON payload が含まれていないためです。これは意図通りの動作です。ポートが応答し、プロトコルに従っていることが証明されます。Connection refused または空のレスポンスが返る場合は、プロセスが想定した場所にバインドされていません。journalctl -u mcp-ops -n 50 を確認してください。

Step 4: TLSとリバースプロキシを導入する

サーバーはlocalhostで待機しています。外部からアクセスするには、nginxでTLSを終端し、内部へプロキシする必要があります。nginxをインストールし、CertbotとLet's Encryptを使用したnginxでの証明書取得を行い、locationブロックを記述してください。重要な点はbufferingを無効にすることです。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コンテナにlabelを追加するだけです。いずれの方法でも、リバースプロキシのみが公開ポートで動作し、まだ保護されていないサービスを指している状態になります。URLをどこかに登録する前に、そのセキュリティを確保してください。

Step 5: このトピックにおいて最も重要なセキュリティルール

認証されていない MCP endpoint を決して公開しないでください。 MCP server は read-only な API ではありません。ファイル、データベース、時には shell への tool access を許可します。パブリックインターネット上に開かれた /mcp は、AI agent と同等の権限を持つ第三者と同じです。彼らは tool を列挙し、それらを呼び出します。認証されていない admin socket と全く同じものとして扱ってください。それが実態だからです。

推奨順に3つの防御策を挙げます:

  1. 公開しない。 server を 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 に設定します。これにより、外部に一切公開されません。
  2. プライベートネットワークに配置する。 self-hosted WireGuard VPN の tunnel address に bind し、VPN peer のみがアクセスできるようにします。パブリックインターネットからは、ポートが閉じているように見えます。
  3. 公開が必要な場合は、token を必須にする。 最善の策は、HTTP transport がネイティブでサポートしている MCP OAuth flow です。現実的な最小構成は、proxy でチェックを行う shared 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}'

secret が .mcp.json に plaintext で保存されないよう、shell で MCP_TOKEN を設定してください。Claude Code は読み取り時に環境変数から ${MCP_TOKEN} を展開します。

Step 6: MCP Inspector によるデバッグ

サーバーが正常に動作しない場合、agent の内部から推測しないでください。公式の Web ベース テスト クライアントである Inspector を使用して、直接操作してください。stdio サーバーの場合は、agent が実行しているものと同じコマンドを入力します。

npx @modelcontextprotocol/inspector \
  npx -y @modelcontextprotocol/server-filesystem /tmp

http://localhost:6274 で UI が起動します(最新バージョンでは MCP_PROXY_AUTH_TOKEN クエリ文字列を含む URL が表示されます。そのリンクを正確に使用してください。そうしないと UI が接続を拒否します)。また、6277 ポートでプロキシが起動します。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 の virtualenv が名前だけで参照されている場合に発生します。コマンドを絶対パスに修正するか、ランタイムをインストールしてから再接続してください。

stdio サーバーが接続直後に切断される。 クライアントに Unexpected token 'S', "Server sta"... is not valid JSONFailed to parse message といった JSON パースエラーが記録されます。原因は常に同じです。サーバーが stdout にログを出力しています。stdio モードでは stdout は JSON-RPC チャネルそのものであるため、余計なテキストがストリームを破損させ、ハンドシェイクが失敗します。Node の場合、console.log は stdout に出力されます。console.error を使用してください。Python の場合、通常の print() は stdout に出力されます。sys.stderr に設定された logging でログを書き出すか、file=sys.stderr を渡してください。stdio モードの原則は、「stdout には JSON-RPC のみ、人間向けの出力はすべて stderr に」です。

リモートサーバーがタイムアウトする、またはハンドシェイク中に切断される。 クライアントは MCP error -32000: Connection closed で失敗するか、Inspector が Connect のまま停止し、ツールの一覧が表示されません。nginx の背後で動作している場合、バッファリングが原因です。プロキシが SSE ストリームをフラッシュせずに保持するため、クライアントは応答を待ち続けます。locationproxy_buffering off;(および Step 4 の残りのブロック)を追加してください。パブリック URL に対して curl -N を実行して確認してください。イベントデータが最後にまとめてではなく、逐次届くはずです。

認証が拒否される。 クライアントに Error POSTing to endpoint (HTTP 401) または 401 Unauthorized と表示されます。ヘッダーの欠落、トークンの誤り、あるいはクライアントが設定を読み込む際にシェル変数が空であったことが原因です。変数が未設定の場合、${MCP_TOKEN} は空になり、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とは具体的に何ですか?

MCP serverは、JSON-RPC 2.0を使用して、AI clientにtoolやresourceを提供するプログラムです。AI model自体がtoolを実行することはありません。AI modelがclientに依頼し、clientがMCP serverを呼び出し、serverが実行結果を返します。プロトコルが標準化されているため、Claude Code、Claude Desktop、Gemini CLIなど、準拠したclientであればどのclientとも動作します。

stdioとHTTP transportの違いは何ですか?

stdio serverは、clientによってchild processとして起動されます。stdin/stdoutを介して通信するため、単一のmachine上の単一のclientに紐付き、networkや認証を必要としません。HTTP serverは、複数のclientが同時にアクセス可能な常駐型network serviceです。そのため、TLSと認証が必要です。ローカル環境の単一ユーザー向けtoolにはstdioを使用してください。共有環境や永続的な用途にはHTTP(現在のserverではStreamable HTTP)を使用してください。

リモートのMCP serverをどのように保護しますか?

MCP serverはファイル、database、shellへのアクセス権を付与するため、認証なしで公開しないでください。localhostにバインドし、SSH tunnelやprivate VPN経由でアクセスさせるのが最善です。公開が必要な場合は、bearer tokenまたはMCP OAuth flowを強制するreverse proxyの背後に配置してください。openssl rand -hex 32でtokenを生成してください。また、これらの保護手段なしに、serverを0.0.0.0にバインドしないでください。

起動しないserverをどのようにデバッグしますか?

まず claude mcp list を確認してください。✗ Failed to connectspawn ... ENOENT の場合、commandまたはruntimeが不足しています。pathを修正するか、インストールしてください。接続後にJSON parse errorで切断される場合は、serverがstdoutにログを出力し、JSON-RPC streamを破損させています。すべてのログをstderrに移動してください。その他の問題については、MCP Inspectorでコマンドを直接実行してください。MCP Inspectorはserverを隔離して動作させるため、serverのバグかclient-configのバグかを判別できます。