Esta página se muestra en inglés. Una traducción al español está en curso.
MCP ConfigsMar 24, 2026·4 min de lectura

Chrome MCP

Chrome MCP

Listo para agents

Staging seguro para este activo

Este activo primero queda en staging. El prompt copiado pide inspeccionar los archivos staged antes de activar scripts, config MCP o config global.

Stage only · 19/100Política: staging
Superficie agent
Cualquier agent MCP/CLI
Tipo
Mcp Config
Instalación
Stage only
Confianza
Publisher verificado
Entrada
Chrome MCP 完整运维指南
Comando de staging seguro
npx -y tokrepo@latest install d5db19b8-971e-4d85-84d2-a27e9d1e0ddd --target codex

Primero deja archivos en staging; la activación requiere revisar el README y el plan staged.

这套东西解决什么问题

Chrome MCP(chrome-devtools-mcp)让 AI Agent 操控你日常使用的真实 Chrome(带登录态、插件、书签)。但直连有三大痛点:

痛点 表现 本方案的解法
Chrome 146+ 授权弹窗 每条新调试连接都弹一次"要允许远程调试吗" 持久代理只建一条连接,弹窗只出现一次
抢焦点 Agent 工作时 Chrome 窗口不停跳到前台 代理拦截 activateTarget/bringToFront,tab 全部后台创建
多 Agent 冲突 多个 Agent 同时操作时请求串线、互踩 tab 请求 ID 重映射 + 事件按 sessionId 路由

安装步骤(Agent 按顺序执行)

前置要求

  • macOS + Google Chrome(用户日常使用的那个)
  • Node.js ≥ 18(node -v 验证;没有则先装 Node)

Step 1 — 创建目录

mkdir -p "$HOME/scripts" "$HOME/chrome-profiles/pids" "$HOME/chrome-profiles/logs"

Step 2 — 写入三个脚本(源码见附录)

  • 附录 A 全文 → 写入 $HOME/scripts/chrome-mcp-proxy.sh
  • 附录 B 全文 → 写入 $HOME/scripts/cdp-proxy.mjs
  • 附录 C 全文 → 写入 $HOME/scripts/kill-old-chrome-mcp.sh
chmod +x "$HOME/scripts/chrome-mcp-proxy.sh" "$HOME/scripts/kill-old-chrome-mcp.sh"

Step 3 — 安装 ws 依赖(cdp-proxy.mjs 需要)

npm install ws --prefix "$HOME/scripts"

Step 4 — Chrome 一次性授权(⚠️ 需要用户手动做,Agent 无法代劳)

提示用户:在 Chrome 地址栏打开 chrome://inspect/#remote-debugging,勾选 Allow remote debugging for this browser instance。这是一次性设置,Chrome 重启后仍然有效。

Step 5 — 配置 .mcp.json

在项目或全局 ~/.mcp.jsonmcpServers 中加入:

"chrome": {
  "command": "bash",
  "args": ["-c", "exec \"$HOME/scripts/chrome-mcp-proxy.sh\" 9401 9222"]
}

两个注意点:

  • .mcp.json 不会展开 ~ 或环境变量,所以用 bash -c + $HOME 的写法(或者手写成你机器上的绝对路径)
  • 不要给这个 server 配任何 http_proxy/https_proxy env 变量(脚本内部已 unset)

Step 6 — 验证

curl -s --noproxy '*' http://127.0.0.1:9401/proxy/status
  • 第一次连接时 Chrome 会弹一次"允许远程调试"授权框 → 让用户点允许
  • 预期输出包含 "chromeConnected": true
  • 然后在 Agent 客户端里执行 /mcp 重连,mcp__chrome__* 工具即可用

安装到此完成。以下为运维知识库。


架构总览

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.activateTargetPage.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 所有代理环境变量
  • 如果你的 shell(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 记录

架构上,所有 Agent 共享同一个 Chrome、同一个 proxy,各自创建后台 tab,通过不同的 sessionId/targetId 操作,互不干扰。

配置文件

~/.mcp.json(chrome 部分)

"chrome": {
  "command": "bash",
  "args": ["-c", "exec \"$HOME/scripts/chrome-mcp-proxy.sh\" 9401 9222"]
}

注意:不要加 env 代理变量!

~/.claude/settings.json(权限)

MCP 工具权限不能用通配符 mcp__chrome__*(会导致整个 settings 文件被跳过),必须逐一列出:

"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 是否在运行

pgrep -x "Google Chrome" && echo "running" || echo "not running"

没运行就启动:open -a "Google Chrome"

步骤2: 检查 DevToolsActivePort 文件

cat ~/Library/Application\ Support/Google/Chrome/DevToolsActivePort

步骤3: 检查 proxy 状态

curl -s --noproxy '*' http://127.0.0.1:9401/proxy/status

应该看到 chromeConnected: true。如果是 false,Chrome 可能需要点一次"允许"弹窗。

步骤4: 用 /mcp 重连

步骤5: 安全清理旧进程

bash ~/scripts/kill-old-chrome-mcp.sh

步骤6: 最后手段 — 退出重启 Claude Code

症状:每次开新 Claude Code 都弹远程调试弹窗

检查 proxy 是否在持久运行:

curl -s --noproxy '*' http://127.0.0.1:9401/proxy/status

如果 proxy 没在运行,说明上次退出时被杀了。手动启动:

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 点一次"允许",之后就不再弹了。

症状:多 Agent 操作时互相干扰

检查 proxy 版本是否为 v3:

curl -s --noproxy '*' http://127.0.0.1:9401/proxy/status

v3 会返回 sessionsclientDetails 字段。如果没有,需要重启 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 事件路由

相关资产(进阶场景)

场景 资产
单独获取三个脚本 chrome-mcp-proxy.sh · cdp-proxy.mjs · kill-old-chrome-mcp.sh
现成的 .mcp.json 片段 MCP Config: chrome · MCP Config: Chrome Beta
Arc / Chrome Beta 多浏览器并行 Multi-Browser MCP Proxies
多 Agent 浏览器池(共享登录态) Chrome Fleet
Codex 专用打包 Codex Chrome MCP Proxy v3

附录 A — chrome-mcp-proxy.sh(写入 $HOME/scripts/chrome-mcp-proxy.sh

#!/bin/bash
# Chrome MCP + 持久 Proxy
# 用法: chrome-mcp-proxy.sh [proxy端口] [chrome端口]
# 依赖: ~/scripts/cdp-proxy.mjs (TokRepo: https://tokrepo.com/en/workflows/cdp-websocket-agent-e0f83e28)
#       npm install ws --prefix "$HOME/scripts"
# 前置: Chrome 打开 chrome://inspect/#remote-debugging 勾选 Allow remote debugging(一次性)
# Proxy 保持和 Chrome 的持久连接,授权弹窗只出现一次

PROXY_PORT=${1:-9401}
CHROME_PORT=${2:-9222}
PROXY_SCRIPT="$HOME/scripts/cdp-proxy.mjs"
PID_DIR="$HOME/chrome-profiles/pids"
LOG_DIR="$HOME/chrome-profiles/logs"

mkdir -p "$PID_DIR" "$LOG_DIR"

# 本地连接绕过代理
unset http_proxy HTTP_PROXY https_proxy HTTPS_PROXY all_proxy ALL_PROXY
export no_proxy="127.0.0.1,localhost"
export NO_PROXY="127.0.0.1,localhost"

# 如果 Proxy 没跑,启动它(持久运行,不随 Claude Code 退出而死)
if ! lsof -i :${PROXY_PORT} >/dev/null 2>&1; then
    nohup node "${PROXY_SCRIPT}" \
        --port ${PROXY_PORT} \
        --chrome-port ${CHROME_PORT} \
        &>"${LOG_DIR}/proxy-${PROXY_PORT}.log" &
    echo $! > "${PID_DIR}/proxy-${PROXY_PORT}.pid"
    disown
    sleep 2
fi

# 启动 chrome-devtools-mcp,连接到 Proxy
exec npx -y chrome-devtools-mcp@latest --browserUrl "http://127.0.0.1:${PROXY_PORT}"

附录 B — cdp-proxy.mjs(写入 $HOME/scripts/cdp-proxy.mjs

#!/usr/bin/env node
/**
 * CDP Background Proxy v3 — 持久连接 + 多 Agent 隔离
 *
 * 1. 持久 WebSocket 连接到 Chrome(弹窗只出现一次)
 * 2. 拦截 Target.activateTarget / Page.bringToFront(防抢焦点)
 * 3. 强制 createTarget background=true
 * 4. 请求 ID 重映射(防多客户端 ID 冲突)
 * 5. 事件按 sessionId 路由到对应客户端(防多 Agent 互扰)
 * 6. 自动重连
 *
 * 依赖: npm install ws --prefix "$HOME/scripts"  (ws 包需与本文件同目录层级)
 * 用法: node cdp-proxy.mjs [--port 9401] [--chrome-port 9222]
 */

import http from 'http';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { WebSocketServer, WebSocket } from 'ws';

const args = process.argv.slice(2);
const getArg = (name, def) => {
    const i = args.indexOf(name);
    return i >= 0 ? args[i + 1] : def;
};

const PROXY_PORT = parseInt(getArg('--port', '9401'));
const CHROME_PORT = parseInt(getArg('--chrome-port', '9222'));
const CHROME_HOST = getArg('--chrome-host', '127.0.0.1');

const DEVTOOLS_PORT_FILE = getArg('--devtools-port-file',
    path.join(os.homedir(), 'Library/Application Support/Google/Chrome/DevToolsActivePort'));

// ═══════════════════════════════════════════
// 持久 Chrome 连接
// ═══════════════════════════════════════════

let chromeWs = null;
let chromeReady = false;
let reconnecting = false;

const BLOCKED_METHODS = new Set([
    'Target.activateTarget',
    'Page.bringToFront',
]);

function safeSend(ws, data) {
    if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(typeof data === 'string' ? data : JSON.stringify(data));
    }
}

function readDevToolsActivePort() {
    try {
        const content = fs.readFileSync(DEVTOOLS_PORT_FILE, 'utf-8').trim();
        const lines = content.split('\n');
        if (lines.length >= 2) {
            return { port: parseInt(lines[0]), wsPath: lines[1] };
        }
    } catch (e) { /* ignore */ }
    return null;
}

function getChromeWsUrl() {
    const portInfo = readDevToolsActivePort();
    if (portInfo) {
        return `ws://${CHROME_HOST}:${portInfo.port}${portInfo.wsPath}`;
    }
    return null;
}

// ═══════════════════════════════════════════
// 多客户端隔离
// ═══════════════════════════════════════════

// 全局自增 ID,保证唯一
let globalIdCounter = 1;

// proxyId → { clientWs, originalId, method }
const pendingRequests = new Map();

// sessionId → clientWs(哪个客户端 attach 了这个 session)
const sessionOwners = new Map();

// clientWs → { tabs: Set, sessions: Set, proxyIds: Set }
const clientState = new Map();

function getOrCreateState(clientWs) {
    if (!clientState.has(clientWs)) {
        clientState.set(clientWs, { tabs: new Set(), sessions: new Set(), proxyIds: new Set() });
    }
    return clientState.get(clientWs);
}

// ═════════════════════���═════════════════════
// Chrome 连接管理
// ═══════════════════════════════════════════

function connectToChrome() {
    if (reconnecting) return;
    reconnecting = true;

    const wsUrl = getChromeWsUrl();
    if (!wsUrl) {
        console.error('[Proxy] Chrome 未运行,5秒后重试...');
        setTimeout(() => { reconnecting = false; connectToChrome(); }, 5000);
        return;
    }

    console.log(`[Proxy] 连接 Chrome: ${wsUrl}`);
    chromeWs = new WebSocket(wsUrl);

    chromeWs.on('open', () => {
        console.log('[Proxy] ✓ Chrome 已连接');
        chromeReady = true;
        reconnecting = false;
    });

    chromeWs.on('message', (rawData) => {
        try {
            const msg = JSON.parse(rawData.toString());

            // ── 响应消息(有 id)→ 路由到发起者 ──
            if (msg.id !== undefined && pendingRequests.has(msg.id)) {
                const { clientWs, originalId, method } = pendingRequests.get(msg.id);
                pendingRequests.delete(msg.id);

                const state = clientState.get(clientWs);

                // 追踪 createTarget
                if (method === 'Target.createTarget' && msg.result?.targetId) {
                    if (state) state.tabs.add(msg.result.targetId);
                    console.log(`[Proxy] 新 tab: ${msg.result.targetId}`);
                }

                // 追踪 attachToTarget → 记录 session 归属
                if (method === 'Target.attachToTarget' && msg.result?.sessionId) {
                    const sid = msg.result.sessionId;
                    sessionOwners.set(sid, clientWs);
                    if (state) state.sessions.add(sid);
                    console.log(`[Proxy] session ${sid.slice(0, 8)}... → 客户端`);
                }

                // 还原原始 ID
                msg.id = originalId;
                safeSend(clientWs, msg);
                return;
            }

            // ── 事件消息(无 id)→ 按 sessionId 路由 ──
            if (msg.method) {
                // 带 sessionId 的事件:发给对应客户端
                if (msg.sessionId && sessionOwners.has(msg.sessionId)) {
                    safeSend(sessionOwners.get(msg.sessionId), msg);
                    return;
                }

                // Target 域的全局事件:按 targetId 路由或广播
                if (msg.method.startsWith('Target.')) {
                    const targetId = msg.params?.targetInfo?.targetId || msg.params?.targetId;
                    if (targetId) {
                        // 找到拥有这个 tab 的客户端
                        for (const [ws, state] of clientState) {
                            if (state.tabs.has(targetId)) {
                                safeSend(ws, msg);
                                return;
                            }
                        }
                    }
                    // 找不到归属,广播
                    for (const [ws] of clientState) {
                        safeSend(ws, msg);
                    }
                    return;
                }

                // 其他无 sessionId 的事件,广播
                for (const [ws] of clientState) {
                    safeSend(ws, msg);
                }
                return;
            }

            // 其他消息,广播
            for (const [ws] of clientState) {
                safeSend(ws, rawData);
            }
        } catch (e) {
            for (const [ws] of clientState) {
                safeSend(ws, rawData);
            }
        }
    });

    chromeWs.on('close', () => {
        console.log('[Proxy] Chrome 断开,5秒后重连...');
        chromeReady = false;
        chromeWs = null;
        reconnecting = false;
        // 清理所有 session 归属(Chrome 重启后 session 失效)
        sessionOwners.clear();
        setTimeout(() => connectToChrome(), 5000);
    });

    chromeWs.on('error', (err) => {
        console.error('[Proxy] Chrome 错误:', err.message);
        chromeReady = false;
        chromeWs = null;
        reconnecting = false;
        setTimeout(() => connectToChrome(), 5000);
    });
}

// ═══════════════════════════════════════════
// HTTP 端点
// ═══════════════════════════════════════════

function cdpRequest(method, params = {}) {
    return new Promise((resolve, reject) => {
        if (!chromeReady) return reject(new Error('Chrome not connected'));
        const proxyId = globalIdCounter++;
        const timeout = setTimeout(() => {
            pendingRequests.delete(proxyId);
            reject(new Error('timeout'));
        }, 5000);
        const fakeClient = {
            send: (data) => {
                clearTimeout(timeout);
                resolve(typeof data === 'string' ? JSON.parse(data) : data);
            },
            readyState: WebSocket.OPEN,
        };
        pendingRequests.set(proxyId, { clientWs: fakeClient, originalId: proxyId, method });
        chromeWs.send(JSON.stringify({ id: proxyId, method, params }));
    });
}

const server = http.createServer(async (req, res) => {
    try {
        if (req.url === '/json/version') {
            const portInfo = readDevToolsActivePort();
            const wsUrl = portInfo
                ? `ws://${CHROME_HOST}:${PROXY_PORT}${portInfo.wsPath}`
                : `ws://${CHROME_HOST}:${PROXY_PORT}/devtools/browser/proxy`;
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ Browser: 'Chrome (via CDP Proxy v3)', webSocketDebuggerUrl: wsUrl }));
            return;
        }

        if (req.url === '/json/list' || req.url === '/json') {
            if (!chromeReady) { res.writeHead(503); res.end('Chrome not connected'); return; }
            try {
                const result = await cdpRequest('Target.getTargets');
                const targets = (result.result?.targetInfos || []).map(t => ({
                    ...t,
                    webSocketDebuggerUrl: `ws://${CHROME_HOST}:${PROXY_PORT}/devtools/page/${t.targetId}`,
                }));
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify(targets));
            } catch (e) { res.writeHead(500); res.end(e.message); }
            return;
        }

        if (req.url === '/json/new') {
            if (!chromeReady) { res.writeHead(503); res.end('Chrome not connected'); return; }
            try {
                const result = await cdpRequest('Target.createTarget', { url: 'about:blank', background: true });
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify(result.result || {}));
            } catch (e) { res.writeHead(500); res.end(e.message); }
            return;
        }

        if (req.url === '/proxy/status') {
            const clientList = [];
            for (const [, state] of clientState) {
                clientList.push({ tabs: state.tabs.size, sessions: state.sessions.size });
            }
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({
                chromeConnected: chromeReady,
                clients: clientState.size,
                sessions: sessionOwners.size,
                pendingRequests: pendingRequests.size,
                clientDetails: clientList,
            }, null, 2));
            return;
        }

        res.writeHead(404);
        res.end('Not found');
    } catch (e) { res.writeHead(500); res.end(e.message); }
});

// ═══════════════════���═══════════════════════
// WebSocket 客户端处理
// ═══════════════════════════════════════════

const wss = new WebSocketServer({ server });

wss.on('connection', (clientWs, req) => {
    const state = getOrCreateState(clientWs);
    console.log(`[Proxy] 客户端连接 (共 ${clientState.size} 个)`);

    clientWs.on('message', (data) => {
        if (!chromeReady) {
            try {
                const msg = JSON.parse(data.toString());
                if (msg.id !== undefined) {
                    safeSend(clientWs, { id: msg.id, error: { code: -1, message: 'Chrome not connected' } });
                }
            } catch (e) { /* ignore */ }
            return;
        }

        try {
            const msg = JSON.parse(data.toString());

            // 拦截抢焦点命令
            if (BLOCKED_METHODS.has(msg.method)) {
                console.log(`[Proxy] 拦截: ${msg.method}`);
                const reply = { id: msg.id, result: {} };
                if (msg.sessionId) reply.sessionId = msg.sessionId;
                safeSend(clientWs, reply);
                return;
            }

            // 强制后台创建 tab
            if (msg.method === 'Target.createTarget') {
                if (!msg.params) msg.params = {};
                msg.params.background = true;
                console.log(`[Proxy] 后台创建 tab: ${msg.params.url || 'about:blank'}`);
            }

            // ── ID 重映射 ──
            if (msg.id !== undefined) {
                const proxyId = globalIdCounter++;
                pendingRequests.set(proxyId, {
                    clientWs,
                    originalId: msg.id,
                    method: msg.method,
                });
                state.proxyIds.add(proxyId);
                msg.id = proxyId;
            }

            safeSend(chromeWs, msg);
        } catch (e) {
            safeSend(chromeWs, data);
        }
    });

    const cleanup = () => {
        // 清理这个客户端的 session 归属
        for (const sid of state.sessions) {
            sessionOwners.delete(sid);
        }
        // 清理未完成的请求
        for (const pid of state.proxyIds) {
            pendingRequests.delete(pid);
        }
        clientState.delete(clientWs);
        console.log(`[Proxy] 客户端断开 (剩 ${clientState.size} 个, 释放 ${state.tabs.size} tab, ${state.sessions.size} session)`);
    };

    clientWs.on('close', cleanup);
    clientWs.on('error', cleanup);
});

// ═══════════════════════════════════════════
// 启动
// ═══════════════════════════════════════════

process.on('uncaughtException', (err) => {
    console.error('[Proxy] 异常(已恢复):', err.message);
});
process.on('unhandledRejection', (reason) => {
    console.error('[Proxy] Promise 拒绝(已恢复):', reason);
});

server.listen(PROXY_PORT, () => {
    console.log(`
╔══════════════════════════════════════════════════════╗
║   CDP Background Proxy v3 — 持久连接 + 多Agent隔离  ║
║                                                      ║
║   Proxy:  http://127.0.0.1:${PROXY_PORT}                    ║
║                                                      ║
║   ✓ 持久连接(弹窗只出现一次)                       ║
║   ✓ 拦截 activateTarget / bringToFront               ║
║   ✓ 强制 createTarget background=true                ║
║   ✓ 请求 ID 重映射(防多客户端冲突)                 ║
║   ✓ 事件按 sessionId 路由(防多 Agent 互扰)         ║
║   ✓ ���动重连                                         ║
║                                                      ║
║   状态: http://127.0.0.1:${PROXY_PORT}/proxy/status          ║
╚══════════════════════════════════════════════════════╝
`);
    connectToChrome();
});

附录 C — kill-old-chrome-mcp.sh(写入 $HOME/scripts/kill-old-chrome-mcp.sh

#!/bin/bash
# kill-old-chrome-mcp.sh
# 只杀旧的 Chrome MCP 进程,保留最新的(当前 Claude Code session 的)
#
# 用法:bash ~/scripts/kill-old-chrome-mcp.sh

PIDS=$(ps aux | grep "[c]hrome-devtools-mcp" | grep -v watchdog | grep -v "npm exec" | awk '{print $2}')

if [ -z "$PIDS" ]; then
    echo "没有找到 chrome-devtools-mcp 进程"
    exit 0
fi

COUNT=$(echo "$PIDS" | wc -l | tr -d ' ')

if [ "$COUNT" -le 1 ]; then
    echo "只有 1 个 Chrome MCP 进程 (PID: $PIDS),无需清理"
    exit 0
fi

# 保留最新的(PID最大的),杀掉其他所有
NEWEST=$(echo "$PIDS" | sort -n | tail -1)
OLD_PIDS=$(echo "$PIDS" | sort -n | head -n -1)

echo "发现 $COUNT 个 Chrome MCP 进程"
echo "保留最新: PID $NEWEST"
echo "杀掉旧的: $OLD_PIDS"

for pid in $OLD_PIDS; do
    # 同时杀掉关联的 npm exec 和 watchdog 进程
    PARENT_PIDS=$(ps aux | grep -E "npm exec.*chrome-devtools|watchdog.*parent-pid=$pid" | grep -v grep | awk '{print $2}')
    kill $pid $PARENT_PIDS 2>/dev/null
    echo "  已杀: PID $pid (及关联进程)"
done

echo "✅ 清理完成,保留 PID $NEWEST"

Discusión

Inicia sesión para unirte a la discusión.
Aún no hay comentarios. Sé el primero en compartir tus ideas.

Activos relacionados