# Chrome MCP Operations Runbook — Iron Rules, Architecture & Troubleshooting > Operations skill for running chrome-devtools-mcp against your real Chrome at scale. Covers the proxy architecture, five iron rules (always-via-proxy, real-browser-only, no-env-proxy, never-kill-current-session, persistent-proxy), Chrome 146+ remote-debug popup workaround, multi-agent isolation guarantees, configuration recipes for ~/.mcp.json and ~/.claude/settings.json (with the 'no glob in permissions' gotcha), step-by-step troubleshooting flow, and four field notes from real incidents — port cleanup heuristics that backfire, protocol-layer hang detection, why 'newest = keep' is wrong, and why heavy pages need filePath-first take_snapshot to avoid 25k token overflow. Pairs with the 'Chrome MCP Background Proxy' script bundle. ## Install Save the content below to `.claude/skills/` or append to your `CLAUDE.md`: # Chrome MCP 完整运维 Skill ## 架构总览 ``` Claude Code (stdio) → chrome-mcp-proxy.sh → chrome-devtools-mcp (连接 proxy 的 WebSocket) → cdp-proxy.mjs v3 (port 9401) ├── 持久 WebSocket 连接到 Chrome(弹窗只出现一次) ├── 请求 ID 重映射(防多客户端冲突) ├── 事件按 sessionId 路由(防多 Agent 互扰) └── 拦截抢焦点命令 → 真实 Chrome (通过 DevToolsActivePort 文件连接) ``` **核心原则:必须通过 proxy 中间层,绝不直连 Chrome。** ## 铁律(绝不可违反) ### 1. 必须经过 proxy - proxy (`cdp-proxy.mjs`) 拦截 `Target.activateTarget` 和 `Page.bringToFront`,防止浏览器抢用户焦点 - 强制 `createTarget` 在后台创建 tab(`background: true`) - `.mcp.json` 中 chrome 配置必须用 `chrome-mcp-proxy.sh`,不能用 `chrome-devtools-mcp` 直连 ### 2. 必须连接真实浏览器 - 用户日常使用的 Chrome(有登录态、插件、书签) - Chrome 需在 `chrome://inspect/#remote-debugging` 中开启远程调试(一次性设置) - proxy 通过读取 `~/Library/Application Support/Google/Chrome/DevToolsActivePort` 文件自动连接 - **绝对禁止**用 `--user-data-dir=/tmp/xxx` 或 `~/.cache/chrome-devtools-mcp/chrome-profile` 启动无状态 Chrome ### 3. 不走网络代理 - `.mcp.json` 中 chrome 服务**不配 env 代理变量** - `chrome-mcp-proxy.sh` 脚本内部已 `unset` 所有代理环境变量 - 本机 zshrc 有全局代理 (http_proxy=127.0.0.1:7897),会拦截本地连接,所以必须 unset ### 4. 绝不杀当前 session 的 MCP 进程 - Chrome MCP 是 Claude Code 通过 stdio 管道启动的子进程 - 进程一死,管道断裂,当前 session 永远无法恢复 - 清理多实例**只用安全脚本**: `bash ~/scripts/kill-old-chrome-mcp.sh` ### 5. proxy 是持久服务 - proxy 用 `nohup` + `disown` 启动,不随 Claude Code 退出而死 - 启动时立即建立到 Chrome 的持久 WebSocket 连接 - 所有 Claude Code 会话共用这一条连接 - Chrome 断开后自动每 5 秒重连 ## Chrome 146+ 远程调试弹窗问题 ### 问题 Chrome 146 开始,外部程序通过 WebSocket 连接调试端口时,Chrome 会弹出"要允许远程调试吗?"授权窗口。每次**新建** WebSocket 连接都会弹一次。 ### 解决方案:持久连接 cdp-proxy.mjs v3 在**启动时就建立一条持久连接**到 Chrome,所有客户端共用。弹窗只在以下情况出现: - proxy 首次启动时(点一次"允许") - Chrome 重启后 proxy 重连时(点一次"允许") 之后新开 Claude Code 窗口**不再弹窗**,因为复用已有连接。 ### 前置条件 Chrome 地址栏打开 `chrome://inspect/#remote-debugging`,勾选 **Allow remote debugging for this browser instance**(一次性设置)。 ### 无效方案(已验证不可行) | 方案 | 结果 | |------|------| | `--remote-debugging-port=9222` | Chrome 146 要求 `--user-data-dir` 为非默认目录,丢失登录态 | | `--silent-debugger-extension-api` | 对远程调试弹窗无效 | | `--disable-features=AutomationControlled` | 对弹窗无效 | | `defaults write com.google.Chrome DevToolsRemoteDebuggingAllowed` | Chrome 不读取 | | macOS managed preferences plist | Chrome 不读取 | | `.mobileconfig` 配置描述文件 | Chrome 不读取 | | `devtools.remote_debugging.allowed` in Local State | 无效 | | `chrome-devtools-mcp --autoConnect` | 仍然弹窗 | | 锁定 chrome-devtools-mcp 旧版本 (0.19.0) | 无效 | ## 多 Agent 隔离(v3 新增) 多个 Agent(或 sub-agent)同时操作 Chrome 时,proxy v3 保证互不干扰: | 机制 | 说明 | |------|------| | **请求 ID 重映射** | 每个客户端的请求 ID 统一映射为全局唯一 ID,响应精准路由回原客户端 | | **事件按 sessionId 路由** | `Target.attachToTarget` 的 session 归属记录到对应客户端,后续该 session 的事件只发给该客户端 | | **Tab 归属追踪** | `Target.createTarget` 的响应记录 tab 归属,Target 域的全局事件按 targetId 路由 | | **客户端断开清理** | 客户端断开时清理其 session 归属、未完成请求、tab 记录 | | **空闲自动回收** | 客户端 idle 超 5min(`--idle-timeout 300000`)→ 对其全部 session 发 `Target.detachFromTarget`,WebSocket 保留,下次操作时 chrome-devtools-mcp 自动重新 attach(无感) | 架构上,所有 Agent 共享同一个 Chrome、同一个 proxy,各自创建后台 tab,通过不同的 sessionId/targetId 操作,互不干扰。 ## 配置文件 ### ~/.mcp.json(chrome 部分) ```json "chrome": { "command": "bash", "args": [ "/Users/wangkai/scripts/chrome-mcp-proxy.sh", "9401", "9222" ] } ``` **注意:不要加 env 代理变量!** ### ~/.claude/settings.json(权限) MCP 工具权限不能用通配符 `mcp__chrome__*`(会导致整个 settings 文件被跳过),必须逐一列出: ```json "permissions": { "allow": [ "mcp__chrome__click", "mcp__chrome__close_page", "mcp__chrome__take_screenshot", ... ] } ``` ### ~/.claude/settings.local.json(MCP 启用) 同样需要逐一列出 MCP 工具名,不能用通配符。 ## 关键脚本 | 脚本 | 路径 | 用途 | |------|------|------| | chrome-mcp-proxy.sh | `~/scripts/chrome-mcp-proxy.sh` | MCP 启动入口:unset 代理 → 启动持久 proxy(nohup+disown) → 启动 chrome-devtools-mcp | | cdp-proxy.mjs | `~/scripts/cdp-proxy.mjs` | CDP 持久代理 v3:持久连接 + ID 重映射 + 事件路由 + 拦截抢焦点 + 自动重连 | | kill-old-chrome-mcp.sh | `~/scripts/kill-old-chrome-mcp.sh` | 安全清理:只杀旧进程,保留最新 | ## 关键文件 | 文件 | 路径 | 用途 | |------|------|------| | DevToolsActivePort | `~/Library/Application Support/Google/Chrome/DevToolsActivePort` | Chrome 自动写入,包含调试端口和 WebSocket 路径 | | proxy 日志 | `~/chrome-profiles/logs/proxy-9401.log` | proxy 运行日志 | | proxy PID | `~/chrome-profiles/pids/proxy-9401.pid` | proxy 进程 PID | ## 排障流程 ### 症状:Chrome MCP 工具报错 "Could not connect to Chrome" #### 步骤1: 检查 Chrome 是否在运行 ```bash pgrep -x "Google Chrome" && echo "running" || echo "not running" ``` 没运行就启动:`open -a "Google Chrome"` #### 步骤2: 检查 DevToolsActivePort 文件 ```bash cat ~/Library/Application\ Support/Google/Chrome/DevToolsActivePort ``` #### 步骤3: 检查 proxy 状态 ```bash curl -s --noproxy '*' http://127.0.0.1:9401/proxy/status ``` 应该看到 `chromeConnected: true`。如果是 `false`,Chrome 可能需要点一次"允许"弹窗。 #### 步骤4: 用 `/mcp` 重连 #### 步骤5: 安全清理旧进程 ```bash bash ~/scripts/kill-old-chrome-mcp.sh ``` #### 步骤6: 最后手段 — 退出重启 Claude Code ### 症状:每次开新 Claude Code 都弹远程调试弹窗 检查 proxy 是否在持久运行: ```bash curl -s --noproxy '*' http://127.0.0.1:9401/proxy/status ``` 如果 proxy 没在运行,说明上次退出时被杀了。手动启动: ```bash unset http_proxy HTTP_PROXY https_proxy HTTPS_PROXY all_proxy ALL_PROXY nohup node ~/scripts/cdp-proxy.mjs --port 9401 --chrome-port 9222 &>~/chrome-profiles/logs/proxy-9401.log & disown ``` 然后在 Chrome 点一次"允许",之后就不再弹了。 ### 症状:感觉 "DevTools 协议被占满"(命令变慢/超时) `/proxy/status` 看 `sessions` 数,如果远大于 `tabs` 总数,说明有客户端 session 泄漏(老 Claude Code 窗口关的时候 chrome-devtools-mcp 没 detach 干净)。 - v3.1 后 proxy 自带空闲回收:idle > 5min 自动 detach 全部 session - 想立即清理:关掉闲置 Claude Code 窗口(触发 `clientDisconnect`),或在闲置窗口里 `/mcp` disable chrome - 重启 proxy 是最暴力的清理(但所有窗口都要重连) ### 症状:多 Agent 操作时互相干扰 检查 proxy 版本是否为 v3: ```bash curl -s --noproxy '*' http://127.0.0.1:9401/proxy/status ``` v3 会返回 `sessions` 和 `clientDetails` 字段。如果没有,需要重启 proxy 加载新版 cdp-proxy.mjs。 ## 历史事故教训 | 日期 | 事故 | 教训 | |------|------|------| | 2026-03-15 | 手动 kill 全部进程导致 session 失效 | 只用安全脚本清理 | | 2026-03-24 | Chrome 更新到 146,远程调试开始弹窗 | 改用持久连接 proxy,弹窗只出现一次 | | 2026-03-24 | settings.json 用 `mcp__chrome__*` 通配符导致整个文件被跳过 | MCP 权限必须逐一列出工具名 | | 2026-03-24 | `--remote-debugging-port` 在 Chrome 146 要求非默认 user-data-dir | 不能用此方式,会丢失登录态 | | 2026-03-24 | macOS managed preferences / mobileconfig 设置 Chrome 策略 | Chrome 不读取这些策略,无效 | | 2026-03-24 | proxy v2 多客户端共用连接时请求 ID 冲突 | v3 加入 ID 重映射 + sessionId 事件路由 | --- ## Field Notes — Lessons Learned the Hard Way Real incidents that shaped the iron rules above. Read these before you reach for `pkill` or assume a port without LISTEN is an orphan. ### Note 1 — Port without LISTEN is not always an orphan 杀 chrome-devtools-mcp 重复进程时,不要假设"目标端口没监听 = 孤儿可杀"。多个 npm exec 实例配不同 --browserUrl(9401/9402/9501),但实际服务本会话 MCP 的可能是其中任意一个 npm exec 链路,且不一定连到正在监听的浏览器端口(mcp 进程可能持有自己的 stdio 通信,与 browserUrl 状态无关)。 **Why:** 2026-05-04 在 claude.ai 处理 TokRepo YouTube 选题任务时,发现 9401/9402 端口没 LISTEN、9501 有 ESTABLISHED,按"孤儿"判断 pkill 9401/9402 链,结果本会话 mcp__chrome__* 和 mcp__beta__* 全部 disconnected(推测当前 session MCP 走 9401/9402),且会话内无法重新连接(MCP 配置在 session 启动时绑定)。 **How to apply:** 1. 杀之前先 `lsof -nP -p ` 看具体 npm exec / chrome-devtools-mcp 进程的 fd 状态,stdio 上有连接才说明在为某 session 服务。 2. 更稳妥:只用 `pkill` 杀超过 24h 的明显僵尸(看 STARTED 时间),近期启动的全部保留。 3. 最稳妥:在用户明确同意失去当前会话 MCP 能力的前提下再清理;否则把"清理"延后到 session 结束后。 4. 如果会话中确实必须清理:提前告诉用户"清理可能导致本会话 chrome MCP 失联,需要重启 Claude Code",再动手。 5. 一旦失联:Arc MCP 是兜底,但若用户限定只能 chrome,则只能切纯文本交付让用户手动操作。 ### Note 2 — Cleanup safety: 'newest = keep' is wrong 清理 chrome-devtools-mcp 重复进程时,不能仅按启动时间挑"最新"保留——Claude Code session 连接的 PID 不一定是最新启动的那组。 **Why:** 2026-04-15 C55 按 "每端口留最新 kill 其余" 逻辑清理,结果本 session 连接的 beta+arc MCP 双双掉线,用户被迫 /mcp 重连。 **How to apply:** - 清理前先 `lsof -i :9402 -i :9501 -i :9401 | grep ESTABLISHED` 找出活跃连接的 PID - 或问用户 "我看到 N 组重复,session 现在用的是哪个?" 再动手 - 若无法确认,只 kill 明显孤儿(无 cdp-proxy 对应端口的 9401 那种) - 重启路径:用户 /mcp → 选中掉线的 server → reconnect;或整个 Claude Code 重启 - cdp-proxy 永远别 kill(72446/26413/26239 是核心桥接,kill 掉 Chrome 调试口直断) ### Note 3 — Protocol-layer hang vs. healthy proxy 2026-04-23 01:24 C63 触发 3h cron,准备做 HN/Reddit/GitHub 3 平台,但 `mcp__beta__*` 全部超时: - `new_page` → "Chrome not connected" - `navigate_page`/`reload` → "Navigation timeout of 10000/30000 ms exceeded" - `evaluate_script` → "Runtime.evaluate timed out" - `list_pages` / `select_page` 仍然能响应(只返回 tab 元数据,不调 CDP protocol) 但 `curl http://127.0.0.1:9501/json/version` 正常返回: ``` {"Browser":"Chrome (via CDP Proxy v3)","webSocketDebuggerUrl":"ws://127.0.0.1:9501/devtools/browser/..."} ``` **Why:** chrome-devtools-mcp 的 Puppeteer 层或 WebSocket session 卡在一个 pending call 上(可能是之前的长 take_snapshot/大 evaluate_script 没正常断开),但 HTTP proxy 还活着,所以看起来"端口通但操作不通"。这个状态不会自愈 — puppeteer 的 CDP session 僵死后只能重建。 **How to apply:** ## 1. 僵死诊断(按顺序) - `mcp__beta__list_pages` 能否返回?→ 能(这是 HTTP 端点,不经 CDP session) - `mcp__beta__evaluate_script` 简单语句(`() => document.title`)是否 timeout?→ timeout = CDP session 僵死 - `curl http://127.0.0.1:9501/json/version` 是否 200?→ 是 = Chrome 自己活着 - 三个条件都满足 = MCP 协议层 hang,不是 Chrome/页面/网络问题 ## 2. 跳过策略(agent 能做的) - **不要**伪造/编造任何动作——"假设 HN 发了 comment" 会污染 log - **不要**试图重建连接(kill chrome-devtools-mcp 进程)—— 可能干扰 user 别处正在用的 session - **要做**: 1. 更新 social-media-posted-log.md 记录"C63 因 MCP hang 跳过,冷却表保持不变" 2. 在响应里明确告诉 user "Chrome MCP 协议层 hang,需手动重启 MCP" 3. 不占用 X/其他平台动作额度 4. 下次 cron (3h 后) 再试 ## 3. 用户恢复建议(写在响应里) - 在 Claude Code 里重新连接 MCP(一般 `/mcp` 或重启 Claude Code) - 或 `pkill -f chrome-devtools-mcp && 重新 npx chrome-devtools-mcp --browserUrl http://127.0.0.1:9501` - 重启后不需要重新登录账号(Chrome cookie/session 都在) ## 4. 本次 hang 疑似起因 本次 session 从 C60 开始连续做了 9 平台 + Bluesky/Reddit/Medium 重页面 take_snapshot(都走 filePath 落盘)。可能某个 take_snapshot 的 a11y tree 大到让 Puppeteer 内部缓冲卡住。后续考虑:单次 take_snapshot 节点数超 5k 后主动关闭并新开 tab。 ## 5. Hang→Disconnect 升级模式(2026-04-23 补充观察) Hang 状态 **不会自愈**,且 Claude Code 会在持续一段时间(本次约 7.5h,从 01:24 到 08:52)后主动把 MCP server 标记为 disconnected: - Phase 1 (hang): `list_pages` 响应但 `evaluate_script` timeout — 部分工具假装可用 - Phase 2 (disconnect): 所有 `mcp____*` 工具从 tool list 消失,runtime 发 "MCP server disconnected" 通知 两个阶段 agent 的应对都是:跳过 cycle,不伪造动作,提示 user 重启。区别只在响应文案(hang 阶段说"MCP 协议层僵死",disconnect 阶段说"MCP server 已断开")。 Agent 不要主动 `pkill chrome-devtools-mcp` — 本次 session 多个窗口可能共享 MCP,误伤风险大。建议写在响应里让 user 选择是否执行。 ## 6. Hang 复发模式(2026-04-24 补充 / 3 次复发确认) 同一 session 内 hang **可多次复发**,不只"开机一次"。本次序列: - 2026-04-23 01:24–13:13 第一次 hang(持续 ~12h,user 重启恢复) - 2026-04-23 13:15–13:53 C63 执行期(正常) - 2026-04-24 00:34 第二次 hang(C63 完成后 ~10h,触发 C64 时发现) - 2026-04-24 00:50–01:16 C64+C65 执行期(正常,user /mcp 重连后) - 2026-04-24 04:08 **第三次 hang**(C65 完成后 ~2.7h,触发 C66 时发现) **复发间隔假设(待验证)**:最初假设 12h → 10h → 2.7h 间隔递减,但第 3 次 hang 至少持续 17h+(2026-04-24 04:08–21:00+ 仍未重启),所以"间隔递减"结论**不成立**或不稳定。实际观察:hang 本身不会自愈,user 重启间隔取决于 user 何时注意到 + 何时有空介入,不是 MCP 内在规律。 真正可靠的结论只有两点: 1. Hang 不会自愈,必须外部重启 2. 同 session 可多次复发(不只"开机一次") 间隔数据不足以下结论。**不要**向 user 建议"每 2-3h 预防性重启" — 这过度反应,本次 17h hang 证明复发不稳定。 Agent 应对:每次 cycle 开始先跑 `mcp__beta__evaluate_script` 简单语句做 health check(`() => document.readyState`),timeout = hang,立刻停并告知 user。不要硬上 `new_page`/`navigate` 等重操作。 **Health check 一行**:轮转开始第一步,在已有 tab 上 evaluate_script 返回 `document.title` 或 `location.href`,1–2 秒内应该返回。超过 3 秒 = hang,跳过 cycle。 **User 长期对策建议**: - 考虑定时自动 `pkill -f chrome-devtools-mcp` 每 2-3h 预防性重启(避免 in-cycle hang 打断) - 或减少 beta MCP 的重负载操作(take_snapshot 不超 5k nodes / navigate 之间 close 旧 tabs) - tab 数量长期保持 <10(目前 session 10+ tabs 在活) ### Note 4 — Heavy pages need filePath-first snapshots Chrome Beta MCP (mcp__beta__*) 在重 feed 页面上的 inline 响应经常超过 25k token 限制,工具直接报错 "exceeds maximum allowed tokens"。 **Why:** Bluesky following feed、Reddit subreddit/post page、HN 带长帖正文的页面,单次 a11y tree 可达 70-220k chars。这些工具默认把快照内联到响应,Claude 看到的已经是截断版/错误版,影响判断。 **How to apply:** - `take_snapshot`: 永远带 `filePath: "/tmp/xxx.txt"` 参数落盘,用 Grep/Bash sed 按需提取 uid - `click(uid, includeSnapshot: true)`: 重页面禁用 `includeSnapshot`;改为 click → take_snapshot(filePath) → Grep 两步 - `wait_for`: 重页面可能也会溢出,用小的 text 数组 + 较短 timeout;若溢出就单独 take_snapshot 到文件 - 轻量页面(HN item、X compose)可以继续 inline 响应 - 记住:Bluesky home / Reddit sub / 大 thread = 默认 file-first --- Source: https://tokrepo.com/en/workflows/chrome-mcp-operations-runbook-iron-rules-architecture-2080f98d Author: henuwangkai