Skip to main content
Glama
Juanma519

mcptesis

by Juanma519
README.md
# mcptesis — MCP server (solo lectura) sobre la base de flota

Servidor [MCP](https://modelcontextprotocol.io) que expone la base de datos del sistema de
gestión de flota como **tools de solo lectura**. Lo consume el chatbot del backend
(`/api/chatbot`), que corre el agent loop contra un LLM (vía OpenRouter) y usa estas tools
para responder preguntas en lenguaje natural.

```
backend /api/chatbot  ──HTTP + Bearer──►  mcptesis (:4000/mcp)  ──SELECT──►  PostgreSQL (rol read-only)
```

## Tools expuestas

| Tool | Args | Qué hace |
| --- | --- | --- |
| `list_schema` | `tables?` | Esquema completo como DDL: columnas con tipo exacto, PK, FKs, valores permitidos, CHECKs, índices con semántica y cantidad real de filas. |
| `describe_table` | `table` | Lo mismo para una sola tabla. Sugiere nombres parecidos si no existe. |
| `refresh_schema` | — | Invalida el cache de introspección. |
| `sample_table` | `table`, `limit?` | Filas reales + valores distintos de cada columna de baja cardinalidad. |
| `search_values` | `table`, `column`, `term`, `limit?` | Busca cómo está escrito un valor (ignora mayúsculas y tildes). |
| `explain_query` | `sql` | Corre `EXPLAIN`: valida nombres y sintaxis sin ejecutar. |
| `run_query` | `sql`, `max_rows?` | Ejecuta **una** consulta `SELECT`/`WITH` y devuelve una tabla. |
| `get_query_examples` | — | Glosario del dominio + consultas verificadas para adaptar. |

### Por qué estas tools y no sólo `run_query`

El esquema le dice al modelo qué columnas existen, pero no cómo se ven los datos.
La mayoría de las consultas mal formuladas no son errores de sintaxis: son
literales inventados (`'activo'` en vez de `'en_progreso'`), joins por la columna
equivocada, y filtros de negocio omitidos. Cada tool ataca una de esas causas:

- **Valores permitidos en el esquema.** Los `CHECK ... IN (...)` y los enums se
  muestran inline en cada columna, así que el modelo no tiene que adivinarlos.
- **`sample_table` / `search_values`.** Para los valores que ningún `CHECK`
  declara (un apellido, una localidad, una marca), le muestran el literal exacto.
- **`get_query_examples`.** Trae los joins canónicos y las reglas que el esquema
  no dice: que `vehiculos` se identifica por `patente` y no por un id, que
  `actividades.id_empresa` apunta a `empresas.rut`, que las métricas de viaje
  sólo existen cuando `actividad_confirmada.estado = 'cerrada'`.
- **Errores con contexto.** Si un `SELECT` falla, el mensaje incluye las columnas
  reales de la tabla y sugerencias por cercanía, para que el reintento del agente
  sea dirigido en vez de otra adivinanza.
- **`instructions`.** El `initialize` devuelve el método de trabajo y el glosario;
  el backend los inyecta en el system prompt (ver `chatbot.mcp.ts`).

El conocimiento de dominio vive en [`src/domain.ts`](src/domain.ts). Es el archivo
a mantener cuando cambia el modelo de negocio.

### Candados de solo lectura (defensa en profundidad)

1. **Rol Postgres read-only** — el server se conecta con `DATABASE_URL_READONLY`, un rol que
   solo tiene `SELECT`.
2. **Validación estructural** (`src/guards.ts`) — una sola sentencia, debe empezar con
   `SELECT`/`WITH`, sin `;`, y rechaza palabras clave de escritura/DDL.
3. **Transacción `READ ONLY`** con `statement_timeout` y `LIMIT` forzado (`src/db.ts`).

## Requisitos

- Node.js 20+ (probado en 22).
- Acceso a la base PostgreSQL del sistema de flota.

## Setup

```bash
npm install
cp .env.example .env      # y completá los valores (ver abajo)
```

`.env`:

```env
PORT=4000
DATABASE_URL_READONLY=postgresql://fleet_readonly:PASSWORD@host:5432/db
DB_SSL=true                 # true en Render/RDS
MCP_AUTH_TOKEN=<mismo token que el backend>
QUERY_TIMEOUT_MS=5000
MAX_ROWS=500
```

### Crear el rol de solo lectura

Automático (usa un usuario owner para crear el rol):

```bash
ADMIN_DATABASE_URL="postgresql://OWNER:PASS@host:5432/db" \
READONLY_PASSWORD="una-password-fuerte" \
DB_SSL=true \
npm run create-readonly-role
```

O manual: ver [`sql/create_readonly_role.sql`](sql/create_readonly_role.sql).

> En bases administradas (Render) el usuario provisto puede no tener permiso para
> `CREATE ROLE`. Si es el caso, apuntá `DATABASE_URL_READONLY` a la conexión normal:
> los guards de la app fuerzan solo lectura igual (candados 2 y 3), aunque perdés el
> candado 1.

## Correr

```bash
npm run dev      # desarrollo (tsx watch)
npm run build && npm start   # producción
npm run smoke    # con el server corriendo: lista tools + prueba list_schema/run_query
```

Health check: `GET http://localhost:4000/health`.

## Estructura

```
src/
  config.ts         # env validado con zod
  db.ts             # pool pg + runReadOnlyQuery (txn read-only, timeout, limit)
  guards.ts         # validación SELECT-only (ignora literales y comentarios)
  introspection.ts  # esquema desde pg_catalog (cacheado con TTL)
  render.ts         # esquema como DDL + resultados como tabla
  errors.ts         # errores de Postgres traducidos con sugerencias
  domain.ts         # glosario del negocio + consultas de referencia
  tools.ts          # registro de las 8 tools MCP
  server.ts         # McpServer + instructions
  index.ts          # Express + Streamable HTTP transport + auth Bearer
scripts/
  create-readonly-role.ts
  smoke.ts
sql/
  create_readonly_role.sql
```