Files
dbmole-mcp/docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md
T
2026-06-11 22:07:34 +05:00

174 lines
11 KiB
Markdown
Raw 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.
# 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 <path>` или 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 и инвалидации достаточно)
- Миграции формата стора