docs: add dbmole-mcp design spec
This commit is contained in:
commit
47218d09b8
1 file changed
+173
@@ -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 <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 и инвалидации достаточно)
|
||||||
|
- Миграции формата стора
|
||||||
Reference in new issue
Block a user