browser-ipc-cdp 1.7.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 +7 -6
- package/brave_cdp.bat +17 -0
- package/brave_ipc.py +733 -0
- package/brave_mcp_launcher.js +166 -0
- package/lib/mcp.js +6 -4
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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:
|
|
51
|
-
//
|
|
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: '
|
|
54
|
-
args: [
|
|
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.
|
|
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
|
-
"
|
|
37
|
+
"brave_ipc.py",
|
|
38
|
+
"brave_mcp_launcher.js",
|
|
39
|
+
"brave_cdp.bat",
|
|
38
40
|
"README.md",
|
|
39
41
|
"LICENSE"
|
|
40
42
|
]
|