diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..aead93a --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,10 @@ +export const parseArgs = ( + argv: string[], + env: NodeJS.ProcessEnv = process.env +): { configPath?: string } => { + const flagIndex = argv.indexOf('--config') + if (flagIndex !== -1 && argv[flagIndex + 1]) { + return { configPath: argv[flagIndex + 1] } + } + return { configPath: env.DBMOLE_CONFIG } +} diff --git a/src/index.ts b/src/index.ts index 7f814eb..aa2f23c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,35 @@ -export const placeholder = true +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' +import { createManager } from './db/manager.js' +import { createServer } from './server.js' + +const main = async () => { + const { configPath } = parseArgs(process.argv.slice(2)) + const registry = createRegistry({ storePath: defaultStorePath(), configPath }) + const manager = createManager(registry) + const server = createServer(registry, manager) + + let shuttingDown = false + const shutdown = async () => { + if (shuttingDown) { + return + } + shuttingDown = true + await manager.disposeAll().catch(() => {}) + process.exit(0) + } + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.stdin.on('close', shutdown) + + await server.connect(new StdioServerTransport()) + console.error('dbmole: MCP server listening on stdio') +} + +main().catch((error) => { + const message = error instanceof Error ? error.message : String(error) + console.error('dbmole: fatal: ' + message) + process.exit(1) +}) diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..97d4cd8 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,16 @@ +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' +import { registerQueryTools } from './tools/query.js' +import { registerSchemaTools } from './tools/schema.js' + +export const SERVER_VERSION = '0.1.0' + +export const createServer = (registry: Registry, manager: Manager): McpServer => { + const server = new McpServer({ name: 'dbmole-mcp', version: SERVER_VERSION }) + registerConnectionTools(server, registry, manager) + registerQueryTools(server, manager) + registerSchemaTools(server, manager) + return server +} diff --git a/test/unit/cli.test.ts b/test/unit/cli.test.ts new file mode 100644 index 0000000..b39554c --- /dev/null +++ b/test/unit/cli.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { parseArgs } from '../../src/cli.js' + +describe('parseArgs', () => { + it('prefers the --config flag', () => { + expect(parseArgs(['--config', '/tmp/c.json'], { DBMOLE_CONFIG: '/env.json' })).toEqual({ + configPath: '/tmp/c.json' + }) + }) + + it('falls back to DBMOLE_CONFIG env', () => { + expect(parseArgs([], { DBMOLE_CONFIG: '/env.json' })).toEqual({ configPath: '/env.json' }) + }) + + it('returns undefined without flag or env', () => { + expect(parseArgs([], {})).toEqual({ configPath: undefined }) + }) + + it('ignores --config without a value', () => { + expect(parseArgs(['--config'], {})).toEqual({ configPath: undefined }) + }) +}) diff --git a/test/unit/server.test.ts b/test/unit/server.test.ts new file mode 100644 index 0000000..8a719cb --- /dev/null +++ b/test/unit/server.test.ts @@ -0,0 +1,37 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { createRegistry } from '../../src/config/registry.js' +import { createServer } from '../../src/server.js' +import { connectClient, fakeManager } from './helpers.js' + +describe('createServer', () => { + let dir: string + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'dbmole-server-')) + }) + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }) + }) + + it('registers all nine tools', async () => { + const registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} }) + const server = createServer(registry, fakeManager()) + const client = await connectClient(server) + const { tools } = await client.listTools() + expect(tools.map((tool) => tool.name).sort()).toEqual([ + 'add_connection', + 'describe_table', + 'execute_sql', + 'list_connections', + 'list_databases', + 'list_tables', + 'remove_connection', + 'test_connection', + 'update_connection' + ]) + }) +}) diff --git a/test/unit/smoke.test.ts b/test/unit/smoke.test.ts deleted file mode 100644 index 525482f..0000000 --- a/test/unit/smoke.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it } from 'vitest' - -describe('smoke', () => { - it('runs', () => { - expect(1 + 1).toBe(2) - }) -})