Skip to main content
Glama
Advaith789

SQLGuard MCP

by Advaith789

SQLGuard MCP

CI Python 3.10+ License: Apache 2.0 MCP

Un envoltorio de seguridad y gobernanza para el acceso de agentes a almacenes SQL.

Cada servidor MCP de almacén hoy en día toma una cadena SQL de un modelo y la ejecuta. La conexión nunca fue la parte difícil. La parte difícil es todo lo que la rodea: demostrar que la consulta solo lee, saber lo que costará antes de pagar por ello, vincularla a los permisos de quien la solicita en lugar de los de la cuenta de servicio, devolver un resultado sobre el que un agente pueda realmente razonar, y dejar un registro que un humano pueda revisar después.

SQLGuard se sitúa entre el modelo y el almacén y aplica las cinco.

  model ──▶ AST guard ──▶ policy ──▶ cost estimate ──▶ budget ──▶ warehouse
              │             │             │              │
              └── read-only └── identity  └── dry run    └── ceilings
                  proof         scoped        or bound       + session cap
                                                                  │
                                          governed results ◀──────┘
                                          (capped, summarized, cursored)
                                                    │
                                          append-only audit log
                                          (including refusals, with intent)

Una sesión gobernada: una consulta ordinaria, cuatro rechazos y el rastro de auditoría

Salida real de python scripts/demo.py — nada en esa transcripción está simulado.

El resultado que motivó el diseño

El corpus consta de 76 consultas etiquetadas: 46 ataques y 30 piezas de análisis legítimos. "Bypass" significa que se permitió un ataque. "Falsa alarma" significa que se bloqueó trabajo real.

Guardia

Elusiones

Falsas alarmas

F1

latencia p50

startswith("SELECT"/"WITH")

17/46 (37%)

0/30

0.773

<0.01 ms

lista negra de palabras clave (regex)

12/46 (26%)

5/30 (17%)

0.800

<0.01 ms

prefijo + rechazar punto y coma

13/46 (28%)

0/30

0.835

<0.01 ms

Análisis AST, solo nodo raíz

12/46 (26%)

0/30

0.850

0.04 ms

SQLGuard (raíz + recorrido completo)

0/46 (0%)

0/30 (0%)

1.000

0.11 ms

Reproducir con python evals/run_eval.py.

La cuarta fila es la interesante. Analizar el SQL correctamente y verificar el tipo de nodo de nivel superior — el enfoque de apariencia sofisticada — aún falla en una cuarta parte del corpus. Tres sentencias son la razón:

WITH d AS (DELETE FROM orders RETURNING *) SELECT * FROM d   -- Postgres
SELECT * INTO staging_copy FROM orders                       -- T-SQL / PG
SELECT * FROM orders FOR UPDATE                              -- row locks

Las tres se analizan con Select en la raíz. La primera elimina la tabla. La aplicación de solo lectura debe recorrer todo el árbol, no inspeccionar su parte superior.

Related MCP server: MCPBridge

Lo que aplica

1. Solo lectura, a nivel de AST. Dos capas independientes, y una sentencia debe sobrevivir a ambas: una lista blanca de nodos raíz, y un recorrido completo del árbol contra un conjunto denegado que cubre DML, DDL, mutación de sesión, control de transacciones, egreso de datos (COPY TO, EXPORT DATA), mutación de catálogo, SELECT ... INTO, cláusulas de bloqueo y funciones con efectos secundarios. Cualquier cosa que el analizador no pueda modelar cae en un nodo de comando genérico y se deniega por esa razón — desconocido significa denegado, que es lo que detiene EXECUTE IMMEDIATE, CALL y extensiones de proveedor.

2. Límites de costo, en tres ámbitos y dos dimensiones. En BigQuery, la estimación es una ejecución en seco real: bytes exactos, gratuita, antes de que se facture nada. Las consultas que superan el límite son rechazadas con una carga útil estructurada que nombra la estimación, el límite, el exceso y la corrección derivada del propio AST de la consulta. Un límite de sesión se acumula entre llamadas, porque el modo de fallo del agente es la repetición, no el tamaño.

La segunda dimensión es la cardinalidad de salida, y existe debido a una consulta que pasó todas las comprobaciones de bytes: un auto-join con ON 1=1 escanea 15 MB y emite 14.4 mil millones de filas. Los bytes escaneados limitan E/S, no trabajo.

3. Política con ámbito de identidad. Las tablas están en lista blanca, las columnas restringidas son rechazadas al ser referenciadas, y los filtros de fila se inyectan en el AST — cada tabla gobernada se reescribe como una subconsulta filtrada, por lo que el predicado sobrevive a joins, uniones y anidamientos. Concatenar una cláusula WHERE como cadena sería derrotada por el primer OR 1=1 que apareciera.

La identidad es configuración, nunca un parámetro de herramienta. Ninguna herramienta acepta un argumento principal, y hay una prueba que afirma que ninguna lo hará jamás. Un agente que puede nombrar su propio principal no tiene principal.

4. Gobernanza de resultados. Los resultados tienen un límite, la truncación se indica explícitamente en lugar de silenciosamente, y la continuación utiliza un manejador de cursor del lado del servidor. El cursor es un id opaco en un almacén al que el modelo no puede escribir — puede decir "más de eso", nunca influir en qué fue "eso". Cada página se reestima y se recarga, porque la paginación vuelve a escanear en la mayoría de los almacenes.

5. Rastro de auditoría, incluyendo rechazos. JSONL de solo añadido, fsynced por registro. Cada llamada lleva una cadena intent — la propia declaración del modelo de por qué ejecutó la consulta, requerida en el momento de la llamada. Un registro de almacén dice que una cuenta de servicio escaneó 4 TB de la tabla de pagos a las 03:14. Esto dice que un agente la escaneó porque estaba conciliando una discrepancia de reembolso. Solo uno es revisable.

La superficie de herramientas

Cinco herramientas, no cuarenta. Un servidor que expone una herramienta por tabla degrada la selección de herramientas y consume la ventana de contexto antes de que el modelo haya leído el esquema.

Herramienta

Propósito

describe_schema(table?)

Tablas legibles, luego las columnas de una tabla. Divulgación progresiva.

plan_query(sql)

Validar y cotizar sin ejecutar. Gratuito.

run_query(sql, intent, max_rows?)

El pipeline completo. La única herramienta que cuesta dinero.

fetch_page(cursor)

Continuar un resultado truncado.

session_status()

Presupuesto restante, para que el agente pueda dimensionar su trabajo.

plan_query es la herramienta que más cambia el comportamiento del agente. Dada una forma gratuita de preguntar "¿esto estaría permitido y cuánto costaría?", un modelo la usa — y sus costosos errores se convierten en rechazos baratos contra los que puede iterar. Sin ella, la única forma de descubrir que una consulta es demasiado cara es que se le facture.

Inicio rápido

pip install "sqlguard-mcp[duckdb] @ git+https://github.com/Advaith789/ast-level-sql-mcp"

O desde un clon, para ejecutar las pruebas y la evaluación:

python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python examples/seed_demo.py          # builds a 120k-row demo warehouse
.venv/bin/python -m pytest -q                   # 73 tests
.venv/bin/python evals/run_eval.py              # the table above
.venv/bin/python scripts/demo.py                # the walkthrough pictured above

Registrar con un cliente MCP:

{
  "mcpServers": {
    "sqlguard": {
      "command": "/path/to/.venv/bin/sqlguard-mcp",
      "args": ["--config", "/path/to/examples/policy.example.yaml"]
    }
  }
}

Política

dialect: bigquery
driver:
  name: bigquery
  project: my-project

principal: analyst@example.com     # never a tool parameter

roles:
  analyst:
    tables: ["analytics.*"]
    denied_columns:
      analytics.customers: [ssn, email]
    row_filters:
      analytics.orders: "region = 'US'"   # injected into the AST
    budget:
      per_query_bytes: 50GB          # one catastrophic scan
      per_session_bytes: 500GB       # one runaway conversation
      per_day_bytes: 2TB             # durable: survives restarts
      max_estimated_rows: 10000000   # output size, not just input
      max_rows: 200

Reglas de combinación cuando un principal tiene varios roles: unión de concesiones (tablas, visibilidad de filas, presupuestos), unión de denegaciones (una columna denegada por cualquier rol permanece denegada). Denegar gana. Los valores predeterminados de implementación solo llenan campos no establecidos — nunca amplían un límite que un conjunto de roles haya establecido, lo cual fue un error real encontrado en pruebas y ahora tiene una prueba de regresión.

Lo que esto no hace

Declarado claramente, porque los límites determinan dónde es seguro usarlo.

  • El corpus no es independiente. Yo escribí los ataques y la guardia. Demuestra la clase de bypass que derrota enfoques más simples; no es una afirmación de completitud contra un atacante adaptativo. Los casos de ataque contribuidos son la contribución más útil posible.

  • La seguridad de la guardia está limitada por el analizador de sqlglot. Una construcción de dialecto que sqlglot analice incorrectamente como un nodo benigno no sería detectada. Las construcciones que falla al analizar son denegadas, por lo que el modo de fallo está sesgado hacia el rechazo, pero "sesgado hacia seguro" no es "seguro".

  • El controlador de BigQuery está escrito contra la API documentada y no se ha ejecutado contra un proyecto real. La ruta de DuckDB está completamente ejercitada por las pruebas.

  • Las referencias a columnas no calificadas fallan de forma cerrada. Sin resolución de nombres consciente del esquema, un ssn desnudo en una consulta de múltiples tablas es rechazado si alguna tabla en el ámbito lo restringe. El exceso de rechazo es recuperable calificando la columna; la falta de rechazo filtraría.

  • Las estimaciones de costo de DuckDB son límites superiores, no ejecuciones en seco — escaneos completos de cada tabla referenciada, sin crédito por pushdown. Solo BigQuery proporciona números exactos previos a la ejecución.

  • La política a nivel de columna no enmascara, rechaza. Devolver silenciosamente columnas diferentes a las que el modelo solicitó produce análisis que son incorrectos de maneras que nadie puede ver.

Estructura

src/sqlguard/
  ast_guard.py    read-only enforcement (the core)
  policy.py       identity-scoped table/column/row policy
  cost.py         estimation, budgets, actionable refusals
  governance.py   result caps, summaries, cursors
  audit.py        append-only JSONL trail
  spend.py        durable per-principal spend ledger (SQLite)
  errors.py       structured refusals
  server.py       the five MCP tools
  drivers/        duckdb (offline) + bigquery (dry run)
evals/            labeled corpus, baselines, metrics runner
tests/            73 tests: adversarial corpus + end-to-end pipeline
scripts/          demo walkthrough + SVG renderer
.github/          CI: tests on 3.10-3.12, evaluation, package build

Las contribuciones son bienvenidas — consulte CONTRIBUTING.md. La más útil es un ataque que logre pasar.

Apache-2.0.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Secure SQL proxy for AI agents. Translates natural language to safe SQL via Claude, validates at the AST level (SELECT-only, no DDL/DML), enforces per-agent row-level security, and audit-logs every query.
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Connects AI assistants to PostgreSQL databases with production-grade safety features including query validation, guarded writes, rate limiting, and audit logging.
    3
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    SQL guardrails for AI agents, sitting between the agent and Postgres to enforce policies, block destructive queries, and audit all access.
    5
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Lets AI agents estimate the cost and result size of BigQuery and Snowflake queries before they run, then execute them only within per-call byte, row, and dollar bounds. Estimates are labeled with an accuracy tier so agents never over-trust an approximate figure.
    4
    327 PyPI
    1
    MIT