Skip to main content
Glama
Sa3fa

pg-analytics-mcp

by Sa3fa

pg-analytics-mcp

Конфигурационный read-only Postgres MCP-сервер для Claude. Открывает схему Postgres для Claude через Streamable HTTP; схема и значения enum интроспектируются из работающей базы данных при запуске, а всё клиент-специфичное хранится в одном YAML-файле.

Спроектирован для работы за Cloudflare Access на стеке cloudflared → reverse-proxy (в комплекте идёт полный playbook по развёртыванию), но сам сервер не зависит от Cloudflare и работает где угодно.

Не зависит от клиента. Внутри server/ нет ничего, что знало бы о конкретном клиенте. Чтобы обслужить нового клиента: скопируйте репозиторий, напишите конфигурационный файл, задайте .env.

Зачем это существует

Предыдущее решение нагромождало три процесса, чтобы обойти сторонний пакет:

supergateway  →  enrich.py  →  postgres-mcp  →  Postgres

postgres-mcp поддерживает только stdio/SSE (Cloudflare требует Streamable HTTP), у него нет ни поверхности конфигурации, а, supergateway порождал по дочернему процессу на каждую MCP-сессию, которые никогда не завершались, — измерено 23 дочерних процесса / 15 соединений при лимите роли 20, что проявлялось как «работает около 9 вызовов, а затем всё падает, включая SELECT 1».

Этот сервер — это один процесс с одним общим пулом. Измерено: 1 процесс после 30 вызовов инструментов.

Related MCP server: mcp-postgres

Архитектура

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

Граница безопасности — роль базы данных, а не этот сервер.

Быстрый старт

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

Затем следуйте docs/PLAYBOOK-NEW-CLIENT.md для части, связанной с Cloudflare.

Конфигурация

.env — специфичное для хоста, единственное, что меняется между VPS:

Переменная

Назначение

DATABASE_URI

Роль только для чтения. В Supavisor pooler имя пользователя обязано содержать .PROJECT_REF.

MCP_CONTAINER_NAME

Контейнер, тег образа и имя роутера traefik

MCP_HOSTNAME

Публичное имя хоста; автоматически добавляется в allowlist транспортной безопасности

TRAEFIK_NETWORK

Внешняя docker-сеть, за которой следит traefik

MCP_CONFIG

Путь к клиентскому YAML внутри образа

MCP_LOCAL_PORT

Публикуемый порт на стороне хоста (по умолчанию 8000)

config/<client>.yaml — предметная область. Не перечисляйте здесь колонки или значения enum: они интроспектируются из живой базы при запуске и поэтому не могут устареть. Пишите только то, что интроспекция не может узнать, — бизнес-смысл и ловушки.

Инструменты

Встроенные:

  • execute_sql(sql) — сырой SQL только для чтения. Его описание собирается при запуске из вашего авторского текста плюс сгенерированного списка схемы и enum.

  • list_views() — все доступные для чтения объекты с колонками, количеством строк и enum.

  • describe_view(name) — колонки одного объекта.

Определяемые конфигурацией: каждая запись в tools.queries становится настоящим MCP-инструментом с типизированными параметрами. Параметры привязываются через именованные плейсхолдеры psycopg — никогда через строковую интерполяцию, — а min/max проверяются до привязки.

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)

Именно это закрывает разрыв с n8n: добавление инструмента — это текст + SQL, а не Python.

Почему описания живут здесь

Описания инструментов — это единственный контекст, который модель видит всякий раз, когда инструмент доступен, — для каждого клиентаи каждого разговора, без загрузки навыков и без инструкций проекта. Знание предметной области, хранящееся во внешнем документе, — это знание, которого у модели часто нет.

Половина каждого описания — авторская (суждения), половина — сгенерированная (факты). Сгенерированная половина — причина, по которой платформа/процессор boxy и частота daily больше не могут пропасть так, как они пропали в написанном вручную промпте, который был раньше.

Эксплуатация

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

Изменение конфигурации или схемы требует перезапуска: интроспекция намеренно кэшируется на всё время жизни процесса, чтобы поведение не могло изменяться посреди работы.

Пять граничных тестов

Перезапустите после любого изменения представлений, грантов или конфигурации. Все пять обязаны упасть:

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 существует, но по умолчанию выключен: роль — это граница, а SQL-валидатор поверх неё блокирует корректные read-only конструкции без всякой выгоды — именно поэтому ограниченный режим postgres-mcp был и abandoned.

Грабли, оплаченные кровью

  • Ключи labels в Compose не подставляют переменные. Labels должны быть в же списке (- "traefik...=value"), иначе вы получите роутер, буквально названный ${MCP_CONTAINER_NAME}, и traefik отдаёт 404.

  • Защита от DNS-rebinding включена по умолчанию в MCP SDK. Проброшенный Host за прокси должен быть разрешён; MCP_HOSTNAME и MCP_LOCAL_PORT добавляются автоматически.

  • Монтирование MCP-приложения под ваше Starlette-приложение заменяет его жизненный цикл. Менеджер сессий нужно запускать явно (server.session_manager.run()), иначе каждый запрос падает коду 500 «Task group is not initialized».

  • set_read_only / set_autocommit должны предшествовать любому execute() на соединении, иначе пул падает с ошибкой «connection in transaction status INTRANS».

  • pg_class.reltuples бессмысленна для представлений, поэтому оценка количества строк при запуске переходит на ограниченный count(*).

  • Supavisor переписывает application_name в «Supavisor», поэтому атрибуция соединения по клиентам через коллектор невозможна.

  • MCP SDK 2.0 переименовал FastMCP в MCPServer и вынес его из mcp.server.fastmcp. Именно поэтому requirements.txt — это полный lock-файл.

Лицензия

MIT — см. 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