# MCP 远程执行与开发机操作指南

> **最后更新**: 2026-05-16
> **下次复查触发条件**: MCP server 位置变化（云端 → 本地 / 反之）/ 项目接 SSH 隧道方式变化 / 新增 dev server 类型 / Playwright MCP 大版本升级 / 季度复查
> **沉淀来源**: 2026-05 累计 6 条 `.learnings/` 远程开发机协作踩坑
> **适用范围**: AI 在 SSH 远程开发机上跑 L2 / 用 Playwright MCP / 起本地 dev server 的所有场景

---

## 0. 核心认知：AI 完全有能力自己跑 L2，不要把"给用户的 SSH 隧道命令"当限制

### 反模式（已踩坑）

用户报 bug 后 AI 跳过 L2 直接合 PR，给的理由：

> "你的开发机是 SSH 远程的，起前端 dev server 后还要给你 SSH 隧道命令本地浏览器才能访问"

**这违反 CLAUDE.md 明文规则**："For UI or frontend changes, start the dev server and use the feature in a browser before reporting the task as complete."

### 真相

memory `feedback_ssh_remote_dev.md` 的意思是：当**用户想用自己本地浏览器**点页面时，AI 起完 dev server 应主动给 `ssh -L 3000:localhost:3000` 命令模板。

**这条规则的对象是用户，不是 AI 自己。**

AI 在开发机上**完全有能力**闭环跑 L2：

| 资源 | 用途 | SSH 隧道相关？ |
|---|---|---|
| Bash `cd backend && npm run start:dev` | 后台起后端 | ❌ 不需要 |
| Bash `cd frontend && npm run dev` | 后台起前端 | ❌ 不需要 |
| **Playwright MCP**（`mcp__plugin_playwright_playwright__*`） | 无头/有头浏览器访问 `http://localhost:3000` | ❌ 不需要 |
| curl `http://localhost:3001/api/v1/...` | 验证后端 | ❌ 不需要 |

**整条链路全在开发机内部闭环**——SSH 隧道**只**在用户想用本地 macOS/Windows 浏览器看页面时才需要。

---

## 1. Playwright MCP 跑在云端，不在 Mac 上 → 本地 HTML 必须走公网 URL

### 现象

老 memory（来自 MyBrain）写"mac-playwright MCP 通过 SSH stdio 调用 Mac 上的 MCP"，但**当前接入的 `mcp__plugin_playwright_playwright__*` MCP 跑在云端 / 沙箱**，不在 Mac 上。表现：

```
# ❌ 都不通
mcp__..._navigate("file:///tmp/...")        # "Access to 'file:' protocol is blocked"
mcp__..._navigate("http://localhost:18765/x")  # ERR_CONNECTION_REFUSED（即使 Mac 上 python -m http.server 已监听）

# ✅ 公网 URL OK
mcp__..._navigate("https://example.com")
```

**结论**：Playwright 浏览器进程**没在 Mac 上、也没在开发机上**，跟两边 localhost 都不通；但**能上公网**。

### 正确路径：开发机 Caddy 反代到公网域名

走开发机 Caddy 反代到 `*.test.jiachentao.com`（公网解析到开发机 IP）：

```bash
# 1. 选已映射的 slot 端口（看 ~/caddy-config/Caddyfile）
grep -n 'slot-' ~/caddy-config/Caddyfile
# 比如 slot-3 的 slot-3.chentao.test.jiachentao.com → host.docker.internal:3700

# 2. 该 slot 起 server（关键：必须 0.0.0.0，不是 127.0.0.1）
cd /home/chentao/Code/ffworkspace-wt/.agent-pool/slot-3
python3 -m http.server <port> --bind 0.0.0.0

# 3. 公网 URL
# https://slot-3.chentao.test.jiachentao.com/<path-from-server-root>

# 4. MCP 直接打这个 URL
mcp__plugin_playwright_playwright__browser_navigate("https://slot-3.chentao.test.jiachentao.com/...")
```

### 坑

**`--bind 127.0.0.1` 会让 Caddy 走 `host.docker.internal` 时 connection refused** —— Caddy 在 Docker 容器里，到宿主机走 `host.docker.internal`，绑 127.0.0.1 不可达。**起 server 必确认是 `0.0.0.0`**。

### 端口残留

`python -m http.server` 用 `nohup` 起，杀错 PID（bash PID vs python PID）后端口仍占。**用 `ss -tlnp | grep :<port>` 拿真实 PID 后 kill**：

```bash
ss -tlnp | grep ':<port>'                # → users:(("python3",pid=12345,fd=3))
kill 12345
```

---

## 2. Bash `run_in_background: true` 配合 dev server 的正确写法

### 反模式（已踩坑）

```bash
cd frontend && npm run dev > /tmp/log 2>&1 &
echo "started pid=$!"
```

配合 `run_in_background: true`。

**结果**：Claude Code 立刻报 task `completed` (exit 0)，看起来 dev server 已退出。但 `ps -ef | grep next dev` 显示进程其实还活着，`ss -tlnp` 也能看到 port 在 listen。

### 根因

- `&` 让 npm run dev 进入子 shell 后台
- 父 shell 立刻执行 `echo` 然后退出（exit 0）
- Claude Code wrapper 看到父 shell 退出，认为 "task completed"
- 但 OS 层面子进程并未被回收，继续运行——直到 SIGHUP 或手动 kill

**误报**：`completed` 不代表 server 已退出，反过来也不能靠它判断 server 已 ready。

### 正确写法：让 wrapper 自己 detach，命令里**不**加 `&`

```bash
cd frontend && PORT=4000 npm run dev
```

配合 `run_in_background: true`。Claude 会在 server **真的退出**（成功或失败）时给一次准确通知。

### 等"server ready"的写法

需要等"开始监听"信号时，**单独跑一条 Bash**：

```bash
until lsof -i :3000 | grep -q LISTEN; do sleep 1; done; echo ready
```

或针对后端 health endpoint：

```bash
until curl -sf http://localhost:3001/api/v1/health; do sleep 2; done; echo backend-ready
```

---

## 3. `pkill -f "<pattern>"` 在 Claude Code Bash 工具里会**自杀**

### 现象

```bash
pkill -f "next.*dev" && sleep 2 && ...
```

bash 进程被 SIGTERM 杀掉，整个命令 exit **144**（128 + 16 = SIGTERM），后续步骤未执行。

### 根因

`pkill -f` 匹配进程的**完整命令行**（`/proc/<pid>/cmdline`）。Claude Code 的 Bash 工具启动 bash 进程时，**它自己的 cmdline 包含被执行的完整命令字符串**——所以 `pkill -f "next.*dev"` 的 cmdline 本身就含 `next.*dev` 子串，被自己匹配，自杀。

### 修法（三选一）

```bash
# 1. 用更精确的字面匹配（命令行里不会出现这串）
pkill -f "next dev -p"

# 2. 指定 PID 而非模式
NPID=$(pgrep -f "next dev -p 3300" | head -1)
[ -n "$NPID" ] && kill "$NPID"

# 3. 用 ss/lsof 拿 PID 后 kill（更精确）
PID=$(ss -tlnp | grep ':3000' | grep -oP 'pid=\K[0-9]+' | head -1)
[ -n "$PID" ] && kill "$PID"
```

---

## 4. 完整 L2 验证流程（任何 UI / 前端改动后必跑）

```bash
# 1. 起后端（后台）
cd backend && npm run start:dev    # Bash run_in_background: true，命令里不加 &

# 2. 等 backend ready
until curl -sf http://localhost:3001/api/v1/health; do sleep 2; done

# 3. 起前端（后台）
cd frontend && npm run dev          # 同上，run_in_background: true

# 4. 等 frontend ready
until lsof -i :3000 | grep -q LISTEN; do sleep 1; done

# 5. 用 Playwright MCP 走业务流程
#    本地页面：必须经 Caddy 公网域名（见 §1）
#    或开发机 IP 上：worktree.{slot}.chentao.test.jiachentao.com
mcp__plugin_playwright_playwright__browser_navigate("https://slot-X.chentao.test.jiachentao.com")
mcp__plugin_playwright_playwright__browser_snapshot()
# ... 点 / 填表 / 校 ...

# 6. 双语回归
mcp__plugin_playwright_playwright__browser_evaluate("() => { localStorage.setItem('locale','en-US'); location.reload(); }")
# ... 重复 step 5 检查所有 UI ...

# 7. 完事
mcp__plugin_playwright_playwright__browser_close()
# 杀掉 dev server
PID=$(ss -tlnp | grep ':3000' | grep -oP 'pid=\K[0-9]+' | head -1)
[ -n "$PID" ] && kill "$PID"
```

---

## 5. 纯前端组件验证（跳过 backend 整套链路）

某些纯组件改动（renderer / 表单字段 / 通用 UI）不依赖真实 backend，可用**临时 mock 路由**：

```bash
# 1. 在 frontend/src/app/<temp-name>/page.tsx 写 'use client' 页面
#    直接构造 schema / uiSchema / formData 喂给组件
# 2. 起 npm run dev（无需 backend）
# 3. MCP 访问 + snapshot 验流程
# 4. 完事 rm -rf 临时路由
```

整个验证 < 5 分钟，比起后端 + 登录链路省 80% 时间。

**关键点**：

- **路由放 `app/` 顶层**（不进 `(modules)` route group），避免 layout 里的认证 / 组织上下文请求 backend
- **接受 console 里 `ECONNREFUSED localhost:4001` 的网络 error**——来自 `useAuth.reload` / `OrganizationContext`，跟你组件无关。只看是否有**新增** runtime error
- **测完务必删临时路由再 commit**——别让 dev-only 路由进 PR

**不适用**：

- 跟 backend API 契约相关的改动（必须真实接口）
- 权限 / 多租户上下文敏感的页面
- 跨页面导航流程

---

## 6. 开发机 ↔ Mac ↔ 内网跳板机的反向通路（SAP 隧道事故恢复）

### 拓扑

```
Linux 开发机 (43.159.171.147)
  └─ ssh -p 2222 Chentao@localhost            ← autossh 反向隧道，自愈
       └─ ssh chentao@10.68.100.57            ← Mac 走公司 Wi-Fi/VPN 到跳板机
            └─ 恢复 -R 19443 / -R 19022 三条隧道
```

### 触发场景

生产 SAP 同步报 `connect ECONNREFUSED 127.0.0.1:19443` —— 内网跳板机 `10.68.100.57` 到三台公网服务器的 SSH 反向隧道全断。文档"已知限制"：crontab `@reboot` 不自愈，机器没重启就只能人工救。

### 不直观

跳板机入口只有公司内网。AI（公网开发机）平时没法触达——唯一外部入口 `-R 19022` 反向回连跟着挂了。**但**开发机和用户 Mac 之间另有一条反向 SSH 隧道（autossh 维护），**Mac 平时挂在公司 Wi-Fi/VPN，能直连 `10.68.100.57`**。

### 前置条件

Mac `~/.ssh/id_ed25519.pub` 必须在跳板机 `~/.ssh/authorized_keys` 里。配好后**下次同类故障 AI 可全自动恢复**：

```bash
# 开发机上自动跑
ssh -p 2222 Chentao@localhost 'ssh chentao@10.68.100.57 "<恢复隧道命令>"'
```

详见 [`.learnings/2026-05-14-sap-tunnel-recovery-via-mac-bridge.md`](../../../.learnings/2026-05-14-sap-tunnel-recovery-via-mac-bridge.md)。

---

## 7. 前置 checklist（任何 L2 / MCP 操作前）

- [ ] 确认 MCP 跑在哪：本地 / 开发机 / 云端（`mcp__plugin_playwright_*` 是云端，访问需公网 URL）
- [ ] dev server 命令**不加 `&`**，靠 Bash `run_in_background: true` detach
- [ ] `python -m http.server` 用 `--bind 0.0.0.0`（不是默认的 127.0.0.1 + Docker 不可达）
- [ ] 杀 dev server 用 `ss -tlnp | grep PORT` 拿 PID，**不**用 `pkill -f "pattern"`（自杀）
- [ ] 等 server ready 用 `until lsof / curl health` 轮询，不靠 sleep 固定时间
- [ ] 改完临时 mock 路由先 `rm -rf` 再 commit
- [ ] 双语回归切 zh-CN / en-US 两套 locale 跑（CLAUDE.md L2 硬规则）
- [ ] 走 Caddy 公网域名时确认 `~/caddy-config/Caddyfile` 已映射该 slot 端口

---

## 8. 相关 learning 与 memory

- [`.learnings/2026-05-01-ai-can-run-l2-locally-via-mcp.md`](../../../.learnings/2026-05-01-ai-can-run-l2-locally-via-mcp.md)
- [`.learnings/2026-05-14-playwright-mcp-runs-in-cloud-not-mac.md`](../../../.learnings/2026-05-14-playwright-mcp-runs-in-cloud-not-mac.md)
- [`.learnings/2026-05-01-bash-bg-detach.md`](../../../.learnings/2026-05-01-bash-bg-detach.md)
- [`.learnings/2026-05-11-pkill-self-match-trap.md`](../../../.learnings/2026-05-11-pkill-self-match-trap.md)
- [`.learnings/2026-05-01-frontend-only-test-via-mock-route.md`](../../../.learnings/2026-05-01-frontend-only-test-via-mock-route.md)
- [`.learnings/2026-05-14-sap-tunnel-recovery-via-mac-bridge.md`](../../../.learnings/2026-05-14-sap-tunnel-recovery-via-mac-bridge.md)
- User memory `reference_dev_machine_caddy_domain.md`（用户私有 memory，跨 session 持久但非项目知识）
- User memory `feedback_ssh_remote_dev.md`（同上，描述"给用户的 SSH 隧道"是对用户的，不是 AI 自己的限制）
