Files
gp_mcp/README.md
Радик ddaf277703 Add heuristic warnings to plan summary
Detects 7 common GP plan anti-patterns (broadcast of large table,
redistribute of large table, hot node, nested loop with large outer,
large sort, spill to disk, stale row estimates, planning-heavy queries)
and surfaces them as summary.warnings[] with severity + evidence.

Raw plan and raw summary metrics are preserved so the agent can verify
each warning against the underlying numbers.
2026-05-31 14:17:52 +03:00

331 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# gp-mcp
MCP-сервер для оценки плана запросов dbt-моделей в Greenplum 6.x.
Запускается локально по `stdio` рядом с AI-агентом, который рефакторит легаси PL/SQL
в dbt-модели. Сервер:
1. компилирует выбранную dbt-модель (`dbt compile --select <model>`);
2. подключается к Greenplum под read-only пользователем
(`SET default_transaction_read_only = on`, `statement_timeout`);
3. выполняет `EXPLAIN (ANALYZE, VERBOSE, FORMAT JSON)`;
4. возвращает JSON-план + краткую сводку с GP-метриками (motion-узлы,
самый медленный узел, ошибка оценки строк).
## Tools
| Tool | Параметры | Что делает |
|------|-----------|------------|
| `explain_sql` | `sql: str`, `statement_timeout_ms?: int` | EXPLAIN ANALYZE для произвольного SQL |
| `explain_dbt_model` | `model_name: str`, `statement_timeout_ms?: int` | `dbt compile` + EXPLAIN ANALYZE для модели |
Возвращаемый JSON:
```json
{
"summary": {
"total_cost": 12345.6,
"plan_rows": 100000,
"actual_rows": 98412,
"execution_time_ms": 842.3,
"planning_time_ms": 12.1,
"slowest_node": { "node_type": "Seq Scan", "actual_total_time_ms": 700.2, "...": "..." },
"motion_nodes": [{ "node_type": "Redistribute Motion", "...": "..." }],
"rows_misestimation_factor": 1.02,
"warnings": [
{
"code": "broadcast_large_table",
"severity": "critical",
"message": "Broadcast Motion ships 5,400,000 rows to every segment ...",
"evidence": { "node_type": "Broadcast Motion", "actual_rows": 5400000, "...": "..." }
}
]
},
"plan": [ /* raw EXPLAIN JSON пруф для каждого warning */ ],
"statement_timeout_ms": 300000,
"compiled_sql": "select ...",
"model_name": "fct_orders"
}
```
### Warnings
`summary.warnings[]` — это эвристический разбор плана для агента: какие узлы выглядят
неоптимально и что с этим делать. Сырые числа из плана **остаются** в `summary.*` и
`plan[]` — каждый warning несёт `evidence` со ссылкой на конкретные значения, чтобы
агент мог перепроверить рекомендацию, а не доверять ей слепо.
| `code` | `severity` | Триггер |
|--------|------------|---------|
| `rows_misestimation` | warning | plan vs actual rows расходятся в > 10× — статистика устарела |
| `broadcast_large_table` | critical | `Broadcast Motion` гонит > 1M строк на все сегменты |
| `redistribute_large_table` | warning | `Redistribute Motion` тасует > 10M строк — проверить distribution key |
| `hot_node` | warning | Один узел занимает > 60% `execution_time_ms` |
| `nested_loop_large` | critical | `Nested Loop` с > 10 000 итераций |
| `large_sort` | warning | `Sort` по > 10M строк |
| `spill_to_disk` | warning | Узел льётся на диск (`Disk Usage > 0`, `Sort Method: external merge Disk` и т.п.) |
| `planning_heavy` | info | `planning_time_ms / execution_time_ms > 20%` |
Пороги — константы в верхней части [src/gp_mcp/explain.py](src/gp_mcp/explain.py)
(`ROWS_MISESTIMATION_FACTOR`, `BROADCAST_LARGE_ROWS`, …), калибруются под кластер.
## Установка
```bash
cd /Users/admin/Projects/vpn
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
## Конфигурация
Все настройки — через переменные окружения. Скопируй `.env.example` в `.env`
и заполни.
| Переменная | Обязательная | Назначение |
|------------|:-:|---|
| `GP_HOST` | + | Хост Greenplum master |
| `GP_PORT` | + | Порт |
| `GP_USER` | + | Read-only пользователь (см. ниже) |
| `GP_PASSWORD` | + | Пароль |
| `GP_DATABASE` | + | Имя БД |
| `GP_SCHEMA` | | `search_path`, можно через запятую |
| `DBT_PROJECT_DIR` | + | Каталог dbt-проекта (содержит `dbt_project.yml`) |
| `DBT_PROFILES_DIR` | + | Каталог с `profiles.yml` |
| `DBT_TARGET` | + | Имя target из `profiles.yml` (напр. `dev`) |
| `DBT_EXECUTABLE` | | Путь к `dbt`, по умолчанию `dbt` из PATH |
| `STATEMENT_TIMEOUT_MS` | + | Дефолтный `statement_timeout` для EXPLAIN ANALYZE |
| `MAX_STATEMENT_TIMEOUT_MS` | + | Верхняя граница, агент не сможет превысить |
| `LOG_LEVEL` | | `DEBUG`/`INFO`/`WARNING`/`ERROR`, дефолт `INFO` |
Если обязательная переменная не задана — сервер не стартует и пишет в stderr
имя недостающей переменной.
## Read-only роль в Greenplum
Сервер требует, чтобы доступ был ограничен на уровне БД. Минимум:
```sql
CREATE ROLE dbt_explain LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE <db> TO dbt_explain;
GRANT USAGE ON SCHEMA <schema> TO dbt_explain;
GRANT SELECT ON ALL TABLES IN SCHEMA <schema> TO dbt_explain;
ALTER DEFAULT PRIVILEGES IN SCHEMA <schema>
GRANT SELECT ON TABLES TO dbt_explain;
```
Сервер дополнительно ставит сессионный `default_transaction_read_only = on`,
но GRANT-ы — единственная надёжная защита.
## Запуск
Локально (для отладки):
```bash
python -m gp_mcp.server
```
Сервер ничего не печатает в stdout (это канал MCP) — все логи идут в stderr.
## Подключение к клиенту
Сервер общается по `stdio`, поэтому клиент должен сам его запускать.
Конфиг — стандартный MCP JSON: одинаковая форма для Claude Code и Cursor,
различаются только пути к файлам настроек.
Общий блок, который пригодится ниже:
```json
{
"command": "/Users/admin/Projects/vpn/.venv/bin/python",
"args": ["-m", "gp_mcp.server"],
"cwd": "/Users/admin/Projects/vpn/src",
"env": {
"GP_HOST": "gp-master.internal",
"GP_PORT": "5432",
"GP_USER": "dbt_explain",
"GP_PASSWORD": "REPLACE_ME",
"GP_DATABASE": "analytics",
"GP_SCHEMA": "analytics,public",
"DBT_PROJECT_DIR": "/Users/admin/Projects/dbt-analytics",
"DBT_PROFILES_DIR": "/Users/admin/.dbt",
"DBT_TARGET": "dev",
"STATEMENT_TIMEOUT_MS": "300000",
"MAX_STATEMENT_TIMEOUT_MS": "900000",
"LOG_LEVEL": "INFO"
}
}
```
Важно:
- `command`**абсолютный** путь к Python из venv проекта. Клиенты MCP
обычно стартуют без активированного окружения, поэтому полагаться на
`python` из PATH нельзя.
- `cwd` указан на `src/`, чтобы Python нашёл пакет `gp_mcp` без установки
(`pip install -e .` не делаем).
- Секреты держим в `env` соответствующего конфига клиента, **не** в коде
и **не** в репозитории.
---
### Claude Code
Есть три способа добавить сервер — выбери один.
**1. Через CLI (быстрее всего)**
```bash
claude mcp add gp-mcp \
--scope user \
--env GP_HOST=gp-master.internal \
--env GP_PORT=5432 \
--env GP_USER=dbt_explain \
--env GP_PASSWORD=REPLACE_ME \
--env GP_DATABASE=analytics \
--env DBT_PROJECT_DIR=/Users/admin/Projects/dbt-analytics \
--env DBT_PROFILES_DIR=/Users/admin/.dbt \
--env DBT_TARGET=dev \
--env STATEMENT_TIMEOUT_MS=300000 \
--env MAX_STATEMENT_TIMEOUT_MS=900000 \
-- /Users/admin/Projects/vpn/.venv/bin/python -m gp_mcp.server
```
Флаг `--scope`:
- `user` — для всех проектов (пишется в `~/.claude.json`);
- `project` — общий для команды, кладётся в `.mcp.json` в корне проекта,
его можно коммитить в git (секреты тогда задают через `${VAR}`-подстановку
из окружения, а не хардкодом);
- `local` — только в текущем проекте, только у тебя.
**2. Вручную, user-scope: `~/.claude.json`**
```json
{
"mcpServers": {
"gp-mcp": { /* см. общий блок выше */ }
}
}
```
**3. Вручную, project-scope: `.mcp.json` в корне dbt-репозитория**
```json
{
"mcpServers": {
"gp-mcp": {
"command": "/Users/admin/Projects/vpn/.venv/bin/python",
"args": ["-m", "gp_mcp.server"],
"cwd": "/Users/admin/Projects/vpn/src",
"env": {
"GP_HOST": "${GP_HOST}",
"GP_PORT": "${GP_PORT}",
"GP_USER": "${GP_USER}",
"GP_PASSWORD": "${GP_PASSWORD}",
"GP_DATABASE": "${GP_DATABASE}",
"DBT_PROJECT_DIR": "${DBT_PROJECT_DIR}",
"DBT_PROFILES_DIR": "${DBT_PROFILES_DIR}",
"DBT_TARGET": "${DBT_TARGET}",
"STATEMENT_TIMEOUT_MS": "300000",
"MAX_STATEMENT_TIMEOUT_MS": "900000"
}
}
}
}
```
**Проверка:**
```bash
claude mcp list # gp-mcp должен быть в списке
claude mcp get gp-mcp # детали конфига
```
В сессии `/mcp` покажет статус подключения и список tool'ов. Если статус
`failed`, посмотри `~/Library/Logs/Claude/` — сервер пишет ошибки запуска
(включая отсутствующие env-переменные) в stderr.
---
### Cursor IDE
Cursor использует тот же MCP-формат, но свой файл настроек.
**1. Через UI**
`Settings``Cursor Settings``MCP & Integrations``New MCP Server`
откроется `mcp.json` для редактирования.
**2. Вручную, глобально: `~/.cursor/mcp.json`**
Доступно во всех проектах.
```json
{
"mcpServers": {
"gp-mcp": {
"command": "/Users/admin/Projects/vpn/.venv/bin/python",
"args": ["-m", "gp_mcp.server"],
"cwd": "/Users/admin/Projects/vpn/src",
"env": {
"GP_HOST": "gp-master.internal",
"GP_PORT": "5432",
"GP_USER": "dbt_explain",
"GP_PASSWORD": "REPLACE_ME",
"GP_DATABASE": "analytics",
"DBT_PROJECT_DIR": "/Users/admin/Projects/dbt-analytics",
"DBT_PROFILES_DIR": "/Users/admin/.dbt",
"DBT_TARGET": "dev",
"STATEMENT_TIMEOUT_MS": "300000",
"MAX_STATEMENT_TIMEOUT_MS": "900000"
}
}
}
}
```
**3. Вручную, для проекта: `.cursor/mcp.json` в корне dbt-репозитория**
Видно только в этом проекте. Удобно, когда у разных dbt-проектов разные
`DBT_PROJECT_DIR`/`DBT_TARGET`.
**Проверка:**
`Settings``MCP & Integrations` — справа от `gp-mcp` должен загореться
зелёный индикатор и появиться список tool'ов (`explain_sql`,
`explain_dbt_model`). В чате tools будут доступны Agent-режиму.
Если индикатор красный — раскрой сервер в этом же окне, там показывается
stderr запуска (включая `Configuration error: Required environment variable
'...' is not set`).
---
### Общие проблемы при подключении
| Симптом | Причина |
|---------|---------|
| `Configuration error: Required environment variable 'X' is not set` | Переменная `X` не задана в `env` конфига клиента |
| `ModuleNotFoundError: No module named 'gp_mcp'` | Неверный `cwd` — должен указывать на `src/`, или Python не из venv |
| `ModuleNotFoundError: No module named 'mcp'` | `command` указывает не на Python из venv, где установлены зависимости |
| Сервер стартует, но tools не появляются | Клиент не перезапущен / нет permissions в Cursor для MCP |
| `dbt: command not found` при вызове `explain_dbt_model` | Поставь `DBT_EXECUTABLE=/абсолютный/путь/к/dbt` в `env` |
## Структура
```
vpn/
├── .env.example
├── .gitignore
├── requirements.txt
├── README.md
└── src/
└── gp_mcp/
├── __init__.py
├── config.py # загрузка и валидация env
├── db.py # psycopg2 + read-only + timeout
├── dbt_runner.py # subprocess dbt compile + чтение compiled SQL
├── explain.py # EXPLAIN ANALYZE + summary
└── server.py # FastMCP, регистрация tools, stdio
```