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:
2026-05-30 17:28:47 +02:00
parent a2efdcf003
commit fd5787c55f
44 changed files with 3924 additions and 64 deletions
@@ -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