chore: auto-commit (43 archivos)
- .mcp.json - bash/functions/infra/write_mcp_jupyter_config.md - bash/functions/infra/write_mcp_jupyter_config.sh - cpp/CMakeLists.txt - cpp/apps/chart_demo - cpp/apps/shaders_lab - cpp/functions/gfx/gl_framebuffer.cpp - cpp/functions/gfx/gl_framebuffer.h - cpp/functions/gfx/gl_framebuffer.md - cpp/functions/gfx/mesh_gpu.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: chrome_load_extensions
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "chrome_load_extensions [--port N] [--profile DIR] --ext PATH [--ext PATH ...] [--proxy URL] [--url URL]"
|
||||
description: "Lanza Chrome con extensiones unpacked via --load-extension (WSL2→Windows chrome.exe, paths traducidos, join sin echo, setsid anti-exit-144). OJO: --load-extension SOLO funciona en Chrome for Testing/Chromium/Dev. En Chrome STABLE 138+ esta DESACTIVADO (feature DisableLoadExtensionCommandLineSwitch + bloqueo duro en 148) y carga 0 extensiones aunque el cmdline sea correcto. Para Chrome stable usar install via Web Store (1-clic, persiste en perfil) o enterprise policy ExtensionInstallForcelist (requiere HKLM/HKCU Policies escribible — denegado en maquinas gestionadas)."
|
||||
tags: [chrome, cdp, browser, extensions, wsl2, navegator]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
params:
|
||||
- name: "--port N"
|
||||
desc: "Puerto de remote debugging CDP. Default: 9222."
|
||||
- name: "--profile DIR"
|
||||
desc: "Chrome user-data-dir. Acepta ruta Windows (C:\\...) o ruta WSL/Linux (se traduce via wslpath -w). Default: C:\\Users\\<USERNAME>\\AppData\\Local\\fn-chrome-cdp-profile (WSL2) o /tmp/fn-chrome-cdp-profile (Linux nativo)."
|
||||
- name: "--ext PATH"
|
||||
desc: "Ruta a un directorio de extensión unpacked. Repetible. Acepta ruta Windows (se pasa intacta) o ruta WSL/Linux (se traduce via wslpath -w). Obligatorio al menos uno."
|
||||
- name: "--proxy URL"
|
||||
desc: "Proxy opcional, ej. http://127.0.0.1:8889. Agrega --proxy-server=URL a Chrome."
|
||||
- name: "--url URL"
|
||||
desc: "URL inicial opcional para abrir con --new-window."
|
||||
output: "PID del proceso Chrome lanzado (stdout). Mensajes de estado en stderr. CDP listo en 127.0.0.1:<port>."
|
||||
file_path: "bash/functions/browser/chrome_load_extensions.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/browser/chrome_load_extensions.sh
|
||||
|
||||
chrome_load_extensions \
|
||||
--port 9222 \
|
||||
--profile 'C:\Users\lucas\AppData\Local\fn-chrome-cdp-profile' \
|
||||
--ext 'C:\Users\lucas\hls-dl-ext' \
|
||||
--ext 'C:\Users\lucas\ubol' \
|
||||
--proxy http://127.0.0.1:8889 \
|
||||
--url https://www.gnularetro.cc/
|
||||
```
|
||||
|
||||
Sin proxy ni URL, sólo extensiones:
|
||||
|
||||
```bash
|
||||
source bash/functions/browser/chrome_load_extensions.sh
|
||||
|
||||
pid=$(chrome_load_extensions \
|
||||
--ext '/home/lucas/dev/hls-dl-ext' \
|
||||
--ext '/home/lucas/dev/ubol')
|
||||
# Paths WSL traducidos automáticamente a Windows.
|
||||
# CDP listo en 127.0.0.1:9222.
|
||||
echo "Chrome PID: $pid"
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites Chrome CDP con extensiones unpacked cargadas (HLS downloader, uBlock Origin, extensiones en desarrollo) y `chrome_launch_go_browser` no sirve porque hardcodea `--disable-extensions`. WSL2→Windows. Ideal para sesiones de navegator con proxy + extensión activa.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **MUERTO en Chrome STABLE 138+ (validado 2026-05-30, Chrome 148)**: `--load-extension` NO carga nada en el canal stable, ni con `--disable-extensions-except` ni con `--disable-features=DisableLoadExtensionCommandLineSwitch`. `chrome://version` muestra el flag correcto pero `chrome://extensions` sale vacío. Google lo bloqueó duro en stable. La función SOLO sirve en **Chrome for Testing / Chromium / Dev/Canary**, donde el switch sigue activo. Para stable: ver opciones abajo.
|
||||
- **Instalar en Chrome STABLE (las que SÍ funcionan)**:
|
||||
1. **Web Store 1-clic** — abre la página del store en el perfil CDP, el humano da "Añadir a Chrome". Persiste en el perfil para siempre (futuros lanzamientos ya con la extensión, sin flags). El popup de confirmación es UI del navegador (no DOM) → NO es CDP-clickable, requiere gesto humano. Único método no-admin que persiste por-perfil.
|
||||
2. **Enterprise policy** `ExtensionInstallForcelist` (HKCU/HKLM `\Software\Policies\Google\Chrome`) — force-install sin clic desde el store, browser-wide. El key `Policies\Google\Chrome` puede dar "Access denied" al escribir (visto 2026-05-30 incluso en máquina personal vía reg.exe/PowerShell desde WSL — Chrome/Windows protege el subárbol Policies). Si funciona, requiere relanzar Chrome para que descargue del store. Método global (afecta todos los perfiles).
|
||||
3. Extensiones **unpacked custom** (no en store, ej. un HLS downloader propio) en stable: no hay vía no-admin. Empaquetar a CRX + self-host `update_url` + policy, o usar Chrome for Testing. A menudo innecesario si la lógica vive fuera (ej. `grab_stream.py` descarga sin extensión).
|
||||
- **Combo flags (solo Chrome for Testing/dev)**: requiere AMBOS `--load-extension=p1,p2` Y `--disable-extensions-except=p1,p2` juntos + `--disable-features=DisableLoadExtensionCommandLineSwitch`. **NUNCA `--disable-extensions`** (desactiva todo).
|
||||
- **join sin `echo`**: rutas Windows `C:\Users\...` tienen `\U`; el `echo` de zsh (o sh con xpg_echo) lo interpreta como escape unicode y trunca la ruta a `C:`. La función usa acumulador `+=`, no `echo`. Verificable en `chrome://version` (debe verse el path completo, no `--load-extension=C:`).
|
||||
- **exit 144 en Bash tool**: si el proceso Chrome retiene el pipe stdout, la herramienta devuelve exit 144. Esta función lanza con `setsid ... </dev/null >log 2>&1 &` + `disown` para desacoplar completamente. El log queda en `/tmp/chrome_ext_<port>.log`.
|
||||
- **WSL2: traducir paths con `wslpath -w`**: los paths de `--ext` y `--profile` que sean rutas Linux se traducen automáticamente. Las rutas Windows (`C:\...`) se pasan intactas. `wslpath` debe estar disponible (estándar en WSL2 desde Windows 10 1903+).
|
||||
- **Perfil ya abierto**: si Chrome ya tiene ese perfil abierto, relanzar añade una ventana extra a la misma instancia. La función detecta si CDP ya responde en el puerto y avisa por stderr, pero procede igualmente.
|
||||
- **Web Store vs unpacked**: instalar extensiones desde la Web Store (un clic) persiste en el perfil sin necesidad de flags y sobrevive reinicios. Esta función es para extensiones unpacked en desarrollo o que no están en la Web Store. Si usas ambas, los flags no interfieren con las instaladas del store.
|
||||
- **zsh globbing**: `--remote-allow-origins=*` está dentro de comillas en la función, no se expande. Si lo pasas desde la línea de comandos, entrecomillarlo.
|
||||
- **Proxy + extensión**: si usas proxy para captura de tráfico (Burp, mitmproxy, gost), el proxy se aplica a toda la sesión Chrome, incluyendo el tráfico de las extensiones.
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
# chrome_load_extensions — lanza Chrome (WSL2→Windows chrome.exe) con extensiones unpacked cargadas en un perfil CDP.
|
||||
# Chrome 148+: requiere --load-extension=<paths> Y --disable-extensions-except=<same paths> juntos.
|
||||
# NUNCA pasar --disable-extensions (desactiva todo, incluyendo las que quieres cargar).
|
||||
|
||||
chrome_load_extensions() {
|
||||
local port=9222
|
||||
local profile=""
|
||||
local proxy=""
|
||||
local url=""
|
||||
local -a ext_paths=()
|
||||
|
||||
# --- Parse args ---
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--port)
|
||||
port="$2"; shift 2 ;;
|
||||
--profile)
|
||||
profile="$2"; shift 2 ;;
|
||||
--ext)
|
||||
ext_paths+=("$2"); shift 2 ;;
|
||||
--proxy)
|
||||
proxy="$2"; shift 2 ;;
|
||||
--url)
|
||||
url="$2"; shift 2 ;;
|
||||
--*)
|
||||
echo "chrome_load_extensions: flag desconocido: $1" >&2; return 1 ;;
|
||||
*)
|
||||
# Positional = extra ext path
|
||||
ext_paths+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ${#ext_paths[@]} -eq 0 ]]; then
|
||||
echo "chrome_load_extensions: se requiere al menos un --ext PATH de extension unpacked" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- Detectar chrome.exe ---
|
||||
local chrome_bin=""
|
||||
if command -v chrome.exe &>/dev/null; then
|
||||
chrome_bin="chrome.exe"
|
||||
elif [[ -f "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" ]]; then
|
||||
chrome_bin="/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
|
||||
elif [[ -f "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" ]]; then
|
||||
chrome_bin="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
|
||||
else
|
||||
echo "chrome_load_extensions: chrome.exe no encontrado en PATH ni en rutas conocidas" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- Detectar WSL2 ---
|
||||
local wsl2=0
|
||||
if grep -qi 'microsoft\|wsl' /proc/version 2>/dev/null; then
|
||||
wsl2=1
|
||||
fi
|
||||
|
||||
# --- Traducir paths de extensiones a Windows si hace falta ---
|
||||
local -a win_ext_paths=()
|
||||
for p in "${ext_paths[@]}"; do
|
||||
if [[ $wsl2 -eq 1 ]] && [[ "$p" != [A-Za-z]:\\* ]]; then
|
||||
# Path Linux → traducir a Windows
|
||||
local win_p
|
||||
win_p=$(wslpath -w "$p" 2>/dev/null) || {
|
||||
echo "chrome_load_extensions: wslpath -w '$p' falló" >&2
|
||||
return 1
|
||||
}
|
||||
win_ext_paths+=("$win_p")
|
||||
else
|
||||
win_ext_paths+=("$p")
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Resolver perfil ---
|
||||
if [[ -z "$profile" ]]; then
|
||||
# Default: perfil canónico fn-chrome-cdp-profile en Windows
|
||||
local win_user="${USERNAME:-${USER:-lucas}}"
|
||||
if [[ $wsl2 -eq 1 ]]; then
|
||||
profile="C:\\Users\\${win_user}\\AppData\\Local\\fn-chrome-cdp-profile"
|
||||
else
|
||||
profile="/tmp/fn-chrome-cdp-profile"
|
||||
fi
|
||||
elif [[ $wsl2 -eq 1 ]] && [[ "$profile" != [A-Za-z]:\\* ]]; then
|
||||
# Path Linux del perfil → traducir a Windows
|
||||
profile=$(wslpath -w "$profile" 2>/dev/null) || {
|
||||
echo "chrome_load_extensions: wslpath -w '$profile' falló" >&2
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
# --- Construir lista de paths separada por coma (para Chrome) ---
|
||||
# Chrome usa coma como separador en --load-extension y --disable-extensions-except.
|
||||
# NO usar `echo` para el join: rutas Windows como C:\Users tienen \U, y el echo de
|
||||
# zsh (o sh con xpg_echo) interpreta \U como escape unicode y trunca la ruta a "C:".
|
||||
# Acumulador con += y printf-safe, sin interpretacion de backslashes.
|
||||
local ext_list=""
|
||||
local p
|
||||
for p in "${win_ext_paths[@]}"; do
|
||||
ext_list+="${ext_list:+,}${p}"
|
||||
done
|
||||
|
||||
# --- Construir args de Chrome ---
|
||||
local -a args=(
|
||||
"--remote-debugging-port=${port}"
|
||||
"--user-data-dir=${profile}"
|
||||
"--no-first-run"
|
||||
"--no-default-browser-check"
|
||||
"--remote-allow-origins=*"
|
||||
"--load-extension=${ext_list}"
|
||||
"--disable-extensions-except=${ext_list}"
|
||||
# Chrome 137+ activa por defecto el feature DisableLoadExtensionCommandLineSwitch,
|
||||
# que IGNORA silenciosamente --load-extension. Hay que desactivarlo o las
|
||||
# extensiones unpacked no cargan (chrome://extensions sale vacio).
|
||||
"--disable-features=DisableLoadExtensionCommandLineSwitch"
|
||||
)
|
||||
|
||||
# WSL2: bind en 0.0.0.0 para que sea accesible desde la red WSL
|
||||
if [[ $wsl2 -eq 1 ]]; then
|
||||
args+=("--remote-debugging-address=0.0.0.0")
|
||||
fi
|
||||
|
||||
if [[ -n "$proxy" ]]; then
|
||||
args+=("--proxy-server=${proxy}")
|
||||
fi
|
||||
|
||||
if [[ -n "$url" ]]; then
|
||||
args+=("--new-window" "$url")
|
||||
fi
|
||||
|
||||
# --- Revisar si CDP ya responde en el puerto ---
|
||||
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
|
||||
echo "chrome_load_extensions: CDP ya activo en puerto ${port}; lanzando ventana extra" >&2
|
||||
fi
|
||||
|
||||
# --- Lanzar Chrome desacoplado del proceso padre ---
|
||||
# setsid + redirección evita el exit 144 en el Bash tool (el pipe no queda retenido).
|
||||
setsid "$chrome_bin" "${args[@]}" </dev/null >"/tmp/chrome_ext_${port}.log" 2>&1 &
|
||||
local chrome_pid=$!
|
||||
disown "$chrome_pid"
|
||||
|
||||
echo "chrome_load_extensions: Chrome lanzado PID=${chrome_pid} puerto=${port}" >&2
|
||||
|
||||
# --- Esperar a que CDP esté listo (hasta 15 segundos) ---
|
||||
local deadline=$(( $(date +%s) + 15 ))
|
||||
local ready=0
|
||||
while [[ $(date +%s) -lt $deadline ]]; do
|
||||
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [[ $ready -eq 1 ]]; then
|
||||
echo "chrome_load_extensions: CDP listo en 127.0.0.1:${port}"
|
||||
else
|
||||
echo "chrome_load_extensions: advertencia — CDP no respondió en 15s en puerto ${port}; Chrome puede estar iniciando lentamente" >&2
|
||||
fi
|
||||
|
||||
echo "$chrome_pid"
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: start_nordvpn_socks_bridge
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "start_nordvpn_socks_bridge([--port N] [--socks-host HOST] [--socks-port N] [--user U] [--pass P]) -> JSON"
|
||||
description: "Levanta un proxy HTTP local sin auth que reenvía al servidor SOCKS5 de NordVPN con auth usando gost v3. Resuelve la limitación de Chrome, que no soporta SOCKS5-con-auth: el navegador apunta a http://127.0.0.1:<port> (sin auth) y el tráfico sale por NordVPN. Idempotente: si el puerto ya escucha, no relanza."
|
||||
tags: [navegator, vpn, proxy, nordvpn, socks5, gost, chrome, cdp]
|
||||
params:
|
||||
- name: "--port"
|
||||
desc: "Puerto HTTP local del bridge (default 8889)"
|
||||
- name: "--socks-host"
|
||||
desc: "Servidor SOCKS5 de NordVPN (default socks-nl1.nordvpn.com)"
|
||||
- name: "--socks-port"
|
||||
desc: "Puerto del servidor SOCKS5 (default 1080)"
|
||||
- name: "--user"
|
||||
desc: "Service username de NordVPN. Si se omite, lee NORDVPN_SOCKS_USER del entorno"
|
||||
- name: "--pass"
|
||||
desc: "Service password de NordVPN. Si se omite, lee NORDVPN_SOCKS_PASS del entorno"
|
||||
output: "JSON en stdout: {proxy_url, pid, socks_host, status}. status puede ser 'running' (lanzado ahora) o 'already_running' (puerto ya escuchaba). Errores a stderr + exit 1."
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/start_nordvpn_socks_bridge.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Con env vars (recomendado para scripts):
|
||||
NORDVPN_SOCKS_USER=xxx NORDVPN_SOCKS_PASS=yyy \
|
||||
bash bash/functions/infra/start_nordvpn_socks_bridge.sh \
|
||||
--port 8889 \
|
||||
--socks-host socks-nl1.nordvpn.com
|
||||
|
||||
# Salida (primera vez):
|
||||
# {"proxy_url":"http://127.0.0.1:8889","pid":12345,"socks_host":"socks-nl1.nordvpn.com","status":"running"}
|
||||
|
||||
# Salida (idempotente, ya corría):
|
||||
# {"proxy_url":"http://127.0.0.1:8889","pid":null,"socks_host":"socks-nl1.nordvpn.com","status":"already_running"}
|
||||
|
||||
# Luego Chrome (o el flujo CDP del navegator) apunta al bridge:
|
||||
# chrome.exe --proxy-server=http://127.0.0.1:8889
|
||||
|
||||
# Verificar que el tráfico sale por NordVPN:
|
||||
# curl -x http://127.0.0.1:8889 https://api.ipify.org
|
||||
# -> 109.202.99.x (IP NordVPN NL, no la IP de casa)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesitas que Chrome (o cualquier app que solo acepta proxy HTTP sin auth) salga por NordVPN, pero NordVPN solo ofrece SOCKS5-con-auth. Chrome no soporta SOCKS5-with-authentication — este bridge actúa de intermediario sin auth local. Útil especialmente en el flujo CDP del navegator (cdp-cli + agente browser) cuando quieres que el browser de automatización salga con IP NordVPN para evadir DPI del ISP o geo-bloqueos, sin exponer las credenciales NordVPN al proceso del browser.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **NordVPN SOCKS5 exige service credentials**, no el usuario/contraseña de la cuenta NordVPN. Se obtienen en dashboard.nordvpn.com → Manual Setup → Service credentials.
|
||||
- **Chrome no soporta SOCKS5-auth** nativamente (a diferencia de Firefox que sí). Por eso este bridge HTTP-sin-auth es necesario.
|
||||
- **gost escucha en todas las interfaces** (`-L http://:PORT`). El puerto local NO tiene autenticación. No exponer en redes no confiables. Para bind solo a loopback cambiar el flag a `-L http://127.0.0.1:PORT` en el script si es necesario.
|
||||
- **Servidores NordVPN SOCKS5 disponibles**: `socks-nl1..8.nordvpn.com`, `socks-de1..4.nordvpn.com`, `socks-us1..8.nordvpn.com`, etc. La lista completa en el dashboard de NordVPN.
|
||||
- **Si gost no está instalado**: se descarga automáticamente `gost v3.0.0 linux amd64` a `~/.local/bin/gost`. Requiere curl y tar.
|
||||
- **Log**: en `/tmp/nordvpn_socks_bridge_<port>.log`. Consultar si el bridge no arranca.
|
||||
- **PID null en already_running**: cuando el puerto ya escuchaba, el PID del proceso no se recupera (habría que hacer `lsof`/`ss` para identificarlo).
|
||||
- **Consumidor principal**: flujo `navegator`/CDP — ver `docs/capabilities/navegator.md`. El agente browser lanza este bridge antes de abrir Chrome con `--proxy-server=http://127.0.0.1:<port>`.
|
||||
- **Gotcha invocación desde el Bash tool de Claude (exit 144)**: el script deja gost en background (`nohup ... & disown`); ese daemon retiene el pipe de stdout del tool → el harness mata el proceso con SIGSTKFLT (exit 144) AUNQUE el bridge SÍ arranca bien. Lanzar con `run_in_background:true` o redirigiendo todo (`>/tmp/x 2>&1 </dev/null`) para evitarlo. En terminal real (o `fn run` interactivo) no ocurre. Verificado 2026-05-30: el bridge queda corriendo y funcional pese al 144.
|
||||
- **Windows→WSL**: si Chrome corre en Windows (chrome.exe) y gost en WSL2, Chrome alcanza `127.0.0.1:<port>` vía localhostForwarding de WSL2. Verificar con `curl -x http://127.0.0.1:<port> https://api.ipify.org` desde ambos lados.
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# start_nordvpn_socks_bridge — Levanta proxy HTTP local sin auth que reenvía a SOCKS5 NordVPN con auth via gost v3.
|
||||
# Resuelve la limitacion de Chrome que no soporta SOCKS5-con-auth.
|
||||
set -euo pipefail
|
||||
|
||||
# --- defaults ---
|
||||
PORT=8889
|
||||
SOCKS_HOST="socks-nl1.nordvpn.com"
|
||||
SOCKS_PORT=1080
|
||||
VPN_USER="${NORDVPN_SOCKS_USER:-}"
|
||||
VPN_PASS="${NORDVPN_SOCKS_PASS:-}"
|
||||
|
||||
# --- parse args ---
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--port) PORT="$2"; shift 2 ;;
|
||||
--socks-host) SOCKS_HOST="$2"; shift 2 ;;
|
||||
--socks-port) SOCKS_PORT="$2"; shift 2 ;;
|
||||
--user) VPN_USER="$2"; shift 2 ;;
|
||||
--pass) VPN_PASS="$2"; shift 2 ;;
|
||||
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- validate creds ---
|
||||
if [[ -z "$VPN_USER" ]]; then
|
||||
echo "error: NORDVPN_SOCKS_USER not set. Use --user or export NORDVPN_SOCKS_USER" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$VPN_PASS" ]]; then
|
||||
echo "error: NORDVPN_SOCKS_PASS not set. Use --pass or export NORDVPN_SOCKS_PASS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_FILE="/tmp/nordvpn_socks_bridge_${PORT}.log"
|
||||
|
||||
# --- check idempotencia: ya escucha? ---
|
||||
if ss -ltn 2>/dev/null | grep -q ":${PORT} "; then
|
||||
echo "{\"proxy_url\":\"http://127.0.0.1:${PORT}\",\"pid\":null,\"socks_host\":\"${SOCKS_HOST}\",\"status\":\"already_running\"}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- asegurar gost ---
|
||||
GOST_BIN=""
|
||||
if command -v gost &>/dev/null; then
|
||||
GOST_BIN="$(command -v gost)"
|
||||
elif [[ -x "$HOME/.local/bin/gost" ]]; then
|
||||
GOST_BIN="$HOME/.local/bin/gost"
|
||||
else
|
||||
echo "gost not found, downloading v3.0.0 linux amd64..." >&2
|
||||
GOST_URL="https://github.com/go-gost/gost/releases/download/v3.0.0/gost_3.0.0_linux_amd64.tar.gz"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
curl -fsSL "$GOST_URL" -o "$TMP_DIR/gost.tar.gz" >&2
|
||||
tar -xzf "$TMP_DIR/gost.tar.gz" -C "$TMP_DIR" >&2
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
cp "$TMP_DIR/gost" "$HOME/.local/bin/gost"
|
||||
chmod +x "$HOME/.local/bin/gost"
|
||||
rm -rf "$TMP_DIR"
|
||||
GOST_BIN="$HOME/.local/bin/gost"
|
||||
echo "gost installed to $GOST_BIN" >&2
|
||||
fi
|
||||
|
||||
# --- url-encode user y pass (puede tener caracteres especiales) ---
|
||||
url_encode() {
|
||||
python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=''))" "$1"
|
||||
}
|
||||
|
||||
ENC_USER="$(url_encode "$VPN_USER")"
|
||||
ENC_PASS="$(url_encode "$VPN_PASS")"
|
||||
|
||||
# --- lanzar gost en background ---
|
||||
nohup "$GOST_BIN" \
|
||||
-L "http://:${PORT}" \
|
||||
-F "socks5://${ENC_USER}:${ENC_PASS}@${SOCKS_HOST}:${SOCKS_PORT}" \
|
||||
>"$LOG_FILE" 2>&1 &
|
||||
GOST_PID=$!
|
||||
disown $GOST_PID
|
||||
|
||||
# --- esperar ~2s y verificar que el puerto escucha ---
|
||||
sleep 2
|
||||
if ! ss -ltn 2>/dev/null | grep -q ":${PORT} "; then
|
||||
echo "error: gost did not start. Last lines of $LOG_FILE:" >&2
|
||||
tail -10 "$LOG_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "{\"proxy_url\":\"http://127.0.0.1:${PORT}\",\"pid\":${GOST_PID},\"socks_host\":\"${SOCKS_HOST}\",\"status\":\"running\"}"
|
||||
@@ -3,11 +3,11 @@ name: write_mcp_jupyter_config
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "write_mcp_jupyter_config([project_dir: string], [port: int]) -> string"
|
||||
description: "Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server apuntando al venv local y puerto dado. Merge con jq si ya existe."
|
||||
tags: [mcp, jupyter, config, setup, infra]
|
||||
description: "Genera o actualiza .mcp.json con la config de jupyter-mcp-server apuntando al console-script del venv local (transport stdio + flags --jupyter-url/--jupyter-token). Merge con jq reemplazando la entrada jupyter entera."
|
||||
tags: [mcp, jupyter, config, setup, infra, notebook]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -30,10 +30,28 @@ file_path: "bash/functions/infra/write_mcp_jupyter_config.sh"
|
||||
|
||||
```bash
|
||||
source write_mcp_jupyter_config.sh
|
||||
path=$(write_mcp_jupyter_config /home/lucas/analysis/finanzas 8890)
|
||||
path=$(write_mcp_jupyter_config /home/lucas/fn_registry/analysis/finanzas 8890)
|
||||
echo "Config MCP en: $path"
|
||||
# Genera .mcp.json con:
|
||||
# "command": ".../.venv/bin/jupyter-mcp-server"
|
||||
# "args": ["--transport","stdio","--jupyter-url","http://localhost:8890","--jupyter-token",""]
|
||||
```
|
||||
|
||||
## Notas
|
||||
## Cuando usarla
|
||||
|
||||
El MCP se invoca como modulo Python (`python -m jupyter_mcp_server`) usando el python del venv local, nunca una instalacion global. Si `.mcp.json` ya existe y jq esta disponible, hace merge conservando otros servidores MCP. Sin jq, sobrescribe el archivo.
|
||||
- Al crear un analysis Jupyter nuevo (la usa el pipeline `init_jupyter_analysis`).
|
||||
- Tras mover/recrear un venv y necesitar regenerar el `.mcp.json` del analysis.
|
||||
- Para reparar un `.mcp.json` con el comando viejo roto (`python -m jupyter_mcp_server.server`).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **NUNCA `python -m jupyter_mcp_server.server`** — `server.py` no tiene bloque `__main__`; el proceso importa y sale 0 y el MCP nunca arranca. El entrypoint real es la CLI (`jupyter_mcp_server.CLI:server`), expuesta como console-script `jupyter-mcp-server`. Sin subcomando arranca en stdio por defecto.
|
||||
- **No usa env vars** `SERVER_URL`/`TOKEN`. La CLI lee flags `--jupyter-url` / `--jupyter-token` (cubren document + runtime). Configs viejas con bloque `env` quedan inertes.
|
||||
- **Tolera Jupyter apagado al boot**: el MCP responde `initialize` tras un connect-timeout (~10s) y sirve igual. Arrancar Jupyter despues en `:port` y los tools se enganchan. No hace falta reiniciar Claude por tener Jupyter caido al inicio.
|
||||
- **Requiere `jupyter-mcp-server` instalado en el venv**: `uv pip install jupyter-mcp-server`. La funcion aborta si el console-script no existe.
|
||||
- **Path atado al venv del analysis**: si borras el analysis, ese `.mcp.json` apunta a un binario inexistente. Para un MCP jupyter global e independiente, el `.mcp.json` raiz de `fn_registry` usa el binario del venv canonico `python/.venv/bin/jupyter-mcp-server` (sobrevive el borrado de cualquier analysis).
|
||||
- **Merge con jq usa `+` (shallow)** en el mapa de servidores para reemplazar la entrada `jupyter` entera; `*` (deep) dejaba keys huerfanas de configs viejas.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-05-28) — fix comando roto: console-script `jupyter-mcp-server` + flags stdio en vez de `python -m ...server` + env vars. Merge `+` para reemplazar entrada entera. Tag `notebook`.
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
# write_mcp_jupyter_config
|
||||
# -------------------------
|
||||
# Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server.
|
||||
# Usa el python del venv local con -m jupyter_mcp_server.server.
|
||||
# Configura via env vars (SERVER_URL, TOKEN) — no CLI args.
|
||||
# Usa el console-script `jupyter-mcp-server` del venv local con transport stdio
|
||||
# y los flags --jupyter-url / --jupyter-token (NO env vars, NO `-m ...server`).
|
||||
# Hace merge si ya existe .mcp.json (requiere jq).
|
||||
#
|
||||
# GOTCHA (2026-05-28): `python -m jupyter_mcp_server.server` NO arranca nada —
|
||||
# server.py no tiene bloque __main__, asi que el proceso importa y sale 0 y el
|
||||
# MCP nunca levanta. El entrypoint real es la CLI (`jupyter_mcp_server.CLI:server`,
|
||||
# expuesta como console-script `jupyter-mcp-server`), que sin subcomando arranca
|
||||
# en stdio por defecto. La config tampoco lee SERVER_URL/TOKEN: usa los flags
|
||||
# --jupyter-url / --jupyter-token. El MCP tolera que Jupyter este apagado al
|
||||
# arrancar (responde `initialize` tras un connect-timeout ~10s y sirve igual).
|
||||
#
|
||||
# USO (sourced):
|
||||
# source write_mcp_jupyter_config.sh
|
||||
# write_mcp_jupyter_config /path/to/project 8888
|
||||
@@ -17,14 +25,15 @@ write_mcp_jupyter_config() {
|
||||
abs_project="$(cd "$project_dir" && pwd)"
|
||||
|
||||
local python_bin="${abs_project}/.venv/bin/python"
|
||||
local mcp_bin="${abs_project}/.venv/bin/jupyter-mcp-server"
|
||||
if [ ! -f "$python_bin" ]; then
|
||||
echo "write_mcp_jupyter_config: python no encontrado en ${python_bin}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verificar que el modulo esta instalado
|
||||
if ! "$python_bin" -c "import jupyter_mcp_server" 2>/dev/null; then
|
||||
echo "write_mcp_jupyter_config: jupyter_mcp_server no instalado en el venv" >&2
|
||||
# Verificar que el console-script esta instalado
|
||||
if [ ! -x "$mcp_bin" ]; then
|
||||
echo "write_mcp_jupyter_config: jupyter-mcp-server no instalado en el venv (${mcp_bin}). Instala con: uv pip install jupyter-mcp-server" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -33,12 +42,12 @@ write_mcp_jupyter_config() {
|
||||
{
|
||||
"mcpServers": {
|
||||
"jupyter": {
|
||||
"command": "${python_bin}",
|
||||
"args": ["-m", "jupyter_mcp_server.server"],
|
||||
"env": {
|
||||
"SERVER_URL": "http://localhost:${port}",
|
||||
"TOKEN": ""
|
||||
}
|
||||
"command": "${mcp_bin}",
|
||||
"args": [
|
||||
"--transport", "stdio",
|
||||
"--jupyter-url", "http://localhost:${port}",
|
||||
"--jupyter-token", ""
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,8 +55,10 @@ EOF
|
||||
)
|
||||
|
||||
if [ -f "$mcp_file" ] && command -v jq &>/dev/null; then
|
||||
# Merge conservando otros servidores MCP
|
||||
jq -s '.[0] * {mcpServers: ((.[0].mcpServers // {}) * (.[1].mcpServers // {}))}' \
|
||||
# Merge conservando otros servidores MCP. Usa `+` (shallow) en el mapa de
|
||||
# servidores para REEMPLAZAR la entrada `jupyter` entera — `*` (deep) dejaba
|
||||
# keys huerfanas de configs viejas (ej. bloque `env` obsoleto).
|
||||
jq -s '.[0] * {mcpServers: ((.[0].mcpServers // {}) + (.[1].mcpServers // {}))}' \
|
||||
"$mcp_file" <(echo "$new_config") > "${mcp_file}.tmp"
|
||||
mv "${mcp_file}.tmp" "$mcp_file"
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user