不要一遇到 MCP 连接失败就开放公网端口、关闭防火墙或重装服务。
按下面四层定位:
① systemd / Python 进程
↓
② TCP 监听 / SSH tunnel / Docker 私网
↓
③ MCP initialize / tools list
↓
④ exec_vps 真实工具调用
上一层没有通过,不要跳到下一层。
检查:
systemctl status exec-vps-mcp.service --no-pager --full
journalctl -u exec-vps-mcp.service -n 100 --no-pager
systemctl cat exec-vps-mcp.service常见原因:
/opt/exec-vps-mcp不存在;.venv/bin/python没有创建;- 依赖没有安装;
- systemd 单元仍指向旧路径;
- Python 版本低于 3.10;
- 端口已被其他进程占用。
直接验证 Python 入口:
cd /opt/exec-vps-mcp
.venv/bin/python -m unittest -v
.venv/bin/python server.py手动运行只用于查看即时错误。完成后用 Ctrl+C 停止,再交还 systemd 管理,避免两个进程争用端口。
检查:
ss -lntp 'sport = :8798'
systemctl show exec-vps-mcp.service -p Environment -p ExecStart -p User默认预期:
127.0.0.1:8798
User=root
MCP_HOST=127.0.0.1
MCP_PORT=8798
修改 unit 后必须运行:
systemctl daemon-reload
systemctl restart exec-vps-mcp.service先确认普通 SSH:
ssh root@YOUR_SERVER_IP再建立 tunnel,并增加诊断输出:
ssh -v -N -L 8798:127.0.0.1:8798 root@YOUR_SERVER_IP如果提示本地端口占用:
Linux / macOS:
lsof -nP -iTCP:8798 -sTCP:LISTENWindows PowerShell:
Get-NetTCPConnection -LocalPort 8798 -State Listen可以改用本地 18798:
ssh -N -L 18798:127.0.0.1:8798 root@YOUR_SERVER_IP然后同步修改 Codex URL:
url = "http://127.0.0.1:18798/mcp"不要修改 VPS 服务端口来解决单纯的本地端口冲突。
先从真实网络取值:
docker network ls
docker network inspect YOUR_NETWORK确认:
- 客户端容器确实加入目标网络;
- 使用的是该网络的宿主 bridge gateway;
- MCP service 设置为
MCP_HOST=0.0.0.0; - UFW 规则允许正确的 bridge、subnet、gateway 和
8798; - 云厂商安全组没有把
8798开到公网。
从客户端容器做 TCP/HTTP 探测,但不要携带任何生产凭据:
docker exec YOUR_CONTAINER python -c "import socket; socket.create_connection(('YOUR_BRIDGE_GATEWAY', 8798), 5); print('tcp ok')"TCP 成功只证明网络可达,不证明 MCP initialize 和工具调用成功。
先用对应客户端的管理命令检查:
codex mcp list
codex mcp --help
claude mcp get exec-vps
opencode mcp list确认 URL、端口和配置入口属于当前实际运行的客户端。完整配置见 client-setup.md。例如 Codex 配置应位于当前 Codex host 使用的 ~/.codex/config.toml:
[mcp_servers.exec-vps]
url = "http://127.0.0.1:8798/mcp"
enabled = true
required = true
enabled_tools = ["exec_vps"]
tool_timeout_sec = 130Claude Code 应检查当前配置 scope;OpenCode 应确认读取的是当前项目或全局 opencode.json。修改后重启客户端或重新建立 session,不要假设已运行的 Agent 会自动重新加载全部 MCP 配置。
检查:
enabled_tools是否拼写为exec_vps;- 当前启动的是否是本仓库的
server.py; - systemd 是否仍指向旧目录;
- 服务重启后日志是否出现初始化错误;
- MCP 客户端是否已经重启并重新获取 tools list。
本地可用 MCP Inspector 检查工具目录:
npx -y @modelcontextprotocol/inspector在 Inspector 中连接:
http://127.0.0.1:8798/mcp
先确认 tools list,再调用一条无副作用命令并检查结构化返回字段。
有两层 timeout:
exec_vps 参数上限:120 秒
Codex 示例 tool_timeout_sec:130 秒
客户端 timeout 应略大于服务端上限,让服务端有机会返回 timed_out: true 的结构化结果。
timeout 只限制 MCP 调用等待多久,并在超时后终止直接启动的 shell。它不是 sandbox、资源限制或完整的 process-tree containment;fork、后台运行或 daemonize 的后代进程可能继续存在。
如果命令本来需要持续运行,不要简单把 timeout 无限增大。把长期程序交给 systemd、tmux、容器运行时或它自己的 supervisor,再用 exec_vps 查询状态。
每次调用都返回同一结构:
{
"exit_code": 0,
"stdout": "...",
"stderr": "...",
"timed_out": false,
"truncated": false
}判断规则:
exit_code始终直接报告 shell 退出状态;如果进程根本没有成功启动,则为null。- stdout 和 stderr 都为空时仍返回空字符串,不再用拼接文本代替退出码。
- 不要只因为
stderr非空就认定失败;有些程序会把进度或警告写到 stderr,应结合exit_code和命令语义判断。 - stdout 与 stderr 分别最多返回 1 MiB。任一路超限时,
truncated为true,返回内容只保留各自前 1 MiB。 - 输出先落到临时文件,再按上限读取;这保护 Python 内存和 MCP response,但不限制命令自身的磁盘、CPU、网络或进程资源。
如果 ss 显示 0.0.0.0:8798,而你没有明确配置私有 Docker bridge 边界:
- 先停止 MCP 服务;
- 在云厂商安全组关闭公网
8798; - 核对 UFW / nftables / iptables;
- 把 systemd 中
MCP_HOST恢复为127.0.0.1; daemon-reload并重启服务;- 从另一台公网机器确认
8798不可达; - 再恢复 SSH tunnel 使用。
命令:
systemctl stop exec-vps-mcp.service
systemctl edit --full exec-vps-mcp.service
systemctl daemon-reload
systemctl start exec-vps-mcp.service
ss -lntp 'sport = :8798'不要为了恢复连接临时设置 0.0.0.0 后忘记收回。
如果调用影响了 SSH、网络、防火墙或服务器启动:
- 停止继续发送猜测性命令;
- 使用云厂商控制台、VNC/串口或救援模式恢复;
- 先保存错误信息和被修改文件的元数据;
- 从已知备份恢复精确目标;
- 验证 SSH 和 MCP 之前,先验证系统基础网络与磁盘;
- 记录导致故障的命令和缺少的保护步骤。
这正是为什么 Agent 在修改 SSH、防火墙、网络、数据库和广泛路径前必须停下来与人确认。
排查完成后,给人类的回执至少说明:
失败发生在哪一层
观察到的证据
执行了哪些只读检查
修改了哪些文件 / 服务
现在监听在哪个地址
MCP initialize 是否成功
exec_vps 无害调用是否成功
仍有什么风险
怎样回滚
不要用“应该好了”代替真实验证。