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
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues