Skip to main content
Glama
TheNesdark

MariaDB-MCP

by TheNesdark
README.md
# MariaDB MCP Server (JavaScript)

Servidor MCP escrito en **Node.js / JavaScript** para interactuar con bases de datos **MariaDB/MySQL**.

## Características

- **Transporte stdio** compatible con el protocolo MCP (Model Context Protocol).
- **Transporte HTTP/SSE** preparado para extenderse con un bridge web.
- **Conexión pool** usando `mysql2/promise` con tamaño configurable.
- **Soporte SSL** con CA, certificados cliente y configuración de verificación.
- **Modo solo lectura (READ_ONLY)** que bloquea queries de escritura y funciones de archivo (`LOAD_FILE`, `INTO OUTFILE`).
- **Límites de ejecución**: tiempo máximo por consulta (`MCP_QUERY_TIMEOUT`) y tope de filas devueltas (`MCP_MAX_ROWS`), con aviso explícito cuando un resultado se recorta.
- **Autenticación por API key** en el transporte HTTP (`MCP_API_KEY`), con comparación en tiempo constante.
- **Logging** a `stderr` (compatible con stdio transport) y archivos rotados diariamente con `winston`.
- **Docker y Docker Compose** listos para usar.

## Herramientas MCP expuestas

| Herramienta | Descripción |
|-------------|-------------|
| `list_databases` | Lista todas las bases de datos accesibles. |
| `list_tables` | Lista las tablas de una base de datos. |
| `get_table_schema` | Obtiene columnas, tipos, nullable, keys, defaults y extras. |
| `get_table_schema_with_relations` | Igual que `get_table_schema` pero incluye claves foráneas. |
| `execute_sql` | Ejecuta queries `SELECT/SHOW/DESCRIBE`. Valida parámetros, modo solo lectura, tiempo máximo y tope de filas. |
| `create_database` | Crea una base de datos (requiere `MCP_READ_ONLY=false`). |

### Selección de base de datos

Todas las herramientas que trabajan sobre una base de datos aceptan el parámetro
`database_name` **y sus alias `database`, `db` y `schema`** (útil para clientes que
renombran el parámetro). La conexión se cambia explícitamente con `changeUser()` y
se verifica con `SELECT DATABASE()` antes de ejecutar la query, de modo que la
consulta **siempre** corre sobre la base de datos indicada en la herramienta y nunca
sobre la de una llamada anterior.

- `execute_sql`: si no se envía `database_name`, se usa `DB_NAME` (la configurada).
  El servidor avisa en el log cuando se ejecuta sin base de datos explícita.
- El resto de herramientas exigen `database_name` y devuelven un error claro con los
  alias aceptados si falta.

### Límites de ejecución

- **Tiempo máximo por consulta** (`MCP_QUERY_TIMEOUT`, 30 s por defecto): se aplica con
  `SET SESSION max_statement_time` (MariaDB). Si el servidor no lo admite se intenta
  `max_execution_time` (MySQL, en milisegundos y solo para `SELECT`); si no hay ninguno
  disponible, el límite se desactiva y se avisa una sola vez en el log, en lugar de
  romper todas las consultas.
- Cuando una consulta agota el tiempo, el servidor la cancela y devuelve un error claro
  (`MCP_QUERY_TIMEOUT=...`). La conexión sigue siendo válida y vuelve al pool, así que un
  `SELECT` desbocado no bloquea el servidor ni contamina las llamadas siguientes.
- **Tope de filas** (`MCP_MAX_ROWS`, 1000 por defecto): lo aplica el propio servidor con
  `sql_select_limit = MCP_MAX_ROWS + 1`, de modo que un resultado gigante **no viaja por
  la red**. Al recortar, `execute_sql` responde así en lugar de devolver un array:

```json
{
  "rows": [ { "n": 0 }, { "n": 1 } ],
  "truncated": true,
  "row_limit": 1000,
  "received_rows": 1001,
  "message": "Resultado truncado a las primeras 1000 filas (límite MCP_MAX_ROWS)..."
}
```

  Si el resultado cabe en el tope, la respuesta sigue siendo el array de filas de
  siempre (no hay cambio de forma). Con `MCP_MAX_ROWS=0` el tope se desactiva.
- Las herramientas de metadatos (`list_databases`, `list_tables`, `get_table_schema`,
  `get_table_schema_with_relations`) **fallan con un error claro si el tope recorta su
  resultado**, en lugar de devolver un listado incompleto: es peor concluir que una tabla
  no existe que recibir el aviso de subir `MCP_MAX_ROWS`.
- Los límites se reaplican tras cada cambio de base de datos, porque `COM_CHANGE_USER`
  devuelve las variables de sesión a su valor por defecto.

## Requisitos

- **Node.js** >= 18 (recomendado 20 LTS)
- **MariaDB/MySQL** accesible desde la red del servidor

## Instalación

```bash
# Clonar o copiar la carpeta MariaDB-MCP
cd MariaDB-MCP

# Instalar dependencias
npm install
```

## Configuración

Copia el archivo de ejemplo y ajusta tus credenciales:

```bash
cp .env.example .env
```

Variables disponibles:

| Variable | Descripción | Default |
|----------|-------------|---------|
| `DB_HOST` | Host del servidor MariaDB | `localhost` |
| `DB_PORT` | Puerto | `3306` |
| `DB_USER` | Usuario | `root` |
| `DB_PASSWORD` | Contraseña | *(vacío)* |
| `DB_NAME` | Base de datos por defecto | `genoma` |
| `DB_CHARSET` | Charset de conexión | `utf8mb4` |
| `DB_SSL` | Habilitar SSL | `false` |
| `DB_SSL_CA` | Ruta al certificado CA | *(vacío)* |
| `DB_SSL_CERT` | Ruta al certificado cliente | *(vacío)* |
| `DB_SSL_KEY` | Ruta a la clave privada cliente | *(vacío)* |
| `DB_SSL_VERIFY_CERT` | Verificar certificado | `true` |
| `DB_SSL_VERIFY_IDENTITY` | Verificar identidad del host | `true` |
| `MCP_READ_ONLY` | Modo solo lectura | `true` |
| `MCP_MAX_POOL_SIZE` | Tamaño máximo del pool | `10` |
| `MCP_QUERY_TIMEOUT` | Segundos máximos por consulta (`0` = sin límite) | `30` |
| `MCP_MAX_ROWS` | Filas máximas devueltas por herramienta (`0` = sin límite) | `1000` |
| `MCP_API_KEY` | API key(s) exigida(s) en el transporte HTTP, separadas por coma | *(vacío = sin auth)* |
| `MCP_AUTH_REQUIRED` | `true` exige API key, `false` la desactiva | *exigir si hay clave* |
| `LOG_LEVEL` | Nivel de log (`debug`,`info`,`warn`,`error`) | `info` |
| `ALLOWED_ORIGINS` | Orígenes CORS separados por coma | *(vacío)* |
| `ALLOWED_HOSTS` | Hosts permitidos separados por coma | `localhost,127.0.0.1` |

## Uso

### Modo stdio (por defecto)

```bash
npm start
# o
node src/index.js
```

### Modo HTTP/SSE

```bash
node src/index.js --http 3000
```

### Ayuda

```bash
node src/index.js --help
```

### Autenticación API key (transporte HTTP)

Define una o varias claves en `.env` para proteger el endpoint MCP:

```bash
MCP_API_KEY=mi-clave-secreta
# o varias: MCP_API_KEY=clave-1,clave-2
```

Con la clave definida, **todas las rutas salvo `/health`** devuelven `401` si no
se envía. El cliente debe incluir una de estas cabeceras:

```http
Authorization: Bearer mi-clave-secreta
X-API-Key: mi-clave-secreta
```

```bash
# Verificar
$ curl -i -X POST http://localhost:3005/mcp -H 'Authorization: Bearer mi-clave-secreta'
HTTP/1.1 401 Unauthorized            # sin clave
HTTP/1.1 200 OK                      # con clave correcta

$ curl -i http://localhost:3005/health   # sigue sin clave para el healthcheck
#   → {"status":"healthy"}                  (sin credenciales: solo el estado)
```

`/health` nunca exige credenciales, para no romper el healthcheck de Docker y de los
orquestadores. Cuando hay autenticación configurada solo responde `{ status }`; el
detalle (`server`, `version`, `database`, `readOnly`, `pool`) requiere la misma clave que
`/mcp`, igual que el mensaje de error cuando la base de datos no responde.

Variables relacionadas:

| Variable | Descripción |
|----------|-------------|
| `MCP_API_KEY` | Claves válidas separadas por coma. Vacío → autenticación deshabilitada (se avisa en el log). |
| `MCP_AUTH_REQUIRED` | `true` = exigir clave (el servidor **no arranca** si `MCP_API_KEY` está vacía). `false` = desactivar la autenticación aunque haya claves. Sin definir = exigir solo si hay claves. |

> La comparación de claves se hace en tiempo constante (hash SHA-256 + `timingSafeEqual`).
> La comparación no se registra en los logs.

## Docker

### Construir y ejecutar

```bash
# Modo stdio ( foreground )
docker-compose run --rm mcp-server

# Modo HTTP ( background )
docker-compose up -d mcp-server
```

> Asegúrese de que las variables de entorno estén definidas en un archivo `.env` o en el shell antes de ejecutar `docker-compose`.

## Seguridad

- **MULTI_STATEMENTS** y **LOCAL_INFILE** están desactivados en el pool de conexiones.
- En modo `READ_ONLY`, solo se permiten queries que inicien con `SELECT`, `SHOW`, `DESC`, `DESCRIBE` o `USE`.
- Se bloquean explícitamente las funciones `LOAD_FILE()` y sentencias `INTO OUTFILE/DUMPFILE`.
- Se detecta y se alerta si el usuario de base de datos posee el privilegio global `FILE`.
- **Límites de ejecución** por sesión (tiempo máximo de consulta y tope de filas), que
  además acotan el consumo de memoria del servidor y el tamaño de las respuestas.
- **Autenticación API key** opcional en el transporte HTTP (`MCP_API_KEY`), con
  comparación en tiempo constante; `/health` queda abierto para el healthcheck, pero
  solo devuelve `{ status }` cuando hay autenticación configurada.
- **Solo lectura real con grants**: `MCP_READ_ONLY` filtra por prefijo de sentencia y es
  una comodidad, no una frontera de seguridad (un CTE con `UPDATE`, una función
  almacenada o una sentencia preparada se le escapan). La garantía la da el usuario de
  base de datos, y al arrancar se avisa si ese usuario tiene privilegios de escritura.
  Ejemplo listo para ejecutar en `examples/mariadb-readonly-user.sql`:

  ```sql
  CREATE USER IF NOT EXISTS 'mcp_ro'@'%' IDENTIFIED BY '...';
  GRANT SELECT, SHOW VIEW ON `genoma`.* TO 'mcp_ro'@'%';
  ```

  y en `.env`: `DB_USER=mcp_ro` con su `DB_PASSWORD`.
- Cada ejecución verifica con `SELECT DATABASE()` que corre sobre la base de datos
  solicitada; si no coincide, la conexión se descarta y se devuelve un error en vez
  de resultados de otra base de datos.

## Estructura del proyecto

```
MariaDB-MCP/
├── src/
│   ├── index.js      # Punto de entrada y CLI
│   ├── server.js     # Lógica MCP, pool y herramientas
│   ├── config.js     # Variables de entorno
│   └── logger.js     # Configuración de winston
├── test/             # Pruebas (node --test)
├── examples/         # Ejemplos, p. ej. usuario MariaDB de solo lectura
├── logs/             # Archivos de log rotados
├── .env.example
├── .gitignore
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
├── package.json
└── README.md
```

## Licencia

MIT

TDQS

A3.7/5.0

Scored across 6 tools

Disambiguation5/5

Each tool has a clear, non-overlapping purpose: database creation, SQL execution, schema retrieval (with optional relations), and listing databases/tables. The two schema tools are differentiated by the inclusion of foreign key relations.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in snake_case (e.g., create_database, list_databases, get_table_schema). The style is uniform and predictable.

Tool Count5/5

Six tools cover essential database operations—listing, schema inspection, SQL execution, and database creation—without being excessive or sparse for the server's scope.

Completeness3/5

The set covers read operations well but lacks explicit tools for dropping databases or altering tables. The execute_sql tool can perform write operations if mode allows, but dedicated tools for update/delete are missing, leaving notable gaps.

Maintenance

ActivityMaintained
ResponsivenessNo issues