pg-analytics-mcp
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 → Postgrespostgres-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:
Переменная | Назначение |
| Роль только для чтения. В Supavisor pooler имя пользователя обязано содержать |
| Контейнер, тег образа и имя роутера traefik |
| Публичное имя хоста; автоматически добавляется в allowlist транспортной безопасности |
| Внешняя docker-сеть, за которой следит traefik |
| Путь к клиентскому YAML внутри образа |
| Публикуемый порт на стороне хоста (по умолчанию 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 existlimits.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.
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