如何在 VPS 上部署 MCP 服务器:stdio 与 HTTP 模式详解
本指南详细介绍在 VPS 上部署 MCP 服务器的完整流程。涵盖 stdio 子进程通信与远程 HTTP 传输配置,重点解决 systemd 管理、TLS 加密及身份验证等关键安全痛点,助您为 AI 智能体构建稳定可靠的工具环境。
构建目标
在同一台 VPS 上部署两个可用的 MCP 环境。第一个是 stdio 服务器,即由 Claude Code 作为子进程启动并通过管道通信的文件系统或数据库工具。第二个是 remote HTTP 服务器,它作为长驻网络服务运行,由 systemd 管理,并置于配置了 TLS 的 Nginx 反向代理之后,可供任何指向它的 MCP 客户端访问。两者的安装过程都很简便。本指南的重点在于两个关键难点:保持 JSON-RPC 数据流的完整性,以及严禁将未经身份验证的工具端点暴露在公网。
MCP 的本质
Model Context Protocol 是一种标准,供 AI 客户端(如 Claude Code、Claude Desktop、VPS 上的 Gemini CLI 或您自己的脚本)调用外部工具并读取外部资源。模型本身不执行任何操作。它向客户端发出请求,客户端通过 JSON-RPC 2.0 与 MCP 服务器通信,服务器执行工具并将结果返回。当人们提到 智能体框架 (agent harness) 时,指的就是这个客户端:它是围绕模型运行的循环,负责管理工具列表、权限检查和会话状态;而 MCP 只是扩展其工具功能的方式。由于采用统一协议,您编写一次服务器,即可在所有支持 MCP 的客户端上运行。如果您对这种架构拆分感到陌生,尤其是对模型如何决定调用工具的机制不了解,建议在为这些服务器配置真实凭据前,花一小时阅读 智能体基础的分步指南。
该协议有两种传输方式,本指南的其余部分将按此划分:
- stdio。客户端将服务器作为子进程启动,并通过标准输入和标准输出交换以换行符分隔的 JSON-RPC 消息。无需网络、端口或身份验证,信任边界即为进程本身。几乎所有本地工具都采用这种方式。
- Streamable HTTP(及其旧版本 HTTP+SSE)。服务器作为长期运行的 Web 服务。客户端通过 HTTP 连接,服务器可以通过 Server-Sent Events 流式传输响应。当您需要与多个客户端共享一个服务器,或运行必须永久驻留在服务器上的工具时,请使用此方式。
当工具仅属于一台机器和一个用户时,请选择 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 服务器,TLS 需要一个解析到该 VPS 的域名。stdio 示例完全不需要 DNS。
- 512 MB 内存绰绰有余。 MCP 服务器是轻量级的 JSON-RPC 进程;内存开销取决于工具本身的操作(如数据库驱动、文件缓存),而非协议本身。
- 规范尚处于早期演进阶段。 2025-03-26 版本修订版用 Streamable HTTP 取代了 HTTP+SSE,并将 SSE 标记为弃用。目前 SSE 仍然有效,且许多服务器仍在使用,因此请将任何传输协议的锁定视为需要根据服务器发行说明重新核对的内容,而非绝对准则。
第 1 步:将 stdio 服务器接入 Claude Code
从 filesystem 服务器开始,它是官方维护且活跃的项目,仅需 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 会话驻留在 VPS 的 tmux 中,其 stdio 服务器直接在旁运行,能够直接访问项目文件和本地服务,无需网络往返。一旦代理同时拥有了 write_file 和 read_file,就值得配合 引导其进行最小可行性变更的技能,因为文件系统工具使得大规模重写与两行代码的修复成本一样低廉。同样的接入方式也适用于本地文件之外的场景:如果您已经在 VPS 上运行了搜索引擎,可以将 您自己的 SearXNG 实例作为搜索工具交给代理,这样查询请求会保留在您的服务器上,同时将不受信任的页面文本直接拉取到代理随后处理的上下文中。
第 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 中 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 和 Let's Encrypt on nginx 获取证书,然后编写 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 重新加载配置。如果你已运行多个容器,可以使用 Traefik 反向代理及自动 TLS 完成此任务,它会自动签发证书并根据主机名路由,你只需为 MCP 容器添加标签即可。无论采用哪种方式,反向代理现在是公网端口上唯一的入口,它指向的服务尚未加固。在任何地方注册该 URL 之前,请先完成安全加固。
第 5 步:本主题中最核心的安全规则
严禁暴露未经身份验证的 MCP 端点。 MCP 服务器并非只读 API。它授予工具访问权限,包括访问你的文件、数据库,有时甚至是 shell。在公网上开放 /mcp 等同于让陌生人拥有与你的 AI 智能体相同的权限:他们可以列出你的工具,然后直接调用。请将其视为未经身份验证的管理套接字,因为本质上就是如此。被窃取的令牌能带来多大危害取决于其背后的服务器:随 openGym 运动追踪器发布的只读 MCP 服务器 只能返回训练数据,而文件系统或 shell 工具则会直接交出服务器控制权。
按优先级排序,有三种防御措施:
- 不要发布它。 将服务器保持在
127.0.0.1,并通过 SSH 隧道从笔记本电脑访问:ssh -L 8000:127.0.0.1:8000 matt@vps,然后将客户端指向http://127.0.0.1:8000/mcp。这样没有任何内容会被暴露。 - 将其置于私有网络中。 绑定 自托管 WireGuard VPN 的隧道地址,仅允许 VPN 对等节点访问。公网看到的将是一个关闭的端口。
- 如果必须公开,请要求令牌。 正确的做法是使用 HTTP 传输原生支持的 MCP OAuth 流程。最起码的务实方案是在代理层检查共享的 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 生成令牌,在没有上述任何一种保护措施的情况下,严禁将服务器直接绑定到 0.0.0.0。客户端随后会将令牌作为标头发送。在 Claude Code 中:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'在 shell 中设置 MCP_TOKEN,以确保密钥不会以明文形式存入 .mcp.json,Claude Code 会在读取时从环境变量中解析 ${MCP_TOKEN}。
上述每种防御措施都是为了保护端点,而非保护已经持有令牌的智能体,后者是问题的另一半:如果你的客户端是 DeepSeek Harness,限制智能体可调用工具并扫描工具输出以防止指令注入的插件 可以覆盖这方面的安全需求。
第 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 无法找到。请将命令修改为绝对路径或安装运行时,然后重新连接。
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;(以及第 4 步中的其余代码块)添加到 location 中。使用 curl -N 对公共 URL 进行验证,你应该能看到事件数据是增量到达的,而不是在最后一次性输出。
身份验证被拒绝。 客户端报告 Error POSTing to endpoint (HTTP 401) 或直接显示 401 Unauthorized。这通常是因为缺少请求头、令牌错误,或者客户端读取配置时 Shell 变量为空。这是一个常见陷阱,因为如果变量未设置,${MCP_TOKEN} 会展开为空,导致 nginx 接收到 Bearer 且没有值。请使用 echo 打印变量,重新添加请求头,并核实字节流与 nginx if 中的令牌完全一致。
服务无法在 systemd 下启动。 journalctl -u mcp-ops 显示 ModuleNotFoundError: No module named 'mcp',ExecStart 指向了系统 Python 而非 venv 解释器。或者出现 Address already in use,说明其他进程占用了 8000 端口;请使用 sudo ss -ltnp | grep 8000 查找该进程。
FAQ
究竟什么是 MCP 服务器?
它是一个通过 Model Context Protocol(模型上下文协议)向 AI 客户端公开工具和资源的程序,使用 JSON-RPC 2.0 进行通信。AI 模型本身不会运行工具,而是向其客户端发出请求,客户端调用 MCP 服务器,由服务器执行并返回结果。由于该协议是标准化的,因此一个服务器可以与任何兼容的客户端配合使用,无论是 Claude Code、Claude Desktop 还是 Gemini CLI。
stdio 和 HTTP 传输方式有什么区别?
stdio 服务器由客户端作为子进程启动,并通过 stdin/stdout 进行通信,因此它与单台机器上的单个客户端共存亡,无需网络或身份验证。HTTP 服务器是一个长期运行的网络服务,可供多个客户端同时访问,因此它需要 TLS 和身份验证。对于本地单用户工具,请使用 stdio;对于任何共享或持久化的场景,请使用 HTTP(当前服务器支持流式 HTTP)。
如何保护远程 MCP 服务器的安全?
请假设它拥有访问您的文件、数据库或 shell 的权限,切勿在未经身份验证的情况下将其暴露。最佳做法是将其绑定到 localhost,并通过 SSH 隧道或私有 VPN 访问;如果必须公开,请将其置于强制执行 bearer token 或 MCP OAuth 流程的反向代理之后。使用 openssl rand -hex 32 生成令牌,在没有上述保护措施的情况下,切勿将服务器绑定到 0.0.0.0。
如何调试无法启动的服务器?
首先检查 claude mcp list 和 ✗ Failed to connect。如果出现 spawn ... ENOENT,则表示命令或运行时缺失,请修复路径或进行安装。如果连接后因 JSON 解析错误而断开,说明服务器正在向 stdout 输出日志并破坏了 JSON-RPC 流;请将所有日志输出重定向到 stderr。对于其他问题,请在 MCP Inspector 下运行该命令,它会在隔离环境中驱动服务器,以便您区分服务器缺陷与客户端配置错误。