自托管的 MCP 服务器会越来越多。先是一个给内部文档用,然后一个给数据库用,再后来有人给部署流水线写了个小的。每个都有自己的端口、URL 和密钥,团队里每个人都要把这三样全粘进客户端配置。换一次令牌就得通知所有人。
网关把这一切变成一个地址和一个令牌。客户端连接 https://mcp.example.com/docs/mcp、.../db/mcp 等地址。网关校验令牌,把每个路径转发到 localhost 上对应的服务器,服务器本身从不暴露。所需极少:一台带独立 IPv4 的小 VPS 加 Caddy。
下面的配置我们先在测试服务器上搭好,并用真实客户端验证过才写出来。
需要准备什么
- 一台有公网 IPv4 的 VPS,让客户端能访问 443。除非你的 MCP 服务器自己要干重活,Nano-IP 就够了。
- 一个 A 记录指向服务器的域名,这里用
mcp.example.com。 - Ubuntu 24.04 或 Debian 12/13。两者的标准软件源里都有 Caddy。
1. 在 localhost 上运行 MCP 服务器
每个服务器作为 systemd 服务运行。为了让示例自成一体,这里把参考服务器 server-everything 跑了两份;实际换成你自己的。它需要 Node.js 20 或更高版本:
mkdir -p /opt/mcp && cd /opt/mcp
npm install @modelcontextprotocol/server-everything
# /etc/systemd/system/mcp-everything-1.service
[Unit]
Description=MCP server everything #1
After=network.target
[Service]
WorkingDirectory=/opt/mcp
Environment=PORT=3101
ExecStart=/opt/mcp/node_modules/.bin/mcp-server-everything streamableHttp
Restart=always
User=nobody
[Install]
WantedBy=multi-user.target
第二份使用 3102 端口。然后:
systemctl daemon-reload
systemctl enable --now mcp-everything-1 mcp-everything-2
测试时我们发现:这个服务器监听所有网卡,而不只是 127.0.0.1,很多其他服务器也是这样。第 3 步会处理这个问题。
2. Caddy:先校验令牌,再按路径路由
生成一个令牌,通过环境文件交给 Caddy:
apt install -y caddy
echo "MCP_GATEWAY_TOKEN=$(openssl rand -hex 32)" > /etc/caddy/gateway.env
chmod 600 /etc/caddy/gateway.env
mkdir -p /etc/systemd/system/caddy.service.d
printf '[Service]\nEnvironmentFile=/etc/caddy/gateway.env\n' > /etc/systemd/system/caddy.service.d/env.conf
systemctl daemon-reload
Caddyfile:
mcp.example.com {
route {
@unauthorized not header Authorization "Bearer {$MCP_GATEWAY_TOKEN}"
respond @unauthorized 401
handle_path /everything/* {
reverse_proxy 127.0.0.1:3101 {
flush_interval -1
}
}
handle_path /tools2/* {
reverse_proxy 127.0.0.1:3102 {
flush_interval -1
}
}
respond 404
}
}
caddy validate --config /etc/caddy/Caddyfile && systemctl restart caddy
mcp.example.com 的证书由 Caddy 自动获取。
route 块不是摆设。我们第一版把令牌校验和 handle_path 块并列放在顶层,结果没带令牌的请求直接以 200 通过了。Caddy 会按自己的顺序排列指令,handle_path 在 respond 之前执行。而在 route 里面,它们按书写顺序执行。如果你要改这份配置,第一件事就是测试“不带令牌”的情况。
handle_path 会去掉前缀,所以 /everything/mcp 到达服务器时是 /mcp。flush_interval -1 让流式事件一到就转发。
3. 除了 SSH 和 HTTPS 全部关闭
apt install -y ufw
ufw default deny incoming
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw enable
这样之后,在我们的测试中,3101 和 3102 端口从服务器外部无法访问,而 22 和 443 正常响应。80 端口保留开放,只是为了让 Caddy 申请和续期证书,并重定向到 HTTPS。
4. 验证
TOKEN=$(cut -d= -f2 /etc/caddy/gateway.env)
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"1"}}}'
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/everything/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT"
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/everything/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT"
第一个请求应输出 401,第二个输出 200。我们实测时,错误的令牌和截断的令牌都得到 401,未知路径即使带着正确令牌也得到 404。
然后从客户端测试,例如 Claude Code:
claude mcp add --transport http everything https://mcp.example.com/everything/mcp \
--header "Authorization: Bearer $TOKEN"
claude mcp list
两个服务器都显示 ✔ Connected。同一个客户端换成错误令牌后显示 ✘ Failed to connect。客户端配置的更多内容见将 EQVPS MCP 接入 Claude Code 和 Codex。
添加下一台服务器
在新的 localhost 端口上启动它,加一个 handle_path 块,重载 Caddy。谁的配置都不用变,只是同一个主机、同一个令牌下多了一个新 URL。
需要了解的限制
- 所有人共用一个令牌很简单,轮换也只需一个文件,但你没法只撤销一个人的权限而不影响所有人。需要按用户授权时,在前面加一个认证代理,或者给每个人单独的路由和令牌。
- 网关并不会让服务器变安全。 能执行 shell 命令的 MCP 服务器,对持有令牌的任何人来说仍然是一个 shell。别把令牌放在共享配置或 git 里。
- 按服务器而不是按网关选配置。 Caddy 本身只占几十 MB。如果某台服务器要跑本地模型或索引大型代码库,决定套餐的是它,而不是代理。
只有一个 MCP 服务器?用于 MCP 服务器的 VPS 讲的是那种更简单的情况。想让你的智能体也管理它们所在的服务器?EQVPS MCP 服务器 可以做到(喜欢写脚本的话用 REST API),带独立 IPv4 的套餐每月 $8 起。
评论
暂无评论。来做第一个吧。