SSD Nodes Learn
指南 Matt Connor作者: Matt Connor · 已更新 2026-07-24

如何在 VPS 部署 MCP Server 給 AI Agent 使用

本指南教學如何在 VPS 上架設 Model Context Protocol 伺服器,包含 stdio 與 remote HTTP 兩種傳輸方式,並深入探討如何透過 systemd、nginx 與 TLS 確保服務穩定與安全,解決 JSON-RPC 串流與身份驗證等核心挑戰。

您的建置目標

在單台 VPS 上建立兩套運作中的 MCP 設定。首先是 stdio 伺服器 — 這是一種檔案系統或資料庫工具,由 Claude Code 作為子程序啟動,並透過 pipe 進行通訊。接著是 remote HTTP 伺服器,它作為長駐網路服務執行於 systemd 後端,並透過 nginx 反向代理與 TLS 提供服務,任何 MCP 客戶端皆可連線。這兩者的安裝步驟都很簡短。本指南的大部分內容將著重於兩個核心挑戰:確保 JSON-RPC 串流的純淨,以及絕不將未經身分驗證的工具端點暴露於公開網路。

MCP 的本質

Model Context Protocol 是一種標準化方式,讓 AI 客戶端(例如 Claude Code、Claude Desktop、VPS 上的 Gemini CLI 或您自訂的腳本)能夠呼叫外部工具並讀取外部資源。模型本身不執行任何操作。模型會向客戶端發出請求,客戶端透過 JSON-RPC 2.0 與 MCP server 通訊,最後由 server 執行工具並回傳結果。由於使用統一協定,您只需撰寫一次 server,即可與所有支援 MCP 的客戶端相容。

本指南將根據兩種傳輸方式(transports)進行說明:

  • stdio。客戶端將 server 作為子程序(child process)啟動,並透過標準輸入(standard input)與標準輸出(standard output)交換以換行符號分隔的 JSON-RPC 訊息。無需網路、連接埠或身份驗證,信任邊界即為該程序本身。幾乎所有本地工具都採用此方式。
  • Streamable HTTP(以及舊版的 HTTP+SSE)。server 為持續運行的 Web 服務。客戶端透過 HTTP 連線,server 可透過 Server-Sent Events 串流回傳回應。此方式適用於讓單一 server 供多個客戶端使用,或執行必須永久運行在主機上的工具。

若工具僅限於單一機器與單一使用者使用,請選擇 stdio。若為共享服務,請選擇 HTTP。

前置條件與注意事項

假設您擁有一台全新的 Ubuntu 24.04 KVM VPS,並具備 root 或 sudo 權限。除此之外:

  • 伺服器所使用的執行環境。 大多數參考伺服器使用 Node 或 Python。Ubuntu 24.04 內建 Node 18,但許多目前的 MCP 套件需要 Node 20 或更高版本,因此請從 NodeSource 或 nvm 安裝最新的 LTS 版本,不要直接依賴 apt。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 標記為已棄用(deprecated)。由於 SSE 仍可運作且仍有許多伺服器支援,請將任何傳輸層的限制視為需根據伺服器版本說明重新確認的項目,而非絕對準則。

Step 1: 將 stdio server 接入 Claude Code

首先從 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 的 flag。這會在專案根目錄寫入一個 .mcp.json

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/home/matt/projects/api"
      ]
    }
  }
}

目前尚未執行任何程序。下次在此目錄啟動 Claude Code 時,agent 會讀取 .mcp.json,並將 npx -y @modelcontextprotocol/server-filesystem ... 作為子程序 (child process) 啟動,接著透過該程序的 stdin/stdout 進行 MCP handshake。確認是否成功:

claude mcp list

運作正常的 server 會印出其指令與綠色勾選符號 —— filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected。在 session 內,使用 /mcp slash command 可列出該 server 提供的工具 (read_file, write_file, list_directory),且 agent 現在可以在你允許的路徑上呼叫這些工具。資料庫工具的結構相同 —— 只需更換套件並將連線字串作為最後一個參數傳入 —— 但請務必至該 server 的官方 repository 確認目前的套件名稱,因為參考用的 Postgres server 已多次更換維護者。

這正是直接在主機上執行 agent 的核心目的:Claude Code session 在 tmux 內的 VPS 中執行,其 stdio servers 會與之並行運作,並能直接存取專案檔案與本地服務,無需經過網路往返。

Step 2: 建立遠端 HTTP server

stdio server 會隨父程序結束。若需要一個能持續為所有 client 提供服務的工具(例如共用的維運工具、資料庫閘道,或同時供筆電與 CI 呼叫的工具),則必須使用 HTTP 傳輸協定並建立正式服務。以下是使用官方 SDK 建立的最小化 Python server,僅公開一個 tool:

# /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,外部無法直接連線,這在尚未建立驗證機制時是正確的做法。請將其安裝在獨立的 virtualenv 中,以確保 systemd 擁有穩定的 interpreter 路徑:

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 維持運行

若 Agent 嘗試呼叫時工具已停止運作,其影響比沒有工具更糟。請撰寫 /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,否則程序會以 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/mcp

status 的結果應為 active (running)curl 會回傳 HTTP/1.1 400 Bad Request 並在主體中包含 JSON-RPC 錯誤 — 這是因為請求缺少 session 與有效的 JSON 載荷 — 而這正是預期結果:這證明了 port 有回應且符合協定。若出現 Connection refused 或空回應,代表程序未綁定在預期位置;請參閱 journalctl -u mcp-ops -n 50

Step 4: 設定 TLS 與反向代理

伺服器目前僅監聽 localhost。若要從外部連線,須在 nginx 終止 TLS 並進行反向代理。請安裝 nginx,並參考 Certbot and Let's Encrypt on 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 重新載入設定。若您已運行大量容器,可以使用 Traefik reverse proxy with automatic TLS 來自動完成此作業 — 它會自動核發憑證並依據 hostname 進行路由,您只需為 MCP 容器新增 labels。無論使用哪種方式,反向代理現在是唯一對外開放公用連接埠的服務,且它指向一個尚未受保護的服務。在將 URL 註冊到任何地方之前,請務必完成安全性設定。

Step 5: 核心安全準則

絕不要公開未經身分驗證的 MCP 端點。 MCP 伺服器並非僅供讀取的 API。它提供工具存取權限,包含檔案、資料庫,有時甚至包含 shell。在公開網際網路開放 /mcp,等同於讓陌生人擁有與你的 AI agent 相同的權限:他們可以列出你的工具並直接呼叫。請將其視為未經身分驗證的 admin socket,因為事實正是如此。

三種防禦方案(按優先順序排列):

  1. 不要對外發布。 將伺服器保留在 127.0.0.1,並透過 SSH tunnel 從你的筆記型電腦進行連線:先執行 ssh -L 8000:127.0.0.1:8000 matt@vps,然後將 client 指向 http://127.0.0.1:8000/mcp。這樣任何內容都不會暴露在公網。
  2. 部署於私有網路。 綁定 self-hosted WireGuard VPN 的 tunnel 位址,並僅允許 VPN peer 連線。公網看到的將是關閉的 port。
  3. 若必須公開,請要求使用 token。 最正確的做法是使用 HTTP transport 原生支援的 MCP OAuth 流程。務實的最低要求是在 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,且在伺服器前方沒有上述任何防禦機制前,絕不要將伺服器本身綁定至 0.0.0.0。接著,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}

Step 6: 使用 MCP Inspector 進行除錯

當伺服器運作異常時,請勿嘗試從 agent 內部進行猜測。請直接使用官方網頁測試工具 Inspector 進行測試。針對 stdio 伺服器,請提供與 agent 相同的執行指令:

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

該工具會在 http://localhost:6274 啟動 UI(新版本會印出包含 MCP_PROXY_AUTH_TOKEN 查詢字串的 URL,請使用該完整連結,否則 UI 會拒絕連線),並在 6277 啟動 proxy。點擊 Connect,接著點擊 List Tools,最後使用實際參數點擊 Call Tool。若在 Inspector 中運作正常但在 agent 中失敗,代表問題出在 client 設定而非伺服器。針對遠端 HTTP 伺服器,請選擇 Streamable HTTP 傳輸協定,輸入 https://mcp.example.com/mcp,加入 Authorization header 並連線。這是排除 agent 介入前,驗證驗證機制與 proxy 是否正確最快的方法。

保持伺服器版本更新

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 的界限,可能會改變客戶端必須請求的傳輸方式。

Failure modes, with the strings you will see

The agent shows the server failed. claude mcp list 會印出 ✗ Failed to connect,且 TUI 會回報 MCP server 'filesystem' failed to start。執行 claude --debug 通常會看到 Error: spawn npx ENOENT — 該指令不在 agent 的 PATH 中。執行環境缺失或路徑錯誤:Node 未安裝、缺少 npx,或是 Python virtualenv 使用了簡寫名稱。請將指令改為絕對路徑或安裝執行環境,然後重新連線。

A stdio server connects, then instantly drops. Client 會記錄 JSON 解析錯誤 — 例如 Unexpected token 'S', "Server sta"... is not valid JSONFailed to parse message。原因固定為:server 將日誌寫入了 stdout。在 stdio 模式下,stdout 即是 JSON-RPC 通道,任何多餘的文字都會導致串流損毀並使握手失敗。在 Node 中,console.log 會輸出至 stdout — 請改用 console.error。在 Python 中,直接使用 print() 會輸出至 stdout — 請將 logging 設定為 sys.stderr 來寫入日誌,或傳遞 file=sys.stderr。規則是絕對的:在 stdio 模式下,stdout 僅限 JSON-RPC,所有人類可讀內容必須輸出至 stderr。

A remote server times out or closes mid-handshake. Client 會出現 MCP error -32000: Connection closed 錯誤,或 Inspector 會卡在 Connect 狀態且無法列出 tools。若位於 nginx 後方,這是緩衝 (buffering) 造成的:proxy 攔截了 SSE 串流而未即時沖刷 (flush),導致 client 持續等待永遠不會抵達的回應。請在 location 中加入 proxy_buffering off;(以及 Step 4 中的其餘區塊)。使用 curl -N 對公用 URL 進行測試以確認 — 你應該看到事件資料逐一傳入,而非全部在最後才一次出現。

Auth is rejected. Client 會回報 Error POSTing to endpoint (HTTP 401) 或直接顯示 401 Unauthorized。原因為 Header 缺失、Token 錯誤,或是 client 讀取 config 時該 shell 變數為空值 — 這是一個常見陷阱,因為若變數未設定,${MCP_TOKEN} 會展開為空值,導致 nginx 看到沒有值的 Bearer 。請使用 echo 檢查變數,重新加入 Header,並確認其位元組與 nginx if 中的 Token 完全一致。

The service will not start under systemd. journalctl -u mcp-ops 顯示 ModuleNotFoundError: No module named 'mcp'ExecStart 指向系統 Python 而非 venv 解譯器。或是 Address already in use — 另一個程序佔用了 8000 port;請使用 sudo ss -ltnp | grep 8000 找出該程序。

FAQ

什麼是 MCP server?

這是一個透過 Model Context Protocol 並使用 JSON-RPC 2.0,向 AI client 提供 tools 與 resources 的程式。AI model 本身不會執行 tool;它會向 client 發出請求,由 client 呼叫 MCP server,最後由 server 執行並回傳結果。由於該協定具備標準化特性,任何符合規範的 client(例如 Claude Code、Claude Desktop 或 Gemini CLI)皆可使用同一個 server。

stdio 與 HTTP transport 有何差異?

stdio server 由 client 作為 child process 啟動,並透過 stdin/stdout 進行通訊。因此,它與單一機器上的單一 client 綁定,且不需要網路或 authentication。HTTP server 則是長駐的網路服務,可供多個 client 同時存取,因此需要 TLS 與 authentication。若為本地端單人使用的工具,請使用 stdio;若需共享或持久化服務,請使用 HTTP(目前 server 使用 Streamable HTTP)。

如何保護遠端 MCP server 的安全性?

假設該 server 擁有存取檔案、資料庫或 shell 的權限,絕不可在未經驗證的情況下將其公開。最佳做法是將其綁定至 localhost,並透過 SSH tunnel 或私有 VPN 進行存取;若必須公開,請將其置於反向代理(reverse proxy)後方,並強制執行 bearer token 或 MCP OAuth 流程。請使用 openssl rand -hex 32 產生 token,且在未配置上述機制的情況下,絕不要將 server 綁定至 0.0.0.0

如何除錯無法啟動的 server?

首先檢查 claude mcp list✗ Failed to connect 搭配 spawn ... ENOENT 表示指令或 runtime 缺失,請修正 path 或進行安裝。若連線後因 JSON parse error 而中斷,代表 server 正在將日誌輸出至 stdout 並破壞 JSON-RPC stream;請將所有日誌移至 stderr。若遇到其他問題,請在 MCP Inspector 下執行該指令,透過隔離環境驅動 server,以判別問題是出在 server bug 還是 client-config bug。