MCP Configs2026年3月24日·4 分钟阅读

完整运维指南

完整运维知识库 + 一键安装:持久代理防弹窗/防抢焦点/多 Agent 隔离。三个脚本全文内联,复制本文给 AI Agent 即可完成安装

Agent 就绪

这个资产会安全暂存

这个资产会先安全暂存。复制的指令会要求 Agent 读取暂存文件,并在激活脚本、MCP 配置或全局配置前先确认。

Stage only · 19/100策略:需暂存
Agent 入口
任意 MCP/CLI Agent
类型
Mcp Config
安装
Stage only
信任
已验证发布者
入口
Chrome MCP 完整运维指南
安全暂存命令
npx -y tokrepo@latest install d5db19b8-971e-4d85-84d2-a27e9d1e0ddd --target codex

先暂存文件;激活前需要读取暂存 README 和安装计划。

这套东西解决什么问题

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"

讨论

登录后参与讨论。
还没有评论,来写第一条吧。

相关资产