Files
dbmole-mcp/docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md
T
smartass cf52ac9420 docs: switch to stable MCP SDK v1
npm has v2 as alpha only. v1 differences: ZodRawShape inputSchema,
subpath imports, zod@^3.25, single sdk package.
2026-06-11 23:00:37 +05:00

185 lines
12 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` v1 (stable) — McpServer + StdioServerTransport;
Client и InMemoryTransport из того же пакета для тестов. (v2 на npm — только
alpha; решение зафиксировано: v1, миграция на v2 после stable-релиза)
- `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 (атомарен на POSIX).
Внутри процесса мутации сериализованы синхронным выполнением (в read-modify-write
нет await-точек, event loop не прерывает его). Между процессами остаётся окно
check-then-write — принято осознанно: последняя запись побеждает, конфликт
означает потерю одной мутации и чинится повторным add/update.
- У каждого подключения в выдаче `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: startup-параметр libpq `options='-c default_transaction_read_only=on'`
сессия рождается read-only, гонки с выдачей коннекта из пула исключены
- mysql: `SET SESSION TRANSACTION READ ONLY` при первом checkout каждого
физического соединения пула (у mysql2 нет init-SQL опции в конфиге пула)
Блокирует DML и DDL на стороне движка. Проверено эмпирически на mysql:8.4:
INSERT/UPDATE/CREATE TABLE под session read-only падают с ERROR 1792
«Cannot execute statement in a READ ONLY transaction», SELECT работает.
Обходится только явным `SET` в SQL — это защита от случайной записи, не от
злого умысла. Жёсткая гарантия — read-only пользователь БД; рекомендация
фиксируется в README.
## Тулзы
Все входы — 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 и инвалидации достаточно)
- Миграции формата стора