MCP ConfigsApr 6, 2026·2 min read

Chrome MCP Background Proxy — Fix Popups, Focus Stealing & Multi-Agent Conflicts

Persistent CDP proxy + entry script that lets chrome-devtools-mcp run against your real, logged-in Chrome without the Chrome 146+ consent popup spamming on every connection, without focus stealing (Target.activateTarget / Page.bringToFront are intercepted, createTarget is forced to background), and without request-ID / event collisions when multiple Claude Code windows or sub-agents share one Chrome. Includes the proxy core (cdp-proxy.mjs v3), entry script, safe cleanup, pre-flight healthcheck, and a launchd-style self-healing watchdog with Feishu alerts.

Agent ready

This asset can be read and installed directly by agents

TokRepo exposes a universal CLI command, install contract, metadata JSON, adapter-aware plan, and raw content links so agents can judge fit, risk, and next actions.

Stage only · 19/100Stage only
Agent surface
Any MCP/CLI agent
Kind
Mcp Config
Install
Stage only
Trust
Verified publisher
Entrypoint
README.md
Universal CLI install command
npx tokrepo install 29944683-2033-4e30-9449-6d18dffa60f5

Chrome MCP Background Proxy

Fixes three breaking issues that hit anyone running chrome-devtools-mcp against their real, logged-in Chrome:

  1. Chrome 146+ remote-debugging consent popup — appears every time a fresh WebSocket attaches to the debug port. The persistent proxy holds one long-lived connection so the popup only ever fires once (and once again after a Chrome restart).
  2. Focus stealingchrome-devtools-mcp aggressively calls Target.activateTarget and Page.bringToFront, yanking the foreground tab away while you're typing. The proxy intercepts both methods and forces Target.createTarget to run with background: true.
  3. Multi-agent / multi-window conflicts — when several Claude Code windows (or sub-agents) share one Chrome, request IDs collide and CDP events get delivered to the wrong client. The proxy rewrites every request ID and routes events back to the correct client by sessionId / targetId.

Architecture

Claude Code (stdio)
   chrome-mcp-proxy.sh
     chrome-devtools-mcp (connects to the proxy's WebSocket)
       cdp-proxy.mjs v3 (port 9401)
          ├─ Persistent WebSocket to Chrome (popup only fires once)
          ├─ Request-ID remapping (no cross-client collisions)
          ├─ Event routing by sessionId (multi-agent isolation)
          └─ Blocks Target.activateTarget / Page.bringToFront
         real Chrome (auto-discovered via DevToolsActivePort)

Files

File Role
chrome-mcp-proxy.sh MCP entry script — unsets local proxy env vars, ensures the persistent proxy is running (nohup + disown), then execs chrome-devtools-mcp@latest against the proxy. Wire this into ~/.mcp.json.
cdp-proxy.mjs The persistent CDP proxy (Node 18+, only dep is ws). Holds one WebSocket to Chrome and multiplexes any number of MCP clients on top.
kill-old-chrome-mcp.sh Safe cleanup — keeps the newest chrome-devtools-mcp process and kills the rest. Read the iron rules below before running.
chrome-mcp-healthcheck.sh Pre-flight check (Chrome up? proxy up? CDP connected? process count sane?). Returns 0/1 and posts a Feishu card on failure. Wire it as a pre-step before any MCP-driven automation.
chrome-mcp-watchdog.sh launchd-friendly self-healing loop with a 10-min Feishu alert cooldown. Auto-restarts the proxy and runs cleanup when it drifts.

Setup

# 1. Drop the scripts into ~/scripts/ and chmod +x them
chmod +x ~/scripts/chrome-mcp-proxy.sh ~/scripts/kill-old-chrome-mcp.sh \
        ~/scripts/chrome-mcp-healthcheck.sh ~/scripts/chrome-mcp-watchdog.sh

# 2. One-time Chrome setup: open chrome://inspect/#remote-debugging
#    and tick "Allow remote debugging for this browser instance"

# 3. Wire it into ~/.mcp.json (NO env proxy vars — the script unsets them):
#    "chrome": {
#      "command": "bash",
#      "args": ["/Users/<you>/scripts/chrome-mcp-proxy.sh", "9401", "9222"]
#    }

# 4. Sanity check
curl -s --noproxy '*' http://127.0.0.1:9401/proxy/status | jq

You should see "chromeConnected": true and the popup-once promise comes true: new Claude Code windows reuse the existing proxy connection.

Iron rules (do not violate)

  1. Always go through the proxy. Never point chrome-devtools-mcp at port 9222 directly — you lose popup suppression and focus protection.
  2. Connect to the real Chrome (the one with your logins, extensions, bookmarks). Never start an empty --user-data-dir=/tmp/x Chrome — Chrome 146+ requires a custom data dir for --remote-debugging-port, which kills your session state.
  3. No HTTP_PROXY env vars anywhere near the MCP config. The script unsets them; if your shell rc forces them back you'll fail to connect to 127.0.0.1:9401.
  4. Never kill the current session's MCP processes.
    • The Claude Code → MCP pipe is stdio; killing the child = permanent loss of mcp__chrome__* tools for the running session.
    • When you need to clean up, check lsof -nP -p <pid> for ESTABLISHED stdio first. Only kill processes with no live client.
    • "Newest = keep" is not safe — the active session is not always the newest PID. Run lsof -i :9401 -i :9402 | grep ESTABLISHED first.
  5. The proxy is a long-lived service. Started via nohup + disown — it survives Claude Code exits and reconnects to Chrome every 5s. Don't kill it casually.

Troubleshooting

Symptom Likely cause Fix
Could not connect to Chrome Chrome not running OR DevToolsActivePort missing open -a "Google Chrome", then check ~/Library/Application Support/Google/Chrome/DevToolsActivePort exists
chromeConnected: false in /proxy/status Chrome restarted and the consent popup is waiting Click the popup once — proxy auto-reattaches
Popup appears on every new Claude Code window Proxy died last time Restart it: unset http_proxy; nohup node ~/scripts/cdp-proxy.mjs --port 9401 --chrome-port 9222 &>~/chrome-profiles/logs/proxy-9401.log & disown
Commands slow / timing out Session leak — old chrome-devtools-mcp clients didn't detach Idle clients auto-detach after 5 min (--idle-timeout 300000); to force, close the idle Claude Code window or /mcp disable chrome there
Multi-agent runs interfering Proxy is < v3 Check /proxy/status for sessions and clientDetails fields — if missing, restart the proxy to load v3
evaluate_script times out but /json/version returns 200 MCP CDP session is hung (heavy take_snapshot or large evaluate_script poisoned the channel) The proxy can't fix this — only /mcp reconnect or restart the Claude Code window helps

Tested environment

  • macOS 14+, Chrome 146/147 stable, Node 18+
  • chrome-devtools-mcp@latest from npm
  • Single Chrome + 3-5 concurrent Claude Code windows / sub-agents

License

MIT — copy, fork, modify freely.

#!/bin/bash

Chrome MCP + 持久 Proxy

用法: chrome-mcp-proxy.sh [proxy端口] [chrome端口]

Proxy 保持和 Chrome 的持久连接,弹窗只出现一次

PROXY_PORT=${1:-9401} CHROME_PORT=${2:-9222} PROXY_SCRIPT="/Users/wangkai/scripts/cdp-proxy.mjs" PID_DIR="/Users/wangkai/chrome-profiles/pids" LOG_DIR="/Users/wangkai/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}"

#!/usr/bin/env node /**

  • CDP Background Proxy v3 — 持久连接 + 多 Agent 隔离
    1. 持久 WebSocket 连接到 Chrome(弹窗只出现一次)
    1. 拦截 Target.activateTarget / Page.bringToFront(防抢焦点)
    1. 强制 createTarget background=true
    1. 请求 ID 重映射(防多客户端 ID 冲突)
    1. 事件按 sessionId 路由到对应客户端(防多 Agent 互扰)
    1. 自动重连
  • 用法: 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 IDLE_TIMEOUT_MS = parseInt(getArg('--idle-timeout', '300000')); const IDLE_CHECK_INTERVAL_MS = parseInt(getArg('--idle-check', '60000'));

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, createdAt } const pendingRequests = new Map();

// 每30秒清理超过60秒未响应的 pending requests setInterval(() => { const now = Date.now(); let cleaned = 0; for (const [proxyId, req] of pendingRequests) { if (now - req.createdAt > 60000) { // 给客户端返回超时错误,避免它也卡住 safeSend(req.clientWs, { id: req.originalId, error: { code: -32000, message: 'CDP request timeout (60s)' } }); pendingRequests.delete(proxyId); const state = clientState.get(req.clientWs); if (state) state.proxyIds.delete(proxyId); cleaned++; } } if (cleaned > 0) { console.log([Proxy] 清理 ${cleaned} 个超时请求 (剩 ${pendingRequests.size} 个)); } }, 30000);

// 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(), lastActivityAt: Date.now(), }); } return clientState.get(clientWs); }

// 空闲客户端自动回收:idle 超阈值则 detach 其全部 session(WebSocket 保留) setInterval(() => { if (!chromeReady) return; const now = Date.now(); for (const [, state] of clientState) { const idleMs = now - state.lastActivityAt; if (idleMs < IDLE_TIMEOUT_MS || state.sessions.size === 0) continue;

    const sids = Array.from(state.sessions);
    console.log(`[Proxy] 空闲回收: idle=${Math.floor(idleMs / 1000)}s, detach ${sids.length} sessions`);

    for (const sid of sids) {
        cdpRequest('Target.detachFromTarget', { sessionId: sid }).catch(() => {});
        sessionOwners.delete(sid);
        state.sessions.delete(sid);
    }
}

}, IDLE_CHECK_INTERVAL_MS);

// ═══════════════════════════════════════════ // 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);
        }
    }
});

const handleChromeDisconnect = (reason) => {
    if (!chromeReady && chromeWs === null) return; // 已处理过
    console.log(`[Proxy] Chrome 断开 (${reason}),立即释放 ${pendingRequests.size} 个 pending 请求,5秒后重连...`);
    chromeReady = false;
    chromeWs = null;
    reconnecting = false;

    // 关键修复:给所有 pending 请求立即发 error,让客户端 Promise 立即 reject
    // 否则 chrome-devtools-mcp 的 Promise 永远悬空 → 后续命令排队僵死
    for (const [, req] of pendingRequests) {
        safeSend(req.clientWs, {
            id: req.originalId,
            error: { code: -32000, message: `Chrome disconnected (${reason}), request aborted` },
        });
    }
    pendingRequests.clear();

    // Chrome 重启后旧 session / tab 全部失效,清理客户端 state(保留 WebSocket)
    sessionOwners.clear();
    for (const [, state] of clientState) {
        state.sessions.clear();
        state.tabs.clear();
        state.proxyIds.clear();
    }

    setTimeout(() => connectToChrome(), 5000);
};

chromeWs.on('close', () => handleChromeDisconnect('close'));

chromeWs.on('error', (err) => {
    console.error('[Proxy] Chrome 错误:', err.message);
    handleChromeDisconnect('error');
});

}

// ═══════════════════════════════════════════ // 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 = [];
        const now = Date.now();
        for (const [, state] of clientState) {
            clientList.push({
                tabs: state.tabs.size,
                sessions: state.sessions.size,
                idleMs: now - state.lastActivityAt,
            });
        }
        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) => {
    state.lastActivityAt = Date.now();
    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,
                createdAt: Date.now(),
            });
            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 互扰) ║ ║ ✓ 自动重连 ║ ║ ✓ 空闲客户端自动回收 session ║ ║ ║ ║ 状态: http://127.0.0.1:${PROXY_PORT}/proxy/status ║ ╚══════════════════════════════════════════════════════╝); console.log([Proxy] 空闲回收阈值: ${IDLE_TIMEOUT_MS / 1000}s, 检查间隔: ${IDLE_CHECK_INTERVAL_MS / 1000}s); connectToChrome(); });

#!/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"

#!/bin/bash

Chrome MCP 健康检查(纯检查+飞书告警,不自动修复)

发帖前调用,有问题飞书通知 William 手动处理

返回: 0=健康 1=有问题(已发飞书)

PROXY_PORT=9401 FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/REPLACE-WITH-YOUR-WEBHOOK-ID"

export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/sbin:/usr/bin:/bin:/sbin:$PATH" 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"

alert_feishu() { local title="$1" detail="$2" curl -s -o /dev/null --max-time 5
-H "Content-Type: application/json"
-d "{"msg_type":"interactive","card":{"header":{"title":{"tag":"plain_text","content":"⚠️ ${title}"},"template":"orange"},"elements":[{"tag":"div","text":{"tag":"plain_text","content":"${detail}"}},{"tag":"note","elements":[{"tag":"plain_text","content":"$(date '+%Y-%m-%d %H:%M:%S') · healthcheck"}]}]}}"
"$FEISHU_WEBHOOK" 2>/dev/null }

ERRORS=""

检查1:Chrome 是否运行

DEVTOOLS_FILE="$HOME/Library/Application Support/Google/Chrome/DevToolsActivePort" if [ ! -f "$DEVTOOLS_FILE" ]; then ERRORS="${ERRORS}Chrome未运行(DevToolsActivePort不存在)\n" fi

检查2:Proxy 端口

if ! lsof -i :${PROXY_PORT} >/dev/null 2>&1; then ERRORS="${ERRORS}Proxy未运行(端口${PROXY_PORT}无监听)\n" else # 检查3:Proxy HTTP健康 STATUS=$(curl -s --max-time 3 "http://127.0.0.1:${PROXY_PORT}/proxy/status" 2>/dev/null) if [ $? -ne 0 ] || [ -z "$STATUS" ]; then ERRORS="${ERRORS}Proxy端口占用但HTTP无响应\n" else # 检查4:Proxy与Chrome连接(JSON可能有换行/空格) if ! echo "$STATUS" | tr -d ' \n' | grep -q '"chromeConnected":true' 2>/dev/null; then ERRORS="${ERRORS}Proxy运行但与Chrome的WebSocket断开\n" fi fi fi

检查5:MCP进程数

MCP_COUNT=$(pgrep -f "chrome-devtools-mcp" | wc -l | tr -d ' ') if [ "$MCP_COUNT" -gt 30 ]; then ERRORS="${ERRORS}chrome-devtools-mcp进程堆积(${MCP_COUNT}个)\n" fi

输出结果

if [ -n "$ERRORS" ]; then echo "UNHEALTHY" echo -e "$ERRORS" alert_feishu "Chrome MCP 异常,社媒发不了" "发帖前健康检查失败:\n${ERRORS}请手动处理后重试" exit 1 else echo "HEALTHY" exit 0 fi

#!/bin/bash

Chrome MCP 守护脚本 — 弹性巡检 + 飞书告警

launchd 每60秒触发,但脚本内部有冷却机制防止重复告警

PROXY_PORT=9401 CHROME_PORT=9222 LOG="/Users/wangkai/chrome-profiles/logs/watchdog.log" PROXY_SCRIPT="/Users/wangkai/scripts/cdp-proxy.mjs" PID_DIR="/Users/wangkai/chrome-profiles/pids" LOG_DIR="/Users/wangkai/chrome-profiles/logs" STATE_DIR="/Users/wangkai/chrome-profiles/watchdog-state" MAX_LOG_LINES=500 FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/REPLACE-WITH-YOUR-WEBHOOK-ID"

告警冷却:同类错误10分钟内不重复发

ALERT_COOLDOWN=600

export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/sbin:/usr/bin:/bin:/sbin:$PATH" 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"

mkdir -p "$PID_DIR" "$LOG_DIR" "$STATE_DIR"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG" }

飞书告警(带冷却)

$1=告警类型key $2=标题 $3=详情

alert_feishu() { local key="$1" title="$2" detail="$3" local cooldown_file="${STATE_DIR}/alert_${key}"

# 冷却检查
if [ -f "$cooldown_file" ]; then
    local last_alert=$(cat "$cooldown_file")
    local now=$(date +%s)
    local diff=$((now - last_alert))
    if [ "$diff" -lt "$ALERT_COOLDOWN" ]; then
        return 0  # 冷却中,跳过
    fi
fi

# 发送飞书
local payload=$(cat <<EOFPAYLOAD

{ "msg_type": "interactive", "card": { "header": { "title": {"tag": "plain_text", "content": "⚠️ Chrome MCP: ${title}"}, "template": "orange" }, "elements": [ {"tag": "div", "text": {"tag": "plain_text", "content": "${detail}"}}, {"tag": "note", "elements": [{"tag": "plain_text", "content": "$(date '+%Y-%m-%d %H:%M:%S') · watchdog"}]} ] } } EOFPAYLOAD )

local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
    --max-time 5 \
    -H "Content-Type: application/json" \
    -d "$payload" \
    "$FEISHU_WEBHOOK" 2>/dev/null)

if [ "$http_code" = "200" ]; then
    date +%s > "$cooldown_file"
    log "ALERT: Feishu sent [${key}] ${title}"
else
    log "ALERT_FAIL: Feishu HTTP ${http_code} for [${key}]"
fi

}

日志轮转

if [ -f "$LOG" ] && [ "$(wc -l < "$LOG")" -gt "$MAX_LOG_LINES" ]; then tail -200 "$LOG" > "${LOG}.tmp" && mv "${LOG}.tmp" "$LOG" fi

===== 检查1:Chrome 是否在运行 =====

DEVTOOLS_FILE="$HOME/Library/Application Support/Google/Chrome/DevToolsActivePort" if [ ! -f "$DEVTOOLS_FILE" ]; then # Chrome 没开不算错误,静默跳过 exit 0 fi

===== 检查2:Proxy 是否健康 =====

PROXY_OK=false PROXY_ERR="" if lsof -i :${PROXY_PORT} >/dev/null 2>&1; then STATUS=$(curl -s --max-time 3 "http://127.0.0.1:${PROXY_PORT}/proxy/status" 2>/dev/null) if [ $? -eq 0 ] && [ -n "$STATUS" ]; then PROXY_OK=true else PROXY_ERR="端口${PROXY_PORT}占用但HTTP健康检查无响应" fi else PROXY_ERR="端口${PROXY_PORT}无进程监听" fi

if [ "$PROXY_OK" = false ]; then log "WARN: Proxy unhealthy — ${PROXY_ERR}, restarting..."

# 杀旧进程
if [ -f "${PID_DIR}/proxy-${PROXY_PORT}.pid" ]; then
    kill "$(cat "${PID_DIR}/proxy-${PROXY_PORT}.pid")" 2>/dev/null
    sleep 1
fi
lsof -ti :${PROXY_PORT} | xargs kill -9 2>/dev/null
sleep 1

# 重启
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

# 验证
if lsof -i :${PROXY_PORT} >/dev/null 2>&1; then
    log "OK: Proxy restarted on port ${PROXY_PORT}"
    alert_feishu "proxy_restart" "Proxy已自动重启" "原因: ${PROXY_ERR}\n结果: 重启成功,端口${PROXY_PORT}已恢复"
else
    TAIL_LOG=$(tail -5 "${LOG_DIR}/proxy-${PROXY_PORT}.log" 2>/dev/null | tr '\n' ' ')
    log "ERROR: Proxy restart failed"
    alert_feishu "proxy_fail" "Proxy重启失败!" "原因: ${PROXY_ERR}\n重启后仍无法监听端口${PROXY_PORT}\n日志: ${TAIL_LOG}"
fi

fi

===== 检查3:清理累积进程 =====

MCP_COUNT=$(pgrep -f "chrome-devtools-mcp" | wc -l | tr -d ' ') if [ "$MCP_COUNT" -gt 5 ]; then log "WARN: ${MCP_COUNT} chrome-devtools-mcp processes, cleaning..." bash /Users/wangkai/scripts/kill-old-chrome-mcp.sh >> "$LOG" 2>&1 log "OK: Cleanup done (was ${MCP_COUNT})" alert_feishu "mcp_cleanup" "MCP进程堆积已清理" "清理前: ${MCP_COUNT}个chrome-devtools-mcp进程\n已自动清理保留最新" fi

===== 检查4:Proxy连接Chrome是否正常 =====

if [ "$PROXY_OK" = true ]; then # 检查 proxy status 里的连接状态 CHROME_CONNECTED=$(echo "$STATUS" | grep -o '"chromeConnected":true' 2>/dev/null) if [ -z "$CHROME_CONNECTED" ]; then log "WARN: Proxy running but Chrome connection lost" alert_feishu "chrome_disconn" "Proxy与Chrome断开" "Proxy在端口${PROXY_PORT}正常运行,但与Chrome的WebSocket连接已断开\nProxy会自动重连,如持续收到此告警请检查Chrome" fi fi

exit 0

Discussion

Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.

Asset graph

How this asset connects to the rest of the registry.

Related Assets

Multi-Browser MCP Proxies — Arc Browser & Chrome Beta Variants

Companion to 'Chrome MCP Background Proxy' for running parallel, isolated MCP fleets against Arc Browser and Chrome Beta on top of the same cdp-proxy.mjs. Arc-specific proxy auto-discovers the WebSocket path from /json/version (Arc doesn't write a DevToolsActivePort file in the standard location); Chrome Beta proxy points at Beta's own DevToolsActivePort. Lets you run mcp__chrome__*, mcp__beta__*, and mcp__arc__* side-by-side with independent client state and no cross-talk.

MCP Configs
henuwangkai

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.

SkillsMCP Configs
henuwangkai

Chrome Fleet — Multi-Agent Browser Pool with Shared Login State

Multi-agent control plane for chrome-devtools-mcp. Two modes: (1) shared main Chrome — N CDP proxies on 9401/9402/9403... all multiplexing onto one logged-in Chrome :9222 so every agent inherits your real cookies/extensions, with focus protection and ID isolation handled by cdp-proxy.mjs; (2) isolated agent Chromes — dedicated Chrome instance per agent on :930N with its own user-data-dir for multi-account / persona-isolation testing. Includes a status tool to inspect the running fleet.

MCP Configs
henuwangkai

Chrome DevTools MCP — Browser Debugging for AI Agents

Give your AI coding agent full access to Chrome DevTools for browser automation, debugging, and performance analysis. Works with Claude, Cursor, Copilot, and 15+ AI tools.

MCP Configs
MCP Hub