ScriptsMay 25, 2026·1 min read

Codex Chrome MCP Proxy v3

Sanitized public Chrome MCP bundle for Codex: persistent CDP proxy, real Chrome login-state control, background tabs, focus protection, and multi-agent isolation.

Agent ready

Copy a safe staging path for this asset

This asset is staged first. The copied prompt tells the agent to inspect the staged files and ask before activating scripts, MCP config, or global config.

Stage only · 19/100Policy: stage
Agent surface
Any MCP/CLI agent
Kind
Mcp Config
Install
Stage only
Trust
Verified publisher
Entrypoint
README.md
Safe staging command
npx -y tokrepo@latest install 5846fbcf-238f-4730-9553-e36d111a6619 --target codex

Stages files first; activation requires review of the staged README and plan.

Browser Control Rule

Any browser-related operation must use Chrome MCP through the persistent proxy connected to the user's real Chrome.

  • Use Chrome MCP for opening pages, navigation, clicking, typing, screenshots, DOM/page inspection, network/console/performance checks, authenticated pages, existing Chrome tabs, file uploads, and visual verification.
  • Do not use Codex Browser, Codex Chrome plugin, Computer Use, AppleScript, Playwright against a temporary browser profile, or shell-based UI automation as a browser fallback.
  • The required path is Chrome MCP proxy v3 -> 127.0.0.1:9401 -> real Chrome.
  • Use scripts/chrome-mcp-proxy.sh for the chrome MCP server.
  • Never connect directly to chrome-devtools-mcp without the proxy.
  • Never launch a stateless Chrome profile with --user-data-dir=/tmp/... or .cache/chrome-devtools-mcp/chrome-profile.
  • Do not kill the current Chrome MCP session processes. If cleanup is needed, use scripts/kill-old-chrome-mcp.sh only.
  • Keep browser work in background tabs unless the user explicitly asks to show a page.

Non-browser tasks such as reading files, editing code, running tests, using CLIs, or calling non-browser APIs should use the appropriate local tool directly.


name: codex-chrome-mcp-proxy-v3 description: Codex-ready Chrome MCP proxy bundle for controlling the user's real Chrome through a persistent CDP proxy with focus protection and multi-agent isolation.

Codex Chrome MCP Proxy v3

Use this asset when Codex browser work must go through the user's real Google Chrome instead of Codex Browser, a temporary Playwright profile, or a browser extension bridge.

This bundle packages a public, sanitized version of the Chrome MCP setup:

  • scripts/cdp-proxy.mjs - persistent CDP proxy with request-id remapping, session routing, background tabs, timeout cleanup, and reconnect handling.
  • scripts/chrome-mcp-proxy.sh - MCP stdio entrypoint that starts/reuses the persistent proxy and launches chrome-devtools-mcp.
  • scripts/kill-old-chrome-mcp.sh - conservative cleanup helper for stale chrome-devtools-mcp processes.
  • templates/codex-config.toml - Codex MCP server config snippet.
  • templates/mcp.json - generic MCP config snippet.
  • AGENTS.md - agent rule that forces browser operations through Chrome MCP.

What It Solves

Codex's built-in browser tools are useful for ordinary page testing, but they are not enough when a task requires:

  • the user's real Chrome login state, cookies, extensions, bookmarks, and existing tabs;
  • CDP-level network, console, runtime, performance, and target control;
  • background tabs that do not steal focus from the user;
  • multiple agents sharing one Chrome without request or event collisions;
  • a persistent connection that avoids repeated Chrome remote-debugging prompts.

The required path is:

Codex MCP client
  -> scripts/chrome-mcp-proxy.sh
  -> chrome-devtools-mcp
  -> scripts/cdp-proxy.mjs on 127.0.0.1:9401
  -> real Google Chrome DevTools endpoint

Install

  1. Copy the scripts to a stable local directory, for example:
mkdir -p "$HOME/scripts"
cp scripts/cdp-proxy.mjs scripts/chrome-mcp-proxy.sh scripts/kill-old-chrome-mcp.sh "$HOME/scripts/"
chmod +x "$HOME/scripts/chrome-mcp-proxy.sh" "$HOME/scripts/kill-old-chrome-mcp.sh"
  1. Install Node dependencies next to cdp-proxy.mjs:
cd "$HOME/scripts"
npm install ws
  1. Enable remote debugging for the current Chrome profile:
Open chrome://inspect/#remote-debugging
Enable "Allow remote debugging for this browser instance"
  1. Add the Codex MCP server config from templates/codex-config.toml to ~/.codex/config.toml.

  2. Restart Codex so the new MCP server is loaded.

Codex Routing Rule

For browser-related tasks, route through Chrome MCP only:

  • page open/navigation/click/type;
  • screenshots and visual verification;
  • DOM/page inspection;
  • authenticated pages and existing tabs;
  • network, console, runtime, performance, memory checks;
  • file upload flows.

Do not fall back to Codex Browser, a temporary browser profile, AppleScript, shell UI automation, or a direct chrome-devtools-mcp connection.

Non-browser work such as code edits, tests, CLI commands, and API calls can still use the normal local tools.

Health Check

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

Expected:

{
  "chromeConnected": true,
  "pendingRequests": 0
}

If Chrome restarted, the proxy should reconnect. Chrome may ask for remote-debugging permission once after a restart.

Safety Rules

  • Always go through cdp-proxy.mjs; never connect chrome-devtools-mcp directly to Chrome.
  • Do not launch a throwaway Chrome profile for authenticated work.
  • Do not include proxy environment variables in the MCP server config.
  • Do not kill the active MCP process from the current session.
  • Use scripts/kill-old-chrome-mcp.sh only for stale process cleanup.
  • Treat cookies, local storage, passwords, tokens, and private page data as sensitive.
  • Prefer background tabs unless the user explicitly wants a visible page.

Why stage_only

This asset can modify a user's browser session, inspect authenticated pages, and run JavaScript in pages. It should be installed as a staged bundle and reviewed before activation.

{ "name": "codex-chrome-mcp-proxy-v3", "type": "module", "dependencies": { "ws": "^8.18.0" } }

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

Chrome MCP + persistent CDP proxy.

Usage: chrome-mcp-proxy.sh [proxy_port] [chrome_port]

set -euo pipefail

PROXY_PORT="${1:-9401}" CHROME_PORT="${2:-9222}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROXY_SCRIPT="${SCRIPT_DIR}/cdp-proxy.mjs" PID_DIR="${HOME}/chrome-profiles/pids" LOG_DIR="${HOME}/chrome-profiles/logs"

mkdir -p "$PID_DIR" "$LOG_DIR"

Local CDP traffic must not be intercepted by user-level HTTP proxies.

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"

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" 2>&1 & echo $! > "${PID_DIR}/proxy-${PROXY_PORT}.pid" disown || true sleep 2 fi

exec npx -y chrome-devtools-mcp@0.19.0 --browserUrl "http://127.0.0.1:${PROXY_PORT}"

#!/bin/bash

Conservative cleanup helper for stale chrome-devtools-mcp processes.

It keeps the newest chrome-devtools-mcp process and terminates older ones.

set -euo pipefail

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

if [ -z "$PIDS" ]; then echo "No chrome-devtools-mcp process found." exit 0 fi

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

if [ "$COUNT" -le 1 ]; then echo "Only one Chrome MCP process exists (PID: $PIDS). Nothing to clean." exit 0 fi

NEWEST="$(echo "$PIDS" | sort -n | tail -1)" OLD_PIDS="$(echo "$PIDS" | sort -n | head -n -1)"

echo "Found $COUNT Chrome MCP processes." echo "Keeping newest PID: $NEWEST" echo "Stopping older PIDs: $OLD_PIDS"

for pid in $OLD_PIDS; do RELATED_PIDS="$(ps aux | grep -E "npm exec.*chrome-devtools|watchdog.*parent-pid=$pid" | grep -v grep | awk '{print $2}' || true)" kill "$pid" $RELATED_PIDS 2>/dev/null || true echo "Stopped PID $pid and related processes." done

echo "Cleanup complete. Kept PID $NEWEST."

[mcp_servers.chrome] command = "bash" args = ["$HOME/scripts/chrome-mcp-proxy.sh", "9401", "9222"]

Optional: keep Codex's bundled browser plugins disabled if you want strict

"browser work must use Chrome MCP" routing.

[plugins."browser@openai-bundled"] enabled = false

[plugins."chrome@openai-bundled"] enabled = false

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

Discussion

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

Related Assets

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.

MCP ConfigsScripts
henuwangkai· ⭐ 1

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 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

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