cf52ac9420
npm has v2 as alpha only. v1 differences: ZodRawShape inputSchema, subpath imports, zod@^3.25, single sdk package.
185 lines
12 KiB
Markdown
185 lines
12 KiB
Markdown
# 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 и инвалидации достаточно)
|
||
- Миграции формата стора
|