Skip to main content
Glama
les-k

pg-readonly-mcp

by les-k

pg-readonly-mcp

Un servidor MCP de Postgres de solo lectura que analiza SQL en lugar de buscar patrones, porque la alternativa ya ha fallado en público, una vez, de una manera que merece ser específica.

La vulnerabilidad que esto existe para cerrar

El servidor MCP de Postgres de referencia de Anthropic imponía «solo lectura» envolviendo cada consulta en una transacción de solo lectura. También aceptaba entrada multi-sentencia delimitada por punto y coma. Esa combinación es explotable:

SELECT 1; COMMIT; DROP SCHEMA public CASCADE;

El COMMIT finaliza la transacción de solo lectura antes de tiempo. Todo lo que va después se ejecuta con privilegios completos de sesión. Datadog Security Labs reveló esto en 2026; el servidor fue desaprobado y archivado — y el paquete vulnerable seguía obteniendo 21 000 descargas semanales después de eso.

Una transacción de solo lectura es una propiedad de cómo se ejecuta una consulta. No dice nada sobre qué es la consulta, que es exactamente por lo que era posible salirse de ella. Este servidor verifica la segunda cosa en su lugar.

Related MCP server: postgres-mcp-readonly

Dos capas independientes

Cualquiera de las dos por sí sola habría detenido la vulnerabilidad revelada. Ambas existen porque una falla en una no debería ser lo único que se interponga entre un agente y una escritura.

1. El SQL se analiza sintácticamente, no se escanea. guard.py utiliza sqlglot para construir un árbol sintáctico real y aplica tres comprobaciones:

  • Exactamente una sentencia. sqlglot.parse separa punto y coma que terminan sentencias como lo haría un controlador, por lo que el payload anterior se convierte en tres sentencias y se rechaza antes de que cualquiera de ellas llegue a una conexión.

  • La sentencia externa es una forma de lecturaSELECT, UNION, INTERSECT, EXCEPT, o una CTE construida a partir de estas. DROP TABLE users se rechaza aquí.

  • Sin escritura en ningún lugar del árbol, recorrido por completo. Esta es la comprobación que las otras dos no pueden reemplazar: Postgres permite que una CTE contenga una sentencia de modificación de datos, por lo que WITH x AS (DELETE FROM t RETURNING *) SELECT * FROM x es, a nivel externo, un SELECT. Una comprobación que solo mira la forma externa pasa esto por alto por completo. Recorrer cada nodo encuentra el DELETE independientemente de cuántos niveles hacia abajo se encuentre.

Una forma de sentencia no reconocida — cualquier cosa para la cual sqlglot no tenga una regla específica — se rechaza por la misma regla que todo lo demás. Desconocido no es lo mismo que seguro.

2. La conexión misma no puede escribir, independientemente de lo que se le pida ejecutar. check_connection_is_readonly se niega a iniciar si el rol conectado es un superusuario, puede crear bases de datos o roles, puede omitir la seguridad a nivel de fila, o tiene algún permiso más allá de SELECT en alguna tabla visible. No exhaustivo — los privilegios de Postgres también pueden llegar a través de la propiedad, permisos PUBLIC, o políticas RLS que esta comprobación no enumera — pero captura las dos formas en que una mala configuración se manifiesta más comúnmente, declaradas aquí en lugar de implícitas.

Lo que no protege

  • Una cadena de conexión privilegiada que se le proporcione de todas formas. La comprobación de inicio captura las formas comunes de exceso de privilegios; no es una auditoría de privilegios exhaustiva, y lo dice arriba en lugar de implicar lo contrario.

  • Agotamiento de recursos dentro de los límites. Una consulta que devuelve legalmente 1000 filas de datos muy anchos, o que es legítimamente costosa de planificar, sigue costando lo que cuesta. Los límites de filas y un tiempo de espera de sentencia acotan el daño; no hacen que una lectura costosa sea gratuita.

  • Lo que el agente hace con los datos una vez que los tiene. Esto es una compuerta de consultas, no una herramienta de prevención de pérdida de datos. El acceso de lectura a una tabla es acceso de lectura a lo que contenga.

  • Una discrepancia del analizador sintáctico. sqlglot y el analizador de Postgres son dos implementaciones independientes de la misma gramática. No está demostrado que coincidan en cada caso límite que Postgres acepta — una brecha genuina, aunque estrecha, en un proyecto cuya premisa completa es no confiar en una sola capa. La capa dos existe en parte por esto: incluso si una diferencia de analizadores dejara pasar algo no deseado a través de la guardia, la conexión subyacente aún no puede escribir.

Resultados del escáner

Ejecutado contra agent-audit 0.19.2 el 18 de agosto de 2026: 15 hallazgos, 11 suprimidos automáticamente, 4 procesables — 1 BLOQUEO, 3 ADVERTENCIA.

El hallazgo de BLOQUEO es el más interesante y vale la pena leerlo completo. server.py:156, confianza 1.0: cur.execute(sql) — marcado como inyección SQL mediante ejecución sin parámetros. Esa línea es real. También es la línea más defendida de todo este código: para cuando sql llega a ella, validate_readonly() ya la ha analizado, confirmado que es exactamente una sentencia, confirmado que esa sentencia es una forma de lectura, y recorrido cada nodo de ella verificando que no haya una escritura. El escáner no tiene forma de ver nada de eso — es una coincidencia de patrones de un solo archivo, y la validación ocurre en una función diferente, en un módulo diferente, varias líneas antes. Identifica correctamente la forma que es peligrosa en general y no puede ver que la forma ya ha sido verificada.

Tampoco podría haberse solucionado haciendo lo que sugiere. Las consultas parametrizadas protegen valores sustituidos en una forma de consulta fija — WHERE id = %s. No aplican aquí, porque la estructura de la consulta es la entrada que esta herramienta existe para aceptar. No existe un esquema de parametrización solo de valores para «ejecuta cualquier SQL de solo lectura que el llamante pida». La solución para esta clase de herramienta es validar la estructura, que es para lo que sirve el resto de este archivo.

La ADVERTENCIA en la definición de query() (AGENT-034, «sin validación de entrada en el cuerpo de la función») es el mismo punto ciego desde un ángulo diferente: la primera línea de la función llama a validate_readonly(sql) dentro de un try/except. Una llamada a una función importada no es un patrón que el escáner considere como validación.

Las dos ADVERTENCIAS sobre credenciales codificadas están en tests/conftest.py: el DSN de administrador predeterminado (postgres:postgres@localhost:5432/postgres, el valor predeterminado estándar de Postgres local/CI) y la contraseña literal utilizada para roles que el conjunto de pruebas crea y elimina dentro de la misma prueba. Ambas se identifican correctamente como cadenas con forma de credencial; ninguna es una credencial que proteja algo — una apunta a una base de datos local desechable, la otra vive durante la duración de una sola prueba.

Los 11 hallazgos restantes — todos AGENT-041, todos en fixtures de prueba que construyen sentencias CREATE SCHEMA / GRANT / DROP ROLE a partir de nombres derivados de uuid.uuid4() — ya fueron suprimidos automáticamente por el propio escáner.

Un escáner de patrones es un detector de humo, no un juez. Publicar lo que encuentra y por qué vale más que un número limpio por sí solo.

Instalar

pip install -e .

Configurar

Requiere una cadena de conexión, proporcionada como --dsn o mediante PG_READONLY_MCP_DSN — la variable de entorno existe para que una contraseña no tenga que aparecer en una línea de comandos o en un archivo de configuración del cliente que pueda ser comprometido accidentalmente:

{
  "mcpServers": {
    "pg-readonly-mcp": {
      "command": "pg-readonly-mcp",
      "env": { "PG_READONLY_MCP_DSN": "postgresql://readonly_role:...@host:5432/db" }
    }
  }
}

El rol en esa cadena de conexión no debe tener nada más que SELECT. El servidor lo verifica por sí mismo y se niega a iniciar en caso contrario — consulte check_connection_is_readonly arriba.

La herramienta

Una herramienta, deliberadamente. Un servidor cuya propuesta de valor completa es «rechazamos todo excepto lecturas» no necesita una segunda superficie para también acertar en eso.

Herramienta

Qué hace

query

Ejecuta una sentencia con forma de SELECT. Analizada y recorrida antes de tocar la conexión. Limitada a --max-rows (por defecto 1000) y --timeout-ms (por defecto 5000)

Pruebas

37 pruebas. 25 no necesitan base de datos y se ejecutan en cualquier lugar — son la suite de pruebas completa de guard.py, entrada de cadena pura, decisión de salida. Las otras 12 necesitan un Postgres real y son pruebas de integración por diseño: el punto de check_connection_is_readonly es lo que hace contra atributos de rol reales y permisos reales, y una conexión simulada pasaría independientemente de lo que el servidor realmente haga contra una real.

pytest -q --cov=pg_readonly_mcp

CI se ejecuta contra un contenedor de servicio postgres:16 real y falla la compilación si las pruebas respaldadas por base de datos se informan como omitidas allí — la misma regla que sweep-mcp aplica a sus pruebas de enlace simbólico, por la misma razón: una prueba que silenciosamente no hace nada es peor que ninguna prueba.

Entre las 12: una reproducción en vivo del payload exacto de Datadog, impulsada a través de la llamada real a la herramienta MCP en lugar de directamente a través de guard.py — y una aserción de que el esquema objetivo todavía existe después, no solo que se generó una excepción. También cubierto: una escritura oculta dentro de una CTE a través de la misma ruta de llamada, el límite de filas, el tiempo de espera de sentencia, que una consulta cancelada deja la conexión utilizable para la siguiente, y que un rol con CREATEDB y cero permisos de tabla es rechazado solo por el atributo de rol.

Cobertura en CI, contra un Postgres real: 78%, guard.py al 100%. La brecha en server.py es el captura-todo genérico de psycopg.Error» — nada en la suite provoca deliberadamente un error de base de datos que no sea una cancelación — y el argparse de main()y el cableado de transporte, que la suite ejercita a través debuild_server` directamente; el transporte es la parte menos digna de simular y menos probable de que esconda un error real.

La primera versión de esto que se envió no pasó. Dos errores salieron a la luz solo cuando un contenedor de servicio Postgres real ejecutó la suite por primera vez, y ninguno era visible solo desde el código: SET statement_timeout = %s llegó a Postgres como SET statement_timeout = $1 y falló al analizarse, porque SET es una sentencia de utilidad y no acepta un parámetro de enlace como lo hace un SELECT — cada llamada real a la herramienta habría fallado de manera idéntica. Por separado, los fixtures de prueba intentaron DROP ROLE un rol que aún tenía permisos activos, lo que Postgres rechaza; DROP OWNED BY debe ejecutarse primero. Ambos están corregidos, y la ejecución desde entonces es la que describen estos números. Se deja porque una suite que solo informa éxito es una suite que nadie ha visto fallar.

Estructura

src/pg_readonly_mcp/
  guard.py    parses and walks the tree. Opens no connection. 135 lines.
  server.py   the MCP tool, the connection check, the timeout and row cap.
tests/
  test_guard.py    25 tests - no database, run anywhere
  test_server.py   12 tests - live Postgres required, CI-enforced

guard.py no sabe nada sobre MCP o psycopg. Si una consulta alguna vez se rechaza por una razón que vive en server.py en su lugar, eso es un error — el juicio pertenece una capa más abajo, donde se puede probar con una cadena y nada más.

Licencia

MIT.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    -
    quality
    C
    maintenance
    Read-only PostgreSQL MCP server that enables running SELECT queries, listing tables and schemas, and describing columns, with built-in protection against writes and malicious SQL attacks.
    539
    MIT
  • F
    license
    -
    quality
    B
    maintenance
    Read-only MCP server for PostgreSQL enabling schema introspection and SELECT queries via MCP clients like Claude, with multi-layered write protection.
  • A
    license
    -
    quality
    C
    maintenance
    Provides a read-only PostgreSQL MCP server with schema introspection. Enforces least-privilege database roles to prevent any writes, even from malicious SQL.
    MIT

View all related MCP servers

Related MCP Connectors

  • MCP server for managing Prisma Postgres.

  • Read-only MCP server for ClassQuill, a tutoring-business-management platform.

  • MCP server for interacting with the Supabase platform

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/les-k/pg-readonly-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server