#!/usr/bin/env bash # jupyter_mcp_serve — arranca (o reusa) un Jupyter Lab colaborativo y lanza el # Jupyter MCP server enganchado a el por stdio. Pensado para ser el `command` de # la entrada "jupyter" en .mcp.json: garantiza que el MCP SIEMPRE tiene servidor. # # Por que existe: el MCP datalayer NO arranca jupyter, solo se conecta. Si la URL # apunta a un puerto sin jupyter (en esta maquina 8888 = proxy VPN gluetun), el # MCP nunca conecta. Este wrapper levanta su propio jupyter en un puerto propio. # # Env overrides: # JUPYTER_MCP_ROOT raiz de notebooks (default: raiz del repo) # JUPYTER_MCP_PORT puerto del jupyter gestionado (default: 8899) # JUPYTER_MCP_VENV venv (default: /python/.venv) # JUPYTER_MCP_TOKEN token (default: "" — solo escucha en 127.0.0.1) # # stdout esta RESERVADO al protocolo stdio del MCP. Todo log va a stderr + LOGFILE. # Nunca hacer echo a stdout aqui. # # Uso directo / test: # bash jupyter_mcp_serve.sh --dry-run # arranca jupyter, NO exec del MCP, loguea args set -euo pipefail DRY=0 [ "${1:-}" = "--dry-run" ] && DRY=1 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # raiz del repo = tres niveles arriba de bash/functions/infra/ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" VENV="${JUPYTER_MCP_VENV:-$REPO_ROOT/python/.venv}" ROOT_DIR="${JUPYTER_MCP_ROOT:-$REPO_ROOT}" PORT="${JUPYTER_MCP_PORT:-8899}" HOST=127.0.0.1 TOKEN="${JUPYTER_MCP_TOKEN:-}" LOGDIR="${HOME}/.fn_jupyter_mcp" mkdir -p "$LOGDIR" LOGFILE="$LOGDIR/wrapper.log" JLOG="$LOGDIR/jupyterlab.log" log(){ printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOGFILE"; printf '%s\n' "$*" >&2; } JUPYTER="$VENV/bin/jupyter" MCP="$VENV/bin/jupyter-mcp-server" if [ ! -x "$JUPYTER" ]; then log "FATAL: $JUPYTER no existe. Instala: cd $REPO_ROOT/python && uv pip install --python .venv/bin/python3 jupyterlab jupyter-collaboration jupyter-mcp-server" exit 1 fi if [ ! -x "$MCP" ]; then log "FATAL: $MCP no existe. Instala jupyter-mcp-server en el venv." exit 1 fi server_up(){ local code code="$(curl -s -m 3 -o /dev/null -w '%{http_code}' "http://$HOST:$PORT/api/status?token=$TOKEN" 2>/dev/null || true)" [ "$code" = "200" ] } if server_up; then log "reuso jupyter existente en $HOST:$PORT" else log "arranco jupyter colaborativo en $HOST:$PORT (root=$ROOT_DIR)" nohup "$JUPYTER" lab \ --no-browser \ --ServerApp.ip="$HOST" \ --ServerApp.port="$PORT" \ --ServerApp.root_dir="$ROOT_DIR" \ --IdentityProvider.token="$TOKEN" \ --ServerApp.disable_check_xsrf=True \ --ServerApp.allow_origin='*' \ >>"$JLOG" 2>&1 & disown 2>/dev/null || true # esperar hasta ~30s a que levante for _ in $(seq 1 60); do server_up && break sleep 0.5 done if ! server_up; then log "FATAL: jupyter no levanto en 30s. Ver $JLOG" exit 1 fi log "jupyter arriba" fi BASE="http://$HOST:$PORT" # Detectar el dialecto de CLI del MCP (cambia entre versiones de jupyter-mcp-server) HELP="$("$MCP" --help 2>&1 || true)" ARGS=(--transport stdio) if printf '%s' "$HELP" | grep -q -- '--document-url'; then ARGS+=(--document-url "$BASE" --runtime-url "$BASE") printf '%s' "$HELP" | grep -q -- '--document-token' && ARGS+=(--document-token "$TOKEN" --runtime-token "$TOKEN") elif printf '%s' "$HELP" | grep -q -- '--jupyter-url'; then ARGS+=(--jupyter-url "$BASE" --jupyter-token "$TOKEN") else # fallback: variables de entorno que las distintas versiones reconocen export DOCUMENT_URL="$BASE" RUNTIME_URL="$BASE" DOCUMENT_TOKEN="$TOKEN" RUNTIME_TOKEN="$TOKEN" export JUPYTER_URL="$BASE" JUPYTER_TOKEN="$TOKEN" fi log "MCP cmd: $MCP ${ARGS[*]}" if [ "$DRY" = "1" ]; then log "--dry-run: no ejecuto el MCP. Jupyter sigue corriendo en $BASE" exit 0 fi exec "$MCP" "${ARGS[@]}"