Skip to main content
Glama
Sa3fa

pg-analytics-mcp

by Sa3fa

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  →  Postgres

postgres-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.* views

El 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 boot

A 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

DATABASE_URI

Rol de solo lectura. En el pooler de Supavisor, el nombre de usuario debe llevar .PROJECT_REF.

MCP_CONTAINER_NAME

Contenedor, etiqueta de imagen y nombre del router de traefik

MCP_HOSTNAME

Nombre de host público; se añade automáticamente a la lista de permitidos de seguridad de transporte

TRAEFIK_NETWORK

Red docker externa que traefik observa

MCP_CONFIG

Ruta al YAML del cliente dentro de la imagen

MCP_LOCAL_PORT

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 edit

Un 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 exist

limits.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 Host reenviado detrás de un proxy debe estar permitido; MCP_HOSTNAME y MCP_LOCAL_PORT se 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_autocommit deben preceder a cualquier execute() en una conexión, o el pool falla con "connection in transaction status INTRANS".

  • pg_class.reltuples carece de sentido para las vistas, por lo que las estimaciones de filas recurren a un count(*) acotado al arrancar.

  • Supavisor reescribe application_name a "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ó FastMCP a MCPServer y lo sacó de mcp.server.fastmcp. requirements.txt es un bloqueo completo por esa razón.

Licencia

MIT — consulta LICENSE.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Connects 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.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables read-only, SELECT-only querying of any Postgres database through MCP-compatible clients like Claude, with schema introspection and guarded SQL execution.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables 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.
    2
    MIT