@neverinfamous/mysql-mcp 2.2.0 → 2.3.0

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 (213) hide show
  1. package/.github/workflows/docker-publish.yml +1 -2
  2. package/CHANGELOG.md +85 -0
  3. package/CODE_MODE.md +245 -0
  4. package/DOCKER_README.md +59 -36
  5. package/README.md +65 -42
  6. package/VERSION +1 -1
  7. package/dist/adapters/mysql/MySQLAdapter.d.ts +4 -0
  8. package/dist/adapters/mysql/MySQLAdapter.d.ts.map +1 -1
  9. package/dist/adapters/mysql/MySQLAdapter.js +9 -0
  10. package/dist/adapters/mysql/MySQLAdapter.js.map +1 -1
  11. package/dist/adapters/mysql/prompts/index.d.ts +8 -1
  12. package/dist/adapters/mysql/prompts/index.d.ts.map +1 -1
  13. package/dist/adapters/mysql/prompts/index.js +8 -1
  14. package/dist/adapters/mysql/prompts/index.js.map +1 -1
  15. package/dist/adapters/mysql/prompts/routerSetup.d.ts.map +1 -1
  16. package/dist/adapters/mysql/prompts/routerSetup.js +5 -0
  17. package/dist/adapters/mysql/prompts/routerSetup.js.map +1 -1
  18. package/dist/adapters/mysql/resources/capabilities.d.ts.map +1 -1
  19. package/dist/adapters/mysql/resources/capabilities.js +6 -5
  20. package/dist/adapters/mysql/resources/capabilities.js.map +1 -1
  21. package/dist/adapters/mysql/resources/index.d.ts +9 -1
  22. package/dist/adapters/mysql/resources/index.d.ts.map +1 -1
  23. package/dist/adapters/mysql/resources/index.js +9 -1
  24. package/dist/adapters/mysql/resources/index.js.map +1 -1
  25. package/dist/adapters/mysql/tools/admin/backup.d.ts.map +1 -1
  26. package/dist/adapters/mysql/tools/admin/backup.js +3 -3
  27. package/dist/adapters/mysql/tools/admin/backup.js.map +1 -1
  28. package/dist/adapters/mysql/tools/admin/maintenance.d.ts.map +1 -1
  29. package/dist/adapters/mysql/tools/admin/maintenance.js +5 -5
  30. package/dist/adapters/mysql/tools/admin/maintenance.js.map +1 -1
  31. package/dist/adapters/mysql/tools/cluster/innodb-cluster.d.ts.map +1 -1
  32. package/dist/adapters/mysql/tools/cluster/innodb-cluster.js +26 -5
  33. package/dist/adapters/mysql/tools/cluster/innodb-cluster.js.map +1 -1
  34. package/dist/adapters/mysql/tools/codemode/index.d.ts +38 -0
  35. package/dist/adapters/mysql/tools/codemode/index.d.ts.map +1 -0
  36. package/dist/adapters/mysql/tools/codemode/index.js +203 -0
  37. package/dist/adapters/mysql/tools/codemode/index.js.map +1 -0
  38. package/dist/adapters/mysql/tools/core.d.ts.map +1 -1
  39. package/dist/adapters/mysql/tools/core.js +32 -20
  40. package/dist/adapters/mysql/tools/core.js.map +1 -1
  41. package/dist/adapters/mysql/tools/events.js +18 -6
  42. package/dist/adapters/mysql/tools/events.js.map +1 -1
  43. package/dist/adapters/mysql/tools/json/core.d.ts.map +1 -1
  44. package/dist/adapters/mysql/tools/json/core.js +5 -5
  45. package/dist/adapters/mysql/tools/json/core.js.map +1 -1
  46. package/dist/adapters/mysql/tools/json/helpers.d.ts.map +1 -1
  47. package/dist/adapters/mysql/tools/json/helpers.js +9 -3
  48. package/dist/adapters/mysql/tools/json/helpers.js.map +1 -1
  49. package/dist/adapters/mysql/tools/partitioning.d.ts.map +1 -1
  50. package/dist/adapters/mysql/tools/partitioning.js +38 -6
  51. package/dist/adapters/mysql/tools/partitioning.js.map +1 -1
  52. package/dist/adapters/mysql/tools/performance/analysis.d.ts.map +1 -1
  53. package/dist/adapters/mysql/tools/performance/analysis.js +67 -20
  54. package/dist/adapters/mysql/tools/performance/analysis.js.map +1 -1
  55. package/dist/adapters/mysql/tools/performance/optimization.d.ts.map +1 -1
  56. package/dist/adapters/mysql/tools/performance/optimization.js +36 -6
  57. package/dist/adapters/mysql/tools/performance/optimization.js.map +1 -1
  58. package/dist/adapters/mysql/tools/security/data-protection.d.ts.map +1 -1
  59. package/dist/adapters/mysql/tools/security/data-protection.js +9 -4
  60. package/dist/adapters/mysql/tools/security/data-protection.js.map +1 -1
  61. package/dist/adapters/mysql/tools/shell/common.d.ts.map +1 -1
  62. package/dist/adapters/mysql/tools/shell/common.js +28 -2
  63. package/dist/adapters/mysql/tools/shell/common.js.map +1 -1
  64. package/dist/adapters/mysql/tools/shell/restore.d.ts.map +1 -1
  65. package/dist/adapters/mysql/tools/shell/restore.js +54 -4
  66. package/dist/adapters/mysql/tools/shell/restore.js.map +1 -1
  67. package/dist/adapters/mysql/tools/spatial/operations.d.ts.map +1 -1
  68. package/dist/adapters/mysql/tools/spatial/operations.js +10 -2
  69. package/dist/adapters/mysql/tools/spatial/operations.js.map +1 -1
  70. package/dist/adapters/mysql/tools/spatial/setup.d.ts.map +1 -1
  71. package/dist/adapters/mysql/tools/spatial/setup.js +18 -0
  72. package/dist/adapters/mysql/tools/spatial/setup.js.map +1 -1
  73. package/dist/adapters/mysql/tools/sysschema/resources.d.ts.map +1 -1
  74. package/dist/adapters/mysql/tools/sysschema/resources.js +5 -0
  75. package/dist/adapters/mysql/tools/sysschema/resources.js.map +1 -1
  76. package/dist/adapters/mysql/tools/text/fulltext.d.ts.map +1 -1
  77. package/dist/adapters/mysql/tools/text/fulltext.js +6 -4
  78. package/dist/adapters/mysql/tools/text/fulltext.js.map +1 -1
  79. package/dist/adapters/mysql/tools/text/processing.d.ts.map +1 -1
  80. package/dist/adapters/mysql/tools/text/processing.js +10 -45
  81. package/dist/adapters/mysql/tools/text/processing.js.map +1 -1
  82. package/dist/adapters/mysql/tools/transactions.d.ts.map +1 -1
  83. package/dist/adapters/mysql/tools/transactions.js +8 -8
  84. package/dist/adapters/mysql/tools/transactions.js.map +1 -1
  85. package/dist/adapters/mysql/types.d.ts +968 -78
  86. package/dist/adapters/mysql/types.d.ts.map +1 -1
  87. package/dist/adapters/mysql/types.js +1084 -78
  88. package/dist/adapters/mysql/types.js.map +1 -1
  89. package/dist/auth/scopes.d.ts.map +1 -1
  90. package/dist/auth/scopes.js +1 -0
  91. package/dist/auth/scopes.js.map +1 -1
  92. package/dist/cli/args.d.ts.map +1 -1
  93. package/dist/cli/args.js +12 -0
  94. package/dist/cli/args.js.map +1 -1
  95. package/dist/codemode/api.d.ts +69 -0
  96. package/dist/codemode/api.d.ts.map +1 -0
  97. package/dist/codemode/api.js +1035 -0
  98. package/dist/codemode/api.js.map +1 -0
  99. package/dist/codemode/index.d.ts +13 -0
  100. package/dist/codemode/index.d.ts.map +1 -0
  101. package/dist/codemode/index.js +17 -0
  102. package/dist/codemode/index.js.map +1 -0
  103. package/dist/codemode/sandbox-factory.d.ts +72 -0
  104. package/dist/codemode/sandbox-factory.d.ts.map +1 -0
  105. package/dist/codemode/sandbox-factory.js +88 -0
  106. package/dist/codemode/sandbox-factory.js.map +1 -0
  107. package/dist/codemode/sandbox.d.ts +96 -0
  108. package/dist/codemode/sandbox.d.ts.map +1 -0
  109. package/dist/codemode/sandbox.js +345 -0
  110. package/dist/codemode/sandbox.js.map +1 -0
  111. package/dist/codemode/security.d.ts +44 -0
  112. package/dist/codemode/security.d.ts.map +1 -0
  113. package/dist/codemode/security.js +149 -0
  114. package/dist/codemode/security.js.map +1 -0
  115. package/dist/codemode/types.d.ts +137 -0
  116. package/dist/codemode/types.d.ts.map +1 -0
  117. package/dist/codemode/types.js +46 -0
  118. package/dist/codemode/types.js.map +1 -0
  119. package/dist/codemode/worker-sandbox.d.ts +82 -0
  120. package/dist/codemode/worker-sandbox.d.ts.map +1 -0
  121. package/dist/codemode/worker-sandbox.js +244 -0
  122. package/dist/codemode/worker-sandbox.js.map +1 -0
  123. package/dist/codemode/worker-script.d.ts +8 -0
  124. package/dist/codemode/worker-script.d.ts.map +1 -0
  125. package/dist/codemode/worker-script.js +113 -0
  126. package/dist/codemode/worker-script.js.map +1 -0
  127. package/dist/constants/ServerInstructions.d.ts +1 -1
  128. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  129. package/dist/constants/ServerInstructions.js +33 -9
  130. package/dist/constants/ServerInstructions.js.map +1 -1
  131. package/dist/filtering/ToolConstants.d.ts +11 -11
  132. package/dist/filtering/ToolConstants.d.ts.map +1 -1
  133. package/dist/filtering/ToolConstants.js +37 -19
  134. package/dist/filtering/ToolConstants.js.map +1 -1
  135. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  136. package/dist/filtering/ToolFilter.js +12 -0
  137. package/dist/filtering/ToolFilter.js.map +1 -1
  138. package/dist/server/McpServer.js +1 -1
  139. package/dist/server/McpServer.js.map +1 -1
  140. package/dist/types/modules/server.d.ts +2 -0
  141. package/dist/types/modules/server.d.ts.map +1 -1
  142. package/dist/types/modules/tools.d.ts +1 -1
  143. package/dist/types/modules/tools.d.ts.map +1 -1
  144. package/dist/utils/logger.d.ts +1 -1
  145. package/dist/utils/logger.d.ts.map +1 -1
  146. package/dist/utils/logger.js.map +1 -1
  147. package/package.json +12 -7
  148. package/releases/v2.2.0-release-notes.md +18 -18
  149. package/releases/v2.3.0-release-notes.md +191 -0
  150. package/src/__tests__/perf.test.ts +12 -12
  151. package/src/adapters/mysql/MySQLAdapter.ts +10 -0
  152. package/src/adapters/mysql/__tests__/MySQLAdapter.test.ts +1 -1
  153. package/src/adapters/mysql/prompts/index.ts +8 -1
  154. package/src/adapters/mysql/prompts/routerSetup.ts +5 -0
  155. package/src/adapters/mysql/resources/__tests__/capabilities.test.ts +50 -1
  156. package/src/adapters/mysql/resources/capabilities.ts +6 -4
  157. package/src/adapters/mysql/resources/index.ts +9 -1
  158. package/src/adapters/mysql/tools/__tests__/core.test.ts +68 -0
  159. package/src/adapters/mysql/tools/__tests__/events.test.ts +56 -2
  160. package/src/adapters/mysql/tools/__tests__/json_core.test.ts +1 -1
  161. package/src/adapters/mysql/tools/__tests__/json_helpers.test.ts +46 -4
  162. package/src/adapters/mysql/tools/__tests__/replication.test.ts +144 -42
  163. package/src/adapters/mysql/tools/__tests__/security.test.ts +39 -0
  164. package/src/adapters/mysql/tools/__tests__/spatial.test.ts +39 -7
  165. package/src/adapters/mysql/tools/__tests__/spatial_handler.test.ts +35 -3
  166. package/src/adapters/mysql/tools/__tests__/transactions.test.ts +3 -5
  167. package/src/adapters/mysql/tools/admin/backup.ts +8 -3
  168. package/src/adapters/mysql/tools/admin/maintenance.ts +8 -4
  169. package/src/adapters/mysql/tools/cluster/__tests__/innodb-cluster.test.ts +35 -0
  170. package/src/adapters/mysql/tools/cluster/innodb-cluster.ts +26 -5
  171. package/src/adapters/mysql/tools/codemode/index.ts +249 -0
  172. package/src/adapters/mysql/tools/core.ts +44 -27
  173. package/src/adapters/mysql/tools/events.ts +23 -7
  174. package/src/adapters/mysql/tools/json/__tests__/helpers.test.ts +59 -14
  175. package/src/adapters/mysql/tools/json/core.ts +8 -4
  176. package/src/adapters/mysql/tools/json/helpers.ts +13 -3
  177. package/src/adapters/mysql/tools/partitioning.ts +53 -6
  178. package/src/adapters/mysql/tools/performance/__tests__/analysis.test.ts +227 -4
  179. package/src/adapters/mysql/tools/performance/__tests__/optimization.test.ts +35 -0
  180. package/src/adapters/mysql/tools/performance/analysis.ts +75 -21
  181. package/src/adapters/mysql/tools/performance/optimization.ts +44 -6
  182. package/src/adapters/mysql/tools/security/data-protection.ts +10 -4
  183. package/src/adapters/mysql/tools/shell/__tests__/common.test.ts +46 -0
  184. package/src/adapters/mysql/tools/shell/__tests__/restore.test.ts +28 -1
  185. package/src/adapters/mysql/tools/shell/common.ts +34 -2
  186. package/src/adapters/mysql/tools/shell/restore.ts +70 -7
  187. package/src/adapters/mysql/tools/spatial/__tests__/operations.test.ts +29 -0
  188. package/src/adapters/mysql/tools/spatial/operations.ts +13 -2
  189. package/src/adapters/mysql/tools/spatial/setup.ts +23 -0
  190. package/src/adapters/mysql/tools/sysschema/__tests__/resources.test.ts +21 -0
  191. package/src/adapters/mysql/tools/sysschema/resources.ts +5 -0
  192. package/src/adapters/mysql/tools/text/fulltext.ts +13 -5
  193. package/src/adapters/mysql/tools/text/processing.ts +20 -49
  194. package/src/adapters/mysql/tools/transactions.ts +11 -7
  195. package/src/adapters/mysql/types.ts +1241 -87
  196. package/src/auth/scopes.ts +1 -0
  197. package/src/cli/args.ts +14 -0
  198. package/src/codemode/api.ts +1224 -0
  199. package/src/codemode/index.ts +51 -0
  200. package/src/codemode/sandbox-factory.ts +146 -0
  201. package/src/codemode/sandbox.ts +450 -0
  202. package/src/codemode/security.ts +188 -0
  203. package/src/codemode/types.ts +194 -0
  204. package/src/codemode/worker-sandbox.ts +326 -0
  205. package/src/codemode/worker-script.ts +144 -0
  206. package/src/constants/ServerInstructions.ts +33 -9
  207. package/src/filtering/ToolConstants.ts +37 -19
  208. package/src/filtering/ToolFilter.ts +15 -0
  209. package/src/filtering/__tests__/ToolFilter.test.ts +65 -38
  210. package/src/server/McpServer.ts +1 -1
  211. package/src/types/modules/server.ts +3 -0
  212. package/src/types/modules/tools.ts +2 -1
  213. package/src/utils/logger.ts +2 -1
@@ -289,6 +289,45 @@ describe("Security Tools", () => {
289
289
  expect(result.type).toBe("credit_card");
290
290
  expect(result.warning).toContain("too short");
291
291
  });
292
+
293
+ it("should fully mask 8-digit credit card values with warning", async () => {
294
+ const tool = tools.find((t) => t.name === "mysql_security_mask_data");
295
+ const result = (await tool?.handler(
296
+ { value: "12345678", type: "credit_card" },
297
+ mockContext,
298
+ )) as any;
299
+
300
+ expect(result.original).toBe("12345678");
301
+ expect(result.masked).toBe("********");
302
+ expect(result.type).toBe("credit_card");
303
+ expect(result.warning).toContain("too short");
304
+ });
305
+
306
+ it("should return warning when partial masking is ineffective", async () => {
307
+ const tool = tools.find((t) => t.name === "mysql_security_mask_data");
308
+ const result = (await tool?.handler(
309
+ { value: "AB", type: "partial", keepFirst: 5, keepLast: 5 },
310
+ mockContext,
311
+ )) as any;
312
+
313
+ expect(result.original).toBe("AB");
314
+ expect(result.masked).toBe("AB");
315
+ expect(result.type).toBe("partial");
316
+ expect(result.warning).toContain("Masking ineffective");
317
+ });
318
+
319
+ it("should return warning for empty string partial masking", async () => {
320
+ const tool = tools.find((t) => t.name === "mysql_security_mask_data");
321
+ const result = (await tool?.handler(
322
+ { value: "", type: "partial", keepFirst: 0, keepLast: 0 },
323
+ mockContext,
324
+ )) as any;
325
+
326
+ expect(result.original).toBe("");
327
+ expect(result.masked).toBe("");
328
+ expect(result.type).toBe("partial");
329
+ expect(result.warning).toContain("Masking ineffective");
330
+ });
292
331
  });
293
332
 
294
333
  describe("mysql_security_password_validate", () => {
@@ -115,11 +115,12 @@ describe("Handler Execution", () => {
115
115
 
116
116
  describe("mysql_spatial_create_index", () => {
117
117
  it("should create a spatial index", async () => {
118
- // First call returns column info (NOT NULL column), second call is the index creation
118
+ // First call: column info (NOT NULL), second: no existing index, third: CREATE
119
119
  mockAdapter.executeQuery
120
120
  .mockResolvedValueOnce(
121
121
  createMockQueryResult([{ IS_NULLABLE: "NO", DATA_TYPE: "point" }]),
122
122
  )
123
+ .mockResolvedValueOnce(createMockQueryResult([]))
123
124
  .mockResolvedValueOnce(createMockQueryResult([]));
124
125
 
125
126
  const tool = tools.find((t) => t.name === "mysql_spatial_create_index")!;
@@ -132,8 +133,8 @@ describe("Handler Execution", () => {
132
133
  mockContext,
133
134
  );
134
135
 
135
- expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(2);
136
- const call = mockAdapter.executeQuery.mock.calls[1][0] as string;
136
+ expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(3);
137
+ const call = mockAdapter.executeQuery.mock.calls[2][0] as string;
137
138
  expect(call).toContain("SPATIAL INDEX");
138
139
  expect(result).toHaveProperty("success", true);
139
140
  });
@@ -162,13 +163,18 @@ describe("Handler Execution", () => {
162
163
  });
163
164
  });
164
165
 
165
- it("should handle index creation errors gracefully", async () => {
166
- // First call returns column info, second call fails
166
+ it("should return structured reason for duplicate index", async () => {
167
+ // First call: column info, second: no existing index, third: fails with duplicate key
167
168
  mockAdapter.executeQuery
168
169
  .mockResolvedValueOnce(
169
170
  createMockQueryResult([{ IS_NULLABLE: "NO", DATA_TYPE: "point" }]),
170
171
  )
171
- .mockRejectedValueOnce(new Error("Index already exists"));
172
+ .mockResolvedValueOnce(createMockQueryResult([]))
173
+ .mockRejectedValueOnce(
174
+ new Error(
175
+ "Query failed: Execute failed: Duplicate key name 'idx_locations_geom'",
176
+ ),
177
+ );
172
178
 
173
179
  const tool = tools.find((t) => t.name === "mysql_spatial_create_index")!;
174
180
  const result = await tool.handler(
@@ -182,7 +188,33 @@ describe("Handler Execution", () => {
182
188
 
183
189
  expect(result).toEqual({
184
190
  success: false,
185
- error: "Index already exists",
191
+ reason:
192
+ "Index 'idx_locations_geom' already exists on table 'locations'",
193
+ });
194
+ });
195
+
196
+ it("should handle other index creation errors gracefully", async () => {
197
+ // First call: column info, second: no existing index, third: fails with generic error
198
+ mockAdapter.executeQuery
199
+ .mockResolvedValueOnce(
200
+ createMockQueryResult([{ IS_NULLABLE: "NO", DATA_TYPE: "point" }]),
201
+ )
202
+ .mockResolvedValueOnce(createMockQueryResult([]))
203
+ .mockRejectedValueOnce(new Error("Some other MySQL error"));
204
+
205
+ const tool = tools.find((t) => t.name === "mysql_spatial_create_index")!;
206
+ const result = await tool.handler(
207
+ {
208
+ table: "locations",
209
+ column: "geom",
210
+ indexName: "idx_locations_geom",
211
+ },
212
+ mockContext,
213
+ );
214
+
215
+ expect(result).toEqual({
216
+ success: false,
217
+ error: "Some other MySQL error",
186
218
  });
187
219
  });
188
220
  });
@@ -87,11 +87,12 @@ describe("Spatial Tools Handlers", () => {
87
87
 
88
88
  it("should generate default index name if not provided", async () => {
89
89
  const tool = findTool("mysql_spatial_create_index")!;
90
- // First call returns column info (NOT NULL), second call is index creation
90
+ // First call: column info (NOT NULL), second: no existing index, third: CREATE
91
91
  mockAdapter.executeQuery
92
92
  .mockResolvedValueOnce(
93
93
  createMockQueryResult([{ IS_NULLABLE: "NO", DATA_TYPE: "point" }]),
94
94
  )
95
+ .mockResolvedValueOnce(createMockQueryResult([]))
95
96
  .mockResolvedValueOnce(createMockQueryResult([]));
96
97
 
97
98
  const result = await tool.handler(
@@ -102,15 +103,44 @@ describe("Spatial Tools Handlers", () => {
102
103
  mockContext,
103
104
  );
104
105
 
105
- expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(2);
106
+ expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(3);
106
107
  expect(mockAdapter.executeQuery).toHaveBeenNthCalledWith(
107
- 2,
108
+ 3,
108
109
  expect.stringContaining(
109
110
  "CREATE SPATIAL INDEX `idx_spatial_users_location`",
110
111
  ),
111
112
  );
112
113
  expect(result).toHaveProperty("indexName", "idx_spatial_users_location");
113
114
  });
115
+
116
+ it("should detect existing spatial index on same column", async () => {
117
+ const tool = findTool("mysql_spatial_create_index")!;
118
+ // First call: column info (NOT NULL)
119
+ // Second call: existing spatial index found
120
+ mockAdapter.executeQuery
121
+ .mockResolvedValueOnce(
122
+ createMockQueryResult([{ IS_NULLABLE: "NO", DATA_TYPE: "point" }]),
123
+ )
124
+ .mockResolvedValueOnce(
125
+ createMockQueryResult([{ INDEX_NAME: "idx_existing_geom" }]),
126
+ );
127
+
128
+ const result = await tool.handler(
129
+ {
130
+ table: "locations",
131
+ column: "geom",
132
+ },
133
+ mockContext,
134
+ );
135
+
136
+ expect(result).toEqual({
137
+ success: false,
138
+ reason:
139
+ "Spatial index 'idx_existing_geom' already exists on column 'geom' of table 'locations'",
140
+ });
141
+ // Should have called: 1) column check, 2) existing index check — NOT the CREATE
142
+ expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(2);
143
+ });
114
144
  });
115
145
 
116
146
  describe("mysql_spatial_distance", () => {
@@ -475,6 +505,7 @@ describe("Spatial Tools Handlers", () => {
475
505
 
476
506
  expect((result as any).segmentsApplied).toBe(false);
477
507
  expect((result as any).segments).toBe(4);
508
+ expect((result as any).precision).toBe(6);
478
509
  });
479
510
 
480
511
  it("should include segmentsApplied: true for Cartesian SRID (buffer)", async () => {
@@ -501,6 +532,7 @@ describe("Spatial Tools Handlers", () => {
501
532
 
502
533
  expect((result as any).segmentsApplied).toBe(true);
503
534
  expect((result as any).segments).toBe(4);
535
+ expect((result as any).precision).toBe(6);
504
536
  });
505
537
  });
506
538
  });
@@ -382,11 +382,9 @@ describe("Handler Execution", () => {
382
382
  it("should reject empty statements array", async () => {
383
383
  const tool = tools.find((t) => t.name === "mysql_transaction_execute")!;
384
384
  const result = await tool.handler({ statements: [] }, mockContext);
385
-
386
- expect(result).toEqual({
387
- success: false,
388
- reason: "No statements provided. Pass at least one SQL statement.",
389
- });
385
+ expect(result).toHaveProperty("success", false);
386
+ expect(result).toHaveProperty("reason");
387
+ expect((result as { reason: string }).reason).toContain("No statements");
390
388
  expect(mockAdapter.beginTransaction).not.toHaveBeenCalled();
391
389
  });
392
390
 
@@ -10,7 +10,12 @@ import type {
10
10
  ToolDefinition,
11
11
  RequestContext,
12
12
  } from "../../../../types/index.js";
13
- import { ExportTableSchema, ImportDataSchema } from "../../types.js";
13
+ import {
14
+ ExportTableSchema,
15
+ ExportTableSchemaBase,
16
+ ImportDataSchema,
17
+ ImportDataSchemaBase,
18
+ } from "../../types.js";
14
19
  import { z } from "zod";
15
20
  import {
16
21
  validateIdentifier,
@@ -94,7 +99,7 @@ export function createExportTableTool(adapter: MySQLAdapter): ToolDefinition {
94
99
  title: "MySQL Export Table",
95
100
  description: "Export table data as SQL INSERT statements or CSV format.",
96
101
  group: "backup",
97
- inputSchema: ExportTableSchema,
102
+ inputSchema: ExportTableSchemaBase,
98
103
  requiredScopes: ["read"],
99
104
  annotations: {
100
105
  readOnlyHint: true,
@@ -175,7 +180,7 @@ export function createImportDataTool(adapter: MySQLAdapter): ToolDefinition {
175
180
  title: "MySQL Import Data",
176
181
  description: "Import data into a table from an array of row objects.",
177
182
  group: "backup",
178
- inputSchema: ImportDataSchema,
183
+ inputSchema: ImportDataSchemaBase,
179
184
  requiredScopes: ["write"],
180
185
  annotations: {
181
186
  readOnlyHint: false,
@@ -12,9 +12,13 @@ import type {
12
12
  } from "../../../../types/index.js";
13
13
  import {
14
14
  OptimizeTableSchema,
15
+ OptimizeTableSchemaBase,
15
16
  AnalyzeTableSchema,
17
+ AnalyzeTableSchemaBase,
16
18
  CheckTableSchema,
19
+ CheckTableSchemaBase,
17
20
  FlushTablesSchema,
21
+ FlushTablesSchemaBase,
18
22
  KillQuerySchema,
19
23
  } from "../../types.js";
20
24
  import { z } from "zod";
@@ -25,7 +29,7 @@ export function createOptimizeTableTool(adapter: MySQLAdapter): ToolDefinition {
25
29
  title: "MySQL Optimize Table",
26
30
  description: "Optimize tables to reclaim unused space and defragment data.",
27
31
  group: "admin",
28
- inputSchema: OptimizeTableSchema,
32
+ inputSchema: OptimizeTableSchemaBase,
29
33
  requiredScopes: ["admin"],
30
34
  annotations: {
31
35
  readOnlyHint: false,
@@ -47,7 +51,7 @@ export function createAnalyzeTableTool(adapter: MySQLAdapter): ToolDefinition {
47
51
  description:
48
52
  "Analyze tables to update index statistics for the query optimizer.",
49
53
  group: "admin",
50
- inputSchema: AnalyzeTableSchema,
54
+ inputSchema: AnalyzeTableSchemaBase,
51
55
  requiredScopes: ["admin"],
52
56
  annotations: {
53
57
  readOnlyHint: false,
@@ -68,7 +72,7 @@ export function createCheckTableTool(adapter: MySQLAdapter): ToolDefinition {
68
72
  title: "MySQL Check Table",
69
73
  description: "Check tables for errors.",
70
74
  group: "admin",
71
- inputSchema: CheckTableSchema,
75
+ inputSchema: CheckTableSchemaBase,
72
76
  requiredScopes: ["read"],
73
77
  annotations: {
74
78
  readOnlyHint: true,
@@ -125,7 +129,7 @@ export function createFlushTablesTool(adapter: MySQLAdapter): ToolDefinition {
125
129
  title: "MySQL Flush Tables",
126
130
  description: "Flush tables to ensure data is written to disk.",
127
131
  group: "admin",
128
- inputSchema: FlushTablesSchema,
132
+ inputSchema: FlushTablesSchemaBase,
129
133
  requiredScopes: ["admin"],
130
134
  annotations: {
131
135
  readOnlyHint: false,
@@ -210,6 +210,41 @@ describe("InnoDB Cluster Tools", () => {
210
210
  expect(result.routers[0].attributes.ROEndpoint).toBe("6447");
211
211
  expect(result.routers[0].attributes.Configuration).toBeUndefined();
212
212
  });
213
+
214
+ it("should flag stale routers when lastCheckIn is null or old", async () => {
215
+ const recentTime = new Date().toISOString();
216
+ mockAdapter.executeQuery.mockResolvedValue(
217
+ createMockQueryResult([
218
+ {
219
+ routerId: 1,
220
+ routerName: "active-router",
221
+ address: "192.168.1.1",
222
+ lastCheckIn: recentTime,
223
+ attributes: JSON.stringify({ ROEndpoint: "6447" }),
224
+ },
225
+ {
226
+ routerId: 2,
227
+ routerName: "stale-router",
228
+ address: "192.168.1.2",
229
+ lastCheckIn: null,
230
+ attributes: JSON.stringify({ ROEndpoint: "6447" }),
231
+ },
232
+ ]),
233
+ );
234
+
235
+ const tool = createClusterRouterStatusTool(
236
+ mockAdapter as unknown as MySQLAdapter,
237
+ );
238
+ const result = (await tool.handler(
239
+ { summary: false },
240
+ mockContext,
241
+ )) as any;
242
+
243
+ expect(result.routers).toHaveLength(2);
244
+ expect(result.routers[0].isStale).toBe(false);
245
+ expect(result.routers[1].isStale).toBe(true);
246
+ expect(result.staleCount).toBe(1);
247
+ });
213
248
  });
214
249
 
215
250
  describe("createClusterStatusTool - payload optimization", () => {
@@ -353,6 +353,14 @@ export function createClusterRouterStatusTool(
353
353
  handler: async (params: unknown, _context: RequestContext) => {
354
354
  const { summary } = SummarySchema.parse(params);
355
355
 
356
+ // Compute staleness: null lastCheckIn or >1 hour old
357
+ const computeStale = (lastCheckIn: unknown): boolean => {
358
+ if (lastCheckIn == null) return true;
359
+ const checkInTime = new Date(lastCheckIn as string).getTime();
360
+ if (isNaN(checkInTime)) return true;
361
+ return Date.now() - checkInTime > 3_600_000; // 1 hour
362
+ };
363
+
356
364
  try {
357
365
  // Summary mode: return only essential router info
358
366
  if (summary) {
@@ -369,9 +377,16 @@ export function createClusterRouterStatusTool(
369
377
  FROM mysql_innodb_cluster_metadata.routers
370
378
  `);
371
379
 
380
+ const routers = (result.rows ?? []).map((r) => ({
381
+ ...r,
382
+ isStale: computeStale(r["lastCheckIn"]),
383
+ }));
384
+ const staleCount = routers.filter((r) => r.isStale).length;
385
+
372
386
  return {
373
- routers: result.rows ?? [],
374
- count: result.rows?.length ?? 0,
387
+ routers,
388
+ count: routers.length,
389
+ staleCount,
375
390
  };
376
391
  }
377
392
 
@@ -388,6 +403,7 @@ export function createClusterRouterStatusTool(
388
403
  `);
389
404
 
390
405
  const routers = (result.rows ?? []).map((r) => {
406
+ let processed = r;
391
407
  if (r["attributes"] != null) {
392
408
  try {
393
409
  const attrs =
@@ -395,17 +411,22 @@ export function createClusterRouterStatusTool(
395
411
  ? (JSON.parse(r["attributes"]) as Record<string, unknown>)
396
412
  : (r["attributes"] as Record<string, unknown>);
397
413
  delete attrs["Configuration"];
398
- return { ...r, attributes: attrs };
414
+ processed = { ...r, attributes: attrs };
399
415
  } catch {
400
- return r;
416
+ // Keep original if parsing fails
401
417
  }
402
418
  }
403
- return r;
419
+ return {
420
+ ...processed,
421
+ isStale: computeStale(processed["lastCheckIn"]),
422
+ };
404
423
  });
424
+ const staleCount = routers.filter((r) => r.isStale).length;
405
425
 
406
426
  return {
407
427
  routers,
408
428
  count: routers.length,
429
+ staleCount,
409
430
  };
410
431
  } catch {
411
432
  return {
@@ -0,0 +1,249 @@
1
+ /**
2
+ * mysql-mcp - Code Mode Tool: mysql_execute_code
3
+ *
4
+ * MCP tool that executes LLM-generated code in a sandboxed environment
5
+ * with access to all MySQL tools via the mysql.* API.
6
+ */
7
+
8
+ import { z } from "zod";
9
+ import type { MySQLAdapter } from "../../MySQLAdapter.js";
10
+ import type {
11
+ ToolDefinition,
12
+ RequestContext,
13
+ } from "../../../../types/index.js";
14
+ import {
15
+ createSandboxPool,
16
+ type ISandboxPool,
17
+ type SandboxMode,
18
+ } from "../../../../codemode/sandbox-factory.js";
19
+ import { CodeModeSecurityManager } from "../../../../codemode/security.js";
20
+ import { createMysqlApi } from "../../../../codemode/api.js";
21
+ import type { ExecuteCodeOptions } from "../../../../codemode/types.js";
22
+
23
+ // Schema for mysql_execute_code input
24
+ export const ExecuteCodeSchema = z.object({
25
+ code: z
26
+ .string()
27
+ .describe(
28
+ "TypeScript/JavaScript code to execute. Use mysql.{group}.{method}() for database operations.",
29
+ ),
30
+ timeout: z
31
+ .number()
32
+ .optional()
33
+ .describe("Execution timeout in milliseconds (max 30000, default 30000)"),
34
+ readonly: z
35
+ .boolean()
36
+ .optional()
37
+ .describe("If true, restricts to read-only operations"),
38
+ });
39
+
40
+ // Schema for mysql_execute_code output
41
+ export const ExecuteCodeOutputSchema = z.object({
42
+ success: z.boolean().describe("Whether the code executed successfully"),
43
+ result: z
44
+ .unknown()
45
+ .optional()
46
+ .describe("Return value from the executed code"),
47
+ error: z.string().optional().describe("Error message if execution failed"),
48
+ metrics: z
49
+ .object({
50
+ wallTimeMs: z
51
+ .number()
52
+ .describe("Wall clock execution time in milliseconds"),
53
+ cpuTimeMs: z.number().describe("CPU time used in milliseconds"),
54
+ memoryUsedMb: z.number().describe("Memory used in megabytes"),
55
+ })
56
+ .optional()
57
+ .describe("Execution performance metrics"),
58
+ hint: z.string().optional().describe("Helpful tip or additional information"),
59
+ });
60
+
61
+ // Singleton instances (initialized on first use)
62
+ let sandboxPool: ISandboxPool | null = null;
63
+ let securityManager: CodeModeSecurityManager | null = null;
64
+
65
+ /**
66
+ * Get isolation mode from environment variable
67
+ */
68
+ function getIsolationMode(): SandboxMode {
69
+ const envMode = process.env["CODEMODE_ISOLATION"];
70
+ if (envMode === "worker") return "worker";
71
+ return "vm"; // Default
72
+ }
73
+
74
+ /**
75
+ * Initialize Code Mode infrastructure
76
+ */
77
+ function ensureInitialized(): {
78
+ pool: ISandboxPool;
79
+ security: CodeModeSecurityManager;
80
+ } {
81
+ sandboxPool ??= createSandboxPool(getIsolationMode());
82
+ sandboxPool.initialize();
83
+ securityManager ??= new CodeModeSecurityManager();
84
+ return { pool: sandboxPool, security: securityManager };
85
+ }
86
+
87
+ /**
88
+ * Create the mysql_execute_code tool
89
+ */
90
+ export function createExecuteCodeTool(adapter: MySQLAdapter): ToolDefinition {
91
+ return {
92
+ name: "mysql_execute_code",
93
+ title: "MySQL Execute Code",
94
+ description: `Execute TypeScript/JavaScript code in a sandboxed environment with access to all MySQL tools via the mysql.* API.
95
+
96
+ Available API groups:
97
+ - mysql.core: readQuery, writeQuery, listTables, describeTable, createTable, createIndex (8 methods)
98
+ - mysql.transactions: begin, commit, rollback, savepoint, execute (7 methods)
99
+ - mysql.json: extract, set, insert, remove, contains, keys, merge, diff, stats (17 methods)
100
+ - mysql.text: regexpMatch, likeSearch, soundex, substring, concat, collationConvert (6 methods)
101
+ - mysql.fulltext: fulltextSearch, fulltextCreate, fulltextBoolean, fulltextExpand (5 methods)
102
+ - mysql.performance: explain, explainAnalyze, slowQueries, bufferPoolStats, tableStats (8 methods)
103
+ - mysql.optimization: indexRecommendation, queryRewrite, forceIndex, optimizerTrace (4 methods)
104
+ - mysql.admin: optimizeTable, analyzeTable, checkTable, repairTable, flushTables, killQuery (6 methods)
105
+ - mysql.monitoring: showProcesslist, showStatus, showVariables, innodbStatus, poolStats (7 methods)
106
+ - mysql.backup: createDump, exportTable, importData, restoreDump (4 methods)
107
+ - mysql.replication: masterStatus, slaveStatus, binlogEvents, gtidStatus, replicationLag (5 methods)
108
+ - mysql.partitioning: partitionInfo, addPartition, dropPartition, reorganizePartition (4 methods)
109
+ - mysql.schema: listSchemas, createView, listFunctions, listTriggers (10 methods)
110
+ - mysql.events: eventCreate, eventAlter, eventDrop, eventList, schedulerStatus (6 methods)
111
+ - mysql.sysschema: sysSchemaStats, sysStatementSummary, sysIoSummary (8 methods)
112
+ - mysql.stats: descriptive, percentiles, correlation, regression, timeSeries, histogram (8 methods)
113
+ - mysql.spatial: distance, distanceSphere, point, polygon, buffer (12 methods)
114
+ - mysql.security: sslStatus, userPrivileges, audit, sensitiveTables (9 methods)
115
+ - mysql.cluster: clusterStatus, grStatus, grMembers, clusterTopology (10 methods)
116
+ - mysql.roles: roleCreate, roleGrant, roleAssign, roleList (8 methods)
117
+ - mysql.docstore: docCreateCollection, docFind, docAdd, docModify (9 methods)
118
+ - mysql.router: routerStatus, routerRoutes, routerRouteHealth (9 methods)
119
+
120
+ Example:
121
+ \`\`\`javascript
122
+ const tables = await mysql.core.listTables();
123
+ const results = [];
124
+ for (const t of tables.tables) {
125
+ const count = await mysql.core.readQuery(\`SELECT COUNT(*) as n FROM \\\`\${t.name}\\\`\`);
126
+ results.push({ table: t.name, rows: count.rows[0].n });
127
+ }
128
+ return results;
129
+ \`\`\``,
130
+ group: "codemode",
131
+ inputSchema: ExecuteCodeSchema,
132
+ requiredScopes: ["admin"],
133
+ annotations: {
134
+ readOnlyHint: false,
135
+ destructiveHint: true,
136
+ idempotentHint: false,
137
+ openWorldHint: false,
138
+ },
139
+ handler: async (params: unknown, _context: RequestContext) => {
140
+ const { code, readonly } = params as ExecuteCodeOptions;
141
+
142
+ // Initialize infrastructure
143
+ const { pool, security } = ensureInitialized();
144
+
145
+ // Validate code
146
+ const validation = security.validateCode(code);
147
+ if (!validation.valid) {
148
+ return {
149
+ success: false,
150
+ error: `Code validation failed: ${validation.errors.join("; ")}`,
151
+ metrics: { wallTimeMs: 0, cpuTimeMs: 0, memoryUsedMb: 0 },
152
+ };
153
+ }
154
+
155
+ // Check rate limit
156
+ const clientId = "default";
157
+ if (!security.checkRateLimit(clientId)) {
158
+ return {
159
+ success: false,
160
+ error: "Rate limit exceeded. Please wait before executing more code.",
161
+ metrics: { wallTimeMs: 0, cpuTimeMs: 0, memoryUsedMb: 0 },
162
+ };
163
+ }
164
+
165
+ // Create mysql API bindings
166
+ const mysqlApi = createMysqlApi(adapter);
167
+ const bindings = mysqlApi.createSandboxBindings();
168
+
169
+ // Validate bindings are populated
170
+ const totalMethods = Object.values(bindings).reduce(
171
+ (sum: number, group) => {
172
+ if (typeof group === "object" && group !== null) {
173
+ return sum + Object.keys(group).length;
174
+ }
175
+ return sum;
176
+ },
177
+ 0,
178
+ );
179
+ if (totalMethods === 0) {
180
+ return {
181
+ success: false,
182
+ error:
183
+ "mysql.* API not available: no tool bindings were created. Ensure adapter.getToolDefinitions() returns valid tools.",
184
+ metrics: { wallTimeMs: 0, cpuTimeMs: 0, memoryUsedMb: 0 },
185
+ };
186
+ }
187
+
188
+ // Capture active transactions before execution for cleanup on error
189
+ const transactionsBefore = new Set(adapter.getActiveTransactionIds());
190
+
191
+ // Execute in sandbox
192
+ const result = await pool.execute(code, bindings);
193
+
194
+ // Always cleanup orphaned transactions (uncommitted txns from any execution)
195
+ const transactionsAfter = adapter.getActiveTransactionIds();
196
+ const orphanedTransactions = transactionsAfter.filter(
197
+ (txId: string) => !transactionsBefore.has(txId),
198
+ );
199
+
200
+ for (const txId of orphanedTransactions) {
201
+ try {
202
+ await adapter.rollbackTransaction(txId);
203
+ } catch {
204
+ // Best-effort cleanup
205
+ }
206
+ }
207
+
208
+ // Sanitize result
209
+ if (result.success && result.result !== undefined) {
210
+ result.result = security.sanitizeResult(result.result);
211
+ }
212
+
213
+ // Audit log
214
+ const record = security.createExecutionRecord(
215
+ code,
216
+ result,
217
+ readonly ?? false,
218
+ clientId,
219
+ );
220
+ security.auditLog(record);
221
+
222
+ // Add help hint for discoverability
223
+ const helpHint =
224
+ "Tip: Use mysql.help() to list all groups, or mysql.core.help() for group-specific methods.";
225
+
226
+ return {
227
+ ...result,
228
+ hint: helpHint,
229
+ };
230
+ },
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Get all Code Mode tools
236
+ */
237
+ export function getCodeModeTools(adapter: MySQLAdapter): ToolDefinition[] {
238
+ return [createExecuteCodeTool(adapter)];
239
+ }
240
+
241
+ /**
242
+ * Cleanup Code Mode resources (call on server shutdown)
243
+ */
244
+ export function cleanupCodeMode(): void {
245
+ if (sandboxPool) {
246
+ sandboxPool.dispose();
247
+ sandboxPool = null;
248
+ }
249
+ }