diff --git a/docs/superpowers/plans/2026-06-11-dbmole-mcp.md b/docs/superpowers/plans/2026-06-11-dbmole-mcp.md index 35a1df6..416a1a7 100644 --- a/docs/superpowers/plans/2026-06-11-dbmole-mcp.md +++ b/docs/superpowers/plans/2026-06-11-dbmole-mcp.md @@ -6,7 +6,7 @@ **Architecture:** A `registry` merges three connection sources (env > config file > persistent store, re-read on every call). A `manager` lazily builds per-connection resources (optional ssh2 tunnel + driver with per-database pools) and rebuilds them when the config hash changes. Nine MCP tools sit on top. Spec: `docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md`. -**Tech Stack:** `@modelcontextprotocol/server` (SDK v2), zod v4 (`zod/v4` import), `pg`, `mysql2`, `ssh2`, tsup, Biome, vitest + coverage-v8, testcontainers. +**Tech Stack:** `@modelcontextprotocol/sdk` v1 (stable; v2 is npm-alpha only — decided with the user), zod 3, `pg`, `mysql2`, `ssh2`, tsup, Biome, vitest + coverage-v8, testcontainers. **Conventions for every task:** - Code style: 4 spaces, no semicolons, single quotes, `const fn = () => {}`, no trailing commas before a closing `}`/`]`/`)`. Biome enforces all of this (`semicolons: asNeeded`, `trailingCommas: none`) — run `npx biome check --write src test` before each commit. @@ -58,16 +58,17 @@ - [ ] **Step 2: Install dependencies (pinned major ranges)** ```bash -npm install @modelcontextprotocol/server@^2 pg@^8 mysql2@^3 ssh2@^1 zod@^4 -npm install -D @modelcontextprotocol/client@^2 @biomejs/biome@^2 @types/node@^22 @types/pg@^8 @types/ssh2@^1 @vitest/coverage-v8@^3 testcontainers@^11 @testcontainers/postgresql@^11 @testcontainers/mysql@^11 tsup@^8 tsx@^4 typescript@^5 vitest@^3 +npm install @modelcontextprotocol/sdk@^1 pg@^8 mysql2@^3 ssh2@^1 zod@^3.25 +npm install -D @biomejs/biome@^2 @types/node@^22 @types/pg@^8 @types/ssh2@^1 @vitest/coverage-v8@^3 testcontainers@^11 @testcontainers/postgresql@^11 @testcontainers/mysql@^11 tsup@^8 tsx@^4 typescript@^5 vitest@^3 ``` Expected: both commands exit 0, `package.json` gains dependencies/devDependencies. The committed `package-lock.json` is the exact pin. The majors carry API shape: -MCP SDK v2 split packages (`@modelcontextprotocol/server`/`client` per the official -migration guide), zod 4 (`zod/v4` import), testcontainers 11, vitest 3 -(`test.projects`). If a range fails to resolve, run `npm view versions`, -then stop and re-verify the affected APIs against that package's docs before adapting. +MCP SDK v1 (single package, subpath imports like `@modelcontextprotocol/sdk/server/mcp.js`; +`registerTool` takes a ZodRawShape — a plain object of zod fields, NOT `z.object()`), +zod 3 (peer of SDK v1), testcontainers 11, vitest 3 (`test.projects`). If a range +fails to resolve, run `npm view versions`, then stop and re-verify the affected +APIs against that package's docs before adapting. - [ ] **Step 3: Create tsconfig.json** @@ -290,7 +291,7 @@ Expected: FAIL — cannot find module `src/config/types.js`. `src/config/types.ts`: ```ts -import * as z from 'zod/v4' +import * as z from 'zod' export const sshConfigSchema = z.object({ host: z.string().min(1), @@ -2832,7 +2833,7 @@ git commit -m "feat: connection manager with hash-based cache and tunnel wiring" - [ ] **Step 1: Create src/tools/respond.ts** ```ts -import type { CallToolResult } from '@modelcontextprotocol/server' +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' export const ok = (payload: unknown): CallToolResult => ({ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] @@ -2850,8 +2851,9 @@ export const errorMessage = (error: unknown): string => - [ ] **Step 2: Create test/unit/helpers.ts** ```ts -import { Client } from '@modelcontextprotocol/client' -import { InMemoryTransport, type McpServer } from '@modelcontextprotocol/server' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { type Mock, vi } from 'vitest' import type { Manager } from '../../src/db/manager.js' @@ -2898,7 +2900,7 @@ export const callTool = async ( } ``` -If `InMemoryTransport` fails to import from `@modelcontextprotocol/server`, import it from `@modelcontextprotocol/client` instead (both re-export it in SDK v2). +SDK v1 ships server, client and `InMemoryTransport` in the single `@modelcontextprotocol/sdk` package — the subpath imports above are the documented v1 paths. - [ ] **Step 3: Typecheck** @@ -2928,7 +2930,7 @@ git commit -m "feat: tool response helpers and in-memory test harness" import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { McpServer } from '@modelcontextprotocol/server' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createRegistry } from '../../../src/config/registry.js' import { writeStore } from '../../../src/config/store.js' @@ -3094,8 +3096,8 @@ Expected: FAIL — module not found. `src/tools/connections.ts`: ```ts -import type { McpServer } from '@modelcontextprotocol/server' -import * as z from 'zod/v4' +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import * as z from 'zod' import type { Registry } from '../config/registry.js' import { type ResolvedConnection, @@ -3139,8 +3141,7 @@ export const registerConnectionTools = ( 'list_connections', { description: - 'List configured database connections with their source layer (env/config/store). Secrets are omitted.', - inputSchema: z.object({}) + 'List configured database connections with their source layer (env/config/store). Secrets are omitted.' }, async () => ok(registry.list().map(publicView)) ) @@ -3150,7 +3151,7 @@ export const registerConnectionTools = ( { description: 'Add a named postgres/mysql connection to the persistent store. Supports optional ssh tunnel config and a readonly flag.', - inputSchema: connectionConfigSchema + inputSchema: connectionConfigSchema.shape }, async config => { try { @@ -3166,10 +3167,10 @@ export const registerConnectionTools = ( { description: 'Patch a stored connection by name. Set a field to null to remove it (e.g. "ssh": null). Only store-sourced connections can be edited.', - inputSchema: z.object({ + inputSchema: { name: z.string(), patch: patchSchema - }) + } }, async ({ name, patch }) => { try { @@ -3186,7 +3187,7 @@ export const registerConnectionTools = ( 'remove_connection', { description: 'Remove a stored connection by name and drop its cached pools/tunnel.', - inputSchema: z.object({ name: z.string() }) + inputSchema: { name: z.string() } }, async ({ name }) => { try { @@ -3204,7 +3205,7 @@ export const registerConnectionTools = ( { description: 'Verify a connection works end to end (including ssh tunnel if configured). Reports server version and latency.', - inputSchema: z.object({ name: z.string() }) + inputSchema: { name: z.string() } }, async ({ name }) => { let managed: Awaited> @@ -3254,7 +3255,7 @@ git commit -m "feat: connection management tools (list/add/update/remove/test)" `test/unit/tools/query.test.ts`: ```ts -import { McpServer } from '@modelcontextprotocol/server' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { beforeEach, describe, expect, it, vi } from 'vitest' import { registerQueryTools } from '../../../src/tools/query.js' import { type FakeManager, callTool, connectClient, fakeManager } from '../helpers.js' @@ -3346,8 +3347,8 @@ Expected: FAIL — module not found. `src/tools/query.ts`: ```ts -import type { McpServer } from '@modelcontextprotocol/server' -import * as z from 'zod/v4' +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import * as z from 'zod' import type { Manager } from '../db/manager.js' import { MAX_ROW_LIMIT, clampRowLimit, formatDbError } from '../format.js' import { errorMessage, fail, ok } from './respond.js' @@ -3359,7 +3360,7 @@ export const registerQueryTools = (server: McpServer, manager: Manager): void => description: 'Execute a single SQL statement on a named connection. SELECT returns columns/rows; ' + 'DML returns rowCount (and lastInsertId for mysql). Use positional params: $1.. for postgres, ? for mysql.', - inputSchema: z.object({ + inputSchema: { connection: z.string().describe('connection name, see list_connections'), sql: z.string().min(1), params: z @@ -3371,7 +3372,7 @@ export const registerQueryTools = (server: McpServer, manager: Manager): void => .optional() .describe('database to run against; defaults to the connection default'), rowLimit: z.number().int().min(1).max(MAX_ROW_LIMIT).optional() - }) + } }, async ({ connection, sql, params, database, rowLimit }) => { let managed: Awaited> @@ -3421,7 +3422,7 @@ git commit -m "feat: execute_sql tool" `test/unit/tools/schema.test.ts`: ```ts -import { McpServer } from '@modelcontextprotocol/server' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { beforeEach, describe, expect, it, vi } from 'vitest' import { registerSchemaTools } from '../../../src/tools/schema.js' import { type FakeManager, callTool, connectClient, fakeManager } from '../helpers.js' @@ -3523,8 +3524,8 @@ Expected: FAIL — module not found. `src/tools/schema.ts`: ```ts -import type { McpServer } from '@modelcontextprotocol/server' -import * as z from 'zod/v4' +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import * as z from 'zod' import type { ManagedConnection, Manager } from '../db/manager.js' import { formatDbError } from '../format.js' import { errorMessage, fail, ok } from './respond.js' @@ -3553,9 +3554,9 @@ export const registerSchemaTools = (server: McpServer, manager: Manager): void = 'list_databases', { description: 'List databases on a connection with their size in bytes (system databases hidden).', - inputSchema: z.object({ + inputSchema: { connection: z.string().describe('connection name, see list_connections') - }) + } }, async ({ connection }) => withManaged(manager, connection, managed => managed.driver.listDatabases()) @@ -3566,11 +3567,11 @@ export const registerSchemaTools = (server: McpServer, manager: Manager): void = { description: 'List tables with approximate row counts. For postgres, optionally filter by schema; for mysql, schema is the database.', - inputSchema: z.object({ + inputSchema: { connection: z.string(), database: z.string().optional(), schema: z.string().optional() - }) + } }, async ({ connection, database, schema }) => withManaged(manager, connection, managed => @@ -3583,12 +3584,12 @@ export const registerSchemaTools = (server: McpServer, manager: Manager): void = { description: 'Describe a table: columns (type, nullable, default), primary key, indexes and foreign keys.', - inputSchema: z.object({ + inputSchema: { connection: z.string(), table: z.string(), database: z.string().optional(), schema: z.string().optional().describe('postgres schema, defaults to public') - }) + } }, async ({ connection, table, database, schema }) => withManaged(manager, connection, managed => @@ -3672,7 +3673,7 @@ Expected: FAIL — module not found. - [ ] **Step 3: Write src/server.ts** ```ts -import { McpServer } from '@modelcontextprotocol/server' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import type { Registry } from './config/registry.js' import type { Manager } from './db/manager.js' import { registerConnectionTools } from './tools/connections.js' @@ -3737,7 +3738,7 @@ describe('parseArgs', () => { `src/index.ts`: ```ts -import { StdioServerTransport } from '@modelcontextprotocol/server/stdio' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { parseArgs } from './cli.js' import { createRegistry } from './config/registry.js' import { defaultStorePath } from './config/store.js' @@ -4343,4 +4344,8 @@ git commit -m "chore: final cleanup" # only if there are changes Accepted and folded in: dependency major ranges (Task 1), ssh client leak on listen failure (Task 7), `writeSeq` tmp-name hardening + concurrency-model note (Task 4), dispose-ordering comment and invalidate-during-build test (Task 11), `parseArgs` extracted to testable `src/cli.ts` (Task 16), unit-only coverage justification (Task 1), README security/readonly sections (Task 20). Spec updated to name the SDK v2 packages and to state the atomicity and readonly mechanics precisely. +## SDK version decision (post-review) + +npm has no stable `@modelcontextprotocol/server` 2.x (alpha only, found at install time in Task 1). Decided with the user: stable `@modelcontextprotocol/sdk@^1` + `zod@^3.25`. v1 differences applied throughout the plan: single package with subpath imports (`server/mcp.js`, `server/stdio.js`, `client/index.js`, `inMemory.js`, `types.js`), `registerTool` takes a ZodRawShape (plain object of zod fields — `connectionConfigSchema.shape` for add_connection), tools without input omit `inputSchema`. + Rejected with evidence: "MySQL `SET SESSION TRANSACTION READ ONLY` does not block autocommit DML/DDL" — disproven empirically on mysql:8.4 (INSERT/UPDATE/CREATE TABLE all fail with ERROR 1792, SELECT works), so the readonly integration test in Task 18 is valid as written. In-process store-write races are impossible: the read-modify-write path is fully synchronous. Cross-process last-write-wins is an accepted, documented trade-off. diff --git a/docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md b/docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md index 2a8bdb7..1b940f3 100644 --- a/docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md +++ b/docs/superpowers/specs/2026-06-11-dbmole-mcp-design.md @@ -8,8 +8,9 @@ MCP-сервер (stdio) для работы с PostgreSQL и MySQL через ## Стек - TypeScript, ESM, Node >= 20 -- `@modelcontextprotocol/server` (MCP SDK v2) — McpServer + StdioServerTransport; - `@modelcontextprotocol/client` в devDeps для тестов через InMemoryTransport +- `@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)