chore: auto-commit (7 archivos)
- .mcp.json - bash/functions/pipelines/full_git_push.sh - python/pyproject.toml - python/uv.lock - bash/functions/infra/jupyter_mcp_serve.md - bash/functions/infra/jupyter_mcp_serve.sh - dev/issues/0166-app-to-app-dependencies-tracking.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,8 @@
|
|||||||
"args": ["--enable-run", "--enable-write"]
|
"args": ["--enable-run", "--enable-write"]
|
||||||
},
|
},
|
||||||
"jupyter": {
|
"jupyter": {
|
||||||
"command": "/home/lucas/fn_registry/python/.venv/bin/jupyter-mcp-server",
|
"command": "bash",
|
||||||
"args": [
|
"args": ["/home/enmanuel/fn_registry/bash/functions/infra/jupyter_mcp_serve.sh"]
|
||||||
"--transport", "stdio",
|
|
||||||
"--jupyter-url", "http://localhost:8888",
|
|
||||||
"--jupyter-token", ""
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
name: jupyter_mcp_serve
|
||||||
|
kind: function
|
||||||
|
lang: bash
|
||||||
|
domain: infra
|
||||||
|
version: 1.0.0
|
||||||
|
purity: impure
|
||||||
|
error_type: "error_go_core"
|
||||||
|
signature: "jupyter_mcp_serve.sh [--dry-run]"
|
||||||
|
description: "Arranca (o reusa) un Jupyter Lab colaborativo en un puerto propio y lanza el Jupyter MCP server enganchado por stdio. Entrypoint robusto para la entrada 'jupyter' de .mcp.json: garantiza que el MCP SIEMPRE tiene servidor al que conectarse, sin depender de que haya un jupyter en 8888."
|
||||||
|
tags: [notebook, jupyter, mcp, infra, launcher-glue]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
params:
|
||||||
|
- name: "--dry-run"
|
||||||
|
desc: "Opcional. Arranca/verifica jupyter pero NO hace exec del MCP; loguea el comando elegido. Para tests."
|
||||||
|
output: "Proceso jupyter-mcp-server enganchado por stdio a un Jupyter Lab colaborativo local (127.0.0.1, puerto JUPYTER_MCP_PORT, default 8899). Logs en ~/.fn_jupyter_mcp/. stdout reservado al protocolo MCP."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Que hace
|
||||||
|
|
||||||
|
El MCP de Jupyter (datalayer `jupyter-mcp-server`) **no arranca jupyter**, solo se
|
||||||
|
conecta a uno existente. Si la URL configurada no tiene jupyter detras, el MCP
|
||||||
|
nunca conecta. En esta maquina `localhost:8888` es el **proxy HTTP del contenedor
|
||||||
|
VPN gluetun**, no un jupyter — por eso el MCP fallaba siempre.
|
||||||
|
|
||||||
|
Este wrapper resuelve la cadena entera:
|
||||||
|
|
||||||
|
1. Localiza el venv (`python/.venv`) y los binarios `jupyter` + `jupyter-mcp-server`.
|
||||||
|
2. Si ya hay un jupyter gestionado vivo en `127.0.0.1:$PORT` (`/api/status` = 200) lo reusa.
|
||||||
|
3. Si no, arranca `jupyter lab` colaborativo detached (RTC via `jupyter-collaboration`),
|
||||||
|
en `JUPYTER_MCP_ROOT` (default = raiz del repo, asi cualquier notebook del arbol es lanzable).
|
||||||
|
4. Detecta el dialecto de CLI del MCP (`--document-url` nuevo / `--jupyter-url` viejo / env vars)
|
||||||
|
y hace `exec` del MCP por `--transport stdio`.
|
||||||
|
|
||||||
|
Self-adapting: funciona aunque cambie la version de `jupyter-mcp-server`.
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Como lo usa Claude Code (entrada en .mcp.json):
|
||||||
|
# "jupyter": { "command": "bash", "args": ["bash/functions/infra/jupyter_mcp_serve.sh"] }
|
||||||
|
|
||||||
|
# Test manual (arranca jupyter en 8899, no lanza el MCP):
|
||||||
|
bash bash/functions/infra/jupyter_mcp_serve.sh --dry-run
|
||||||
|
curl -s http://127.0.0.1:8899/api/status # {"started":..., "version":...}
|
||||||
|
|
||||||
|
# Cambiar puerto / raiz de notebooks:
|
||||||
|
JUPYTER_MCP_PORT=8900 JUPYTER_MCP_ROOT=/home/enmanuel/fn_registry/analysis \
|
||||||
|
bash bash/functions/infra/jupyter_mcp_serve.sh --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Cuando quieras que el MCP de Jupyter de Claude Code **siempre** tenga servidor:
|
||||||
|
es el `command` de la entrada `jupyter` en `.mcp.json`. No la invoques a mano salvo
|
||||||
|
para depurar (`--dry-run`) o para levantar el jupyter colaborativo sin el MCP.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **stdout reservado**: el MCP habla por stdout (protocolo stdio). El wrapper jamas
|
||||||
|
escribe a stdout — todo log va a stderr y `~/.fn_jupyter_mcp/wrapper.log`. No metas
|
||||||
|
`echo` a stdout aqui o rompes el handshake del MCP.
|
||||||
|
- **Puerto 8888 ocupado por gluetun** en esta maquina. Por eso el default es **8899**.
|
||||||
|
Si 8899 tambien se ocupa, exporta `JUPYTER_MCP_PORT`.
|
||||||
|
- **Token vacio**: solo escucha en `127.0.0.1` con `disable_check_xsrf` + `allow_origin '*'`.
|
||||||
|
Aceptable en local; NO exponer el puerto a la red.
|
||||||
|
- **venv requerido**: necesita `python/.venv` con `jupyterlab`, `jupyter-collaboration`
|
||||||
|
y `jupyter-mcp-server`. Reconstruir: `cd python && uv sync --extra jupyter`.
|
||||||
|
- El jupyter arrancado queda **detached** (nohup): persiste entre invocaciones del MCP.
|
||||||
|
Para pararlo: `python/.venv/bin/jupyter server stop 8899` o `pkill -f 'jupyter-lab.*8899'`.
|
||||||
|
|
||||||
|
## Capability growth log
|
||||||
|
|
||||||
|
v1.0.0 (2026-06-01) — version inicial. Wrapper auto-start: reusa/levanta jupyter
|
||||||
|
colaborativo en puerto propio (8899) y autodetecta el dialecto de CLI del MCP.
|
||||||
Executable
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/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: <repo>/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[@]}"
|
||||||
@@ -18,8 +18,9 @@ source "$CYBERSEC_DIR/scan_secrets_in_dirty.sh"
|
|||||||
full_git_push() {
|
full_git_push() {
|
||||||
local commit_message="${1:-}"
|
local commit_message="${1:-}"
|
||||||
|
|
||||||
# Resolver raiz del registry
|
# Resolver raiz del registry. Deriva de SCRIPT_DIR (bash/functions/pipelines/)
|
||||||
local registry_root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
# para funcionar en cualquier PC sin path hardcodeado.
|
||||||
|
local registry_root="${FN_REGISTRY_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}"
|
||||||
cd "$registry_root"
|
cd "$registry_root"
|
||||||
|
|
||||||
echo "=== full_git_push: inicio ===" >&2
|
echo "=== full_git_push: inicio ===" >&2
|
||||||
@@ -74,6 +75,25 @@ full_git_push() {
|
|||||||
# Redescubrir repos tras posibles inicializaciones
|
# Redescubrir repos tras posibles inicializaciones
|
||||||
repos=$(discover_git_repos "$registry_root")
|
repos=$(discover_git_repos "$registry_root")
|
||||||
|
|
||||||
|
# --- Paso 1c: Incluir el repo de configuracion de Claude ---
|
||||||
|
# Los archivos de ~/.claude/ (settings.json, commands, skills, CLAUDE.md...)
|
||||||
|
# son symlinks a un repo git externo (dataforge/repo_Claude). Lo resolvemos
|
||||||
|
# de forma portable siguiendo el symlink de settings.json — sin hardcodear
|
||||||
|
# el path, que difiere entre PCs. Si resuelve a un repo git, lo anadimos a
|
||||||
|
# la lista para que pase por scan-secrets + auto-commit + push como los demas.
|
||||||
|
local claude_repo=""
|
||||||
|
if [[ -L "$HOME/.claude/settings.json" ]]; then
|
||||||
|
local _claude_settings_real
|
||||||
|
_claude_settings_real=$(readlink -f "$HOME/.claude/settings.json" 2>/dev/null || true)
|
||||||
|
if [[ -n "$_claude_settings_real" ]]; then
|
||||||
|
claude_repo=$(git -C "$(dirname "$_claude_settings_real")" rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -n "$claude_repo" && -d "$claude_repo/.git" ]]; then
|
||||||
|
echo "[1c] Incluyendo repo de config Claude: $claude_repo" >&2
|
||||||
|
repos="$repos"$'\n'"$claude_repo"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Paso 2: Escanear secrets ---
|
# --- Paso 2: Escanear secrets ---
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "[2/6] Escaneando secrets en dirty trees..." >&2
|
echo "[2/6] Escaneando secrets en dirty trees..." >&2
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
id: "0166"
|
||||||
|
title: "Rastrear dependencias entre apps (app→app) para clonado/build reproducible"
|
||||||
|
status: pendiente
|
||||||
|
type: enhancement
|
||||||
|
domain:
|
||||||
|
- registry-quality
|
||||||
|
- build
|
||||||
|
scope: registry-only
|
||||||
|
priority: media
|
||||||
|
depends: []
|
||||||
|
blocks: []
|
||||||
|
related: []
|
||||||
|
created: 2026-05-31
|
||||||
|
updated: 2026-05-31
|
||||||
|
tags: [build, apps, dependencies, clone, migration]
|
||||||
|
---
|
||||||
|
# 0166 — Rastrear dependencias entre apps (app→app)
|
||||||
|
|
||||||
|
## APP Metadata
|
||||||
|
|
||||||
|
| Campo | Valor |
|
||||||
|
|-------|-------|
|
||||||
|
| **ID** | 0166 |
|
||||||
|
| **Estado** | pendiente |
|
||||||
|
| **Prioridad** | media |
|
||||||
|
| **Tipo** | enhancement — metadata de apps + `fn app clone` / `fn doctor` |
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
Durante la migración a Linux nativo (PC `lucas-linux`, 2026-05-31) al levantar los
|
||||||
|
servicios systemd hubo que clonar+compilar los apps de servicio. El build de
|
||||||
|
**`sqlite_api`** (`projects/fn_monitoring/apps/sqlite_api`) falló con:
|
||||||
|
|
||||||
|
```
|
||||||
|
handlers.go:13:2: package fn-registry/apps/data_factory/datafactory is not in std
|
||||||
|
```
|
||||||
|
|
||||||
|
`sqlite_api` **importa un paquete de otro app** (`apps/data_factory/datafactory`).
|
||||||
|
Como `data_factory` no estaba clonado, no compilaba. Hubo que clonarlo a mano para
|
||||||
|
desbloquear el build. No hay forma declarada de saber, antes de clonar/compilar un app,
|
||||||
|
**qué otros apps necesita**.
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
- `fn app clone <id>` clona solo el repo pedido; no arrastra los apps de los que depende.
|
||||||
|
- `fn sync locations` lista paths por PC pero no relaciones app→app.
|
||||||
|
- El grafo de dependencias entre apps es implícito (vive solo en los `import` de Go),
|
||||||
|
así que clonar el subset correcto en una máquina nueva es ensayo-error.
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Hacer explícitas y consultables las dependencias **app→app**, para que:
|
||||||
|
|
||||||
|
1. Se pueda **saber** qué apps dependen de qué apps (consulta / reporte).
|
||||||
|
2. `fn app clone <id>` pueda **arrastrar** los apps-dependencia (o al menos avisarlos).
|
||||||
|
3. `fn doctor` detecte clones incompletos (app presente pero su dep ausente).
|
||||||
|
|
||||||
|
## Propuesta (a concretar)
|
||||||
|
|
||||||
|
1. **Metadata declarada** en `app.md`: añadir campo `depends_on_apps: [<id>...]`
|
||||||
|
en el frontmatter (análogo a `uses_functions`/`uses_types` de las funciones).
|
||||||
|
2. **Detección automática** (validación): un audit que parsee los `import` de cada app,
|
||||||
|
detecte imports de la forma `fn-registry/apps/<X>/...` o `.../projects/.../apps/<X>/...`
|
||||||
|
y compare con `depends_on_apps` (igual que `audit_uses_functions` para funciones).
|
||||||
|
- Caso semilla confirmado: `sqlite_api` → `data_factory`.
|
||||||
|
3. **`fn app clone --with-deps <id>`**: resuelve el cierre transitivo de `depends_on_apps`
|
||||||
|
y clona todo lo necesario a las rutas de `pc_locations`.
|
||||||
|
4. **`fn doctor`**: marcar `[incomplete-clone]` si un app está clonado pero falta un
|
||||||
|
`depends_on_apps`.
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
- Relacionado (mismo arrastre de migración, issues aparte si procede):
|
||||||
|
- Los `repo_url` de varios apps de servicio (`registry_api`, `services_api`) apuntan al
|
||||||
|
alias `gitea.organic-machine.com` que **no resuelve**; el host real es
|
||||||
|
`gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com`. `fn app clone` falla por eso.
|
||||||
|
- Los `go.mod` de los apps de servicio requieren `go mod tidy` antes de compilar
|
||||||
|
(deps faltantes con go 1.26).
|
||||||
|
|
||||||
|
## Criterio de hecho
|
||||||
|
|
||||||
|
- [ ] Campo `depends_on_apps` documentado y poblado al menos en `sqlite_api`.
|
||||||
|
- [ ] Audit que detecta imports app→app y reporta drift vs `depends_on_apps`.
|
||||||
|
- [ ] `fn app clone` resuelve dependencias (o las avisa).
|
||||||
@@ -35,6 +35,11 @@ nlp = [
|
|||||||
"gliner>=0.2.13",
|
"gliner>=0.2.13",
|
||||||
"glirel>=1.0.0",
|
"glirel>=1.0.0",
|
||||||
]
|
]
|
||||||
|
jupyter = [
|
||||||
|
"jupyterlab>=4.0",
|
||||||
|
"jupyter-collaboration>=2.0",
|
||||||
|
"jupyter-mcp-server",
|
||||||
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
Generated
+1686
-1
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user