pg-analytics-mcp
pg-analytics-mcp
Un servidor MCP de Postgres para Claude, de solo lectura y basado en configuración. Expone un esquema de Postgres a Claude a través de Streamable HTTP, con los valores de esquema y enum introspectados desde la base de datos en vivo al arrancar, y todo lo específico del cliente en un único archivo YAML.
Diseñado para ejecutarse detrás de Cloudflare Access en una pila cloudflared → proxy inverso (se incluye un playbook de aprovisionamiento completo), pero el servidor en sí no tiene dependencia de Cloudflare y se ejecuta en cualquier lugar.
Independiente del cliente. Nada en server/ sabe nada sobre un cliente concreto. Para servir a uno nuevo: copia el repositorio, escribe un archivo de configuración, configura .env.
Por qué existe esto
El predecesor apilaba tres procesos para sortear un paquete de un proveedor:
supergateway → enrich.py → postgres-mcp → Postgrespostgres-mcp solo habla stdio/SSE (Cloudflare exige Streamable HTTP), no tiene ninguna superficie de configuración, y supergateway bifurcaba un hijo por sesión MCP que nunca se recolectaba — se midieron 23 hijos / 15 conexiones frente a un límite de rol de 20, lo que se manifestaba como "funciona durante ~9 llamadas y luego falla todo, incluido SELECT 1".
Este servidor es un solo proceso con un único pool compartido. Medido: 1 proceso después de 30 llamadas a herramientas.
Related MCP server: mcp-postgres
Arquitectura
Claude → portal.<zone> Cloudflare MCP Server Portal (OAuth)
→ mcp-origin.<zone> Access app + Managed OAuth
→ cloudflared tunnel
→ traefik Host-header routing
→ this container uvicorn, Streamable HTTP at /mcp
→ Postgres read-only role → analytics.* viewsEl límite de seguridad es el rol de la base de datos, no este servidor.
Inicio rápido
cp .env.example .env # set DATABASE_URI + the deployment vars
$EDITOR config/example.yaml # domain prose for this client
docker compose up -d --build
curl -s localhost:8000/healthz # ok
curl -s localhost:8000/introspection # what the server decided at bootA continuación, sigue docs/PLAYBOOK-NEW-CLIENT.md para la parte de Cloudflare.
Configuración
.env — específico del host, lo único que cambia entre VPS:
Variable | Propósito |
| Rol de solo lectura. En el pooler de Supavisor, el nombre de usuario debe llevar |
| Contenedor, etiqueta de imagen y nombre del router de traefik |
| Nombre de host público; se añade automáticamente a la lista de permitidos de seguridad de transporte |
| Red docker externa que traefik observa |
| Ruta al YAML del cliente dentro de la imagen |
| Puerto de publicación en el host (por defecto 8000) |
config/<client>.yaml — el dominio. No enumeres columnas ni valores de enum aquí: se introspectan desde la base de datos en vivo al arrancar, por lo que no pueden quedar obsoletos. Escribe solo lo que la introspección no puede saber: significado de negocio y trampas.
Herramientas
Integradas:
execute_sql(sql)— SQL de solo lectura sin procesar. Su descripción se ensambla al arrancar a partir de tu prosa redactada más los esquemas y listas de enum generados.list_views()— todos los objetos legibles con columnas, recuentos de filas y enums.describe_view(name)— las columnas de un objeto.
Definidos por configuración: cada entrada en tools.queries se convierte en una herramienta MCP real con parámetros tipados. Los parámetros se vinculan mediante placeholders nombrados de psycopg — nunca interpolación de cadenas — y min/max se aplican antes de la vinculación.
tools:
queries:
monthly_trend:
description: |
Donations per month. The most recent month is PARTIAL.
params:
months: {type: integer, default: 6, min: 1, max: 36}
sql: |
select ... where donated_at >= date_trunc('month', now())
- make_interval(months => %(months)s - 1)Esta es la pieza que cierra la brecha con n8n: añadir una herramienta es prosa + SQL, no Python.
Por qué las descripciones viven aquí
Las descripciones de las herramientas son el único contexto que un modelo ve siempre que la herramienta está disponible — en cada cliente, en cada conversación, sin carga de habilidades ni instrucciones de proyecto. El conocimiento de dominio guardado en un documento externo es conocimiento que el modelo a menudo no tiene.
La mitad de cada descripción es redactada (juicio), la mitad generada (hechos). La mitad generada es la razón por la que la plataforma/procesador boxy y la frecuencia daily ya no pueden perderse como ocurría en el prompt escrito a mano que precedió a esto.
Operaciones
curl -s localhost:8000/introspection | python3 -m json.tool # objects, enums, tools, limits
docker top <container> # must stay at 1 process
docker compose up -d --build # after a config editUn cambio de configuración o de esquema requiere un reinicio: la introspección se almacena en caché durante toda la vida del proceso, deliberadamente, para que el comportamiento no pueda desviarse a mitad de ejecución.
Las cinco pruebas de límite
Vuelve a ejecutarlos tras cualquier cambio en vistas, permisos o configuración. Los cinco deben fallar:
update donations set amount = 0 where false; -- permission denied for view
select count(*) from public.donations; -- permission denied for table
select count(*) from public.website_orders; -- permission denied for table
create table analytics.t (id int); -- read-only transaction
select phone_number from customers limit 1; -- column does not existlimits.select_only existe pero está desactivado por defecto: el rol es el límite, y un validador SQL encima bloquea construcciones válidas de solo lectura sin ninguna ventaja — por eso se abandonó el modo restringido de postgres-mcp.
Errores pagados con sangre
Las claves de las etiquetas de Compose no se sustituyen con variables. Las etiquetas deben estar en forma de lista (
- "traefik...=value"), o tendrás un router literalmente llamado${MCP_CONTAINER_NAME}y traefik devolverá 404.La protección contra el rebinding de DNS está activada por defecto en el SDK de MCP. El
Hostreenviado detrás de un proxy debe estar permitido;MCP_HOSTNAMEyMCP_LOCAL_PORTse añaden automáticamente.Montar la aplicación MCP bajo tu propio Starlette reemplaza su ciclo de vida. El gestor de sesiones debe iniciarse explícitamente (
server.session_manager.run()) o cada petición devolverá 500 con "Task group is not initialized".set_read_only/set_autocommitdeben preceder a cualquierexecute()en una conexión, o el pool falla con "connection in transaction status INTRANS".pg_class.reltuplescarece de sentido para las vistas, por lo que las estimaciones de filas recurren a uncount(*)acotado al arrancar.Supavisor reescribe
application_namea "Supavisor", por lo que la atribución de conexiones por cliente a través del pooler no es posible.El SDK de MCP 2.0 renombró
FastMCPaMCPServery lo sacó demcp.server.fastmcp.requirements.txtes un bloqueo completo por esa razón.
Licencia
MIT — consulta LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Query your warehouse or a CSV with Claude/ChatGPT over MCP, governed by table-level ACL + audit.
Read-only analytics for Convex apps, queryable via MCP from Claude, Cursor, and other clients.
Text-to-SQL MCP server: read-only queries on PostgreSQL, MySQL and SQL Server with your real schema
- dataOAuthco.thinair
PostgreSQL, MySQL, and SQL Server in one session. 26 read-only MCP tools for AI agents.
Related MCP Servers
- AlicenseAqualityBmaintenanceExposes PostgreSQL query execution, EXPLAIN, and schema inspection tools to MCP-compatible clients like Claude Desktop.31MIT
- FlicenseNot gradedqualityDmaintenanceConnects to a PostgreSQL database and exposes schema inspection and safe SELECT query execution as MCP tools and resources for use with Claude Desktop or any MCP-compatible client.-
- AlicenseNot gradedqualityCmaintenanceEnables read-only, SELECT-only querying of any Postgres database through MCP-compatible clients like Claude, with schema introspection and guarded SQL execution.MIT
- AlicenseAqualityBmaintenanceEnables Claude Desktop and Claude Code to inspect a Postgres schema and run read-only SQL queries against a demo database through the tools describe_schema and query_readonly. Every proposed query passes the same deterministic guardrails — SELECT-only, allow-listed tables, enforced row limits, statement timeouts and a read-only role — before execution.2MIT