Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_)
Expand Down Expand Up @@ -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)
- Debug logs (with sensitive information removed)
11 changes: 9 additions & 2 deletions src/db/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
114 changes: 62 additions & 52 deletions src/db/pools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,68 +6,78 @@ 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<string, Pool>();
export const pools = new Map<Environment, Pool>();
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',
staging: 'STAGING',
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));
}
3 changes: 2 additions & 1 deletion src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,4 +63,4 @@ export function processCommandLineArgs(): void {
if (process.argv.includes('--version') || process.argv.includes('-v')) {
showVersion();
}
}
}
84 changes: 29 additions & 55 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions src/tools/info.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -70,4 +66,4 @@ export async function runInfoTool(params: z.infer<typeof InfoToolSchema>): Promi
const message = error instanceof Error ? error.message : "Unknown error occurred";
throw new Error(`Failed to get database info: ${message}`);
}
}
}
37 changes: 37 additions & 0 deletions tests/db/pools.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});

Expand Down Expand Up @@ -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);
});
});
Loading