browser-ipc-cdp 1.8.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brave_ipc.py +9 -2
- package/brave_mcp_launcher.js +187 -131
- package/package.json +7 -3
package/brave_ipc.py
CHANGED
|
@@ -539,11 +539,18 @@ def launch_browser_ipc(
|
|
|
539
539
|
print()
|
|
540
540
|
return result
|
|
541
541
|
else:
|
|
542
|
-
# Navegador corriendo pero SIN CDP
|
|
543
|
-
print(f" CDP no detectado. Reiniciando {browser_name} con CDP...")
|
|
542
|
+
# Navegador corriendo pero SIN CDP
|
|
544
543
|
if kill_existing:
|
|
544
|
+
print(f" CDP no detectado. Reiniciando {browser_name} con CDP...")
|
|
545
545
|
kill_browser(browser_exe)
|
|
546
546
|
time.sleep(2)
|
|
547
|
+
else:
|
|
548
|
+
# --no-kill activo. No podemos reusar user-data-dir real (lock).
|
|
549
|
+
# Forzar perfil separado para evitar conflicto.
|
|
550
|
+
print(f" CDP no detectado. --no-kill activo: lanzando 2da")
|
|
551
|
+
print(f" instancia en perfil separado (no toca tu sesion).")
|
|
552
|
+
clean = True
|
|
553
|
+
USER_DATA_DIR = CLEAN_USER_DATA
|
|
547
554
|
elif kill_existing:
|
|
548
555
|
print(f" {browser_name} no esta corriendo.")
|
|
549
556
|
|
package/brave_mcp_launcher.js
CHANGED
|
@@ -1,110 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Brave MCP
|
|
2
|
+
* Brave MCP Launcher (refactored)
|
|
3
3
|
*
|
|
4
|
-
* Resuelve
|
|
5
|
-
* chrome-devtools-mcp apuntando al puerto correcto. Si CDP no responde,
|
|
6
|
-
* lanza brave_ipc.py automaticamente para iniciar el navegador.
|
|
4
|
+
* Resuelve el puerto CDP de Brave/Chrome/Edge y arranca chrome-devtools-mcp.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* "command": "node",
|
|
13
|
-
* "args": ["C:\\Users\\NyGsoft\\Desktop\\ipc\\brave_mcp_launcher.js"]
|
|
14
|
-
* }
|
|
15
|
-
* }
|
|
16
|
-
* }
|
|
6
|
+
* Estrategia (rápida → lenta):
|
|
7
|
+
* 1. Leer DevToolsActivePort de cada User Data dir conocido (instantáneo, autoritativo).
|
|
8
|
+
* 2. Auto-discovery via tasklist + netstat (fallback si no hay archivo o stale).
|
|
9
|
+
* 3. Auto-launch via brave_ipc.py --no-kill (último recurso).
|
|
17
10
|
*
|
|
18
|
-
*
|
|
19
|
-
* 1. Lee cdp_info.json -> puerto candidato
|
|
20
|
-
* 2. Verifica CDP (curl /json/version) en hostIP:puerto
|
|
21
|
-
* 3. Si responde -> lanza chrome-devtools-mcp con ese URL
|
|
22
|
-
* 4. Si NO responde -> ejecuta brave_ipc.py --no-kill (auto-launch)
|
|
23
|
-
* 5. Releer cdp_info.json + reverificar -> conectar
|
|
11
|
+
* Usa binario global de chrome-devtools-mcp (sin npx cold cache).
|
|
24
12
|
*/
|
|
13
|
+
|
|
25
14
|
const { execSync, spawn, spawnSync } = require('child_process');
|
|
26
15
|
const fs = require('fs');
|
|
27
16
|
const path = require('path');
|
|
28
17
|
const http = require('http');
|
|
29
18
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const BRAVE_IPC_PY = path.join(
|
|
19
|
+
const HERE = __dirname;
|
|
20
|
+
const CDP_INFO = path.join(HERE, 'cdp_info.json');
|
|
21
|
+
const BRAVE_IPC_PY = path.join(HERE, 'brave_ipc.py');
|
|
33
22
|
|
|
34
23
|
const IS_WIN = process.platform === 'win32';
|
|
35
24
|
let IS_WSL = false;
|
|
36
25
|
try {
|
|
37
26
|
const v = fs.readFileSync('/proc/version', 'utf-8').toLowerCase();
|
|
38
27
|
IS_WSL = v.includes('microsoft') || v.includes('wsl');
|
|
39
|
-
} catch
|
|
40
|
-
|
|
41
|
-
function logErr(msg) {
|
|
42
|
-
process.stderr.write(`[brave-mcp] ${msg}\n`);
|
|
43
|
-
}
|
|
28
|
+
} catch {}
|
|
44
29
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (fs.existsSync(p)) {
|
|
48
|
-
try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch (e) {}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
30
|
+
const HOME = process.env.USERPROFILE || process.env.HOME || '';
|
|
31
|
+
const LOCALAPPDATA = process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local');
|
|
53
32
|
|
|
54
|
-
|
|
55
|
-
|
|
33
|
+
const USER_DATA_DIRS = [
|
|
34
|
+
{ name: 'brave', path: path.join(LOCALAPPDATA, 'BraveSoftware', 'Brave-Browser', 'User Data') },
|
|
35
|
+
{ name: 'chrome', path: path.join(LOCALAPPDATA, 'Google', 'Chrome', 'User Data') },
|
|
36
|
+
{ name: 'edge', path: path.join(LOCALAPPDATA, 'Microsoft', 'Edge', 'User Data') },
|
|
37
|
+
{ name: 'chromium', path: path.join(LOCALAPPDATA, 'Chromium', 'User Data') },
|
|
38
|
+
];
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
const candidates = [];
|
|
59
|
-
const add = (ip) => { if (ip && !seen.has(ip)) { seen.add(ip); candidates.push(ip); } };
|
|
60
|
-
|
|
61
|
-
// 1. Gateway IP via /proc/net/route — IP real del host Windows en WSL2 NAT
|
|
40
|
+
function resolveChromeDevtoolsMcpBin() {
|
|
62
41
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch (e) {}
|
|
78
|
-
|
|
79
|
-
// 2. Localhost (WSL2 mirrored networking mode)
|
|
80
|
-
add('127.0.0.1');
|
|
81
|
-
|
|
82
|
-
// 3. Nameservers de /etc/resolv.conf (último recurso)
|
|
83
|
-
try {
|
|
84
|
-
const resolv = fs.readFileSync('/etc/resolv.conf', 'utf-8');
|
|
85
|
-
const re = /nameserver\s+(\d+\.\d+\.\d+\.\d+)/g;
|
|
86
|
-
let m;
|
|
87
|
-
while ((m = re.exec(resolv))) add(m[1]);
|
|
88
|
-
} catch (e) {}
|
|
42
|
+
return require.resolve('chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js');
|
|
43
|
+
} catch {}
|
|
44
|
+
const candidates = [
|
|
45
|
+
path.join('C:', 'nvm4w', 'nodejs', 'node_modules', 'chrome-devtools-mcp', 'build', 'src', 'bin', 'chrome-devtools-mcp.js'),
|
|
46
|
+
path.join(process.env.APPDATA || '', 'npm', 'node_modules', 'chrome-devtools-mcp', 'build', 'src', 'bin', 'chrome-devtools-mcp.js'),
|
|
47
|
+
path.join('/usr', 'local', 'lib', 'node_modules', 'chrome-devtools-mcp', 'build', 'src', 'bin', 'chrome-devtools-mcp.js'),
|
|
48
|
+
];
|
|
49
|
+
for (const p of candidates) { if (p && fs.existsSync(p)) return p; }
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const CHROME_DEVTOOLS_MCP_BIN = resolveChromeDevtoolsMcpBin();
|
|
89
53
|
|
|
90
|
-
|
|
54
|
+
function log(msg) {
|
|
55
|
+
process.stderr.write(`[brave-mcp] ${msg}\n`);
|
|
91
56
|
}
|
|
92
57
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const url = `http://${host}:${port}`;
|
|
96
|
-
if (await testCdp(url, timeoutMs)) return url;
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
58
|
+
function readJsonSafe(p) {
|
|
59
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch { return null; }
|
|
99
60
|
}
|
|
100
61
|
|
|
101
|
-
function testCdp(url, timeoutMs =
|
|
62
|
+
function testCdp(url, timeoutMs = 1500) {
|
|
102
63
|
return new Promise((resolve) => {
|
|
103
64
|
const req = http.get(`${url}/json/version`, { timeout: timeoutMs }, (res) => {
|
|
65
|
+
if (res.statusCode !== 200) { res.resume(); return resolve(null); }
|
|
104
66
|
let data = '';
|
|
105
67
|
res.on('data', c => data += c);
|
|
106
68
|
res.on('end', () => {
|
|
107
|
-
try {
|
|
69
|
+
try {
|
|
70
|
+
const json = JSON.parse(data);
|
|
71
|
+
if (json && typeof json.webSocketDebuggerUrl === 'string' && json.webSocketDebuggerUrl) {
|
|
72
|
+
resolve(json);
|
|
73
|
+
} else { resolve(null); }
|
|
74
|
+
} catch { resolve(null); }
|
|
108
75
|
});
|
|
109
76
|
});
|
|
110
77
|
req.on('error', () => resolve(null));
|
|
@@ -112,80 +79,169 @@ function testCdp(url, timeoutMs = 3000) {
|
|
|
112
79
|
});
|
|
113
80
|
}
|
|
114
81
|
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
82
|
+
function readDevToolsActivePort(userDataDir) {
|
|
83
|
+
const file = path.join(userDataDir, 'DevToolsActivePort');
|
|
84
|
+
if (!fs.existsSync(file)) return null;
|
|
85
|
+
try {
|
|
86
|
+
const txt = fs.readFileSync(file, 'utf-8');
|
|
87
|
+
const lines = txt.split(/\r?\n/);
|
|
88
|
+
const port = parseInt(lines[0], 10);
|
|
89
|
+
if (Number.isFinite(port) && port > 0) return port;
|
|
90
|
+
} catch {}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function tryDevToolsActivePort() {
|
|
95
|
+
for (const ud of USER_DATA_DIRS) {
|
|
96
|
+
const port = readDevToolsActivePort(ud.path);
|
|
97
|
+
if (!port) continue;
|
|
98
|
+
const version = await testCdp(`http://127.0.0.1:${port}`, 1500);
|
|
99
|
+
if (version) {
|
|
100
|
+
log(`DevToolsActivePort hit: ${ud.name} -> :${port}`);
|
|
101
|
+
return { port, version };
|
|
127
102
|
}
|
|
128
103
|
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
129
106
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
107
|
+
async function discoverViaNetstat() {
|
|
108
|
+
if (!IS_WIN) return null;
|
|
109
|
+
const images = ['brave.exe', 'chrome.exe', 'msedge.exe', 'chromium.exe'];
|
|
110
|
+
const pids = new Set();
|
|
111
|
+
|
|
112
|
+
for (const img of images) {
|
|
113
|
+
try {
|
|
114
|
+
const out = execSync(
|
|
115
|
+
`tasklist /FI "IMAGENAME eq ${img}" /FO CSV /NH`,
|
|
116
|
+
{ encoding: 'utf-8', timeout: 8000, maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }
|
|
117
|
+
);
|
|
118
|
+
for (const line of out.split('\n')) {
|
|
119
|
+
const parts = line.trim().split('","');
|
|
120
|
+
if (parts.length >= 2) {
|
|
121
|
+
const pid = parseInt(parts[1].replace(/"/g, ''), 10);
|
|
122
|
+
if (Number.isFinite(pid) && pid > 0) pids.add(pid);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
135
126
|
}
|
|
136
127
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
if (pids.size === 0) {
|
|
129
|
+
log('Discovery: no Chromium processes');
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
141
132
|
|
|
133
|
+
let netstatOut;
|
|
142
134
|
try {
|
|
143
|
-
|
|
135
|
+
netstatOut = execSync('netstat -ano', {
|
|
136
|
+
encoding: 'utf-8', timeout: 20000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'],
|
|
137
|
+
});
|
|
144
138
|
} catch (e) {
|
|
145
|
-
|
|
139
|
+
log(`Discovery netstat failed: ${e.message}`);
|
|
140
|
+
return null;
|
|
146
141
|
}
|
|
147
142
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
143
|
+
const ports = new Set();
|
|
144
|
+
for (const line of netstatOut.split('\n')) {
|
|
145
|
+
const tokens = line.trim().split(/\s+/);
|
|
146
|
+
if (tokens.length < 5 || tokens[0] !== 'TCP' || !tokens[3].includes('LISTENING')) continue;
|
|
147
|
+
const pid = parseInt(tokens[4], 10);
|
|
148
|
+
if (!pids.has(pid)) continue;
|
|
149
|
+
const m = tokens[1].match(/:(\d+)$/);
|
|
150
|
+
if (!m) continue;
|
|
151
|
+
const port = parseInt(m[1], 10);
|
|
152
|
+
if (port > 1024 && port < 65536) ports.add(port);
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
|
|
155
|
+
log(`Discovery: ${ports.size} candidate ports across ${pids.size} processes`);
|
|
156
|
+
|
|
157
|
+
for (const port of ports) {
|
|
158
|
+
const version = await testCdp(`http://127.0.0.1:${port}`, 1500);
|
|
159
|
+
if (version) {
|
|
160
|
+
log(`Discovery hit: :${port}`);
|
|
161
|
+
return { port, version };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
160
164
|
return null;
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
const
|
|
167
|
+
function writeCdpInfo(port, version, mode) {
|
|
168
|
+
const data = {
|
|
169
|
+
DEBUG_PORT: port,
|
|
170
|
+
DEBUG_WS: version.webSocketDebuggerUrl || '',
|
|
171
|
+
BROWSER: version.Browser || 'Unknown',
|
|
172
|
+
CDP_URL: `http://127.0.0.1:${port}`,
|
|
173
|
+
MODE: mode,
|
|
174
|
+
UPDATED_AT: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
try { fs.writeFileSync(CDP_INFO, JSON.stringify(data, null, 2), 'utf-8'); } catch (e) {
|
|
177
|
+
log(`Write cdp_info failed: ${e.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
165
180
|
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
async function autoLaunch() {
|
|
182
|
+
if (!fs.existsSync(BRAVE_IPC_PY)) {
|
|
183
|
+
log(`brave_ipc.py not found at ${BRAVE_IPC_PY}`);
|
|
184
|
+
return null;
|
|
168
185
|
}
|
|
186
|
+
log('Auto-launch via brave_ipc.py --no-kill');
|
|
187
|
+
const cmd = IS_WSL
|
|
188
|
+
? `cmd.exe /c python "${BRAVE_IPC_PY.replace(/^\/mnt\/([a-z])\//, (_, d) => `${d.toUpperCase()}:\\`).replace(/\//g, '\\\\')}" --no-kill`
|
|
189
|
+
: `python "${BRAVE_IPC_PY}" --no-kill`;
|
|
190
|
+
try {
|
|
191
|
+
spawnSync(cmd, { shell: true, timeout: 60000, stdio: 'pipe' });
|
|
192
|
+
} catch (e) {
|
|
193
|
+
log(`Auto-launch failed: ${e.message}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return await tryDevToolsActivePort();
|
|
197
|
+
}
|
|
169
198
|
|
|
170
|
-
|
|
171
|
-
|
|
199
|
+
async function ensureCdp() {
|
|
200
|
+
let result = await tryDevToolsActivePort();
|
|
201
|
+
if (result) return result;
|
|
172
202
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
? ['/c', 'npx', '-y', 'chrome-devtools-mcp@latest', '--browserUrl', finalUrl]
|
|
177
|
-
: ['-y', 'chrome-devtools-mcp@latest', '--browserUrl', finalUrl],
|
|
178
|
-
{ stdio: ['inherit', 'inherit', 'inherit'], shell: !IS_WIN }
|
|
179
|
-
);
|
|
203
|
+
log('DevToolsActivePort miss. Falling back to netstat discovery.');
|
|
204
|
+
result = await discoverViaNetstat();
|
|
205
|
+
if (result) return result;
|
|
180
206
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
207
|
+
log('No live CDP found. Auto-launching browser.');
|
|
208
|
+
result = await autoLaunch();
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function pickRunner() {
|
|
213
|
+
if (CHROME_DEVTOOLS_MCP_BIN && fs.existsSync(CHROME_DEVTOOLS_MCP_BIN)) {
|
|
214
|
+
return { cmd: process.execPath, args: [CHROME_DEVTOOLS_MCP_BIN] };
|
|
215
|
+
}
|
|
216
|
+
log(`chrome-devtools-mcp bin not resolvable. Falling back to npx (slow first run).`);
|
|
217
|
+
return IS_WIN
|
|
218
|
+
? { cmd: 'cmd.exe', args: ['/c', 'npx', '-y', 'chrome-devtools-mcp@latest'], shell: true }
|
|
219
|
+
: { cmd: 'npx', args: ['-y', 'chrome-devtools-mcp@latest'], shell: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function main() {
|
|
223
|
+
const cdp = await ensureCdp();
|
|
224
|
+
|
|
225
|
+
let browserUrl;
|
|
226
|
+
if (cdp) {
|
|
227
|
+
writeCdpInfo(cdp.port, cdp.version, 'RESOLVED');
|
|
228
|
+
browserUrl = `http://127.0.0.1:${cdp.port}`;
|
|
229
|
+
log(`CDP ready: ${browserUrl}`);
|
|
230
|
+
} else {
|
|
231
|
+
browserUrl = 'http://127.0.0.1:9222';
|
|
232
|
+
log(`CDP unresolved. MCP will retry against fallback ${browserUrl}.`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const runner = pickRunner();
|
|
236
|
+
const args = [...runner.args, '--browserUrl', browserUrl];
|
|
237
|
+
log(`Spawn: ${runner.cmd} ${args.join(' ')}`);
|
|
238
|
+
|
|
239
|
+
const child = spawn(runner.cmd, args, {
|
|
240
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
241
|
+
shell: !!runner.shell,
|
|
185
242
|
});
|
|
243
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
244
|
+
child.on('error', (e) => { log(`spawn error: ${e.message}`); process.exit(1); });
|
|
186
245
|
}
|
|
187
246
|
|
|
188
|
-
main().catch(e => {
|
|
189
|
-
logErr(`fatal: ${e.message}`);
|
|
190
|
-
process.exit(1);
|
|
191
|
-
});
|
|
247
|
+
main().catch(e => { log(`fatal: ${e.message}`); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-ipc-cdp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Control remoto de navegadores Chromium (Brave, Chrome, Edge) via IPC + CDP dinamico. Un comando para conectar Claude Code a tu navegador real.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"browser-ipc-cdp": "bin/cli.js"
|
|
6
|
+
"browser-ipc-cdp": "bin/cli.js",
|
|
7
|
+
"browser-ipc-cdp-mcp": "brave_mcp_launcher.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"chrome-devtools-mcp": "^0.25.0"
|
|
7
11
|
},
|
|
8
12
|
"keywords": [
|
|
9
13
|
"cdp",
|
|
@@ -29,7 +33,7 @@
|
|
|
29
33
|
},
|
|
30
34
|
"homepage": "https://github.com/alexis14kl/browser-ipc-cdp#readme",
|
|
31
35
|
"engines": {
|
|
32
|
-
"node": ">=
|
|
36
|
+
"node": ">=18.0.0"
|
|
33
37
|
},
|
|
34
38
|
"files": [
|
|
35
39
|
"bin/",
|