如何在 VPS 上執行 MCP 伺服器
在自己的 VPS 執行 MCP 伺服器,掌握 stdio 與遠端 HTTP 傳輸、systemd、TLS、驗證設定,並排查 JSON-RPC 串流與公開端點的常見錯誤。
建置內容
在同一台 VPS 上建置 2 種可運作的 MCP 設定。第一種是 stdio 伺服器,例如檔案系統或資料庫工具。Claude Code 會將它作為子程序啟動,並透過管線與它通訊。第二種是遠端 HTTP 伺服器。它會在 systemd 與具備 TLS 的 nginx 反向代理後方,作為長時間執行的網路服務運作。任何指向該服務的 MCP 用戶端都能連線。任一種設定的安裝程序都不長。本指南大部分內容著重於 2 個實際容易出問題的部分:保持 JSON-RPC 串流內容乾淨,以及絕不將未經驗證的工具端點暴露在公開網際網路上。
MCP 實際上是什麼
Model Context Protocol 是一種標準方式,讓 AI client、Claude Code、Claude Desktop、VPS 上的 Gemini CLI 或你自行撰寫的 script 呼叫外部工具並讀取外部資源。模型本身不會執行任何操作。模型會向 client 提出要求,由 client 透過 JSON-RPC 2.0 與 MCP server 通訊,再由 server 執行工具並傳回結果。這個 client 就是人們所稱的 agent harness:它是圍繞模型運作的迴圈,負責管理工具清單、權限檢查與工作階段狀態;MCP 只是用來擴充其中工具部分的方式。採用同一套協定後,你只需撰寫一次 server,就能搭配所有支援 MCP 的 client 使用。如果你不熟悉這種分工,尤其不清楚模型究竟如何決定是否使用工具,建議先花一小時閱讀 循序了解 agent 基礎,再將實際憑證交給這類 server。
這裡有兩種傳輸方式,本指南其餘內容也會依此分為兩部分:
- stdio。 client 會將 server 以子程序啟動,並透過標準輸入與標準輸出交換以換行分隔的 JSON-RPC 訊息。不使用網路、連接埠或驗證;信任邊界就是該程序本身。幾乎所有本機工具都採用這種方式。
- Streamable HTTP(以及較舊的 HTTP+SSE)。server 是長時間執行的 Web 服務。client 透過 HTTP 連線,而 server 可透過 Server-Sent Events 串流傳回回應。這適合讓多個 client 共用一個 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 記錄。 遠端 HTTP 伺服器需要使用可解析至此 VPS 的名稱來設定 TLS。stdio 範例完全不需要 DNS。
- 512 MB RAM 已經足夠。 MCP 伺服器是精簡的 JSON-RPC 程序;記憶體用量取決於工具實際使用的元件,例如資料庫驅動程式或檔案快取,而不是通訊協定本身。
- 規格仍在發展中。 2025-03-26 版本以 Streamable HTTP 取代 HTTP+SSE,並將 SSE 標示為已棄用。SSE 仍可運作,許多伺服器也仍支援它。因此,請將任何固定的傳輸方式視為需要依伺服器版本說明重新確認的設定,不要視為絕對規則。
步驟 1:將 stdio 伺服器接入 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 的旗標。此指令會在專案根目錄建立 .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_file、write_file、list_directory),代理程式現在可以使用這些工具存取你允許的路徑。資料庫工具的形式相同,只要替換套件,並將 connection string 作為最後一個引數傳入即可。不過,請查看該伺服器自己的 repository 以確認目前的套件名稱,因為 reference Postgres server 的維護方曾多次變更。
這正是讓代理程式在該主機上執行的重點:Claude Code 工作階段會在 VPS 的 tmux 中執行,其 stdio servers 也在旁邊執行,可直接存取專案檔案與本機服務,不必經過網路往返。代理程式同時具備 write_file 與 read_file 後,建議搭配引導它採用可行的最小變更的 skill,因為 filesystem tool 會讓大幅改寫與兩行修正的成本完全相同。同樣的接法也能延伸到本機檔案以外:如果 VPS 上已經執行 search engine,就可以將自己的 SearXNG instance 提供給代理程式作為 search tool。如此一來,查詢會留在你的主機上,但不受信任的頁面文字會直接載入代理程式接著採取行動的 context。
步驟 2:建置遠端 HTTP 伺服器
stdio 伺服器會隨父程序結束,且每個用戶端都會各自啟動一個執行個體。因此,如果您在同一台主機上執行兩個會彼此交接工作的 Claude Code 工作階段,每個工作階段都會取得該工具的私有副本。若要讓工具持續為所有用戶端提供服務,例如共用的維運工具、資料庫閘道,或同時供筆記型電腦與 CI 呼叫的服務,就需要使用 HTTP 傳輸與實際的服務。以下是使用官方 SDK 的最小 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,主機外部無法直接連線,這正是尚未建立驗證機制前所需要的設定。請將它安裝在獨立的 virtualenv 中,讓 systemd 使用穩定的直譯器路徑:
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會以 HTTP/1.1 400 Bad Request 回應,且本文包含 JSON-RPC 錯誤;請求沒有工作階段,也沒有有效的 JSON 內容。這正是預期結果:它證明該埠能回應,且使用正確的通訊協定。Connection refused或空白回應表示程序未在你預期的位置繫結;請查看 journalctl -u mcp-ops -n 50。
步驟 4:在前方配置 TLS 與反向代理
伺服器監聽 localhost。若要從任何位置存取,請在 nginx 終止 TLS,再將請求代理至內部。安裝 nginx,使用 Certbot 與 nginx 上的 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 反向代理 可以代為處理相同工作。它會簽發憑證並依主機名稱路由,您只需為 MCP 容器加入 labels。無論採用哪種方式,反向代理現在都是公開連接埠上唯一可存取的服務,但它指向的服務尚未完成安全設定。請先修正此問題,再將 URL 註冊到任何位置。
第 5 步:主導本主題的安全規則
絕對不要公開未經驗證的 MCP endpoint。 MCP server 不只是唯讀 API。它會授予工具存取權,讓工具能存取你的檔案、資料庫,有時甚至是 shell。公開網際網路上的開放 /mcp,等同於讓陌生人取得與 AI agent 相同的存取範圍:對方會先列出你的工具,再呼叫這些工具。請將它視為未經驗證的管理 socket,因為它本質上就是這種介面。遭竊的 token 能造成多大影響,也取決於其後端的 server:隨 openGym workout tracker 一起提供的唯讀 MCP server 只能回傳訓練資料,但檔案系統或 shell 工具則可能直接交出整台主機。
以下 3 種防禦措施,依建議優先順序排列:
- 不要公開它。 將 server 留在
127.0.0.1,再透過 SSH tunnel 從筆電連線:ssh -L 8000:127.0.0.1:8000 matt@vps,然後讓 client 指向http://127.0.0.1:8000/mcp。任何服務都不會公開。 - 將它放在私有網路。 綁定 自行託管的 WireGuard VPN tunnel 位址,只允許 VPN peer 連線。公開網際網路只能看到關閉的埠。
- 如果必須公開,要求 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,除非前方已配置上述其中一項防護。接著,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}。
以上每項防禦措施都是保護 endpoint,而不是保護已持有 token 的 agent。這是問題的另一半:如果你的 client 是 DeepSeek Harness,可限制 agent 能呼叫哪些工具,並掃描工具輸出中的注入指令的 plugins 就能處理這一側。
步驟 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 中失敗,問題就在用戶端設定,而不是伺服器。對於遠端 HTTP 伺服器,選取 Streamable HTTP 傳輸,輸入 https://mcp.example.com/mcp,加入 Authorization 標頭,然後連線。這是在 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 的邊界時,客戶端必須要求的傳輸方式可能會改變。
失敗模式與你會看到的訊息
代理程式顯示伺服器失敗。 claude mcp list 輸出 ✗ Failed to connect,TUI 顯示 MCP server 'filesystem' failed to start。執行 claude --debug,通常會看到 Error: spawn npx ENOENT,表示該命令不在代理程式的 PATH 中。執行環境遺失,或不在代理程式搜尋的位置:未安裝 Node、缺少 npx,或以裸名稱參照 virtualenv Python。將命令修正為絕對路徑,或安裝執行環境,然後重新連線。
stdio 伺服器連線後立即中斷。 用戶端日誌會記錄 JSON 解析錯誤,例如 Unexpected token 'S', "Server sta"... is not valid JSON 或 Failed to parse message。原因一律相同:伺服器將日誌列寫入 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 資料流,沒有立即刷新,因此用戶端一直等待不會到達的回應。將 proxy_buffering off;(以及 Step 4 中該區塊的其餘設定)加入 location。使用 curl -N 測試公開 URL,確認事件資料是逐步到達,而不是在最後一次全部出現。
驗證遭拒。 用戶端會回報 Error POSTing to endpoint (HTTP 401),或直接顯示 401 Unauthorized。可能是缺少標頭、token 錯誤,或用戶端讀取設定時 shell 變數為空。這是常見陷阱,因為變數未設定時,${MCP_TOKEN} 會展開為空,nginx 接著會看到沒有值的 Bearer 。輸出該變數、重新加入標頭,並確認實際位元組與 nginx if 中的 token 完全相同。
服務無法在 systemd 下啟動。 journalctl -u mcp-ops 顯示 ModuleNotFoundError: No module named 'mcp',ExecStart 指向 system Python,而不是 venv 解譯器。或者 Address already in use 顯示另一個程序已占用 8000;使用 sudo ss -ltnp | grep 8000 找出該程序。
FAQ
什麼是 MCP server?
MCP server 是透過 Model Context Protocol 使用 JSON-RPC 2.0,向 AI client 提供工具與資源的程式。AI model 不會直接執行工具,而是向 client 發出要求,由 client 呼叫 MCP server,再由 server 執行並回傳結果。由於此協定是標準,任何相容的 client 都能使用同一個 server,包括 Claude Code、Claude Desktop 或 Gemini CLI。
stdio transport 與 HTTP transport 有何不同?
stdio server 由 client 以子程序方式啟動,並透過 stdin/stdout 通訊。因此,它只會在單一機器上隨一個 client 執行與結束,不需要網路或驗證。HTTP server 是長時間執行的網路服務,可同時接受多個 client 的連線,因此需要 TLS 與驗證。單機、單一使用者的工具適合使用 stdio;共用或需要持續執行的服務則使用 HTTP(目前的 server 使用 Streamable HTTP)。
如何保護遠端 MCP server?
請假設它能存取您的檔案、資料庫或 shell,絕對不要在未驗證的情況下公開。最佳做法是讓它只繫結 localhost,再透過 SSH tunnel 或私有 VPN 存取;如果必須公開,請將它置於可強制執行 bearer token 或 MCP OAuth flow 的 reverse proxy 後方。使用 openssl rand -hex 32 產生 token,且不要在沒有其中一項保護機制的情況下,將 server 繫結至 0.0.0.0。
如何排查無法啟動的 server?
先檢查 claude mcp list;使用 spawn ... ENOENT 時,如果顯示 ✗ Failed to connect,表示缺少 command 或 runtime,請修正路徑或安裝相應元件。如果 server 能建立連線,之後卻因 JSON parse error 中斷,表示 server 將 log 寫入 stdout,導致 JSON-RPC stream 損壞;請將所有 log 改寫至 stderr。其他情況請在 MCP Inspector 下執行完全相同的 command。MCP Inspector 會隔離執行 server,讓您判斷問題是 server bug 還是 client 設定錯誤。