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