commit 47218d09b89405884e991bbb9a307433817ee9e5 Author: smartass Date: Thu Jun 11 22:07:34 2026 +0500 docs: add dbmole-mcp design spec diff --git a/docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md b/docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md new file mode 100644 index 0000000..c1fb357 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md @@ -0,0 +1,173 @@ +# dbmole-mcp — дизайн + +MCP-сервер (stdio) для работы с PostgreSQL и MySQL через именованные подключения. +Решаемая боль: фиксированные env-конфиги требуют перезапуска харнеса при каждом +изменении настроек. dbmole хранит подключения персистентно и позволяет управлять +ими тулзами в рантайме, без перезапуска. Переносимость — npm-пакет и Docker-образ. + +## Стек + +- TypeScript, ESM, Node >= 20 +- `@modelcontextprotocol/sdk` — McpServer + StdioServerTransport +- `zod` — схемы входов тулзов и валидация конфигов +- `pg` — драйвер PostgreSQL, `mysql2` — драйвер MySQL +- `ssh2` — SSH-туннели (pure JS, не требует ssh-бинаря в Docker) +- `tsup` — сборка bin с shebang +- Biome — линт и формат: indent 4 пробела, `semicolons: asNeeded`, + `quoteStyle: single`; функции — `const fn = () => {}` где возможно +- `vitest` + `@vitest/coverage-v8` — тесты, `testcontainers` — интеграционные + +## Каскад источников подключений + +Выше — приоритетнее при совпадении имён: + +1. env `DBMOLE_CONNECTIONS` — JSON-массив конфигов, readonly в рантайме +2. конфиг-файл: `--config ` или env `DBMOLE_CONFIG` (путь), + формат `{ "connections": [...] }`, readonly в рантайме +3. персистентный стор `~/.config/dbmole/connections.json` + (права 0600, путь переопределяется env `DBMOLE_STORE`) — + мутируется тулзами add/update/remove_connection + +Правила: + +- Стор перечитывается при каждом обращении (resolve, list, mutate) — без + кеширования файла. Несколько одновременных инстансов dbmole видят изменения + друг друга сразу. +- Запись стора атомарная: tmp-файл + rename. Перед каждой мутацией — re-read + (read-modify-write), последняя запись побеждает. +- У каждого подключения в выдаче `list_connections` есть `source: env | config | store`. +- `update/remove_connection` по записи из env/config → ошибка с указанием источника. +- `add_connection` с именем, занятым любым слоем, → ошибка (никаких теневых записей). + +## Схема подключения + +```ts +{ + name: string // [a-zA-Z0-9_-]+, уникально в рамках слоя + type: 'postgres' | 'mysql' + host: string + port?: number // default: 5432 (pg) / 3306 (mysql) + user: string + password?: string + database?: string // дефолтная БД для тулзов без явного database + readonly?: boolean // default: false + ssh?: { + host: string + port?: number // default: 22 + user: string + password?: string + privateKey?: string // inline PEM + privateKeyPath?: string // путь, '~' разворачивается + passphrase?: string + agent?: boolean // использовать SSH_AUTH_SOCK + } +} +``` + +Валидация zod на всех входах: и тулзовых, и из env/файлов. Ошибка в одном +конфиге из источника не валит сервер — битая запись скипается с warning в stderr. + +## Runtime-архитектура + +``` +src/ + index.ts — entry: argv/env, wire-up, stdio transport, shutdown + server.ts — McpServer, регистрация тулзов + config/ + types.ts — zod-схемы ConnectionConfig, SshConfig + sources.ts — чтение env-слоя и config-файла + store.ts — персистентный стор: атомарные read/write, 0600 + registry.ts — мердж каскада, CRUD store-слоя, resolve(name) → {config, source, hash} + net/ + tunnel.ts — SshTunnel: ssh2-клиент + локальный TCP-форвард + db/ + driver.ts — интерфейс Driver + postgres.ts — pg-реализация + mysql.ts — mysql2-реализация + manager.ts — ConnectionManager: кеш name → {tunnel?, driver, hash} + tools/ + connections.ts — list/add/update/remove/test_connection + query.ts — execute_sql + schema.ts — list_databases, list_tables, describe_table + format.ts — приведение результатов, rowLimit, truncation +``` + +- **registry** — единственная точка чтения конфигов. `resolve(name)` возвращает + конфиг, источник и стабильный hash (canonical JSON) конфига. +- **manager** — ленивая инициализация: при первом запросе к подключению + поднимает туннель (если есть `ssh`) и создаёт драйвер. Если hash конфига + изменился со времени кеширования — dispose старого (пулы + туннель) + и пересоздание. Это даёт hot-reload настроек без перезапуска. +- **tunnel** — локальный TCP-сервер на `127.0.0.1:0` (эфемерный порт), каждое + входящее соединение форвардится через `ssh2` `forwardOut` на целевые + db host:port. Драйвер подключается к локальному endpoint. +- **driver** — интерфейс: `query`, `listDatabases`, `listTables`, + `describeTable`, `dispose`. Внутри — map пулов per database + (пул на пару connection+database), max 4 соединения на пул. +- Shutdown: SIGINT/SIGTERM/закрытие stdin → dispose всех драйверов и туннелей. + +## Readonly + +Серверное принуждение, не SQL-парсинг. На каждом новом коннекте пула: + +- pg: `SET default_transaction_read_only = on` +- mysql: `SET SESSION TRANSACTION READ ONLY` + +Блокирует DML и DDL на стороне движка. Обходится только явным `SET` в SQL — +для агентского сценария приемлемо (защита от случайной записи, не от злого умысла). + +## Тулзы + +Все входы — zod. Параметр `database` опционален везде, где есть, — +fallback на `database` подключения; если нет ни того ни другого → ошибка. + +| Тулза | Вход | Выход | +|---|---|---| +| `list_connections` | — | `[{name, type, host, port, database, readonly, source, ssh: boolean}]`, без секретов | +| `add_connection` | полный конфиг | ok + запись в стор | +| `update_connection` | `name` + partial-конфиг | ok, патч store-записи, инвалидация кеша manager; явный `null` в поле удаляет опциональное поле (напр. `ssh: null`) | +| `remove_connection` | `name` | ok, удаление из стора, dispose ресурсов | +| `test_connection` | `name` | connect + `SELECT 1`: версия сервера, latency ms | +| `execute_sql` | `connection, sql, params?, database?, rowLimit?` | SELECT: `{columns, rows, rowCount, truncated}`; DML: `{rowCount, lastInsertId?}` | +| `list_databases` | `connection` | имена + размер в байтах | +| `list_tables` | `connection, database?, schema?` | `[{schema, name, rowEstimate}]` (mysql: schema = database) | +| `describe_table` | `connection, table, database?, schema?` | колонки (тип, nullable, default), PK, индексы, FK | + +- `params` — позиционные (`$1` для pg, `?` для mysql), передаются драйверу + как параметризованный запрос. +- `rowLimit`: default 100, max 1000. При срезе `truncated: true`. +- Один SQL-стейтмент на вызов: `multipleStatements` выключен у mysql2, + у pg параметризованные запросы и так single-statement. Меньше сюрпризов. +- Ошибки — структурный текст: `[pg 42P01] relation "x" does not exist` + detail/hint + у pg. «Connection not found» перечисляет доступные имена — агент сам чинится. +- Системные БД скрыты в `list_databases` (template0/1; mysql, sys, + performance_schema, information_schema). + +## Дистрибуция + +- npm: package `dbmole-mcp`, `bin: { "dbmole-mcp": "dist/index.js" }`. + Подключение: `{ "command": "npx", "args": ["-y", "dbmole-mcp"] }` +- Docker: `node:22-alpine`, multi-stage build, + `docker run -i --rm -v dbmole-store:/root/.config/dbmole dbmole-mcp` +- README: оба способа + формат env/config + примеры `.mcp.json` + +## Тестирование + +- **Unit** (моки, без сети): мердж каскада и приоритеты, атомарность стора + и права 0600, zod-валидация (вкл. скип битых записей), форматирование + результатов и truncation, tool-хендлеры с мок-manager, lifecycle туннеля + с мок-ssh2, инвалидация по hash. +- **Интеграция** (testcontainers, требуется Docker): реальные postgres и mysql + контейнеры — полный флоу add_connection → execute_sql → list/describe → + readonly-enforcement (запись падает с ошибкой движка); sshd-контейнер → + e2e туннель до pg за SSH. +- Coverage-пороги в vitest config: >= 90% lines/functions. +- CI-разделение: `npm run test:unit` (быстрые) / `npm run test:int` (docker). + +## Вне скоупа (YAGNI) + +- Другие движки (sqlite, mssql) — интерфейс Driver оставляет дверь открытой +- Шифрование стора (плейнтекст 0600, как ~/.pgpass) +- HTTP/SSE транспорт +- Idle-таймауты туннелей и пулов (dispose по shutdown и инвалидации достаточно) +- Миграции формата стора