browser-ipc-cdp 1.6.0 → 1.8.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/README.md CHANGED
@@ -1,5 +1,10 @@
1
- <<<<<<< HEAD
2
- # Brave IPC CDP - Documentacion
1
+ # browser-ipc-cdp
2
+
3
+ Control remoto de navegadores Chromium (Brave, Chrome, Edge) via IPC con CDP dinamico. Abre tu navegador real con todas tus sesiones, detecta o asigna puerto automaticamente, configura portproxy/firewall para WSL, y actualiza el MCP de Claude Code. Sin puertos fijos, sin hacks de registry, sin configuracion manual.
4
+
5
+ ---
6
+
7
+ # Documentacion
3
8
 
4
9
  ## Que es esto?
5
10
 
@@ -261,7 +266,3 @@ Todo sin necesidad de login adicional — usa tus sesiones activas.
261
266
 
262
267
  7. Listo! Claude Code controla tu Brave real
263
268
  ```
264
- =======
265
- # browser-ipc-cdp
266
- browser-ipc-cdp — Control remoto de navegadores Chromium (Brave, Chrome, Edge) via IPC con CDP dinámico. Abre tu navegador real con todas tus sesiones, detecta o asigna puerto automáticamente, configura portproxy/firewall para WSL, y actualiza el MCP de Claude Code. Sin puertos fijos, sin hacks de registry, sin configuración manual.
267
- >>>>>>> 3ba8518eefb0a7bab267193cab6800ebc9fda58b
package/brave_cdp.bat ADDED
@@ -0,0 +1,17 @@
1
+ @echo off
2
+ net session >nul 2>&1
3
+ if %errorlevel% neq 0 (
4
+ powershell -Command "Start-Process '%~f0' -Verb RunAs"
5
+ exit /b
6
+ )
7
+
8
+ title Brave IPC CDP Launcher
9
+ echo ============================================
10
+ echo Brave IPC CDP Launcher (Admin)
11
+ echo Puerto: Dinamico via IPC
12
+ echo ============================================
13
+ echo.
14
+
15
+ python "%~dp0brave_ipc.py" %*
16
+
17
+ pause
package/brave_ipc.py ADDED
@@ -0,0 +1,733 @@
1
+ """
2
+ Brave IPC CDP Launcher
3
+ ======================
4
+ Lanza Brave via IPC (subprocess pipe) con CDP dinámico.
5
+
6
+ Flujo:
7
+ 1. Proceso padre (este script) lanza Brave con --remote-debugging-port=0
8
+ 2. El OS asigna un puerto aleatorio
9
+ 3. Brave escribe el puerto real en DevToolsActivePort
10
+ 4. Este script lee el puerto y lo reporta
11
+ 5. CDP queda disponible sin puerto fijo ni firewall
12
+
13
+ Uso:
14
+ python brave_ipc.py # Lanza Brave con CDP dinámico
15
+ python brave_ipc.py --port 9222 # Fuerza puerto específico
16
+ python brave_ipc.py --headless # Modo headless (sin ventana)
17
+ python brave_ipc.py --url https://.. # Abre URL al iniciar
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import shutil
24
+ import subprocess
25
+ import sys
26
+ import time
27
+ import urllib.request
28
+ from pathlib import Path
29
+
30
+ # ─── Configuración ────────────────────────────────────────────────────────────
31
+
32
+ # Archivo de configuración persistente (se genera al primer uso)
33
+ CONFIG_FILE = Path(__file__).parent / "browser_config.json"
34
+ IPC_INFO_FILE = Path(__file__).parent / "cdp_info.json"
35
+
36
+ # Rutas de búsqueda por navegador (fallback si no hay config)
37
+ BROWSER_SEARCH_PATHS = {
38
+ "brave": [
39
+ r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe",
40
+ r"C:\Program Files (x86)\BraveSoftware\Brave-Browser\Application\brave.exe",
41
+ os.path.expandvars(r"%LOCALAPPDATA%\BraveSoftware\Brave-Browser\Application\brave.exe"),
42
+ ],
43
+ "chrome": [
44
+ r"C:\Program Files\Google\Chrome\Application\chrome.exe",
45
+ r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
46
+ os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe"),
47
+ ],
48
+ "edge": [
49
+ r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
50
+ r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
51
+ ],
52
+ "chromium": [
53
+ os.path.expandvars(r"%LOCALAPPDATA%\Chromium\Application\chrome.exe"),
54
+ ],
55
+ }
56
+
57
+ BROWSER_USER_DATA = {
58
+ "brave": os.path.expandvars(r"%LOCALAPPDATA%\BraveSoftware\Brave-Browser\User Data"),
59
+ "chrome": os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\User Data"),
60
+ "edge": os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Edge\User Data"),
61
+ "chromium": os.path.expandvars(r"%LOCALAPPDATA%\Chromium\User Data"),
62
+ }
63
+
64
+ CLEAN_USER_DATA = Path(os.path.expandvars(r"%USERPROFILE%\browser-cdp-profile"))
65
+
66
+
67
+ # ─── Utilidades ───────────────────────────────────────────────────────────────
68
+
69
+ def load_config() -> dict:
70
+ """Carga la configuración guardada del navegador."""
71
+ if CONFIG_FILE.exists():
72
+ try:
73
+ return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
74
+ except Exception:
75
+ pass
76
+ return {}
77
+
78
+
79
+ def save_config(data: dict):
80
+ """Guarda la configuración del navegador."""
81
+ CONFIG_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
82
+
83
+
84
+ def detect_browsers() -> list[dict]:
85
+ """Detecta todos los navegadores Chromium instalados en el sistema."""
86
+ found = []
87
+ for name, paths in BROWSER_SEARCH_PATHS.items():
88
+ for p in paths:
89
+ if Path(p).exists():
90
+ found.append({"name": name, "exe": p, "user_data": BROWSER_USER_DATA.get(name, "")})
91
+ break
92
+ # Buscar también en PATH
93
+ for cmd in ["brave", "chrome", "msedge", "chromium"]:
94
+ exe = shutil.which(cmd) or shutil.which(f"{cmd}.exe")
95
+ if exe and not any(b["exe"] == exe for b in found):
96
+ found.append({"name": cmd, "exe": exe, "user_data": ""})
97
+ return found
98
+
99
+
100
+ def find_browser(preferred: str = "") -> tuple[str, str, str]:
101
+ """Busca el navegador a usar. Retorna (name, exe_path, user_data_dir).
102
+
103
+ Prioridad:
104
+ 1. Configuración guardada en browser_config.json
105
+ 2. Flag --browser del CLI
106
+ 3. Detección automática (primer navegador encontrado)
107
+ """
108
+ # 1. Config guardada
109
+ config = load_config()
110
+ if config.get("exe") and Path(config["exe"]).exists():
111
+ return config["name"], config["exe"], config.get("user_data", "")
112
+
113
+ # 2. Preferred del CLI
114
+ browsers = detect_browsers()
115
+ if preferred:
116
+ for b in browsers:
117
+ if b["name"] == preferred.lower():
118
+ save_config(b)
119
+ return b["name"], b["exe"], b["user_data"]
120
+
121
+ # 3. Primer navegador encontrado
122
+ if browsers:
123
+ save_config(browsers[0])
124
+ return browsers[0]["name"], browsers[0]["exe"], browsers[0]["user_data"]
125
+
126
+ return "", "", ""
127
+
128
+
129
+ def test_cdp(port: int, timeout: float = 3.0) -> dict | None:
130
+ """Prueba si CDP responde en el puerto dado. Retorna version info o None."""
131
+ try:
132
+ url = f"http://127.0.0.1:{port}/json/version"
133
+ req = urllib.request.Request(url)
134
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
135
+ return json.loads(resp.read())
136
+ except Exception:
137
+ return None
138
+
139
+
140
+ def read_devtools_active_port(user_data_dir: Path, timeout: int = 30) -> int | None:
141
+ """Lee el archivo DevToolsActivePort que el navegador escribe al iniciar con CDP.
142
+
143
+ Este archivo contiene:
144
+ - Línea 1: puerto TCP
145
+ - Línea 2: WebSocket path token
146
+ """
147
+ port_file = user_data_dir / "DevToolsActivePort"
148
+ deadline = time.time() + timeout
149
+
150
+ while time.time() < deadline:
151
+ if port_file.exists():
152
+ try:
153
+ content = port_file.read_text().strip().splitlines()
154
+ if content:
155
+ port = int(content[0].strip())
156
+ if port > 0:
157
+ return port
158
+ except (ValueError, IndexError):
159
+ pass
160
+ time.sleep(0.5)
161
+
162
+ return None
163
+
164
+
165
+ def get_pages(port: int) -> list[dict]:
166
+ """Lista las páginas abiertas via CDP."""
167
+ try:
168
+ url = f"http://127.0.0.1:{port}/json/list"
169
+ with urllib.request.urlopen(url, timeout=5) as resp:
170
+ return json.loads(resp.read())
171
+ except Exception:
172
+ return []
173
+
174
+
175
+ def kill_browser(exe_path: str):
176
+ """Mata todas las instancias del navegador."""
177
+ exe_name = Path(exe_path).name # brave.exe, chrome.exe, msedge.exe, etc.
178
+ if sys.platform == "win32":
179
+ subprocess.run(["taskkill", "/F", "/IM", exe_name],
180
+ capture_output=True, timeout=10)
181
+ else:
182
+ subprocess.run(["pkill", "-f", exe_name], capture_output=True, timeout=10)
183
+
184
+
185
+ def is_browser_running(exe_path: str) -> bool:
186
+ """Verifica si el navegador ya está corriendo."""
187
+ exe_name = Path(exe_path).name.lower()
188
+ if sys.platform == "win32":
189
+ try:
190
+ result = subprocess.run(
191
+ ["tasklist", "/FI", f"IMAGENAME eq {exe_name}", "/FO", "CSV", "/NH"],
192
+ capture_output=True, text=True, timeout=10,
193
+ )
194
+ return exe_name in result.stdout.lower()
195
+ except Exception:
196
+ return False
197
+ else:
198
+ try:
199
+ result = subprocess.run(["pgrep", "-f", exe_name], capture_output=True, timeout=5)
200
+ return result.returncode == 0
201
+ except Exception:
202
+ return False
203
+
204
+
205
+ def detect_existing_cdp(exe_path: str, user_data_dir: Path) -> int | None:
206
+ """Detecta si el navegador ya tiene CDP activo.
207
+
208
+ Estrategia:
209
+ 1. Leer DevToolsActivePort del user-data-dir
210
+ 2. Escanear command line del proceso por --remote-debugging-port
211
+ 3. Escanear puertos del proceso buscando CDP
212
+ """
213
+ # 1. DevToolsActivePort
214
+ port_file = user_data_dir / "DevToolsActivePort"
215
+ if port_file.exists():
216
+ try:
217
+ content = port_file.read_text().strip().splitlines()
218
+ if content:
219
+ port = int(content[0].strip())
220
+ if port > 0 and test_cdp(port):
221
+ return port
222
+ except (ValueError, IndexError):
223
+ pass
224
+
225
+ # 2. Command line del proceso
226
+ if sys.platform == "win32":
227
+ import re
228
+ exe_name = Path(exe_path).name.lower()
229
+ try:
230
+ result = subprocess.run(
231
+ ["wmic", "process", "where", f"name='{exe_name}'", "get", "commandline", "/format:list"],
232
+ capture_output=True, text=True, timeout=10,
233
+ )
234
+ for line in result.stdout.splitlines():
235
+ m = re.search(r"--remote-debugging-port[=\s](\d{2,5})", line)
236
+ if m:
237
+ port = int(m.group(1))
238
+ if port > 0 and test_cdp(port):
239
+ return port
240
+ except Exception:
241
+ pass
242
+
243
+ # 3. Escanear puertos del proceso (netstat)
244
+ if sys.platform == "win32":
245
+ exe_name = Path(exe_path).name.lower()
246
+ try:
247
+ # Obtener PIDs del navegador
248
+ tasklist = subprocess.run(
249
+ ["tasklist", "/FI", f"IMAGENAME eq {exe_name}", "/FO", "CSV", "/NH"],
250
+ capture_output=True, text=True, timeout=10,
251
+ )
252
+ pids = set()
253
+ for line in tasklist.stdout.splitlines():
254
+ parts = line.strip().strip('"').split('","')
255
+ if len(parts) >= 2:
256
+ try:
257
+ pids.add(int(parts[1]))
258
+ except ValueError:
259
+ pass
260
+
261
+ if pids:
262
+ netstat = subprocess.run(
263
+ ["netstat", "-ano", "-p", "tcp"],
264
+ capture_output=True, text=True, timeout=10,
265
+ )
266
+ for line in netstat.stdout.splitlines():
267
+ tokens = line.split()
268
+ if len(tokens) >= 5 and "LISTENING" in tokens[3]:
269
+ try:
270
+ pid = int(tokens[4])
271
+ if pid in pids:
272
+ port_str = tokens[1].rsplit(":", 1)[1]
273
+ port = int(port_str)
274
+ if port > 1024 and test_cdp(port):
275
+ return port
276
+ except (ValueError, IndexError):
277
+ pass
278
+ except Exception:
279
+ pass
280
+
281
+ return None
282
+
283
+
284
+ def setup_firewall() -> bool:
285
+ """Configura regla de firewall universal para todos los puertos CDP.
286
+
287
+ Solo se ejecuta una vez — verifica si la regla ya existe.
288
+ """
289
+ if sys.platform != "win32":
290
+ return False
291
+
292
+ RULE_NAME = "CDP All Ports (IPC)"
293
+
294
+ # Verificar si ya existe
295
+ try:
296
+ check = subprocess.run(
297
+ ["netsh", "advfirewall", "firewall", "show", "rule", f"name={RULE_NAME}"],
298
+ capture_output=True, text=True, timeout=10,
299
+ )
300
+ if RULE_NAME in check.stdout:
301
+ return True # Ya existe, silencioso
302
+ except Exception:
303
+ pass
304
+
305
+ # Crear regla universal
306
+ try:
307
+ result = subprocess.run(
308
+ ["netsh", "advfirewall", "firewall", "add", "rule",
309
+ f"name={RULE_NAME}", "dir=in", "action=allow",
310
+ "protocol=TCP", "localport=1024-65535"],
311
+ capture_output=True, text=True, timeout=10,
312
+ )
313
+ if result.returncode == 0:
314
+ print(f" Firewall: regla universal creada")
315
+ return True
316
+ except Exception:
317
+ pass
318
+
319
+ return False
320
+
321
+
322
+ def setup_portproxy(port: int) -> bool:
323
+ """Configura netsh portproxy + firewall para que WSL alcance el puerto CDP."""
324
+ if sys.platform != "win32":
325
+ return False
326
+
327
+ # 1. Firewall universal (solo la primera vez)
328
+ setup_firewall()
329
+
330
+ # 2. Verificar si ya existe el portproxy para este puerto
331
+ try:
332
+ check = subprocess.run(
333
+ ["netsh", "interface", "portproxy", "show", "all"],
334
+ capture_output=True, text=True, timeout=10,
335
+ )
336
+ if f"0.0.0.0 {port}" in check.stdout:
337
+ print(f" Portproxy ya existe para puerto {port}")
338
+ return True
339
+ except Exception:
340
+ pass
341
+
342
+ # 3. Crear portproxy
343
+ try:
344
+ cmd = [
345
+ "netsh", "interface", "portproxy", "add", "v4tov4",
346
+ f"listenaddress=0.0.0.0", f"listenport={port}",
347
+ f"connectaddress=127.0.0.1", f"connectport={port}",
348
+ ]
349
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
350
+ if result.returncode == 0:
351
+ print(f" Portproxy configurado para puerto {port}")
352
+ return True
353
+ else:
354
+ print(f" [!] Portproxy fallo (sin permisos?). Ejecuta el .bat como Admin.")
355
+ return False
356
+ except Exception as e:
357
+ print(f" [!] No se pudo configurar portproxy: {e}")
358
+ return False
359
+
360
+
361
+ def get_wsl_host_ip() -> str:
362
+ """Detecta la IP que WSL usa para alcanzar Windows.
363
+
364
+ Desde Windows: ejecuta wsl.exe para leer resolv.conf
365
+ Desde WSL: lee resolv.conf directamente
366
+ """
367
+ import re
368
+
369
+ # 1. Leer directamente (si estamos en WSL)
370
+ resolv = Path("/etc/resolv.conf")
371
+ if resolv.exists():
372
+ try:
373
+ content = resolv.read_text()
374
+ match = re.search(r"nameserver\s+(\d+\.\d+\.\d+\.\d+)", content)
375
+ if match:
376
+ return match.group(1)
377
+ except Exception:
378
+ pass
379
+
380
+ # 2. Desde Windows: leer via wsl.exe
381
+ if sys.platform == "win32":
382
+ try:
383
+ result = subprocess.run(
384
+ ["wsl.exe", "-e", "grep", "nameserver", "/etc/resolv.conf"],
385
+ capture_output=True, text=True, timeout=10,
386
+ )
387
+ match = re.search(r"nameserver\s+(\d+\.\d+\.\d+\.\d+)", result.stdout)
388
+ if match:
389
+ return match.group(1)
390
+ except Exception:
391
+ pass
392
+
393
+ # 3. Fallback: leer resolv.conf desde la ruta WSL en Windows
394
+ if sys.platform == "win32":
395
+ wsl_resolv = Path(r"\\wsl$\Ubuntu\etc\resolv.conf")
396
+ if not wsl_resolv.exists():
397
+ # Intentar con el nombre de la distro por defecto
398
+ for distro in ["Ubuntu", "Ubuntu-22.04", "Ubuntu-24.04", "Debian"]:
399
+ wsl_resolv = Path(rf"\\wsl$\{distro}\etc\resolv.conf")
400
+ if wsl_resolv.exists():
401
+ break
402
+ if wsl_resolv.exists():
403
+ try:
404
+ content = wsl_resolv.read_text()
405
+ match = re.search(r"nameserver\s+(\d+\.\d+\.\d+\.\d+)", content)
406
+ if match:
407
+ return match.group(1)
408
+ except Exception:
409
+ pass
410
+
411
+ return "127.0.0.1"
412
+
413
+
414
+ def update_mcp_json(port: int) -> bool:
415
+ """Actualiza el MCP 'brave' apuntando al wrapper Node dinamico.
416
+
417
+ El wrapper (brave_mcp_launcher.js) lee cdp_info.json en cada invocacion,
418
+ asi el puerto siempre esta fresco sin reescribir .mcp.json.
419
+ """
420
+ wsl_host_ip = get_wsl_host_ip()
421
+ wrapper_path = str(Path(__file__).parent / "brave_mcp_launcher.js")
422
+
423
+ brave_entry = {
424
+ "command": "node",
425
+ "args": [wrapper_path]
426
+ }
427
+
428
+ # Actualizar ambos .mcp.json: el del home y el del proyecto IPC
429
+ mcp_paths = [
430
+ Path(os.path.expandvars(r"%USERPROFILE%\.mcp.json")),
431
+ Path(__file__).parent / ".mcp.json",
432
+ ]
433
+
434
+ updated = 0
435
+ for mcp_path in mcp_paths:
436
+ try:
437
+ if mcp_path.exists():
438
+ data = json.loads(mcp_path.read_text(encoding="utf-8"))
439
+ else:
440
+ data = {"mcpServers": {}}
441
+
442
+ if "mcpServers" not in data:
443
+ data["mcpServers"] = {}
444
+
445
+ data["mcpServers"]["brave"] = brave_entry
446
+ mcp_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
447
+ updated += 1
448
+ except Exception:
449
+ pass
450
+
451
+ print(f" .mcp.json actualizado ({updated} archivos): brave → {wsl_host_ip}:{port}")
452
+ return updated > 0
453
+
454
+
455
+ # ─── Launcher IPC principal ──────────────────────────────────────────────────
456
+
457
+ def launch_browser_ipc(
458
+ port: int = 0,
459
+ headless: bool = False,
460
+ url: str = "",
461
+ kill_existing: bool = True,
462
+ clean: bool = False,
463
+ browser: str = "",
464
+ ) -> dict:
465
+ """
466
+ Lanza un navegador Chromium via IPC con CDP activado.
467
+
468
+ Args:
469
+ port: Puerto CDP. 0 = dinámico (OS asigna).
470
+ headless: Modo sin ventana visible.
471
+ url: URL para abrir al iniciar.
472
+ kill_existing: Matar instancias previas.
473
+ clean: True = perfil limpio separado, False = perfil real con todos tus datos.
474
+ browser: Navegador preferido (brave, chrome, edge). Vacío = auto-detectar.
475
+
476
+ Returns:
477
+ Dict con DEBUG_PORT, DEBUG_WS, BROWSER, USER_DATA, CDP_URL
478
+
479
+ Raises:
480
+ RuntimeError si no puede iniciar o detectar CDP.
481
+ """
482
+ browser_name, browser_exe, user_data = find_browser(preferred=browser)
483
+
484
+ if not browser_exe:
485
+ installed = detect_browsers()
486
+ if installed:
487
+ names = ", ".join(b["name"] for b in installed)
488
+ raise RuntimeError(f"Navegador '{browser}' no encontrado. Disponibles: {names}")
489
+ raise RuntimeError("No se encontró ningún navegador Chromium instalado.")
490
+
491
+ USER_DATA_DIR = CLEAN_USER_DATA if clean else Path(user_data) if user_data else CLEAN_USER_DATA
492
+
493
+ # ─── DETECCION: ¿El navegador ya tiene CDP activo? ───────────────────
494
+ print(f"[1/7] Verificando {browser_name}...")
495
+ print(f" Exe: {browser_exe}")
496
+
497
+ if is_browser_running(browser_exe):
498
+ print(f" {browser_name} ya esta corriendo. Buscando CDP existente...")
499
+ existing_port = detect_existing_cdp(browser_exe, USER_DATA_DIR)
500
+
501
+ if existing_port:
502
+ # CDP ya activo → no reiniciar, solo conectar
503
+ print(f" CDP detectado en puerto {existing_port}. Sin reiniciar!")
504
+ actual_port = existing_port
505
+ version_info = test_cdp(actual_port)
506
+ browser_version = version_info.get("Browser", "Unknown") if version_info else "Unknown"
507
+ ws_url = version_info.get("webSocketDebuggerUrl", "") if version_info else ""
508
+ pages = get_pages(actual_port)
509
+
510
+ print(f"[2/7] Configurando portproxy...")
511
+ setup_portproxy(actual_port)
512
+ print(f"[3/7] Actualizando .mcp.json...")
513
+ update_mcp_json(actual_port)
514
+
515
+ result = {
516
+ "DEBUG_PORT": actual_port,
517
+ "DEBUG_WS": ws_url,
518
+ "BROWSER": browser_version,
519
+ "BROWSER_EXE": browser_exe,
520
+ "PID": 0,
521
+ "USER_DATA": str(USER_DATA_DIR),
522
+ "CDP_URL": f"http://127.0.0.1:{actual_port}",
523
+ "PAGES": len(pages),
524
+ "MODE": "ATTACHED",
525
+ }
526
+ IPC_INFO_FILE.write_text(json.dumps(result, indent=2), encoding="utf-8")
527
+
528
+ print()
529
+ print("=" * 55)
530
+ print(f" MODO: ATTACHED (sin reiniciar)")
531
+ print(f" Navegador: {browser_version} ({Path(browser_exe).name})")
532
+ print(f" Puerto CDP: {actual_port}")
533
+ print(f" Paginas: {len(pages)}")
534
+ print(f" Portproxy: 0.0.0.0:{actual_port} -> 127.0.0.1:{actual_port}")
535
+ print(f" .mcp.json: Actualizado")
536
+ print("=" * 55)
537
+ print()
538
+ print(" /mcp en Claude Code para reconectar.")
539
+ print()
540
+ return result
541
+ else:
542
+ # Navegador corriendo pero SIN CDP → hay que reiniciar
543
+ print(f" CDP no detectado. Reiniciando {browser_name} con CDP...")
544
+ if kill_existing:
545
+ kill_browser(browser_exe)
546
+ time.sleep(2)
547
+ elif kill_existing:
548
+ print(f" {browser_name} no esta corriendo.")
549
+
550
+ # ─── LANZAMIENTO NUEVO ───────────────────────────────────────────────
551
+ # Limpiar DevToolsActivePort anterior
552
+ port_file = USER_DATA_DIR / "DevToolsActivePort"
553
+ if port_file.exists():
554
+ port_file.unlink()
555
+
556
+ profile_label = "LIMPIO" if clean else "REAL (tus datos)"
557
+ print(f"[2/7] Lanzando {browser_name} con CDP...")
558
+ print(f" Perfil: {profile_label}")
559
+
560
+ args = [
561
+ browser_exe,
562
+ f"--remote-debugging-port={port}",
563
+ "--remote-allow-origins=*",
564
+ ]
565
+
566
+ # Solo agregar --user-data-dir si es perfil limpio
567
+ if clean:
568
+ args.append(f"--user-data-dir={USER_DATA_DIR}")
569
+
570
+ args.extend([
571
+ "--disable-backgrounding-occluded-windows",
572
+ ])
573
+
574
+ if headless:
575
+ args.append("--headless=new")
576
+
577
+ if url:
578
+ args.append(url)
579
+
580
+ # ─── LANZAMIENTO IPC ─────────────────────────────────────────────────
581
+ # El proceso padre (este script) lanza Brave como subprocess.
582
+ # La comunicación IPC se da a través del filesystem (DevToolsActivePort)
583
+ # y luego via CDP HTTP/WebSocket.
584
+ print(f" Puerto: {'dinamico' if port == 0 else port}")
585
+
586
+ process = subprocess.Popen(
587
+ args,
588
+ stdout=subprocess.DEVNULL,
589
+ stderr=subprocess.DEVNULL,
590
+ creationflags=subprocess.DETACHED_PROCESS if sys.platform == "win32" else 0,
591
+ )
592
+
593
+ print(f"[3/7] {browser_name} iniciado (PID: {process.pid}). Esperando CDP...")
594
+
595
+ # ─── DETECCIÓN DE PUERTO ─────────────────────────────────────────────
596
+ if port == 0:
597
+ # Puerto dinámico: leer DevToolsActivePort
598
+ detected = read_devtools_active_port(USER_DATA_DIR, timeout=30)
599
+ if not detected:
600
+ raise RuntimeError(
601
+ "No se detectó DevToolsActivePort. "
602
+ "Brave puede haber fallado al iniciar."
603
+ )
604
+ actual_port = detected
605
+ print(f"[4/7] Puerto dinamico detectado via IPC: {actual_port}")
606
+ else:
607
+ actual_port = port
608
+ # Esperar a que el puerto fijo responda
609
+ deadline = time.time() + 30
610
+ while time.time() < deadline:
611
+ if test_cdp(actual_port):
612
+ break
613
+ time.sleep(0.5)
614
+ print(f"[4/7] Puerto fijo confirmado: {actual_port}")
615
+
616
+ # Verificar CDP
617
+ version_info = test_cdp(actual_port)
618
+ if not version_info:
619
+ raise RuntimeError(f"CDP no responde en puerto {actual_port}.")
620
+
621
+ browser_name = version_info.get("Browser", "Unknown")
622
+ ws_url = version_info.get("webSocketDebuggerUrl", "")
623
+ pages = get_pages(actual_port)
624
+
625
+ # ─── PORTPROXY + MCP AUTOMATICO ─────────────────────────────────────
626
+ print(f"[5/7] Configurando portproxy para WSL...")
627
+ setup_portproxy(actual_port)
628
+
629
+ print(f"[6/7] Actualizando .mcp.json...")
630
+ update_mcp_json(actual_port)
631
+
632
+ # Guardar info para otros procesos
633
+ result = {
634
+ "DEBUG_PORT": actual_port,
635
+ "DEBUG_WS": ws_url,
636
+ "BROWSER": browser_name,
637
+ "BROWSER_EXE": browser_exe,
638
+ "PID": process.pid,
639
+ "USER_DATA": str(USER_DATA_DIR),
640
+ "CDP_URL": f"http://127.0.0.1:{actual_port}",
641
+ "PAGES": len(pages),
642
+ }
643
+
644
+ IPC_INFO_FILE.write_text(json.dumps(result, indent=2), encoding="utf-8")
645
+
646
+ print(f"[7/7] Todo listo!")
647
+ print()
648
+ print("=" * 55)
649
+ print(f" MODO: LAUNCHED (nuevo proceso)")
650
+ print(f" Navegador: {browser_name} ({Path(browser_exe).name})")
651
+ print(f" Puerto CDP: {actual_port} (dinamico via IPC)")
652
+ print(f" CDP URL: http://127.0.0.1:{actual_port}")
653
+ print(f" WebSocket: {ws_url}")
654
+ print(f" Paginas: {len(pages)}")
655
+ print(f" PID: {process.pid}")
656
+ print(f" Perfil: {'LIMPIO' if clean else 'REAL (tus datos)'}")
657
+ print(f" Portproxy: 0.0.0.0:{actual_port} -> 127.0.0.1:{actual_port}")
658
+ print(f" .mcp.json: Actualizado")
659
+ print("=" * 55)
660
+ print()
661
+ print(" /mcp en Claude Code para reconectar.")
662
+ print()
663
+
664
+ return result
665
+
666
+
667
+ # ─── CLI ──────────────────────────────────────────────────────────────────────
668
+
669
+ def main() -> int:
670
+ import argparse
671
+ parser = argparse.ArgumentParser(
672
+ description="Browser IPC CDP Launcher - Abre cualquier navegador Chromium con CDP via IPC",
673
+ formatter_class=argparse.RawDescriptionHelpFormatter,
674
+ epilog="""
675
+ Ejemplos:
676
+ python brave_ipc.py # Auto-detecta navegador + CDP
677
+ python brave_ipc.py --browser brave # Forzar Brave
678
+ python brave_ipc.py --browser chrome # Forzar Chrome
679
+ python brave_ipc.py --browser edge # Forzar Edge
680
+ python brave_ipc.py --clean # Perfil limpio separado
681
+ python brave_ipc.py --port 9222 # Puerto fijo
682
+ python brave_ipc.py --url https://n8n.io # Abre URL
683
+ python brave_ipc.py --headless # Sin ventana
684
+ python brave_ipc.py --no-kill # No mata el navegador existente
685
+ python brave_ipc.py --list # Lista navegadores instalados
686
+ """,
687
+ )
688
+ parser.add_argument("--browser", default="",
689
+ help="Navegador a usar (brave, chrome, edge, chromium). Vacío = auto-detectar")
690
+ parser.add_argument("--port", type=int, default=0,
691
+ help="Puerto CDP (0=dinámico, OS asigna)")
692
+ parser.add_argument("--headless", action="store_true",
693
+ help="Modo headless (sin ventana visible)")
694
+ parser.add_argument("--url", default="",
695
+ help="URL para abrir al iniciar")
696
+ parser.add_argument("--no-kill", action="store_true",
697
+ help="No matar instancias previas")
698
+ parser.add_argument("--clean", action="store_true",
699
+ help="Usar perfil limpio separado (sin tus datos)")
700
+ parser.add_argument("--list", action="store_true",
701
+ help="Listar navegadores Chromium instalados")
702
+ args = parser.parse_args()
703
+
704
+ if args.list:
705
+ browsers = detect_browsers()
706
+ if not browsers:
707
+ print("No se encontraron navegadores Chromium instalados.")
708
+ return 1
709
+ print("Navegadores Chromium detectados:")
710
+ for b in browsers:
711
+ print(f" - {b['name']:10s} → {b['exe']}")
712
+ config = load_config()
713
+ if config.get("name"):
714
+ print(f"\nConfigurado: {config['name']} ({config.get('exe', '')})")
715
+ return 0
716
+
717
+ try:
718
+ result = launch_browser_ipc(
719
+ port=args.port,
720
+ headless=args.headless,
721
+ url=args.url,
722
+ kill_existing=not args.no_kill,
723
+ clean=args.clean,
724
+ browser=args.browser,
725
+ )
726
+ return 0
727
+ except RuntimeError as e:
728
+ print(f"\n[ERROR] {e}")
729
+ return 1
730
+
731
+
732
+ if __name__ == "__main__":
733
+ raise SystemExit(main())
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Brave MCP Dynamic Launcher
3
+ *
4
+ * Resuelve dinamicamente el puerto CDP (Brave/Chrome/Edge) y lanza
5
+ * chrome-devtools-mcp apuntando al puerto correcto. Si CDP no responde,
6
+ * lanza brave_ipc.py automaticamente para iniciar el navegador.
7
+ *
8
+ * Uso en .mcp.json:
9
+ * {
10
+ * "mcpServers": {
11
+ * "brave": {
12
+ * "command": "node",
13
+ * "args": ["C:\\Users\\NyGsoft\\Desktop\\ipc\\brave_mcp_launcher.js"]
14
+ * }
15
+ * }
16
+ * }
17
+ *
18
+ * Flujo:
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
24
+ */
25
+ const { execSync, spawn, spawnSync } = require('child_process');
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const http = require('http');
29
+
30
+ const CDP_INFO = path.join(__dirname, 'cdp_info.json');
31
+ const HOME_CDP_INFO = path.join(process.env.USERPROFILE || process.env.HOME || '.', 'cdp_info.json');
32
+ const BRAVE_IPC_PY = path.join(__dirname, 'brave_ipc.py');
33
+
34
+ const IS_WIN = process.platform === 'win32';
35
+ let IS_WSL = false;
36
+ try {
37
+ const v = fs.readFileSync('/proc/version', 'utf-8').toLowerCase();
38
+ IS_WSL = v.includes('microsoft') || v.includes('wsl');
39
+ } catch (e) {}
40
+
41
+ function logErr(msg) {
42
+ process.stderr.write(`[brave-mcp] ${msg}\n`);
43
+ }
44
+
45
+ function readCdpInfo() {
46
+ for (const p of [CDP_INFO, HOME_CDP_INFO]) {
47
+ if (fs.existsSync(p)) {
48
+ try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch (e) {}
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function getHostIP() {
55
+ if (IS_WIN) return '127.0.0.1';
56
+ try {
57
+ const resolv = fs.readFileSync('/etc/resolv.conf', 'utf-8');
58
+ const match = resolv.match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
59
+ if (match) return match[1];
60
+ } catch (e) {}
61
+ return '127.0.0.1';
62
+ }
63
+
64
+ function testCdp(url, timeoutMs = 3000) {
65
+ return new Promise((resolve) => {
66
+ const req = http.get(`${url}/json/version`, { timeout: timeoutMs }, (res) => {
67
+ let data = '';
68
+ res.on('data', c => data += c);
69
+ res.on('end', () => {
70
+ try { resolve(JSON.parse(data)); } catch (e) { resolve(null); }
71
+ });
72
+ });
73
+ req.on('error', () => resolve(null));
74
+ req.on('timeout', () => { req.destroy(); resolve(null); });
75
+ });
76
+ }
77
+
78
+ async function ensureCdpReady() {
79
+ const hostIP = getHostIP();
80
+ let info = readCdpInfo();
81
+ let port = info && info.DEBUG_PORT;
82
+
83
+ // Intento 1: CDP responde con puerto del cdp_info.json
84
+ if (port) {
85
+ const url = `http://${hostIP}:${port}`;
86
+ if (await testCdp(url)) {
87
+ logErr(`CDP activo en ${url}`);
88
+ return url;
89
+ }
90
+ // Intento 2: localhost (caso Windows nativo)
91
+ if (hostIP !== '127.0.0.1') {
92
+ const localUrl = `http://127.0.0.1:${port}`;
93
+ if (await testCdp(localUrl)) {
94
+ logErr(`CDP activo en ${localUrl} (fallback localhost)`);
95
+ return localUrl;
96
+ }
97
+ }
98
+ }
99
+
100
+ // Intento 3: auto-launch via brave_ipc.py
101
+ logErr('CDP no responde. Auto-lanzando Brave via brave_ipc.py...');
102
+ if (!fs.existsSync(BRAVE_IPC_PY)) {
103
+ logErr(`brave_ipc.py no encontrado en ${BRAVE_IPC_PY}`);
104
+ return null;
105
+ }
106
+
107
+ const pythonCmd = IS_WSL ? 'python3' : 'python';
108
+ const launchCmd = IS_WSL
109
+ ? `cmd.exe /c python "${BRAVE_IPC_PY.replace(/^\/mnt\/([a-z])\//, (_, d) => `${d.toUpperCase()}:\\`).replace(/\//g, '\\\\')}" --no-kill`
110
+ : `${pythonCmd} "${BRAVE_IPC_PY}" --no-kill`;
111
+
112
+ try {
113
+ spawnSync(launchCmd, { shell: true, timeout: 60000, stdio: 'pipe' });
114
+ } catch (e) {
115
+ logErr(`Auto-launch fallo: ${e.message}`);
116
+ }
117
+
118
+ // Releer y reverificar
119
+ info = readCdpInfo();
120
+ port = info && info.DEBUG_PORT;
121
+ if (port) {
122
+ const url = `http://${hostIP}:${port}`;
123
+ if (await testCdp(url, 5000)) {
124
+ logErr(`CDP activo despues de auto-launch en ${url}`);
125
+ return url;
126
+ }
127
+ const localUrl = `http://127.0.0.1:${port}`;
128
+ if (await testCdp(localUrl, 5000)) {
129
+ logErr(`CDP activo despues de auto-launch en ${localUrl}`);
130
+ return localUrl;
131
+ }
132
+ }
133
+
134
+ logErr('CDP sigue sin responder despues de auto-launch.');
135
+ return null;
136
+ }
137
+
138
+ async function main() {
139
+ const browserUrl = await ensureCdpReady();
140
+
141
+ if (!browserUrl) {
142
+ logErr('No se pudo establecer CDP. El MCP intentara conectar de todas formas.');
143
+ }
144
+
145
+ const finalUrl = browserUrl || `http://127.0.0.1:9222`;
146
+ logErr(`Lanzando chrome-devtools-mcp -> ${finalUrl}`);
147
+
148
+ const child = spawn(
149
+ IS_WIN ? 'cmd.exe' : 'npx',
150
+ IS_WIN
151
+ ? ['/c', 'npx', '-y', 'chrome-devtools-mcp@latest', '--browserUrl', finalUrl]
152
+ : ['-y', 'chrome-devtools-mcp@latest', '--browserUrl', finalUrl],
153
+ { stdio: ['inherit', 'inherit', 'inherit'], shell: !IS_WIN }
154
+ );
155
+
156
+ child.on('exit', (code) => process.exit(code || 0));
157
+ child.on('error', (e) => {
158
+ logErr(`spawn error: ${e.message}`);
159
+ process.exit(1);
160
+ });
161
+ }
162
+
163
+ main().catch(e => {
164
+ logErr(`fatal: ${e.message}`);
165
+ process.exit(1);
166
+ });
package/lib/mcp.js CHANGED
@@ -47,11 +47,13 @@ function getWslHostIp() {
47
47
  }
48
48
 
49
49
  function updateMcpJson(port, wslIp) {
50
- // Estrategia: ejecutar el MCP desde Windows via cmd.exe
51
- // Asi usa 127.0.0.1 directo, sin portproxy ni firewall
50
+ // Estrategia: apuntar al wrapper Node dinamico.
51
+ // El wrapper lee cdp_info.json en cada invocacion -> puerto siempre fresco.
52
+ // No hay que reescribir .mcp.json cuando cambia el puerto dinamico.
53
+ const wrapperPath = path.join(__dirname, '..', 'brave_mcp_launcher.js');
52
54
  const braveEntry = {
53
- command: 'cmd.exe',
54
- args: ['/c', 'npx', '-y', 'chrome-devtools-mcp@latest', '--browserUrl', `http://127.0.0.1:${port}`],
55
+ command: 'node',
56
+ args: [wrapperPath],
55
57
  };
56
58
 
57
59
  // Rutas base donde buscar .mcp.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-ipc-cdp",
3
- "version": "1.6.0",
3
+ "version": "1.8.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
6
  "browser-ipc-cdp": "bin/cli.js"
@@ -34,7 +34,9 @@
34
34
  "files": [
35
35
  "bin/",
36
36
  "lib/",
37
- "templates/",
37
+ "brave_ipc.py",
38
+ "brave_mcp_launcher.js",
39
+ "brave_cdp.bat",
38
40
  "README.md",
39
41
  "LICENSE"
40
42
  ]