pg-readonly-mcp
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.parsesepara 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 lectura —
SELECT,UNION,INTERSECT,EXCEPT, o una CTE construida a partir de estas.DROP TABLE usersse 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 xes, a nivel externo, unSELECT. Una comprobación que solo mira la forma externa pasa esto por alto por completo. Recorrer cada nodo encuentra elDELETEindependientemente 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.
sqlgloty 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 |
| Ejecuta una sentencia con forma de |
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_mcpCI 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-enforcedguard.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.
This server cannot be installed
Maintenance
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
- Alicense-qualityCmaintenanceRead-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.539MIT
- AlicenseAqualityDmaintenanceA secure, read-only PostgreSQL MCP server that provides safe database introspection and querying capabilities.1415MIT
- Flicense-qualityBmaintenanceRead-only MCP server for PostgreSQL enabling schema introspection and SELECT queries via MCP clients like Claude, with multi-layered write protection.
- Alicense-qualityCmaintenanceProvides a read-only PostgreSQL MCP server with schema introspection. Enforces least-privilege database roles to prevent any writes, even from malicious SQL.MIT
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
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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