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
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ Create or edit your MCP configuration file (e.g., `.cursor/mcp.json` for Cursor

"DEBUG": "false",
"MCP_MYSQL_SSL": "true",
"MCP_MYSQL_REJECT_UNAUTHORIZED": "false"
"MCP_MYSQL_REJECT_UNAUTHORIZED": "false",
"MYSQL_TIMEZONE": "Z"
}
}
}
Expand Down Expand Up @@ -129,6 +130,7 @@ Choose the approach that best fits your workflow. Both methods will work correct
- At least one environment (typically "local") must be configured
- You only need to configure the environments you plan to use
- For security reasons, consider using environment variables or secure credential storage for production credentials
- DATETIME, DATE, and TIMESTAMP columns are returned as strings to preserve the exact value stored in MySQL without host timezone shifting

## Configuration Options

Expand All @@ -143,6 +145,15 @@ Choose the approach that best fits your workflow. Both methods will work correct
| [ENV]_DB_SSL | Enable SSL connection | false |
| MCP_MYSQL_SSL | Enable SSL for all connections | false |
| MCP_MYSQL_REJECT_UNAUTHORIZED | Verify SSL certificates | true |
| MYSQL_TIMEZONE | Timezone mysql2 uses when sending JavaScript Date values in queries | Z |

### Date and Time Values

MySQL DATETIME, DATE, and TIMESTAMP columns are returned as raw strings, for example `2026-05-13 16:12:08`. This preserves the exact stored value and prevents the Node host timezone from shifting results during JSON serialization.

`MYSQL_TIMEZONE` defaults to `Z` (UTC). Override it only if your application intentionally sends JavaScript `Date` objects to MySQL using a different connection timezone.

Callers that already handle date/time values as strings do not need to change. Callers that previously expected JavaScript `Date` objects from this MCP server should parse the returned string explicitly.

## Integration with AI Assistants

Expand Down Expand Up @@ -316,4 +327,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file

---

For more information or support, please [open an issue](https://github.com/devakone/mysql-query-mcp-server/issues) on the GitHub repository.
For more information or support, please [open an issue](https://github.com/devakone/mysql-query-mcp-server/issues) on the GitHub repository.
23 changes: 23 additions & 0 deletions src/db/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { PoolOptions } from "mysql2/promise";

export const DEFAULT_MYSQL_TIMEZONE = "Z";

export function getMysqlTimezone(): string {
return process.env.MYSQL_TIMEZONE || DEFAULT_MYSQL_TIMEZONE;
}

export function buildPoolOptions(envPrefix: string): PoolOptions {
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,
connectionLimit: 5,
enableKeepAlive: true,
keepAliveInitialDelay: 10000,
dateStrings: true,
timezone: getMysqlTimezone(),
};
}
16 changes: 5 additions & 11 deletions src/db/pools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createPool, Pool, PoolOptions } from "mysql2/promise";
import { createPool, Pool } from "mysql2/promise";
import { Environment } from "../types/index.js";
import { buildPoolOptions } from "./options.js";

function debug(message: string, ...args: any[]) {
process.stderr.write(`DEBUG [Pools]: ${message} ${args.map(arg => JSON.stringify(arg)).join(' ')}\n`);
Expand Down Expand Up @@ -34,23 +35,16 @@ Object.values(Environment.enum).forEach((env) => {
debug(`PASS: ${process.env[`${envPrefix}_DB_PASS`] ? 'set' : 'not set'}`);
debug(`SSL: ${process.env.MCP_MYSQL_SSL}`);

const config: PoolOptions = {
host: process.env[`${envPrefix}_DB_HOST`],
user: process.env[`${envPrefix}_DB_USER`],
password: process.env[`${envPrefix}_DB_PASS`],
database: process.env[`${envPrefix}_DB_NAME`],
ssl: process.env.MCP_MYSQL_SSL === "true" ? {} : undefined,
connectionLimit: 5,
enableKeepAlive: true,
keepAliveInitialDelay: 10000,
};
const config = buildPoolOptions(envPrefix);

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
});

Expand Down
36 changes: 36 additions & 0 deletions tests/db/pools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { buildPoolOptions, DEFAULT_MYSQL_TIMEZONE } from '../../src/db/options.js';

describe('database pool options', () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
});

afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
});

it('returns date/time values as raw strings and defaults connection timezone to UTC', () => {
process.env.LOCAL_DB_HOST = 'localhost';
process.env.LOCAL_DB_USER = 'root';
process.env.LOCAL_DB_PASS = 'password';
process.env.LOCAL_DB_NAME = 'test';

const options = buildPoolOptions('LOCAL');

expect(options.dateStrings).toBe(true);
expect(options.timezone).toBe(DEFAULT_MYSQL_TIMEZONE);
});

it('allows the mysql timezone to be overridden', () => {
process.env.MYSQL_TIMEZONE = '+00:00';

const options = buildPoolOptions('LOCAL');

expect(options.timezone).toBe('+00:00');
});
});
34 changes: 34 additions & 0 deletions tests/integration/datetime-strings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { createConnection, type ConnectionOptions, type RowDataPacket } from 'mysql2/promise';
import { DEFAULT_MYSQL_TIMEZONE } from '../../src/db/options.js';

const runIntegrationTest = process.env.MYSQL_DATETIME_INTEGRATION === 'true';
const maybeIt = runIntegrationTest ? it : it.skip;

describe('datetime string integration', () => {
maybeIt('round-trips a known UTC DATETIME as the stored wall-clock string', async () => {
const options: ConnectionOptions = {
host: process.env.MYSQL_DATETIME_TEST_HOST || process.env.LOCAL_DB_HOST,
user: process.env.MYSQL_DATETIME_TEST_USER || process.env.LOCAL_DB_USER,
password: process.env.MYSQL_DATETIME_TEST_PASS || process.env.LOCAL_DB_PASS,
database: process.env.MYSQL_DATETIME_TEST_DB || process.env.LOCAL_DB_NAME,
port: Number(process.env.MYSQL_DATETIME_TEST_PORT || process.env.LOCAL_DB_PORT || 3306),
dateStrings: true,
timezone: process.env.MYSQL_TIMEZONE || DEFAULT_MYSQL_TIMEZONE,
};
const insertedDatetime = '2026-05-13 16:12:08';
const connection = await createConnection(options);

try {
await connection.query('CREATE TEMPORARY TABLE datetime_string_test (created_at DATETIME NOT NULL)');
await connection.execute('INSERT INTO datetime_string_test (created_at) VALUES (?)', [insertedDatetime]);

const [rows] = await connection.query<RowDataPacket[]>('SELECT created_at FROM datetime_string_test');

expect(typeof rows[0].created_at).toBe('string');
expect(rows[0].created_at).toBe(insertedDatetime);
} finally {
await connection.end();
}
});
});
Loading