trazabilidad-mcp
by lucadevv
README.md
# Trazabilidad
Motor de trazabilidad de código full-stack: construye un **grafo de hechos** de tu app
(componentes → handlers → costuras HTTP → queries DB) y lo expone por **web**, **MCP** y **CLI**
desde una sola fuente de verdad. Local-first, sin telemetría, el código nunca sale de tu máquina.
> Estado: **v1 funcional**. El motor completo está implementado y verificado: extractor ts-morph,
> matcher de costuras HTTP, persistencia SQLite con migraciones, detección de doble-fuente, y las
> tres superficies vivas (CLI `trazar`, API REST en :8791, web en :5183, servidor MCP) y soporte
> **multi-proyecto**: un dueño apunta la herramienta a N repos reales.
> Las tres leen la MISMA query layer → una sola fuente de verdad.
## Onboarding de un comando
Requisitos: Docker + Docker Compose.
```bash
git clone <repo> && cd trazabilidad
docker compose -f docker-compose.local.yml up --build -d
```
- Web: http://127.0.0.1:5183 (shell con first-run)
- API: http://127.0.0.1:8791/health (health honesto)
Ambos servicios se publican **solo en 127.0.0.1** — nunca expuestos a la LAN (decisión de
seguridad de la spec §5).
Para frenar todo:
```bash
docker compose -f docker-compose.local.yml down
```
## Ciclo de desarrollo (hot-reload)
El compose usa **bind-mount + `--watch`**, no bakea el código:
- `apps/api` corre con `bun --watch` → al guardar un `.ts`, el proceso reinicia solo.
- `apps/web` corre `vite` con `watch.usePolling` → HMR cruza el FS del contenedor en macOS.
Editás en el host, el cambio se ve sin reconstruir la imagen. Solo necesitás
`up --build` la primera vez (o tras tocar `package.json` / lockfile, para reinstalar deps).
Si cambiás dependencias: `docker compose -f docker-compose.local.yml up --build -d` recrea
los contenedores y reinstala dentro de ellos.
## Configuración de entorno
Convención de tres archivos en `env/`:
| Archivo | Tracked | Contenido |
|---|---|---|
| `env/local.env` | sí | config no-secreta (puertos, NODE_ENV) |
| `env/local.secret.env.example` | sí | **todas** las keys de secretos, sin valores |
| `env/local.secret.env` | **no** (gitignored) | tus valores reales |
**v1 no tiene secretos reales** (local-only, sin auth, sin servicios externos). El `.example`
existe para fijar la convención de cara a v2.
## Estructura (monorepo Bun + Turborepo)
```
packages/
domain/ tipos puros del grafo (sin deps)
core/ FactGraphBuilder + ExtractorPort (HEXAGONAL, no importa ts-morph)
matcher/ SeamMatcher (STRATEGY, confianza descendente)
db/ schema SQL + migraciones
query/ query layer COMPARTIDA (fuente única que leen CLI, API y MCP)
apps/
api/ Hono REST :8791 (bind 127.0.0.1) — /health, /index/status, /journeys, /seams, /findings
web/ React + Vite + TanStack Router :5183 — canvas del grafo con polling de estado
mcp/ servidor MCP stdio (SDK 1.29.0) — trace_flow, list_journeys, get_http_seams, index_status, ...
cli/ `trazar` (index/add/projects/remove/overview/flow/journeys/seams/findings[+suppress/confirm]/serve)
examples/
demo-login/ fixture de test: login React + Hono con plantados verificables
```
## CLI `trazar`
Todos los comandos van sobre la misma query layer que la web y el MCP:
```bash
trazar index <ruta> # indexa un proyecto y construye el grafo de hechos
trazar add <ruta> [--name=X] # registra un proyecto en el registro (lo verás en la web)
trazar projects # lista los proyectos registrados (id, name, ruta, estado)
trazar remove <id> # quita un proyecto del registro
trazar overview [<ruta>] # salud del proyecto (healthScore, capas, findings por severidad)
trazar journeys [<ruta>] # lista los journeys (entry points de UI) detectados
trazar flow <journey> [--mermaid] # camino completo botón→DB de un journey (texto o Mermaid)
trazar seams [<ruta>] [--level=L] # costuras HTTP con confianza y file:line de ambos lados
trazar findings [<ruta>] # findings de doble-fuente con evidencia y estado
trazar findings suppress <key> [--reason=...] # suprime un finding (persiste entre re-index)
trazar findings confirm <key> # confirma un finding
trazar serve # cómo levantar API + web (modo Docker demo o host)
```
`add` y `index` son cosas **distintas**: `add` registra la ruta en el catálogo (para verla en la
web/MCP por id), `index` construye el grafo. `index` **no** auto-registra (deja un hint copiable);
`add` **no** indexa. Mirá la sección **Multi-proyecto** más abajo.
Honestidad por diseño: sin index → comando copiable; **stale** tras editar sin re-indexar → ⚠;
seams no resueltas → declaradas como huecos, nunca ocultadas. Exit codes: `0` ok · `2` uso · `1` error.
El proyecto objetivo de los comandos de lectura sale de: arg explícito > `TRAZABILIDAD_PROJECT` > cwd.
### Instalar `trazar` en el PATH
El `bin` del CLI apunta a `apps/cli/src/index.ts`, que ya tiene shebang `#!/usr/bin/env bun`.
**`bun link` NO sirve acá**: en un monorepo de workspaces solo registra el paquete para enlazarlo
en *otro* proyecto (`bun link @trazabilidad/cli`), **no** instala el binario `trazar` en el PATH
(verificado: tras `bun link`, `which trazar` → not found). Dos formas que sí funcionan:
**Opción A — symlink al bin dir de bun** (recomendada; `~/.bun/bin` ya está en tu PATH si usás bun):
```bash
chmod +x apps/cli/src/index.ts
ln -sf "$(pwd)/apps/cli/src/index.ts" ~/.bun/bin/trazar
trazar journeys examples/demo-login # ✓ funciona por el shebang
```
**Opción B — alias/función en tu shell** (si no querés tocar `~/.bun/bin`):
```bash
# en ~/.zshrc o ~/.bashrc (ajustá la ruta absoluta del repo):
trazar() { bun "/ruta/al/repo/trazabilidad/apps/cli/src/index.ts" "$@"; }
```
Sin instalar nada, el CLI siempre corre con `bun apps/cli/src/index.ts <comando>` desde la raíz del
repo (lo que usan los ejemplos de este README).
## Multi-proyecto
Trazabilidad apunta a **N proyectos** desde un solo dueño. El catálogo de rutas vive en
`~/.trazabilidad/projects.json` (config, separado de la `.db` de cada proyecto). Cada superficie
—web, MCP, CLI— resuelve el proyecto de forma independiente; cuando no se especifica, cae al
**default** (env `TRAZABILIDAD_PROJECT`), así el demo dockerizado y los tests no se rompen.
### Dos modos de arranque
| Modo | Comando | Qué ve | Para qué |
|---|---|---|---|
| **Demo (Docker)** | `docker compose -f docker-compose.local.yml up --build -d` | solo el fixture montado en `/repo` | onboarding de un comando, conocer la herramienta |
| **Host (bun)** | `bun run api:host` + `bun run web:host` | **tus repos reales** (cualquier path absoluto del host) | trabajar sobre tu código de verdad |
La API en Docker está encajonada al volumen montado; para apuntar a tus repos reales usá el modo
host (la API corre con `bun` directo en loopback y ve todo el filesystem). Ambos publican **solo**
en `127.0.0.1`.
### Registrar y listar proyectos (CLI)
```bash
# 1) registrar una o más rutas reales (idempotente; imprime el id estable)
trazar add ~/code/mi-api # → ✓ registrado "mi-api" — id mi-api-3f9a2c
trazar add ~/code/mi-web --name="Front"
# 2) listar el registro con estado (id, name, ruta, ✓/✗ index, ⚠ si la ruta no existe)
trazar projects
# 3) construir el grafo de cada uno (index ≠ add; corré index aparte)
trazar index ~/code/mi-api
# 4) quitar uno del registro (no borra la .db ni el código)
trazar remove mi-api-3f9a2c
```
El **`id`** es estable y se deriva del path **absoluto** (`slug(basename)-hash6`), no del basename:
dos repos `api` en carpetas distintas obtienen ids distintos, sin colisión.
> **El registro es CLI-only.** No hay `POST /add` ni `POST /index` en la API: registrar e indexar
> se hacen por el CLI en el host (decisión v1 — la API es de solo lectura sobre el grafo). La web y
> el MCP **leen** el catálogo, pero quien lo escribe es `trazar add` / `trazar remove`.
### Selector en la web
En modo host (`bun run web:host`), el sidebar muestra un **dropdown** que lista `GET /projects`. El
proyecto activo vive en la **URL** como segmento (`/p/<id>/overview`, deep-linkeable). Al elegir otro
proyecto, todas las vistas (overview/journeys/seams/findings/status) se re-scopean a ese proyecto.
Si un proyecto está sin indexar o su ruta ya no existe, aparece con un punto rojo y un estado honesto.
Sin proyectos registrados → estado vacío con CTAs copiables (`trazar add` / `trazar index`).
### El param `project` del MCP
Cada tool del MCP acepta un `project` opcional (el `id` del registro). Con `project=<id>` responde
sobre **ese** proyecto; sin el param, sobre el default (`TRAZABILIDAD_PROJECT`). La tool
`list_projects` devuelve el catálogo para que el agente descubra los ids.
```jsonc
// un agente consulta dos proyectos en la MISMA sesión, sin reconectar:
{ "tool": "overview", "arguments": { "project": "mi-api-3f9a2c" } }
{ "tool": "list_journeys", "arguments": { "project": "mi-web-b71e04" } }
{ "tool": "overview", "arguments": {} } // ← default (back-compat)
```
Las tres superficies pueden operar sobre **proyectos distintos a la vez** sin interferir: son procesos
independientes que leen `.db`s distintas, y cada uno cae al default cuando no se especifica.
## Flujo completo con el demo
```bash
bun install
# 1) indexar el fixture → 22 nodos, 14 aristas, 3 seams (1 finding de doble-fuente)
bun apps/cli/src/index.ts index examples/demo-login
# 2) ver los journeys de UI detectados (onSubmit, signOut)
bun apps/cli/src/index.ts journeys examples/demo-login
# 3) trazar el journey de login botón→DB (con el puente honesto del extractor)
bun apps/cli/src/index.ts flow onSubmit examples/demo-login
# 4) ver el finding estrella: doble fuente de verdad de `user`
bun apps/cli/src/index.ts findings examples/demo-login
# 5) levantar API + web (consumen la MISMA verdad que el CLI)
TRAZABILIDAD_PROJECT="$(pwd)/examples/demo-login" bun apps/api/src/index.ts # :8791
cd apps/web && bun run dev # :5183
```
Si editás un `.tsx` del demo sin re-indexar, todas las superficies (CLI, `/index/status`, `/health`,
web) reportan **stale** con ⚠ — la verdad sigue siendo honesta hasta que vuelvas a `trazar index`.
## Conectar el MCP a Claude Code / Claude Desktop / Cursor
El servidor MCP (`apps/mcp`) expone el grafo de hechos por **stdio** (`trace_flow`, `list_journeys`,
`get_http_seams`, `index_status`, `find_data_providers`, `list_findings`, `overview` y `list_projects`)
leyendo la MISMA query layer que el CLI y la API. Cada tool acepta un `project` opcional (el `id` del
registro) para apuntar a un proyecto puntual; sin ese param usa la env `TRAZABILIDAD_PROJECT` (back-compat).
Ver la sección **Multi-proyecto** arriba. El MCP solo LEE — indexá antes con `trazar index <ruta>`.
**Claude Code** — un comando (verificá tu sintaxis con `claude mcp add --help`):
```bash
claude mcp add trazabilidad \
-e TRAZABILIDAD_PROJECT="$(pwd)/examples/demo-login" \
-- bun "$(pwd)/apps/mcp/src/index.ts"
```
Comprobá la conexión con `claude mcp get trazabilidad` (debe decir `Status: ✓ Connected`).
Quitalo con `claude mcp remove trazabilidad`. Usá `-s project` para escribirlo en un `.mcp.json`
versionable del repo, o `-s user` para que esté disponible en todos tus proyectos.
**JSON equivalente** (`.mcp.json` en la raíz del repo, o el bloque `mcpServers` de la config de
Claude Desktop / Cursor). Usá **rutas absolutas** — `${PWD}` lo expande Claude Code; otros clientes
pueden no hacerlo, poné la ruta completa:
```json
{
"mcpServers": {
"trazabilidad": {
"type": "stdio",
"command": "bun",
"args": ["/ruta/absoluta/al/repo/apps/mcp/src/index.ts"],
"env": {
"TRAZABILIDAD_PROJECT": "/ruta/absoluta/al/repo/examples/demo-login"
}
}
}
}
```
## El fixture: `examples/demo-login`
App de login completa (React + Hono) que es el **contrato de test** del producto. Planta a
propósito tres situaciones que los lotes 2 y 5 deben detectar (marcadas con `// FIXTURE:`):
1. **Doble fuente de verdad de `user`**: `Dashboard.tsx` lo lee de `AuthContext`,
`Profile.tsx` lo lee directo de `localStorage`. → Finding `double-source` (LOTE 5).
2. **Fetch dinámico** (`fetchProfile`, URL template en runtime) → seam **no-resuelta** visible (LOTE 2).
3. **Fetch literal** `fetch('/api/auth/login')` → costura `literal` con el handler Hono (LOTE 2).
Typecheck del fixture:
```bash
bun install
bun run --filter '@trazabilidad/example-demo-login' typecheck
```
## Limitaciones conocidas v1
Trazabilidad declara sus huecos en vez de ocultarlos. Lo que **v1 todavía NO cubre**:
- **`sessionStorage` y claves dinámicas no se detectan como fuente de estado.** El extractor
reconoce `localStorage` con clave literal y el Context explícito; una doble-fuente sobre
`sessionStorage` (o `localStorage` con una clave armada en runtime) **no** genera finding y
`find_data_providers` devuelve 0 proveedores. Cobertura ampliable en v2.
- **`finding_key` vs renames.** La identidad de un finding (para que la supresión persista entre
re-index) se deriva del contenido del hecho; si renombrás el símbolo/archivo involucrado, la key
cambia y una supresión previa **no** se reasocia automáticamente al finding "equivalente".
- **Puente del flujo botón→DB es heurístico.** Cuando el extractor no puede unir dos tramos con
certeza (p. ej. una URL armada en runtime), declara la costura como **no-resuelta** (hueco visible)
en lugar de inventar una arista — preferimos un hueco honesto a una conexión falsa.
- **La API es de SOLO LECTURA sobre el grafo + supresión/confirmación de findings.** El indexado se
dispara únicamente por CLI (`trazar index`); no hay `POST /index` en v1 (cola + progreso es backlog v2).
- **DB local single-project, single-writer.** La base SQLite usa `journal_mode=DELETE` (no WAL): se
comparte host↔contenedor por bind-mount sobre VirtioFS, donde el `-shm` de WAL no es coherente y
causaba "disk I/O error" intermitentes y estados divergentes. DELETE serializa escritor↔lectores,
aceptable para un dev local; no es una DB pensada para concurrencia multi-cliente.
- **Sin auth ni multi-tenant.** Local-first, bind 127.0.0.1, sin telemetría; multi-usuario/remoto es
la visión SaaS de v2, no v1.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing