From c65f1698ae3732f10ae69ebc8324bf953be33c2a Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Wed, 3 Jun 2026 21:48:01 +0200 Subject: [PATCH] fix(infra): write_mcp_jupyter_config usa wrapper jupyter_mcp_serve con el venv del analysis El .mcp.json generado ahora apunta al wrapper jupyter_mcp_serve.sh con env overrides (JUPYTER_MCP_VENV/ROOT/PORT/TOKEN) en vez del console-script jupyter-mcp-server directo. Antes: el .mcp.json solo CONECTABA a un Jupyter ya existente y, si se abria Claude desde la raiz del repo, el MCP usaba el venv canonico python/.venv (sin las deps del analisis). Ahora el wrapper arranca (o reusa) un Jupyter con el venv del propio analisis, asi que abrir Claude desde el directorio del analisis basta y cada analisis ejecuta con sus dependencias sin contaminar python/.venv. Bump v1.2.0. Declara dependencia jupyter_mcp_serve_bash_infra. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../infra/write_mcp_jupyter_config.md | 36 ++++++---- .../infra/write_mcp_jupyter_config.sh | 71 ++++++++++++++----- 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/bash/functions/infra/write_mcp_jupyter_config.md b/bash/functions/infra/write_mcp_jupyter_config.md index a7d5f5a7..a9ac0c33 100644 --- a/bash/functions/infra/write_mcp_jupyter_config.md +++ b/bash/functions/infra/write_mcp_jupyter_config.md @@ -3,12 +3,12 @@ name: write_mcp_jupyter_config kind: function lang: bash domain: infra -version: "1.1.0" +version: "1.2.0" purity: impure signature: "write_mcp_jupyter_config([project_dir: string], [port: int]) -> string" -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." +description: "Genera o actualiza .mcp.json para un analisis Jupyter. La entrada jupyter usa el wrapper jupyter_mcp_serve.sh con env overrides (venv, root y puerto del analisis), de modo que el MCP arranca su propio Jupyter con el venv del analisis. Merge con jq reemplazando la entrada jupyter entera." tags: [mcp, jupyter, config, setup, infra, notebook] -uses_functions: [] +uses_functions: [jupyter_mcp_serve_bash_infra] uses_types: [] returns: [] returns_optional: false @@ -16,9 +16,9 @@ error_type: "error_go_core" imports: [] params: - name: project_dir - desc: "directorio del proyecto Jupyter (default: directorio actual)" + desc: "directorio del proyecto/analisis Jupyter (default: directorio actual)" - name: port - desc: "puerto Jupyter (default: detectado automáticamente)" + desc: "puerto Jupyter del analisis (default: 8888)" output: "ruta del archivo .mcp.json generado o actualizado" tested: false tests: [] @@ -33,25 +33,33 @@ source write_mcp_jupyter_config.sh path=$(write_mcp_jupyter_config $HOME/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",""] +# "command": "bash" +# "args": [".../bash/functions/infra/jupyter_mcp_serve.sh"] +# "env": { +# "JUPYTER_MCP_VENV": ".../analysis/finanzas/.venv", +# "JUPYTER_MCP_ROOT": ".../analysis/finanzas", +# "JUPYTER_MCP_PORT": "8890", +# "JUPYTER_MCP_TOKEN": "" +# } ``` ## Cuando usarla - 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`). +- Para reparar un `.mcp.json` con el comando viejo (console-script directo que no arranca Jupyter, o `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. +- **Usa el wrapper, no el console-script directo**: el `.mcp.json` apunta a `jupyter_mcp_serve.sh` (ver `jupyter_mcp_serve_bash_infra`), que arranca (o reusa) el Jupyter del analisis con su venv antes de exec del MCP. Con el console-script directo (`jupyter-mcp-server --jupyter-url ...`) el MCP solo se CONECTA: si el server no esta levantado no hay kernel y las operaciones sobre notebooks fallan. Con el wrapper basta abrir Claude desde el analisis — no hace falta lanzar `run-jupyter-lab.sh` aparte. +- **El venv del kernel es el del analisis** (`JUPYTER_MCP_VENV`), no `python/.venv` del repo. Asi cada analisis ejecuta con sus propias dependencias sin contaminar el venv canonico. Este fix nacio de un caso real (analisis `nats`): trabajar desde la raiz de `fn_registry` cargaba el MCP global (8899, venv `python/.venv`) que no tenia `nats-py`. +- **Reuso por puerto**: si ya hay un Jupyter escuchando en `JUPYTER_MCP_PORT` (p.ej. lanzado por `run-jupyter-lab.sh`, que es colaborativo), el wrapper lo reusa en vez de arrancar otro. Si no hay ninguno, el wrapper levanta uno propio (sin `--collaborative`, suficiente para el MCP). Para colaboracion humana en tiempo real, lanzar `run-jupyter-lab.sh` antes. +- **NUNCA `python -m jupyter_mcp_server.server`** — `server.py` no tiene bloque `__main__`; importa y sale 0, el MCP nunca arranca. El entrypoint real es el console-script `jupyter-mcp-server`, que el wrapper localiza dentro del venv del analisis. +- **Requiere `jupyter-mcp-server` instalado en el venv del analisis**: `uv pip install jupyter-mcp-server`. La funcion aborta si el console-script no existe. +- **Localiza el wrapper subiendo directorios** desde `project_dir` (hasta 8 niveles) buscando `bash/functions/infra/jupyter_mcp_serve.sh`; si no lo encuentra, usa `FN_REGISTRY_ROOT`. Aborta si no aparece por ninguna via. +- **Merge con jq usa `+` (shallow)** en el mapa de servidores para reemplazar la entrada `jupyter` entera; `*` (deep) dejaba keys huerfanas de configs viejas (p.ej. el bloque `args` del console-script directo). ## Capability growth log +- v1.2.0 (2026-06-03) — el `.mcp.json` generado usa el wrapper `jupyter_mcp_serve.sh` con env overrides (`JUPYTER_MCP_VENV/ROOT/PORT/TOKEN`) en vez del console-script directo. Garantiza que el MCP arranca su propio Jupyter con el venv del analisis (antes solo conectaba y usaba el venv equivocado si se abria Claude desde la raiz del repo). Declara dependencia `jupyter_mcp_serve_bash_infra`. - 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`. diff --git a/bash/functions/infra/write_mcp_jupyter_config.sh b/bash/functions/infra/write_mcp_jupyter_config.sh index ae6b1e27..8e5bef85 100644 --- a/bash/functions/infra/write_mcp_jupyter_config.sh +++ b/bash/functions/infra/write_mcp_jupyter_config.sh @@ -1,21 +1,32 @@ # write_mcp_jupyter_config # ------------------------- -# Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server. -# 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). +# Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server para un +# analisis/proyecto. La entrada `jupyter` usa el wrapper `jupyter_mcp_serve.sh` +# (no el console-script directo), de modo que el MCP SIEMPRE tiene servidor: el +# wrapper arranca (o reusa) un Jupyter Lab en el puerto indicado usando el venv +# del propio analisis y lo engancha al MCP por stdio. +# +# Por que el wrapper y no el console-script directo: el console-script +# `jupyter-mcp-server --jupyter-url http://localhost:PORT` solo se CONECTA, no +# arranca Jupyter. Si el server no esta levantado, el MCP responde `initialize` +# pero no hay kernel y toda operacion sobre notebooks falla. El wrapper levanta el +# server con el venv correcto (JUPYTER_MCP_VENV) antes de exec del MCP, asi que +# abrir Claude desde el analisis basta — no hace falta lanzar run-jupyter-lab.sh +# aparte. Si ya hay un Jupyter en ese puerto (p.ej. run-jupyter-lab.sh), lo reusa. +# +# Env overrides que se inyectan al wrapper (ver jupyter_mcp_serve.sh): +# JUPYTER_MCP_VENV venv del analisis (su .venv, con jupyter + jupyter-mcp-server) +# JUPYTER_MCP_ROOT root de notebooks = directorio del analisis +# JUPYTER_MCP_PORT puerto del Jupyter gestionado +# JUPYTER_MCP_TOKEN token (vacio: solo escucha en 127.0.0.1) # # 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). +# server.py no tiene bloque __main__. El entrypoint real es el console-script +# `jupyter-mcp-server` (que el wrapper localiza dentro del venv del analisis). # # USO (sourced): # source write_mcp_jupyter_config.sh -# write_mcp_jupyter_config /path/to/project 8888 +# write_mcp_jupyter_config /path/to/analysis 8890 write_mcp_jupyter_config() { local project_dir="${1:-.}" @@ -31,23 +42,47 @@ write_mcp_jupyter_config() { return 1 fi - # Verificar que el console-script esta instalado + # Verificar que el console-script esta instalado en el venv del analisis 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 + # Localizar el wrapper jupyter_mcp_serve.sh subiendo desde el directorio del + # analisis hasta la raiz del repo. Fallback a FN_REGISTRY_ROOT. + local wrapper="" d="$abs_project" + local i + for i in 1 2 3 4 5 6 7 8; do + if [ -f "$d/bash/functions/infra/jupyter_mcp_serve.sh" ]; then + wrapper="$d/bash/functions/infra/jupyter_mcp_serve.sh" + break + fi + d="$(dirname "$d")" + [ "$d" = "/" ] && break + done + if [ -z "$wrapper" ] && [ -n "${FN_REGISTRY_ROOT:-}" ] && [ -f "${FN_REGISTRY_ROOT}/bash/functions/infra/jupyter_mcp_serve.sh" ]; then + wrapper="${FN_REGISTRY_ROOT}/bash/functions/infra/jupyter_mcp_serve.sh" + fi + if [ -z "$wrapper" ]; then + echo "write_mcp_jupyter_config: no encuentro bash/functions/infra/jupyter_mcp_serve.sh subiendo desde ${abs_project} ni en FN_REGISTRY_ROOT" >&2 + return 1 + fi + local new_config new_config=$(cat << EOF { "mcpServers": { "jupyter": { - "command": "${mcp_bin}", + "command": "bash", "args": [ - "--transport", "stdio", - "--jupyter-url", "http://localhost:${port}", - "--jupyter-token", "" - ] + "${wrapper}" + ], + "env": { + "JUPYTER_MCP_VENV": "${abs_project}/.venv", + "JUPYTER_MCP_ROOT": "${abs_project}", + "JUPYTER_MCP_PORT": "${port}", + "JUPYTER_MCP_TOKEN": "" + } } } } @@ -57,7 +92,7 @@ EOF if [ -f "$mcp_file" ] && command -v jq &>/dev/null; then # 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). + # keys huerfanas de configs viejas (ej. flags `args` obsoletos). jq -s '.[0] * {mcpServers: ((.[0].mcpServers // {}) + (.[1].mcpServers // {}))}' \ "$mcp_file" <(echo "$new_config") > "${mcp_file}.tmp" mv "${mcp_file}.tmp" "$mcp_file"