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

11 KiB
Raw Blame History

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 с именем, занятым любым слоем, → ошибка (никаких теневых записей).

Схема подключения

{
    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 и инвалидации достаточно)
  • Миграции формата стора