SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

如何在 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

先使用文件系统服务器。它是官方服务器,维护活跃,除 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_text_file、write_file、list_directory),代理现在可以在你允许的路径上调用这些工具。数据库工具的接入方式相同:替换软件包,并将连接字符串作为最后一个参数传入。但请查看服务器自己的代码仓库以确认当前的软件包名称,因为参考用的 Postgres 服务器已经更换维护方多次。

这正是让代理在服务器上运行的意义:Claude Code 会话在 VPS 的 tmux 中运行,其 stdio 服务器也在旁边运行,可以直接访问项目文件和本地服务,无需经过网络往返。代理同时拥有 write_file 和 read_text_file 后,最好再配合一项引导它采用最小可行修改的 skill,因为文件系统工具会让大范围重写与修改两行代码的成本完全相同。同样的接入方式也适用于本地文件之外的资源:如果 VPS 上已经运行搜索引擎,可以将你自己的 SearXNG 实例作为搜索工具交给代理。这样查询会保留在你的服务器上,但不受信任的页面文本会直接进入代理随后据此执行操作的上下文。

第 2 步:构建远程 HTTP 服务器

stdio 服务器会随其父进程退出,并且每个客户端都会单独生成一个实例。因此,如果您在同一台主机上运行两个相互交接工作的 Claude Code 会话,每个会话都会获得该工具的独立副本。如果您需要一个持续为所有客户端提供服务的工具,例如共享运维工具、数据库网关,或同时供笔记本电脑和 CI 调用的服务,就需要使用 HTTP 传输和真正的服务。下面是一个使用官方 SDK 的最小 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-12 为 2.3.0)。该版本将 FastMCP 重命名为 MCPServer,并将 host 和 port 从构造函数移到了 run();旧的 mcp.server.fastmcp 导入现在会因 ModuleNotFoundError 而失败。请将其安装在独立的 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.target

ExecStart 中 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 错误,这是因为请求未携带会话信息且缺少有效的 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 agent 相同的访问范围:他们可以列出你的工具,然后调用这些工具。应将其完全当作未经身份验证的管理套接字,因为它本质上就是这样。令牌被窃取后能造成多大影响,还取决于其后端服务器:随 openGym 训练跟踪器发布的只读 MCP 服务器只能返回训练数据,而文件系统或 shell 工具则可能直接交出整台主机。

以下 3 种防护措施按优先级排列:

  1. 不要发布它。 将服务器保留在 127.0.0.1,通过 SSH 隧道从笔记本电脑访问:ssh -L 8000:127.0.0.1:8000 matt@vps,然后让客户端指向 http://127.0.0.1:8000/mcp。整个过程中不会暴露任何内容。
  2. 将其放在私有网络中。 绑定 自托管 WireGuard VPN 的隧道地址,只允许 VPN 对等节点访问。公共互联网只能看到一个关闭的端口。
  3. 如果必须公开,则要求令牌。 正确做法是使用 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}。如果该变量未设置,Claude Code 不会将其替换为空字符串,而是会在 claude mcp list 中标记缺少该变量,并使用字面量 ${MCP_TOKEN} 文本。还应自行选择一个名称,因为在远程服务器的请求头中,Claude Code 会有意将自身及云服务提供商的凭据变量(例如 ANTHROPIC_API_KEY)读取为空值。

上述每项防护措施保护的是端点,而不是已经持有令牌的 agent;后者是问题的另一面:如果你的客户端是 DeepSeek Harness,用于限制 agent 可调用的工具并扫描工具输出中注入指令的插件可以覆盖这一侧。

第 6 步:使用 MCP Inspector 调试

服务器行为异常时,不要在 agent 内部猜测原因。应直接使用官方基于 Web 的测试客户端 Inspector 驱动服务器。对于 stdio 服务器,将 agent 运行的相同命令传给它:

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

它会在 http://localhost:6274 上启动一个 Web 服务器,并输出一个包含 MCP_INSPECTOR_API_TOKEN 查询字符串的 URL;请打开该链接,不要凭记忆输入地址。从 v2 开始(截至 2026-10-12,当前版本为 2.10.1),后端还会将令牌注入所提供的页面,因此重新加载页面或使用书签后仍可正常访问。该令牌用于保护可生成进程的 /api/* 路由。独立的 v1 代理(端口 6277)已移除;唯一额外的监听器是用于 MCP Apps 的沙箱,默认使用端口 6275。连接后,打开 Tools 选项卡,选择一个工具,填写实际参数并调用。如果工具在 Inspector 中正常工作,但在 agent 中失败,问题出在客户端配置,而不是服务器。对于远程 HTTP 服务器,在不提供命令的情况下启动 Inspector,使用 Streamable HTTP 传输添加服务器,并填写 https://mcp.example.com/mcp,然后在其设置中添加 Authorization 请求头并连接。这是在不涉及任何 agent 的情况下,最快验证身份验证和代理配置是否正确的方法。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 后,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,或通过裸名称引用了 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;请配置 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。使用 curl -N 针对公网 URL 进行确认。应看到事件数据逐步到达,而不是在末尾一次性全部出现。

身份验证被拒绝。 客户端会报告 Error POSTing to endpoint (HTTP 401),或直接报告 401 Unauthorized。可能是请求头缺失、令牌错误,或变量未展开;后一种情况很常见。如果客户端读取配置时 MCP_TOKEN 未设置,Claude Code 会在 claude mcp list 中警告变量缺失,并发送字面文本,因此 nginx 看到的是 Bearer ${MCP_TOKEN}。如果变量名称与 Claude Code 或云服务商自己的凭据名称相同,该变量会被读取为空值,因此 nginx 看到的是没有值的 Bearer 。输出该变量,重新添加请求头,并确认其实际字节内容与 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 服务器具体是什么?

MCP 服务器是一个通过 Model Context Protocol,使用 JSON-RPC 2.0 向 AI 客户端提供工具和资源的程序。AI 模型不会直接运行工具,而是向客户端发出请求,由客户端调用 MCP 服务器,再由服务器执行并返回结果。由于该协议是标准协议,一个服务器可与任何兼容客户端配合使用,例如 Claude Code、Claude Desktop 或 Gemini CLI。

stdio 传输与 HTTP 传输有什么区别?

stdio 服务器由客户端作为子进程启动,并通过 stdin/stdout 通信。因此,它只在一台机器上随一个客户端启动和退出,不需要网络或身份验证。HTTP 服务器是长期运行的网络服务,可同时供多个客户端访问,因此需要 TLS 和身份验证。对于本地单用户工具,应使用 stdio;对于共享或需要持久运行的服务,应使用 HTTP(当前服务器使用 Streamable 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 中运行完全相同的命令。MCP Inspector 会隔离运行服务器,帮助您区分服务器错误和客户端配置错误。