diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 0b1bb38..003719e 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -80,13 +80,17 @@ PRODUCTION_DB_HOST=prod.example.com # Correct: "PRODUCTION" is recognized 3. **Network/firewall restrictions** - Check if your database allows remote connections - - Verify firewall settings allow connections on the MySQL port (usually 3306) + - Verify firewall settings allow connections on the configured MySQL port (`[ENV]_DB_PORT`, default `3306`) 4. **Missing environment variables** - Ensure all required variables for your environment are set - Run with `DEBUG=true` to see loaded configuration + +5. **Incorrect custom port** + - If your MySQL server is not on `3306`, set `[ENV]_DB_PORT` explicitly + - Ensure the value is a valid integer such as `3307` -5. **Incorrect environment name** +6. **Incorrect environment name** - Verify you're using one of the supported environment names: local, development, staging, production - Environment variables must be prefixed with LOCAL_, DEVELOPMENT_, STAGING_, or PRODUCTION_ - You cannot use custom environment names with this tool (such as DEV_ or PROD_) @@ -225,4 +229,4 @@ If you can't resolve your issue with this guide: - Error messages - Steps to reproduce the issue - Your environment details (OS, Node.js version) - - Debug logs (with sensitive information removed) \ No newline at end of file + - Debug logs (with sensitive information removed) diff --git a/src/db/options.ts b/src/db/options.ts index 454604a..2963737 100644 --- a/src/db/options.ts +++ b/src/db/options.ts @@ -6,14 +6,21 @@ export function getMysqlTimezone(): string { return process.env.MYSQL_TIMEZONE || DEFAULT_MYSQL_TIMEZONE; } +function getDatabasePort(envPrefix: string): number | undefined { + const port = process.env[`${envPrefix}_DB_PORT`]; + return port ? Number.parseInt(port, 10) : undefined; +} + export function buildPoolOptions(envPrefix: string): PoolOptions { + const sslEnv = process.env[`${envPrefix}_DB_SSL`] ?? process.env.MCP_MYSQL_SSL; + return { host: process.env[`${envPrefix}_DB_HOST`], user: process.env[`${envPrefix}_DB_USER`], password: process.env[`${envPrefix}_DB_PASS`], database: process.env[`${envPrefix}_DB_NAME`], - port: process.env[`${envPrefix}_DB_PORT`] ? Number(process.env[`${envPrefix}_DB_PORT`]) : undefined, - ssl: process.env.MCP_MYSQL_SSL === "true" ? {} : undefined, + port: getDatabasePort(envPrefix), + ssl: sslEnv === "true" ? {} : undefined, connectionLimit: 5, enableKeepAlive: true, keepAliveInitialDelay: 10000, diff --git a/src/db/pools.ts b/src/db/pools.ts index 2d29b7e..0bfc3fa 100644 --- a/src/db/pools.ts +++ b/src/db/pools.ts @@ -6,15 +6,9 @@ function debug(message: string, ...args: any[]) { process.stderr.write(`DEBUG [Pools]: ${message} ${args.map(arg => JSON.stringify(arg)).join(' ')}\n`); } -// Connection pools for each environment -export const pools = new Map(); +export const pools = new Map(); +let poolsInitialized = false; -debug('Initializing database pools...'); -debug('Environment enum options:', Object.values(Environment.enum)); -debug('Environment enum type:', typeof Environment); -debug('Environment enum:', Environment); - -// Map of environment to env var prefix const ENV_PREFIX_MAP = { local: 'LOCAL', development: 'DEVELOPMENT', @@ -22,52 +16,68 @@ const ENV_PREFIX_MAP = { production: 'PRODUCTION' } as const; -// Initialize pools -Object.values(Environment.enum).forEach((env) => { - const envPrefix = ENV_PREFIX_MAP[env]; - - debug(`=== Initializing pool for ${env} environment ===`); - debug(`Using prefix: ${envPrefix}`); - debug('Environment variables:'); - debug(`HOST: ${process.env[`${envPrefix}_DB_HOST`]}`); - debug(`USER: ${process.env[`${envPrefix}_DB_USER`]}`); - debug(`DB: ${process.env[`${envPrefix}_DB_NAME`]}`); - debug(`PASS: ${process.env[`${envPrefix}_DB_PASS`] ? 'set' : 'not set'}`); - debug(`SSL: ${process.env.MCP_MYSQL_SSL}`); - - const config = buildPoolOptions(envPrefix); +export function initializePools() { + if (poolsInitialized) { + debug("Pools already initialized"); + return; + } + + debug("Initializing database pools..."); + debug("Environment enum options:", Object.values(Environment.enum)); + + Object.values(Environment.enum).forEach((env) => { + const envPrefix = ENV_PREFIX_MAP[env]; + const config = buildPoolOptions(envPrefix); + + debug(`=== Initializing pool for ${env} environment ===`); + debug(`Using prefix: ${envPrefix}`); + debug(`HOST: ${process.env[`${envPrefix}_DB_HOST`]}`); + debug(`USER: ${process.env[`${envPrefix}_DB_USER`]}`); + debug(`DB: ${process.env[`${envPrefix}_DB_NAME`]}`); + debug(`PASS: ${process.env[`${envPrefix}_DB_PASS`] ? "set" : "not set"}`); + debug(`PORT: ${config.port ?? "default"}`); + debug(`SSL: ${process.env[`${envPrefix}_DB_SSL`] ?? process.env.MCP_MYSQL_SSL}`); + + if (config.host && config.user && config.password && config.database) { + debug(`Creating pool for ${env} with config:`, { + host: config.host, + user: config.user, + database: config.database, + port: config.port ?? 3306, + ssl: config.ssl, + dateStrings: config.dateStrings, + timezone: config.timezone, + hasPassword: !!config.password, + }); - if (config.host && config.user && config.password && config.database) { - debug(`Creating pool for ${env} with config:`, { - host: config.host, - user: config.user, - database: config.database, - ssl: config.ssl, - dateStrings: config.dateStrings, - timezone: config.timezone, - hasPassword: !!config.password - }); - + try { + pools.set(env, createPool(config)); + debug(`Pool created successfully for ${env}`); + } catch (error) { + debug(`Error creating pool for ${env}:`, error); + } + } else { + debug(`Missing configuration for ${env}:`, { + hasHost: !!config.host, + hasUser: !!config.user, + hasPass: !!config.password, + hasDB: !!config.database, + }); + } + }); + + poolsInitialized = true; + debug("Pools map keys:", Array.from(pools.keys())); +} + +export async function closePools() { + for (const [env, pool] of pools.entries()) { try { - const pool = createPool(config); - pools.set(env, pool); - debug(`Pool created successfully for ${env}`); - debug(`Pool type for ${env}:`, typeof pool); - debug(`Pool methods for ${env}:`, Object.keys(pool)); + debug(`Closing pool for ${env}...`); + await pool.end(); + debug(`Pool for ${env} closed successfully`); } catch (error) { - debug(`Error creating pool for ${env}:`, error); + debug(`Error closing pool for ${env}:`, error); } - } else { - debug(`Missing configuration for ${env}:`, { - hasHost: !!config.host, - hasUser: !!config.user, - hasPass: !!config.password, - hasDB: !!config.database - }); } -}); - -debug('Final pools state:'); -debug('Pools map keys:', Array.from(pools.keys())); -debug('Pools map size:', pools.size); -debug('Pools map entries:', Array.from(pools.entries()).map(([env]) => env)); +} diff --git a/src/help.ts b/src/help.ts index bbe7d80..1645b04 100644 --- a/src/help.ts +++ b/src/help.ts @@ -21,6 +21,7 @@ Environment Variables: LOCAL_DB_USER Local database username LOCAL_DB_PASS Local database password LOCAL_DB_NAME Local database name + LOCAL_DB_PORT Local database port (default: 3306) LOCAL_DB_SSL Set to 'true' to enable SSL for local database DEVELOPMENT_DB_* Development environment database settings @@ -62,4 +63,4 @@ export function processCommandLineArgs(): void { if (process.argv.includes('--version') || process.argv.includes('-v')) { showVersion(); } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 2c95ee7..49105c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,31 @@ processCommandLineArgs(); import { config } from "dotenv"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; -import fs from "fs"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { initializePools, closePools } from "./db/pools.js"; +import { + queryToolName, + queryToolDescription, + QueryToolSchema, + runQueryTool, +} from "./tools/query.js"; +import { + infoToolName, + infoToolDescription, + InfoToolSchema, + runInfoTool, +} from "./tools/info.js"; +import { + environmentsToolName, + environmentsToolDescription, + EnvironmentsToolSchema, + runEnvironmentsTool, +} from "./tools/environments.js"; // Get the directory path of the current module const __filename = fileURLToPath(import.meta.url); @@ -39,48 +63,9 @@ debug('Environment variables loaded:', { LOCAL_DB_HOST: process.env.LOCAL_DB_HOST, }); -// Then import pools and MCP server components -debug('Initializing database pools...'); -import { pools } from "./db/pools.js"; +initializePools(); -debug('Importing MCP SDK components...'); -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -debug('Importing tools...'); - -debug('Importing query tool...'); -import { - queryToolName, - queryToolDescription, - QueryToolSchema, - runQueryTool, -} from "./tools/query.js"; -debug('Query tool imported:', { queryToolName }); - -debug('Importing info tool...'); -import { - infoToolName, - infoToolDescription, - InfoToolSchema, - runInfoTool, -} from "./tools/info.js"; -debug('Info tool imported:', { infoToolName }); - -debug('Importing environments tool...'); -import { - environmentsToolName, - environmentsToolDescription, - EnvironmentsToolSchema, - runEnvironmentsTool, -} from "./tools/environments.js"; -debug('Environments tool imported:', { environmentsToolName }); - -debug('All tools imported successfully'); +debug('Tools imported successfully'); /** * MCP server providing MySQL database tools: @@ -256,19 +241,8 @@ debug('CallTool handler registered'); // Handle process termination async function cleanup() { debug('Starting cleanup...'); - - for (const [env, pool] of pools.entries()) { - try { - debug(`Closing pool for ${env}...`); - await pool.end(); - debug(`Pool for ${env} closed successfully`); - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - debug(`Error closing pool for ${env}:`, { error, message }); - } - } - - debug('Cleanup completed'); + await closePools(); + debug('Server cleanup completed'); } // Clean server startup function matching the PostgreSQL example diff --git a/src/tools/info.ts b/src/tools/info.ts index dc4a08f..4fcd492 100644 --- a/src/tools/info.ts +++ b/src/tools/info.ts @@ -1,11 +1,7 @@ import { z } from "zod"; -import { Pool } from "mysql2/promise"; -import { Environment, InfoParams, DatabaseInfo } from "../types/index.js"; -import { config } from "dotenv"; +import { InfoParams, DatabaseInfo } from "../types/index.js"; import { pools } from "../db/pools.js"; -config(); - export const infoToolName = "info"; export const infoToolDescription = "Get information about MySQL databases"; export const InfoToolSchema = InfoParams; @@ -70,4 +66,4 @@ export async function runInfoTool(params: z.infer): Promi const message = error instanceof Error ? error.message : "Unknown error occurred"; throw new Error(`Failed to get database info: ${message}`); } -} \ No newline at end of file +} diff --git a/tests/db/pools.test.ts b/tests/db/pools.test.ts index 2b671cd..266499a 100644 --- a/tests/db/pools.test.ts +++ b/tests/db/pools.test.ts @@ -1,11 +1,26 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { buildPoolOptions, DEFAULT_MYSQL_TIMEZONE } from '../../src/db/options.js'; +const createPool = vi.fn((config) => ({ config })); + +vi.mock('mysql2/promise', () => ({ + createPool, +})); + describe('database pool options', () => { const originalEnv = process.env; beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); process.env = { ...originalEnv }; + + Object.keys(process.env).forEach((key) => { + if (key.includes('_DB_') || key.startsWith('MCP_MYSQL_') || key === 'MYSQL_TIMEZONE') { + delete process.env[key]; + } + }); + vi.spyOn(process.stderr, 'write').mockImplementation(() => true); }); @@ -33,4 +48,26 @@ describe('database pool options', () => { expect(options.timezone).toBe('+00:00'); }); + + it('passes a configured custom port to mysql2 pool options', () => { + process.env.LOCAL_DB_PORT = '3307'; + + const options = buildPoolOptions('LOCAL'); + + expect(options.port).toBe(3307); + }); + + it('does not initialize pools more than once', async () => { + process.env.LOCAL_DB_HOST = 'localhost'; + process.env.LOCAL_DB_USER = 'root'; + process.env.LOCAL_DB_PASS = 'password'; + process.env.LOCAL_DB_NAME = 'testdb'; + + const { initializePools } = await import('../../src/db/pools.js'); + + initializePools(); + initializePools(); + + expect(createPool).toHaveBeenCalledTimes(1); + }); });