From c3278670342829112844274962281bb18c0d3ca0 Mon Sep 17 00:00:00 2001 From: Abou Kone Date: Sat, 16 May 2026 14:12:03 -0400 Subject: [PATCH 1/4] fix: return DATETIME columns as raw strings, default connection TZ to UTC --- CHANGELOG.md | 7 +++++ README.md | 15 +++++++-- package-lock.json | 6 ++-- package.json | 2 +- src/db/options.ts | 23 ++++++++++++++ src/db/pools.ts | 16 +++------- tests/db/pools.test.ts | 36 ++++++++++++++++++++++ tests/integration/datetime-strings.test.ts | 34 ++++++++++++++++++++ 8 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 src/db/options.ts create mode 100644 tests/db/pools.test.ts create mode 100644 tests/integration/datetime-strings.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 265b4d3..8be746e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to the MySQL Query MCP Server will be documented in this fil The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0] - 2026-05-16 + +### Changed +- Return MySQL DATETIME, DATE, and TIMESTAMP values as raw strings to preserve the exact stored value and avoid host timezone shifts. +- Default mysql2 connection timezone to UTC (`Z`) with `MYSQL_TIMEZONE` override support for code paths that send JavaScript `Date` values in queries. +- Callers that previously consumed JavaScript `Date` objects from this MCP server should parse the returned string explicitly. + ## [1.2.0](https://github.com/devakone/mysql-query-mcp-server/compare/mysql-query-mcp-server-v1.1.0...mysql-query-mcp-server-v1.2.0) (2025-04-13) diff --git a/README.md b/README.md index 4a8c0b2..fdc9297 100644 --- a/README.md +++ b/README.md @@ -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" } } } @@ -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 @@ -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 @@ -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. \ No newline at end of file +For more information or support, please [open an issue](https://github.com/devakone/mysql-query-mcp-server/issues) on the GitHub repository. diff --git a/package-lock.json b/package-lock.json index 2b177cd..09173e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mysql-query-mcp-server", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mysql-query-mcp-server", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.9.0", @@ -24,7 +24,7 @@ "vitest": "^3.1.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=22.13.0" } }, "node_modules/@cspotcode/source-map-support": { diff --git a/package.json b/package.json index f0d8f4a..ef5766f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mysql-query-mcp-server", - "version": "1.2.0", + "version": "1.3.0", "type": "module", "description": "MySQL Query MCP server for AI assistants - execute read-only MySQL queries from Cursor IDE, Windsurf, or Claude Desktop", "main": "dist/index.js", diff --git a/src/db/options.ts b/src/db/options.ts new file mode 100644 index 0000000..454604a --- /dev/null +++ b/src/db/options.ts @@ -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(), + }; +} diff --git a/src/db/pools.ts b/src/db/pools.ts index 1dd8db1..2d29b7e 100644 --- a/src/db/pools.ts +++ b/src/db/pools.ts @@ -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`); @@ -34,16 +35,7 @@ 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:`, { @@ -51,6 +43,8 @@ Object.values(Environment.enum).forEach((env) => { user: config.user, database: config.database, ssl: config.ssl, + dateStrings: config.dateStrings, + timezone: config.timezone, hasPassword: !!config.password }); diff --git a/tests/db/pools.test.ts b/tests/db/pools.test.ts new file mode 100644 index 0000000..2b671cd --- /dev/null +++ b/tests/db/pools.test.ts @@ -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'); + }); +}); diff --git a/tests/integration/datetime-strings.test.ts b/tests/integration/datetime-strings.test.ts new file mode 100644 index 0000000..18fb357 --- /dev/null +++ b/tests/integration/datetime-strings.test.ts @@ -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('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(); + } + }); +}); From e9f4bd9a6c58e1ea9c3838e2f4b8d8b2c661556c Mon Sep 17 00:00:00 2001 From: Abou Kone Date: Sat, 16 May 2026 14:13:49 -0400 Subject: [PATCH 2/4] chore: sync release manifest for 1.3.0 --- .release-please-manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f16f003..96f1cd9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.0" -} \ No newline at end of file + ".": "1.3.0" +} From fb712eb00976117ed42a4d1313afa1f3beb7e1c9 Mon Sep 17 00:00:00 2001 From: Abou Kone Date: Sat, 16 May 2026 14:23:52 -0400 Subject: [PATCH 3/4] chore: let release-please handle versioning --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 ------- package-lock.json | 6 +++--- package.json | 2 +- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 96f1cd9..c3f1463 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.3.0" + ".": "1.2.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be746e..265b4d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,6 @@ All notable changes to the MySQL Query MCP Server will be documented in this fil The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.3.0] - 2026-05-16 - -### Changed -- Return MySQL DATETIME, DATE, and TIMESTAMP values as raw strings to preserve the exact stored value and avoid host timezone shifts. -- Default mysql2 connection timezone to UTC (`Z`) with `MYSQL_TIMEZONE` override support for code paths that send JavaScript `Date` values in queries. -- Callers that previously consumed JavaScript `Date` objects from this MCP server should parse the returned string explicitly. - ## [1.2.0](https://github.com/devakone/mysql-query-mcp-server/compare/mysql-query-mcp-server-v1.1.0...mysql-query-mcp-server-v1.2.0) (2025-04-13) diff --git a/package-lock.json b/package-lock.json index 09173e2..2b177cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mysql-query-mcp-server", - "version": "1.3.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mysql-query-mcp-server", - "version": "1.3.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.9.0", @@ -24,7 +24,7 @@ "vitest": "^3.1.1" }, "engines": { - "node": ">=22.13.0" + "node": ">=14.0.0" } }, "node_modules/@cspotcode/source-map-support": { diff --git a/package.json b/package.json index ef5766f..f0d8f4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mysql-query-mcp-server", - "version": "1.3.0", + "version": "1.2.0", "type": "module", "description": "MySQL Query MCP server for AI assistants - execute read-only MySQL queries from Cursor IDE, Windsurf, or Claude Desktop", "main": "dist/index.js", From 685838c3adcadc60a504977fd8ae49da9fe187d0 Mon Sep 17 00:00:00 2001 From: Abou Kone Date: Sat, 16 May 2026 14:24:38 -0400 Subject: [PATCH 4/4] chore: leave release manifest unchanged --- .release-please-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c3f1463..f16f003 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { ".": "1.2.0" -} +} \ No newline at end of file