11 KiB
dbmole-mcp — дизайн
MCP-сервер (stdio) для работы с PostgreSQL и MySQL через именованные подключения. Решаемая боль: фиксированные env-конфиги требуют перезапуска харнеса при каждом изменении настроек. dbmole хранит подключения персистентно и позволяет управлять ими тулзами в рантайме, без перезапуска. Переносимость — npm-пакет и Docker-образ.
Стек
- TypeScript, ESM, Node >= 20
@modelcontextprotocol/sdk— McpServer + StdioServerTransportzod— схемы входов тулзов и валидация конфиговpg— драйвер PostgreSQL,mysql2— драйвер MySQLssh2— SSH-туннели (pure JS, не требует ssh-бинаря в Docker)tsup— сборка bin с shebang- Biome — линт и формат: indent 4 пробела,
semicolons: asNeeded,quoteStyle: single; функции —const fn = () => {}где возможно vitest+@vitest/coverage-v8— тесты,testcontainers— интеграционные
Каскад источников подключений
Выше — приоритетнее при совпадении имён:
- env
DBMOLE_CONNECTIONS— JSON-массив конфигов, readonly в рантайме - конфиг-файл:
--config <path>или envDBMOLE_CONFIG(путь), формат{ "connections": [...] }, readonly в рантайме - персистентный стор
~/.config/dbmole/connections.json(права 0600, путь переопределяется envDBMOLE_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с именем, занятым любым слоем, → ошибка (никаких теневых записей).
Схема подключения
{
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(эфемерный порт), каждое входящее соединение форвардится черезssh2forwardOutна целевые 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 и инвалидации достаточно)
- Миграции формата стора