@neverinfamous/mysql-mcp 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/.github/workflows/codeql.yml +0 -8
  2. package/.github/workflows/docker-publish.yml +11 -10
  3. package/CHANGELOG.md +96 -0
  4. package/CODE_MODE.md +245 -0
  5. package/DOCKER_README.md +71 -254
  6. package/Dockerfile +5 -0
  7. package/README.md +102 -55
  8. package/VERSION +1 -1
  9. package/dist/adapters/mysql/MySQLAdapter.d.ts +4 -0
  10. package/dist/adapters/mysql/MySQLAdapter.d.ts.map +1 -1
  11. package/dist/adapters/mysql/MySQLAdapter.js +9 -0
  12. package/dist/adapters/mysql/MySQLAdapter.js.map +1 -1
  13. package/dist/adapters/mysql/prompts/index.d.ts +8 -1
  14. package/dist/adapters/mysql/prompts/index.d.ts.map +1 -1
  15. package/dist/adapters/mysql/prompts/index.js +8 -1
  16. package/dist/adapters/mysql/prompts/index.js.map +1 -1
  17. package/dist/adapters/mysql/prompts/routerSetup.d.ts.map +1 -1
  18. package/dist/adapters/mysql/prompts/routerSetup.js +5 -0
  19. package/dist/adapters/mysql/prompts/routerSetup.js.map +1 -1
  20. package/dist/adapters/mysql/resources/capabilities.d.ts.map +1 -1
  21. package/dist/adapters/mysql/resources/capabilities.js +6 -5
  22. package/dist/adapters/mysql/resources/capabilities.js.map +1 -1
  23. package/dist/adapters/mysql/resources/index.d.ts +9 -1
  24. package/dist/adapters/mysql/resources/index.d.ts.map +1 -1
  25. package/dist/adapters/mysql/resources/index.js +9 -1
  26. package/dist/adapters/mysql/resources/index.js.map +1 -1
  27. package/dist/adapters/mysql/tools/admin/backup.d.ts.map +1 -1
  28. package/dist/adapters/mysql/tools/admin/backup.js +3 -3
  29. package/dist/adapters/mysql/tools/admin/backup.js.map +1 -1
  30. package/dist/adapters/mysql/tools/admin/maintenance.d.ts.map +1 -1
  31. package/dist/adapters/mysql/tools/admin/maintenance.js +5 -5
  32. package/dist/adapters/mysql/tools/admin/maintenance.js.map +1 -1
  33. package/dist/adapters/mysql/tools/cluster/innodb-cluster.d.ts.map +1 -1
  34. package/dist/adapters/mysql/tools/cluster/innodb-cluster.js +26 -5
  35. package/dist/adapters/mysql/tools/cluster/innodb-cluster.js.map +1 -1
  36. package/dist/adapters/mysql/tools/codemode/index.d.ts +38 -0
  37. package/dist/adapters/mysql/tools/codemode/index.d.ts.map +1 -0
  38. package/dist/adapters/mysql/tools/codemode/index.js +203 -0
  39. package/dist/adapters/mysql/tools/codemode/index.js.map +1 -0
  40. package/dist/adapters/mysql/tools/core.d.ts.map +1 -1
  41. package/dist/adapters/mysql/tools/core.js +32 -20
  42. package/dist/adapters/mysql/tools/core.js.map +1 -1
  43. package/dist/adapters/mysql/tools/events.js +18 -6
  44. package/dist/adapters/mysql/tools/events.js.map +1 -1
  45. package/dist/adapters/mysql/tools/json/core.d.ts.map +1 -1
  46. package/dist/adapters/mysql/tools/json/core.js +5 -5
  47. package/dist/adapters/mysql/tools/json/core.js.map +1 -1
  48. package/dist/adapters/mysql/tools/json/helpers.d.ts.map +1 -1
  49. package/dist/adapters/mysql/tools/json/helpers.js +9 -3
  50. package/dist/adapters/mysql/tools/json/helpers.js.map +1 -1
  51. package/dist/adapters/mysql/tools/partitioning.d.ts.map +1 -1
  52. package/dist/adapters/mysql/tools/partitioning.js +38 -6
  53. package/dist/adapters/mysql/tools/partitioning.js.map +1 -1
  54. package/dist/adapters/mysql/tools/performance/analysis.d.ts.map +1 -1
  55. package/dist/adapters/mysql/tools/performance/analysis.js +67 -20
  56. package/dist/adapters/mysql/tools/performance/analysis.js.map +1 -1
  57. package/dist/adapters/mysql/tools/performance/optimization.d.ts.map +1 -1
  58. package/dist/adapters/mysql/tools/performance/optimization.js +36 -6
  59. package/dist/adapters/mysql/tools/performance/optimization.js.map +1 -1
  60. package/dist/adapters/mysql/tools/security/data-protection.d.ts.map +1 -1
  61. package/dist/adapters/mysql/tools/security/data-protection.js +9 -4
  62. package/dist/adapters/mysql/tools/security/data-protection.js.map +1 -1
  63. package/dist/adapters/mysql/tools/shell/common.d.ts.map +1 -1
  64. package/dist/adapters/mysql/tools/shell/common.js +28 -2
  65. package/dist/adapters/mysql/tools/shell/common.js.map +1 -1
  66. package/dist/adapters/mysql/tools/shell/restore.d.ts.map +1 -1
  67. package/dist/adapters/mysql/tools/shell/restore.js +54 -4
  68. package/dist/adapters/mysql/tools/shell/restore.js.map +1 -1
  69. package/dist/adapters/mysql/tools/spatial/operations.d.ts.map +1 -1
  70. package/dist/adapters/mysql/tools/spatial/operations.js +10 -2
  71. package/dist/adapters/mysql/tools/spatial/operations.js.map +1 -1
  72. package/dist/adapters/mysql/tools/spatial/setup.d.ts.map +1 -1
  73. package/dist/adapters/mysql/tools/spatial/setup.js +18 -0
  74. package/dist/adapters/mysql/tools/spatial/setup.js.map +1 -1
  75. package/dist/adapters/mysql/tools/sysschema/resources.d.ts.map +1 -1
  76. package/dist/adapters/mysql/tools/sysschema/resources.js +5 -0
  77. package/dist/adapters/mysql/tools/sysschema/resources.js.map +1 -1
  78. package/dist/adapters/mysql/tools/text/fulltext.d.ts.map +1 -1
  79. package/dist/adapters/mysql/tools/text/fulltext.js +6 -4
  80. package/dist/adapters/mysql/tools/text/fulltext.js.map +1 -1
  81. package/dist/adapters/mysql/tools/text/processing.d.ts.map +1 -1
  82. package/dist/adapters/mysql/tools/text/processing.js +10 -45
  83. package/dist/adapters/mysql/tools/text/processing.js.map +1 -1
  84. package/dist/adapters/mysql/tools/transactions.d.ts.map +1 -1
  85. package/dist/adapters/mysql/tools/transactions.js +8 -8
  86. package/dist/adapters/mysql/tools/transactions.js.map +1 -1
  87. package/dist/adapters/mysql/types.d.ts +968 -78
  88. package/dist/adapters/mysql/types.d.ts.map +1 -1
  89. package/dist/adapters/mysql/types.js +1084 -78
  90. package/dist/adapters/mysql/types.js.map +1 -1
  91. package/dist/auth/scopes.d.ts.map +1 -1
  92. package/dist/auth/scopes.js +1 -0
  93. package/dist/auth/scopes.js.map +1 -1
  94. package/dist/cli/args.d.ts.map +1 -1
  95. package/dist/cli/args.js +12 -0
  96. package/dist/cli/args.js.map +1 -1
  97. package/dist/codemode/api.d.ts +69 -0
  98. package/dist/codemode/api.d.ts.map +1 -0
  99. package/dist/codemode/api.js +1035 -0
  100. package/dist/codemode/api.js.map +1 -0
  101. package/dist/codemode/index.d.ts +13 -0
  102. package/dist/codemode/index.d.ts.map +1 -0
  103. package/dist/codemode/index.js +17 -0
  104. package/dist/codemode/index.js.map +1 -0
  105. package/dist/codemode/sandbox-factory.d.ts +72 -0
  106. package/dist/codemode/sandbox-factory.d.ts.map +1 -0
  107. package/dist/codemode/sandbox-factory.js +88 -0
  108. package/dist/codemode/sandbox-factory.js.map +1 -0
  109. package/dist/codemode/sandbox.d.ts +96 -0
  110. package/dist/codemode/sandbox.d.ts.map +1 -0
  111. package/dist/codemode/sandbox.js +345 -0
  112. package/dist/codemode/sandbox.js.map +1 -0
  113. package/dist/codemode/security.d.ts +44 -0
  114. package/dist/codemode/security.d.ts.map +1 -0
  115. package/dist/codemode/security.js +149 -0
  116. package/dist/codemode/security.js.map +1 -0
  117. package/dist/codemode/types.d.ts +137 -0
  118. package/dist/codemode/types.d.ts.map +1 -0
  119. package/dist/codemode/types.js +46 -0
  120. package/dist/codemode/types.js.map +1 -0
  121. package/dist/codemode/worker-sandbox.d.ts +82 -0
  122. package/dist/codemode/worker-sandbox.d.ts.map +1 -0
  123. package/dist/codemode/worker-sandbox.js +244 -0
  124. package/dist/codemode/worker-sandbox.js.map +1 -0
  125. package/dist/codemode/worker-script.d.ts +8 -0
  126. package/dist/codemode/worker-script.d.ts.map +1 -0
  127. package/dist/codemode/worker-script.js +113 -0
  128. package/dist/codemode/worker-script.js.map +1 -0
  129. package/dist/constants/ServerInstructions.d.ts +1 -1
  130. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  131. package/dist/constants/ServerInstructions.js +33 -9
  132. package/dist/constants/ServerInstructions.js.map +1 -1
  133. package/dist/filtering/ToolConstants.d.ts +11 -11
  134. package/dist/filtering/ToolConstants.d.ts.map +1 -1
  135. package/dist/filtering/ToolConstants.js +37 -19
  136. package/dist/filtering/ToolConstants.js.map +1 -1
  137. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  138. package/dist/filtering/ToolFilter.js +12 -0
  139. package/dist/filtering/ToolFilter.js.map +1 -1
  140. package/dist/server/McpServer.js +1 -1
  141. package/dist/server/McpServer.js.map +1 -1
  142. package/dist/types/modules/server.d.ts +2 -0
  143. package/dist/types/modules/server.d.ts.map +1 -1
  144. package/dist/types/modules/tools.d.ts +1 -1
  145. package/dist/types/modules/tools.d.ts.map +1 -1
  146. package/dist/utils/logger.d.ts +1 -1
  147. package/dist/utils/logger.d.ts.map +1 -1
  148. package/dist/utils/logger.js.map +1 -1
  149. package/package.json +12 -7
  150. package/releases/v2.2.0-release-notes.md +18 -18
  151. package/releases/v2.3.0-release-notes.md +191 -0
  152. package/releases/v2.3.1-release-notes.md +34 -0
  153. package/src/__tests__/perf.test.ts +12 -12
  154. package/src/adapters/mysql/MySQLAdapter.ts +10 -0
  155. package/src/adapters/mysql/__tests__/MySQLAdapter.test.ts +1 -1
  156. package/src/adapters/mysql/prompts/index.ts +8 -1
  157. package/src/adapters/mysql/prompts/routerSetup.ts +5 -0
  158. package/src/adapters/mysql/resources/__tests__/capabilities.test.ts +50 -1
  159. package/src/adapters/mysql/resources/capabilities.ts +6 -4
  160. package/src/adapters/mysql/resources/index.ts +9 -1
  161. package/src/adapters/mysql/tools/__tests__/core.test.ts +68 -0
  162. package/src/adapters/mysql/tools/__tests__/events.test.ts +56 -2
  163. package/src/adapters/mysql/tools/__tests__/json_core.test.ts +1 -1
  164. package/src/adapters/mysql/tools/__tests__/json_helpers.test.ts +46 -4
  165. package/src/adapters/mysql/tools/__tests__/replication.test.ts +144 -42
  166. package/src/adapters/mysql/tools/__tests__/security.test.ts +39 -0
  167. package/src/adapters/mysql/tools/__tests__/spatial.test.ts +39 -7
  168. package/src/adapters/mysql/tools/__tests__/spatial_handler.test.ts +35 -3
  169. package/src/adapters/mysql/tools/__tests__/transactions.test.ts +3 -5
  170. package/src/adapters/mysql/tools/admin/backup.ts +8 -3
  171. package/src/adapters/mysql/tools/admin/maintenance.ts +8 -4
  172. package/src/adapters/mysql/tools/cluster/__tests__/innodb-cluster.test.ts +35 -0
  173. package/src/adapters/mysql/tools/cluster/innodb-cluster.ts +26 -5
  174. package/src/adapters/mysql/tools/codemode/index.ts +249 -0
  175. package/src/adapters/mysql/tools/core.ts +44 -27
  176. package/src/adapters/mysql/tools/events.ts +23 -7
  177. package/src/adapters/mysql/tools/json/__tests__/helpers.test.ts +59 -14
  178. package/src/adapters/mysql/tools/json/core.ts +8 -4
  179. package/src/adapters/mysql/tools/json/helpers.ts +13 -3
  180. package/src/adapters/mysql/tools/partitioning.ts +53 -6
  181. package/src/adapters/mysql/tools/performance/__tests__/analysis.test.ts +227 -4
  182. package/src/adapters/mysql/tools/performance/__tests__/optimization.test.ts +35 -0
  183. package/src/adapters/mysql/tools/performance/analysis.ts +75 -21
  184. package/src/adapters/mysql/tools/performance/optimization.ts +44 -6
  185. package/src/adapters/mysql/tools/security/data-protection.ts +10 -4
  186. package/src/adapters/mysql/tools/shell/__tests__/common.test.ts +46 -0
  187. package/src/adapters/mysql/tools/shell/__tests__/restore.test.ts +28 -1
  188. package/src/adapters/mysql/tools/shell/common.ts +34 -2
  189. package/src/adapters/mysql/tools/shell/restore.ts +70 -7
  190. package/src/adapters/mysql/tools/spatial/__tests__/operations.test.ts +29 -0
  191. package/src/adapters/mysql/tools/spatial/operations.ts +13 -2
  192. package/src/adapters/mysql/tools/spatial/setup.ts +23 -0
  193. package/src/adapters/mysql/tools/sysschema/__tests__/resources.test.ts +21 -0
  194. package/src/adapters/mysql/tools/sysschema/resources.ts +5 -0
  195. package/src/adapters/mysql/tools/text/fulltext.ts +13 -5
  196. package/src/adapters/mysql/tools/text/processing.ts +20 -49
  197. package/src/adapters/mysql/tools/transactions.ts +11 -7
  198. package/src/adapters/mysql/types.ts +1241 -87
  199. package/src/auth/scopes.ts +1 -0
  200. package/src/cli/args.ts +14 -0
  201. package/src/codemode/api.ts +1224 -0
  202. package/src/codemode/index.ts +51 -0
  203. package/src/codemode/sandbox-factory.ts +146 -0
  204. package/src/codemode/sandbox.ts +450 -0
  205. package/src/codemode/security.ts +188 -0
  206. package/src/codemode/types.ts +194 -0
  207. package/src/codemode/worker-sandbox.ts +326 -0
  208. package/src/codemode/worker-script.ts +144 -0
  209. package/src/constants/ServerInstructions.ts +33 -9
  210. package/src/filtering/ToolConstants.ts +37 -19
  211. package/src/filtering/ToolFilter.ts +15 -0
  212. package/src/filtering/__tests__/ToolFilter.test.ts +65 -38
  213. package/src/server/McpServer.ts +1 -1
  214. package/src/types/modules/server.ts +3 -0
  215. package/src/types/modules/tools.ts +2 -1
  216. package/src/utils/logger.ts +2 -1
@@ -61,7 +61,7 @@ describe("Performance Analysis Tools", () => {
61
61
  );
62
62
 
63
63
  expect(mockAdapter.executeReadQuery).toHaveBeenCalledWith(
64
- "EXPLAIN SELECT * FROM users",
64
+ "EXPLAIN FORMAT=TRADITIONAL SELECT * FROM users",
65
65
  );
66
66
  expect(result).toHaveProperty("plan");
67
67
  });
@@ -243,6 +243,30 @@ describe("Performance Analysis Tools", () => {
243
243
  expect(result.success).toBe(false);
244
244
  expect(result.error).toContain("SQL syntax");
245
245
  });
246
+
247
+ it("should accept sql alias for query", async () => {
248
+ mockAdapter.executeReadQuery.mockResolvedValue(
249
+ createMockQueryResult([
250
+ {
251
+ EXPLAIN:
252
+ "-> Table scan on users (actual time=0.05..0.10 rows=100 loops=1)",
253
+ },
254
+ ]),
255
+ );
256
+
257
+ const tool = createExplainAnalyzeTool(
258
+ mockAdapter as unknown as MySQLAdapter,
259
+ );
260
+ const result = await tool.handler(
261
+ { sql: "SELECT * FROM users" },
262
+ mockContext,
263
+ );
264
+
265
+ expect(mockAdapter.executeReadQuery).toHaveBeenCalledWith(
266
+ "EXPLAIN ANALYZE FORMAT=TREE SELECT * FROM users",
267
+ );
268
+ expect(result).toHaveProperty("analysis");
269
+ });
246
270
  });
247
271
 
248
272
  describe("createSlowQueriesTool", () => {
@@ -284,6 +308,112 @@ describe("Performance Analysis Tools", () => {
284
308
  // Code: AVG_TIMER_WAIT > ${minTime * 1000000000}
285
309
  expect(call).toContain("AVG_TIMER_WAIT > 500000000");
286
310
  });
311
+
312
+ it("should clamp overflowed timer values to -1 with overflow flag", async () => {
313
+ mockAdapter.executeReadQuery.mockResolvedValue(
314
+ createMockQueryResult([
315
+ {
316
+ query: "DROP TABLE IF EXISTS `t`",
317
+ executions: 3,
318
+ avg_time_ms: 18446743555252.1,
319
+ total_time_ms: 55340230665756.3,
320
+ rows_examined: 0,
321
+ rows_sent: 0,
322
+ },
323
+ ]),
324
+ );
325
+
326
+ const tool = createSlowQueriesTool(
327
+ mockAdapter as unknown as MySQLAdapter,
328
+ );
329
+ const result = (await tool.handler({ limit: 10 }, mockContext)) as {
330
+ slowQueries: Record<string, unknown>[];
331
+ };
332
+
333
+ expect(result.slowQueries[0]["avg_time_ms"]).toBe(-1);
334
+ expect(result.slowQueries[0]["total_time_ms"]).toBe(-1);
335
+ expect(result.slowQueries[0]["overflow"]).toBe(true);
336
+ });
337
+
338
+ it("should clamp string-typed overflowed values (MySQL DECIMAL)", async () => {
339
+ mockAdapter.executeReadQuery.mockResolvedValue(
340
+ createMockQueryResult([
341
+ {
342
+ query: "DROP TABLE IF EXISTS `t`",
343
+ executions: 2,
344
+ avg_time_ms: "18446743555.2521",
345
+ total_time_ms: "18446743036.7947",
346
+ rows_examined: 0,
347
+ rows_sent: 0,
348
+ },
349
+ ]),
350
+ );
351
+
352
+ const tool = createSlowQueriesTool(
353
+ mockAdapter as unknown as MySQLAdapter,
354
+ );
355
+ const result = (await tool.handler({ limit: 10 }, mockContext)) as {
356
+ slowQueries: Record<string, unknown>[];
357
+ };
358
+
359
+ expect(result.slowQueries[0]["avg_time_ms"]).toBe(-1);
360
+ expect(result.slowQueries[0]["total_time_ms"]).toBe(-1);
361
+ expect(result.slowQueries[0]["overflow"]).toBe(true);
362
+ });
363
+
364
+ it("should not add overflow flag for normal timer values", async () => {
365
+ mockAdapter.executeReadQuery.mockResolvedValue(
366
+ createMockQueryResult([
367
+ {
368
+ query: "SELECT 1",
369
+ executions: 10,
370
+ avg_time_ms: 500,
371
+ total_time_ms: 5000,
372
+ rows_examined: 0,
373
+ rows_sent: 10,
374
+ },
375
+ ]),
376
+ );
377
+
378
+ const tool = createSlowQueriesTool(
379
+ mockAdapter as unknown as MySQLAdapter,
380
+ );
381
+ const result = (await tool.handler({ limit: 10 }, mockContext)) as {
382
+ slowQueries: Record<string, unknown>[];
383
+ };
384
+
385
+ expect(result.slowQueries[0]["avg_time_ms"]).toBe(500);
386
+ expect(result.slowQueries[0]["total_time_ms"]).toBe(5000);
387
+ expect(result.slowQueries[0]["overflow"]).toBeUndefined();
388
+ });
389
+
390
+ it("should convert string-typed timer values to numbers", async () => {
391
+ mockAdapter.executeReadQuery.mockResolvedValue(
392
+ createMockQueryResult([
393
+ {
394
+ query: "SELECT * FROM users",
395
+ executions: 5,
396
+ avg_time_ms: "209241.7573",
397
+ total_time_ms: "1046208.7865",
398
+ rows_examined: 100,
399
+ rows_sent: 10,
400
+ },
401
+ ]),
402
+ );
403
+
404
+ const tool = createSlowQueriesTool(
405
+ mockAdapter as unknown as MySQLAdapter,
406
+ );
407
+ const result = (await tool.handler({ limit: 10 }, mockContext)) as {
408
+ slowQueries: Record<string, unknown>[];
409
+ };
410
+
411
+ expect(result.slowQueries[0]["avg_time_ms"]).toBe(209241.7573);
412
+ expect(typeof result.slowQueries[0]["avg_time_ms"]).toBe("number");
413
+ expect(result.slowQueries[0]["total_time_ms"]).toBe(1046208.7865);
414
+ expect(typeof result.slowQueries[0]["total_time_ms"]).toBe("number");
415
+ expect(result.slowQueries[0]["overflow"]).toBeUndefined();
416
+ });
287
417
  });
288
418
 
289
419
  describe("createQueryStatsTool", () => {
@@ -323,6 +453,96 @@ describe("Performance Analysis Tools", () => {
323
453
  const call = mockAdapter.executeReadQuery.mock.calls[0][0] as string;
324
454
  expect(call).toContain("ORDER BY AVG_TIMER_WAIT DESC");
325
455
  });
456
+
457
+ it("should clamp overflowed timer values to -1 with overflow flag", async () => {
458
+ mockAdapter.executeReadQuery.mockResolvedValue(
459
+ createMockQueryResult([
460
+ {
461
+ database_name: "testdb",
462
+ query_text: "DROP TABLE IF EXISTS `t`",
463
+ execution_count: 3,
464
+ avg_time_ms: 18446743555252.1,
465
+ max_time_ms: 99999999999,
466
+ total_time_ms: 55340230665756.3,
467
+ total_rows_examined: 0,
468
+ total_rows_sent: 0,
469
+ first_seen: "2026-01-01",
470
+ last_seen: "2026-02-16",
471
+ },
472
+ ]),
473
+ );
474
+
475
+ const tool = createQueryStatsTool(mockAdapter as unknown as MySQLAdapter);
476
+ const result = (await tool.handler({}, mockContext)) as {
477
+ queries: Record<string, unknown>[];
478
+ };
479
+
480
+ expect(result.queries[0]["avg_time_ms"]).toBe(-1);
481
+ expect(result.queries[0]["max_time_ms"]).toBe(-1);
482
+ expect(result.queries[0]["total_time_ms"]).toBe(-1);
483
+ expect(result.queries[0]["overflow"]).toBe(true);
484
+ });
485
+
486
+ it("should not add overflow flag for normal timer values", async () => {
487
+ mockAdapter.executeReadQuery.mockResolvedValue(
488
+ createMockQueryResult([
489
+ {
490
+ database_name: "testdb",
491
+ query_text: "SELECT 1",
492
+ execution_count: 10,
493
+ avg_time_ms: 250,
494
+ max_time_ms: 800,
495
+ total_time_ms: 2500,
496
+ total_rows_examined: 0,
497
+ total_rows_sent: 10,
498
+ first_seen: "2026-01-01",
499
+ last_seen: "2026-02-16",
500
+ },
501
+ ]),
502
+ );
503
+
504
+ const tool = createQueryStatsTool(mockAdapter as unknown as MySQLAdapter);
505
+ const result = (await tool.handler({}, mockContext)) as {
506
+ queries: Record<string, unknown>[];
507
+ };
508
+
509
+ expect(result.queries[0]["avg_time_ms"]).toBe(250);
510
+ expect(result.queries[0]["max_time_ms"]).toBe(800);
511
+ expect(result.queries[0]["total_time_ms"]).toBe(2500);
512
+ expect(result.queries[0]["overflow"]).toBeUndefined();
513
+ });
514
+
515
+ it("should convert string-typed timer values to numbers", async () => {
516
+ mockAdapter.executeReadQuery.mockResolvedValue(
517
+ createMockQueryResult([
518
+ {
519
+ database_name: "testdb",
520
+ query_text: "SELECT * FROM users",
521
+ execution_count: 5,
522
+ avg_time_ms: "209241.7573",
523
+ max_time_ms: "412000.5000",
524
+ total_time_ms: "1046208.7865",
525
+ total_rows_examined: 100,
526
+ total_rows_sent: 10,
527
+ first_seen: "2026-01-01",
528
+ last_seen: "2026-02-16",
529
+ },
530
+ ]),
531
+ );
532
+
533
+ const tool = createQueryStatsTool(mockAdapter as unknown as MySQLAdapter);
534
+ const result = (await tool.handler({}, mockContext)) as {
535
+ queries: Record<string, unknown>[];
536
+ };
537
+
538
+ expect(result.queries[0]["avg_time_ms"]).toBe(209241.7573);
539
+ expect(typeof result.queries[0]["avg_time_ms"]).toBe("number");
540
+ expect(result.queries[0]["max_time_ms"]).toBe(412000.5);
541
+ expect(typeof result.queries[0]["max_time_ms"]).toBe("number");
542
+ expect(result.queries[0]["total_time_ms"]).toBe(1046208.7865);
543
+ expect(typeof result.queries[0]["total_time_ms"]).toBe("number");
544
+ expect(result.queries[0]["overflow"]).toBeUndefined();
545
+ });
326
546
  });
327
547
 
328
548
  describe("createIndexUsageTool", () => {
@@ -455,9 +675,12 @@ describe("Performance Analysis Tools", () => {
455
675
  );
456
676
  const result = await tool.handler({}, mockContext);
457
677
 
458
- expect(mockAdapter.executeReadQuery).toHaveBeenCalledWith(
459
- "SELECT * FROM information_schema.INNODB_BUFFER_POOL_STATS",
460
- );
678
+ expect(mockAdapter.executeReadQuery).toHaveBeenCalled();
679
+ const call = mockAdapter.executeReadQuery.mock.calls[0][0] as string;
680
+ expect(call).toContain("INNODB_BUFFER_POOL_STATS");
681
+ expect(call).toContain("POOL_SIZE");
682
+ expect(call).toContain("HIT_RATE");
683
+ expect(call).not.toContain("SELECT *");
461
684
  expect(result).toHaveProperty("bufferPoolStats");
462
685
  });
463
686
  });
@@ -222,6 +222,21 @@ describe("Performance Optimization Tools", () => {
222
222
  "Table 'testdb.nonexistent' doesn't exist",
223
223
  );
224
224
  });
225
+
226
+ it("should accept sql alias for query parameter", async () => {
227
+ const tool = createQueryRewriteTool(
228
+ mockAdapter as unknown as MySQLAdapter,
229
+ );
230
+ const result = (await tool.handler(
231
+ { sql: "SELECT * FROM users" },
232
+ mockContext,
233
+ )) as { originalQuery: string; suggestions: string[] };
234
+
235
+ expect(result.originalQuery).toBe("SELECT * FROM users");
236
+ expect(result.suggestions).toContain(
237
+ "Consider selecting only needed columns instead of SELECT *",
238
+ );
239
+ });
225
240
  });
226
241
 
227
242
  describe("createForceIndexTool", () => {
@@ -419,5 +434,25 @@ describe("Performance Optimization Tools", () => {
419
434
  'SET optimizer_trace="enabled=off"',
420
435
  );
421
436
  });
437
+
438
+ it("should accept sql alias for query parameter", async () => {
439
+ mockAdapter.executeReadQuery
440
+ .mockResolvedValueOnce(createMockQueryResult([])) // The query
441
+ .mockResolvedValueOnce(createMockQueryResult([{ TRACE: "{}" }])); // The trace
442
+
443
+ const tool = createOptimizerTraceTool(
444
+ mockAdapter as unknown as MySQLAdapter,
445
+ );
446
+ const result = await tool.handler(
447
+ { sql: "SELECT * FROM users" },
448
+ mockContext,
449
+ );
450
+
451
+ expect(mockAdapter.executeReadQuery).toHaveBeenNthCalledWith(
452
+ 1,
453
+ "SELECT * FROM users",
454
+ );
455
+ expect(result).toHaveProperty("trace");
456
+ });
422
457
  });
423
458
  });
@@ -12,19 +12,65 @@ import type {
12
12
  } from "../../../../types/index.js";
13
13
  import {
14
14
  ExplainSchema,
15
+ ExplainSchemaBase,
16
+ ExplainAnalyzeSchema,
17
+ ExplainAnalyzeSchemaBase,
15
18
  SlowQuerySchema,
16
19
  IndexUsageSchema,
20
+ IndexUsageSchemaBase,
17
21
  TableStatsSchema,
22
+ TableStatsSchemaBase,
18
23
  } from "../../types.js";
19
24
  import { z } from "zod";
20
25
 
26
+ /**
27
+ * Maximum reasonable timer value in milliseconds (24 hours).
28
+ * Values exceeding this threshold are timer overflow artifacts from
29
+ * performance_schema's unsigned 64-bit picosecond counters wrapping.
30
+ */
31
+ const MAX_TIMER_MS = 86_400_000;
32
+
33
+ /**
34
+ * Sanitize timer fields in query result rows.
35
+ * Overflowed values (> 24 hours) are clamped to -1 with an `overflow: true` flag.
36
+ */
37
+ function sanitizeTimerRows(
38
+ rows: Record<string, unknown>[] | undefined,
39
+ timerFields: string[],
40
+ ): Record<string, unknown>[] {
41
+ if (!rows) return [];
42
+ return rows.map((row) => {
43
+ let hasOverflow = false;
44
+ const sanitized = { ...row };
45
+ for (const field of timerFields) {
46
+ const value = sanitized[field];
47
+ const numValue =
48
+ typeof value === "number"
49
+ ? value
50
+ : typeof value === "string"
51
+ ? parseFloat(value)
52
+ : NaN;
53
+ if (!isNaN(numValue) && numValue > MAX_TIMER_MS) {
54
+ sanitized[field] = -1;
55
+ hasOverflow = true;
56
+ } else if (!isNaN(numValue)) {
57
+ sanitized[field] = numValue;
58
+ }
59
+ }
60
+ if (hasOverflow) {
61
+ sanitized["overflow"] = true;
62
+ }
63
+ return sanitized;
64
+ });
65
+ }
66
+
21
67
  export function createExplainTool(adapter: MySQLAdapter): ToolDefinition {
22
68
  return {
23
69
  name: "mysql_explain",
24
70
  title: "MySQL EXPLAIN",
25
71
  description: "Get query execution plan using EXPLAIN.",
26
72
  group: "performance",
27
- inputSchema: ExplainSchema,
73
+ inputSchema: ExplainSchemaBase,
28
74
  requiredScopes: ["read"],
29
75
  annotations: {
30
76
  readOnlyHint: true,
@@ -33,12 +79,7 @@ export function createExplainTool(adapter: MySQLAdapter): ToolDefinition {
33
79
  handler: async (params: unknown, _context: RequestContext) => {
34
80
  const { query, format } = ExplainSchema.parse(params);
35
81
 
36
- const sql =
37
- format === "JSON"
38
- ? `EXPLAIN FORMAT=JSON ${query}`
39
- : format === "TREE"
40
- ? `EXPLAIN FORMAT=TREE ${query}`
41
- : `EXPLAIN ${query}`;
82
+ const sql = `EXPLAIN FORMAT=${format} ${query}`;
42
83
 
43
84
  try {
44
85
  const result = await adapter.executeReadQuery(sql);
@@ -66,24 +107,19 @@ export function createExplainTool(adapter: MySQLAdapter): ToolDefinition {
66
107
  export function createExplainAnalyzeTool(
67
108
  adapter: MySQLAdapter,
68
109
  ): ToolDefinition {
69
- const schema = z.object({
70
- query: z.string().describe("SQL query to analyze"),
71
- format: z.enum(["JSON", "TREE"]).optional().default("TREE"),
72
- });
73
-
74
110
  return {
75
111
  name: "mysql_explain_analyze",
76
112
  title: "MySQL EXPLAIN ANALYZE",
77
113
  description:
78
114
  "Get query execution plan with actual timing using EXPLAIN ANALYZE (MySQL 8.0+). Only TREE format is supported.",
79
115
  group: "performance",
80
- inputSchema: schema,
116
+ inputSchema: ExplainAnalyzeSchemaBase,
81
117
  requiredScopes: ["read"],
82
118
  annotations: {
83
119
  readOnlyHint: true,
84
120
  },
85
121
  handler: async (params: unknown, _context: RequestContext) => {
86
- const { query, format } = schema.parse(params);
122
+ const { query, format } = ExplainAnalyzeSchema.parse(params);
87
123
 
88
124
  // MySQL does not support EXPLAIN ANALYZE with FORMAT=JSON
89
125
  // (requires explain_json_format_version=2 which is not widely available).
@@ -145,7 +181,12 @@ export function createSlowQueriesTool(adapter: MySQLAdapter): ToolDefinition {
145
181
  sql += ` ORDER BY AVG_TIMER_WAIT DESC LIMIT ${limit}`;
146
182
 
147
183
  const result = await adapter.executeReadQuery(sql);
148
- return { slowQueries: result.rows };
184
+ return {
185
+ slowQueries: sanitizeTimerRows(result.rows, [
186
+ "avg_time_ms",
187
+ "total_time_ms",
188
+ ]),
189
+ };
149
190
  },
150
191
  };
151
192
  }
@@ -198,7 +239,13 @@ export function createQueryStatsTool(adapter: MySQLAdapter): ToolDefinition {
198
239
  `;
199
240
 
200
241
  const result = await adapter.executeReadQuery(sql);
201
- return { queries: result.rows };
242
+ return {
243
+ queries: sanitizeTimerRows(result.rows, [
244
+ "avg_time_ms",
245
+ "max_time_ms",
246
+ "total_time_ms",
247
+ ]),
248
+ };
202
249
  },
203
250
  };
204
251
  }
@@ -209,7 +256,7 @@ export function createIndexUsageTool(adapter: MySQLAdapter): ToolDefinition {
209
256
  title: "MySQL Index Usage",
210
257
  description: "Get index usage statistics from performance_schema.",
211
258
  group: "performance",
212
- inputSchema: IndexUsageSchema,
259
+ inputSchema: IndexUsageSchemaBase,
213
260
  requiredScopes: ["read"],
214
261
  annotations: {
215
262
  readOnlyHint: true,
@@ -266,7 +313,7 @@ export function createTableStatsTool(adapter: MySQLAdapter): ToolDefinition {
266
313
  description:
267
314
  "Get detailed table statistics including size, rows, and engine info.",
268
315
  group: "performance",
269
- inputSchema: TableStatsSchema,
316
+ inputSchema: TableStatsSchemaBase,
270
317
  requiredScopes: ["read"],
271
318
  annotations: {
272
319
  readOnlyHint: true,
@@ -322,10 +369,17 @@ export function createBufferPoolStatsTool(
322
369
  idempotentHint: true,
323
370
  },
324
371
  handler: async (_params: unknown, _context: RequestContext) => {
325
- // Use SELECT * for compatibility across MySQL versions
326
- // Different MySQL versions have different column sets
327
372
  const result = await adapter.executeReadQuery(
328
- `SELECT * FROM information_schema.INNODB_BUFFER_POOL_STATS`,
373
+ `SELECT POOL_ID, POOL_SIZE, FREE_BUFFERS, DATABASE_PAGES,
374
+ OLD_DATABASE_PAGES, MODIFIED_DATABASE_PAGES, PENDING_DECOMPRESS,
375
+ PENDING_READS, PENDING_FLUSH_LRU, PENDING_FLUSH_LIST,
376
+ PAGES_MADE_YOUNG, PAGES_NOT_MADE_YOUNG,
377
+ PAGES_MADE_YOUNG_RATE, PAGES_MADE_NOT_YOUNG_RATE,
378
+ NUMBER_PAGES_READ, NUMBER_PAGES_CREATED, NUMBER_PAGES_WRITTEN,
379
+ PAGES_READ_RATE, PAGES_CREATE_RATE, PAGES_WRITTEN_RATE,
380
+ HIT_RATE, YOUNG_MAKE_PER_THOUSAND_GETS,
381
+ NOT_YOUNG_MAKE_PER_THOUSAND_GETS
382
+ FROM information_schema.INNODB_BUFFER_POOL_STATS`,
329
383
  );
330
384
 
331
385
  return { bufferPoolStats: result.rows };
@@ -11,6 +11,7 @@ import type {
11
11
  RequestContext,
12
12
  } from "../../../../types/index.js";
13
13
  import { z } from "zod";
14
+ import { preprocessQueryOnlyParams } from "../../types.js";
14
15
 
15
16
  /** Trace summary decision type */
16
17
  interface TraceSummaryDecision {
@@ -230,16 +231,35 @@ export function createIndexRecommendationTool(
230
231
  }
231
232
 
232
233
  export function createQueryRewriteTool(adapter: MySQLAdapter): ToolDefinition {
233
- const schema = z.object({
234
- query: z.string().describe("SQL query to analyze for optimization"),
234
+ const schemaBase = z.object({
235
+ query: z
236
+ .string()
237
+ .optional()
238
+ .describe("SQL query to analyze for optimization"),
239
+ sql: z.string().optional().describe("Alias for query"),
235
240
  });
236
241
 
242
+ const schema = z
243
+ .preprocess(
244
+ preprocessQueryOnlyParams,
245
+ z.object({
246
+ query: z.string().optional(),
247
+ sql: z.string().optional(),
248
+ }),
249
+ )
250
+ .transform((data) => ({
251
+ query: data.query ?? data.sql ?? "",
252
+ }))
253
+ .refine((data) => data.query !== "", {
254
+ message: "query (or sql alias) is required",
255
+ });
256
+
237
257
  return {
238
258
  name: "mysql_query_rewrite",
239
259
  title: "MySQL Query Rewrite",
240
260
  description: "Analyze a query and suggest optimizations.",
241
261
  group: "optimization",
242
- inputSchema: schema,
262
+ inputSchema: schemaBase,
243
263
  requiredScopes: ["read"],
244
264
  annotations: {
245
265
  readOnlyHint: true,
@@ -376,8 +396,9 @@ export function createForceIndexTool(adapter: MySQLAdapter): ToolDefinition {
376
396
  export function createOptimizerTraceTool(
377
397
  adapter: MySQLAdapter,
378
398
  ): ToolDefinition {
379
- const schema = z.object({
380
- query: z.string().describe("Query to trace"),
399
+ const schemaBase = z.object({
400
+ query: z.string().optional().describe("Query to trace"),
401
+ sql: z.string().optional().describe("Alias for query"),
381
402
  summary: z
382
403
  .boolean()
383
404
  .optional()
@@ -386,12 +407,29 @@ export function createOptimizerTraceTool(
386
407
  ),
387
408
  });
388
409
 
410
+ const schema = z
411
+ .preprocess(
412
+ preprocessQueryOnlyParams,
413
+ z.object({
414
+ query: z.string().optional(),
415
+ sql: z.string().optional(),
416
+ summary: z.boolean().optional(),
417
+ }),
418
+ )
419
+ .transform((data) => ({
420
+ query: data.query ?? data.sql ?? "",
421
+ summary: data.summary,
422
+ }))
423
+ .refine((data) => data.query !== "", {
424
+ message: "query (or sql alias) is required",
425
+ });
426
+
389
427
  return {
390
428
  name: "mysql_optimizer_trace",
391
429
  title: "MySQL Optimizer Trace",
392
430
  description: "Get detailed optimizer trace for a query.",
393
431
  group: "optimization",
394
- inputSchema: schema,
432
+ inputSchema: schemaBase,
395
433
  requiredScopes: ["read"],
396
434
  annotations: {
397
435
  readOnlyHint: true,
@@ -124,13 +124,13 @@ export function createSecurityMaskDataTool(
124
124
  case "credit_card": {
125
125
  // Show first 4 and last 4
126
126
  const ccDigits = value.replace(/\D/g, "");
127
- if (ccDigits.length < 8) {
127
+ if (ccDigits.length <= 8) {
128
128
  return Promise.resolve({
129
129
  original: value,
130
130
  masked: maskChar.repeat(value.length),
131
131
  type,
132
132
  warning:
133
- "Value too short for credit_card format (expected at least 8 digits); fully masked instead",
133
+ "Value too short for credit_card format (expected more than 8 digits); fully masked instead",
134
134
  });
135
135
  }
136
136
  maskedValue =
@@ -140,9 +140,15 @@ export function createSecurityMaskDataTool(
140
140
  break;
141
141
  }
142
142
  case "partial": {
143
- // When keepFirst + keepLast covers the entire value, return unchanged
143
+ // When keepFirst + keepLast covers the entire value, return unchanged with warning
144
144
  if (keepFirst + keepLast >= value.length) {
145
- maskedValue = value;
145
+ return Promise.resolve({
146
+ original: value,
147
+ masked: value,
148
+ type,
149
+ warning:
150
+ "Masking ineffective: keepFirst + keepLast covers entire value length; returned unchanged",
151
+ });
146
152
  } else {
147
153
  const maskLength = value.length - keepFirst - keepLast;
148
154
  maskedValue =
@@ -184,4 +184,50 @@ describe("execShellJS", () => {
184
184
 
185
185
  await expect(promise).rejects.toThrow("Fatal Error");
186
186
  });
187
+
188
+ it("should extract specific ERROR lines from Fatal error during dump stderr", async () => {
189
+ const promise = execShellJS("bad");
190
+
191
+ mockChild.stderr.emit(
192
+ "data",
193
+ Buffer.from(
194
+ "ERROR: Unknown column 'invalid_col' in 'where clause'\nWhile 'Dumping data': Fatal error during dump",
195
+ ),
196
+ );
197
+ mockChild.emit("close", 1);
198
+
199
+ await expect(promise).rejects.toThrow(
200
+ "Unknown column 'invalid_col' in 'where clause'",
201
+ );
202
+ });
203
+
204
+ it("should fall back to generic message when Fatal error during dump has no ERROR lines", async () => {
205
+ const promise = execShellJS("bad");
206
+
207
+ mockChild.stderr.emit("data", Buffer.from("Fatal error during dump"));
208
+ mockChild.emit("close", 1);
209
+
210
+ await expect(promise).rejects.toThrow(
211
+ "MySQL Shell dump failed: Fatal error during dump",
212
+ );
213
+ });
214
+
215
+ it("should extract specific ERROR lines from stderr when JSON reports Fatal error during dump", async () => {
216
+ const promise = execShellJS("bad");
217
+
218
+ const jsonOutput = JSON.stringify({
219
+ success: false,
220
+ error: "While 'Dumping data': Fatal error during dump",
221
+ });
222
+ mockChild.stderr.emit(
223
+ "data",
224
+ Buffer.from("ERROR: Unknown column 'bad_col' in 'where clause'"),
225
+ );
226
+ mockChild.stdout.emit("data", Buffer.from(jsonOutput));
227
+ mockChild.emit("close", 0);
228
+
229
+ await expect(promise).rejects.toThrow(
230
+ "Unknown column 'bad_col' in 'where clause'",
231
+ );
232
+ });
187
233
  });