MCP stdio server for Greenplum 6.x query plan evaluation: - explain_sql / explain_dbt_model tools - read-only session enforcement + statement_timeout - dbt compile integration - all settings via env vars (no hardcoded defaults)
302 lines
11 KiB
Markdown
302 lines
11 KiB
Markdown
# 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
|
||
},
|
||
"plan": [ /* raw EXPLAIN JSON */ ],
|
||
"statement_timeout_ms": 300000,
|
||
"compiled_sql": "select ...",
|
||
"model_name": "fct_orders"
|
||
}
|
||
```
|
||
|
||
## Установка
|
||
|
||
```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
|
||
```
|