@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
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import * as child_process from "child_process";
3
+ import * as fsModule from "fs";
3
4
  import {
4
5
  createMockMySQLAdapter,
5
6
  createMockRequestContext,
@@ -13,6 +14,19 @@ vi.mock("child_process", () => ({
13
14
  spawn: vi.fn(),
14
15
  }));
15
16
 
17
+ vi.mock("fs", async () => {
18
+ const actual = await vi.importActual<typeof import("fs")>("fs");
19
+ return {
20
+ ...actual,
21
+ promises: {
22
+ ...actual.promises,
23
+ mkdtemp: vi.fn().mockResolvedValue("/tmp/mysqlsh_script_abc123"),
24
+ writeFile: vi.fn().mockResolvedValue(undefined),
25
+ rm: vi.fn().mockResolvedValue(undefined),
26
+ },
27
+ };
28
+ });
29
+
16
30
  describe("Shell Restore and Script Tools", () => {
17
31
  let mockAdapter: ReturnType<typeof createMockMySQLAdapter>;
18
32
  let mockContext: ReturnType<typeof createMockRequestContext>;
@@ -226,7 +240,7 @@ describe("Shell Restore and Script Tools", () => {
226
240
  expect(args).toContain("--py");
227
241
  });
228
242
 
229
- it("should run sql script", async () => {
243
+ it("should run sql script via secure temp file", async () => {
230
244
  setupMockSpawn("SQL output");
231
245
 
232
246
  const tool = createShellRunScriptTool();
@@ -241,6 +255,19 @@ describe("Shell Restore and Script Tools", () => {
241
255
  expect(result.success).toBe(true);
242
256
  const args = mockSpawn.mock.calls[0][1];
243
257
  expect(args).toContain("--sql");
258
+ expect(args).toContain("--file");
259
+
260
+ // Verify secure temp dir was created and cleaned up
261
+ const fsp = fsModule.promises;
262
+ expect(fsp.mkdtemp).toHaveBeenCalled();
263
+ expect(fsp.writeFile).toHaveBeenCalledWith(
264
+ expect.stringContaining("script.sql"),
265
+ "SELECT 1",
266
+ "utf8",
267
+ );
268
+ expect(fsp.rm).toHaveBeenCalledWith("/tmp/mysqlsh_script_abc123", {
269
+ recursive: true,
270
+ });
244
271
  });
245
272
  });
246
273
  });
@@ -196,8 +196,24 @@ export async function execShellJS(
196
196
  }
197
197
  // Fatal dump errors
198
198
  if (stderrClean.includes("Fatal error during dump")) {
199
+ // Extract specific MySQL error lines (e.g., "ERROR: Unknown column 'x' in 'where clause'")
200
+ const errorLines = stderrClean
201
+ .split(/\r?\n/)
202
+ .filter((line) => /^ERROR:/i.test(line.trim()));
203
+ const specificError =
204
+ errorLines.length > 0
205
+ ? errorLines
206
+ .map((line) => line.trim().replace(/^ERROR:\s*/i, ""))
207
+ .join("; ")
208
+ : null;
209
+
210
+ if (specificError) {
211
+ throw new Error(specificError);
212
+ }
213
+
214
+ // Fallback: no specific error extracted, use generic message with privilege hint
199
215
  throw new Error(
200
- `MySQL Shell dump failed: ${stderrClean}. ` +
216
+ `MySQL Shell dump failed: Fatal error during dump. ` +
201
217
  `This may be caused by missing privileges. For dumpSchemas, try excludeEvents: true. ` +
202
218
  `For dumpTables, try all: false.`,
203
219
  );
@@ -223,7 +239,23 @@ export async function execShellJS(
223
239
  }
224
240
 
225
241
  if (!parsed.success) {
226
- throw new Error(parsed.error ?? "Unknown MySQL Shell error");
242
+ const errorMsg = parsed.error ?? "Unknown MySQL Shell error";
243
+
244
+ // For "Fatal error during dump" errors, check stderr for specific MySQL error details
245
+ if (errorMsg.includes("Fatal error during dump") && stderrClean) {
246
+ const errorLines = stderrClean
247
+ .split(/\r?\n/)
248
+ .filter((line) => /^ERROR:/i.test(line.trim()));
249
+
250
+ if (errorLines.length > 0) {
251
+ const specificError = errorLines
252
+ .map((line) => line.trim().replace(/^ERROR:\s*/i, ""))
253
+ .join("; ");
254
+ throw new Error(specificError);
255
+ }
256
+ }
257
+
258
+ throw new Error(errorMsg);
227
259
  }
228
260
  return parsed.result;
229
261
  }
@@ -94,11 +94,74 @@ export function createShellLoadDumpTool(): ToolDefinition {
94
94
  }
95
95
 
96
96
  try {
97
+ if (dryRun) {
98
+ // For dry runs, use execMySQLShell directly to capture stderr
99
+ // where MySQL Shell outputs the summary of what would be loaded
100
+ const config = getShellConfig();
101
+ const dryRunJsCode = `
102
+ var __result__;
103
+ try {
104
+ __result__ = (function() { ${jsCode} })();
105
+ print(JSON.stringify({ success: true, result: __result__ }));
106
+ } catch (e) {
107
+ print(JSON.stringify({ success: false, error: e.message }));
108
+ }
109
+ `;
110
+ const rawResult = await execMySQLShell(
111
+ ["--uri", config.connectionUri, "--js", "-e", dryRunJsCode],
112
+ { timeout: 3600000 },
113
+ );
114
+
115
+ // Parse stderr for dry run summary, filtering out common warnings
116
+ const stderrClean = rawResult.stderr
117
+ .replace(
118
+ /WARNING: Using a password on the command line interface can be insecure\.\s*/gi,
119
+ "",
120
+ )
121
+ .trim();
122
+
123
+ // Check for errors in the JSON output
124
+ const lines = rawResult.stdout.trim().split("\n");
125
+ for (let i = lines.length - 1; i >= 0; i--) {
126
+ const line = lines[i];
127
+ if (!line) continue;
128
+ const trimmedLine = line.trim();
129
+ if (trimmedLine.startsWith("{")) {
130
+ let parsed: {
131
+ success: boolean;
132
+ result?: unknown;
133
+ error?: string;
134
+ };
135
+ try {
136
+ parsed = JSON.parse(trimmedLine) as {
137
+ success: boolean;
138
+ result?: unknown;
139
+ error?: string;
140
+ };
141
+ } catch {
142
+ continue;
143
+ }
144
+ if (!parsed.success) {
145
+ throw new Error(parsed.error ?? "Unknown MySQL Shell error");
146
+ }
147
+ break;
148
+ }
149
+ }
150
+
151
+ return {
152
+ success: true,
153
+ inputDir,
154
+ dryRun: true,
155
+ localInfileEnabled: updateServerSettings,
156
+ dryRunOutput: stderrClean || undefined,
157
+ };
158
+ }
159
+
97
160
  const result = await execShellJS(jsCode, { timeout: 3600000 });
98
161
  return {
99
162
  success: true,
100
163
  inputDir,
101
- dryRun: dryRun ?? false,
164
+ dryRun: false,
102
165
  localInfileEnabled: updateServerSettings,
103
166
  result,
104
167
  };
@@ -169,10 +232,10 @@ export function createShellRunScriptTool(): ToolDefinition {
169
232
  // SQL scripts with comments or multi-line content break when passed via -e
170
233
  // Use --file approach for SQL to properly handle all syntax
171
234
  if (language === "sql") {
172
- const tempFile = join(
173
- tmpdir(),
174
- `mysqlsh_script_${Date.now()}_${Math.random().toString(36).slice(2)}.sql`,
175
- );
235
+ // Create a secure temp directory via mkdtemp (restrictive permissions,
236
+ // unique path) to avoid CodeQL js/insecure-temporary-file alert.
237
+ const tempDir = await fs.mkdtemp(join(tmpdir(), `mysqlsh_script_`));
238
+ const tempFile = join(tempDir, "script.sql");
176
239
  try {
177
240
  await fs.writeFile(tempFile, script, "utf8");
178
241
  const args = [
@@ -184,8 +247,8 @@ export function createShellRunScriptTool(): ToolDefinition {
184
247
  ];
185
248
  result = await execMySQLShell(args, { timeout });
186
249
  } finally {
187
- // Cleanup temp file
188
- await fs.unlink(tempFile).catch(() => void 0);
250
+ // Cleanup temp directory and its contents
251
+ await fs.rm(tempDir, { recursive: true }).catch(() => void 0);
189
252
  }
190
253
  } else {
191
254
  // JS and Python work fine with -e
@@ -123,6 +123,7 @@ describe("Spatial Operations Tools", () => {
123
123
  expect(result).toHaveProperty("bufferWkt");
124
124
  expect(result).toHaveProperty("bufferGeoJson");
125
125
  expect(result).toHaveProperty("segments", 8);
126
+ expect(result).toHaveProperty("precision", 6);
126
127
  });
127
128
 
128
129
  it("should use ST_Buffer_Strategy with Cartesian SRID", async () => {
@@ -151,6 +152,34 @@ describe("Spatial Operations Tools", () => {
151
152
  const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
152
153
  expect(call).toContain("ST_Buffer_Strategy('point_circle', 4)");
153
154
  expect(result).toHaveProperty("segments", 4);
155
+ expect(result).toHaveProperty("precision", 6);
156
+ });
157
+
158
+ it("should pass custom precision to ST_AsGeoJSON", async () => {
159
+ mockAdapter.executeQuery.mockResolvedValue(
160
+ createMockQueryResult([
161
+ {
162
+ buffer_wkt: "POLYGON(...)",
163
+ buffer_geojson: '{"type":"Polygon"}',
164
+ },
165
+ ]),
166
+ );
167
+
168
+ const tool = createSpatialBufferTool(
169
+ mockAdapter as unknown as MySQLAdapter,
170
+ );
171
+ const result = await tool.handler(
172
+ {
173
+ geometry: "POINT(-73.9857 40.7484)",
174
+ distance: 1000,
175
+ precision: 2,
176
+ },
177
+ mockContext,
178
+ );
179
+
180
+ const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
181
+ expect(call).toContain(", 2) as buffer_geojson");
182
+ expect(result).toHaveProperty("precision", 2);
154
183
  });
155
184
  });
156
185
 
@@ -60,6 +60,15 @@ const BufferSchema = z.object({
60
60
  .describe(
61
61
  "Number of segments per quarter-circle for buffer polygon approximation (default: 8, MySQL default: 32). Lower values produce simpler polygons with smaller payloads. Only effective with Cartesian geometries (SRID 0); geographic SRIDs use MySQL's internal algorithm.",
62
62
  ),
63
+ precision: z
64
+ .number()
65
+ .int()
66
+ .min(0)
67
+ .max(15)
68
+ .default(6)
69
+ .describe(
70
+ "Decimal precision for GeoJSON output coordinates (default: 6, ~0.11m accuracy). Lower values reduce payload size.",
71
+ ),
63
72
  });
64
73
 
65
74
  const TransformSchema = z.object({
@@ -152,7 +161,8 @@ export function createSpatialBufferTool(adapter: MySQLAdapter): ToolDefinition {
152
161
  idempotentHint: true,
153
162
  },
154
163
  handler: async (params: unknown, _context: RequestContext) => {
155
- const { geometry, distance, srid, segments } = BufferSchema.parse(params);
164
+ const { geometry, distance, srid, segments, precision } =
165
+ BufferSchema.parse(params);
156
166
 
157
167
  try {
158
168
  // ST_Buffer_Strategy only works with Cartesian (non-geographic) SRIDs.
@@ -164,7 +174,7 @@ export function createSpatialBufferTool(adapter: MySQLAdapter): ToolDefinition {
164
174
  const result = await adapter.executeQuery(
165
175
  `SELECT
166
176
  ST_AsText(ST_Buffer(ST_GeomFromText(?, ${String(srid)}, 'axis-order=long-lat'), ?${strategyClause})) as buffer_wkt,
167
- ST_AsGeoJSON(ST_Buffer(ST_GeomFromText(?, ${String(srid)}, 'axis-order=long-lat'), ?${strategyClause})) as buffer_geojson`,
177
+ ST_AsGeoJSON(ST_Buffer(ST_GeomFromText(?, ${String(srid)}, 'axis-order=long-lat'), ?${strategyClause}), ${String(precision)}) as buffer_geojson`,
168
178
  [geometry, distance, geometry, distance],
169
179
  );
170
180
 
@@ -175,6 +185,7 @@ export function createSpatialBufferTool(adapter: MySQLAdapter): ToolDefinition {
175
185
  bufferDistance: distance,
176
186
  segments,
177
187
  segmentsApplied: !isGeographic,
188
+ precision,
178
189
  srid,
179
190
  };
180
191
  } catch (error) {
@@ -163,6 +163,23 @@ export function createSpatialCreateIndexTool(
163
163
  }
164
164
  }
165
165
 
166
+ // Check if a SPATIAL index already exists on this column (any name)
167
+ const existingIdx = await adapter.executeQuery(
168
+ `SELECT INDEX_NAME FROM information_schema.STATISTICS
169
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? AND INDEX_TYPE = 'SPATIAL'
170
+ LIMIT 1`,
171
+ [table, column],
172
+ );
173
+
174
+ const existingRow = existingIdx.rows?.[0];
175
+ if (existingRow) {
176
+ const existingName = String(existingRow["INDEX_NAME"]);
177
+ return {
178
+ success: false,
179
+ reason: `Spatial index '${existingName}' already exists on column '${column}' of table '${table}'`,
180
+ };
181
+ }
182
+
166
183
  await adapter.executeQuery(
167
184
  `CREATE SPATIAL INDEX \`${idxName}\` ON \`${table}\`(\`${column}\`)`,
168
185
  );
@@ -181,6 +198,12 @@ export function createSpatialCreateIndexTool(
181
198
  if (msg.includes("Cannot create SPATIAL index on nullable column")) {
182
199
  return { success: false, reason: msg };
183
200
  }
201
+ if (msg.includes("Duplicate key name")) {
202
+ return {
203
+ success: false,
204
+ reason: `Index '${idxName}' already exists on table '${table}'`,
205
+ };
206
+ }
184
207
  return { success: false, error: msg };
185
208
  }
186
209
  },
@@ -52,6 +52,9 @@ describe("Sys Schema Resource Tools", () => {
52
52
  tableStatistics: unknown[];
53
53
  indexStatistics: unknown[];
54
54
  autoIncrementStatus: unknown[];
55
+ tableStatisticsCount: number;
56
+ indexStatisticsCount: number;
57
+ autoIncrementStatusCount: number;
55
58
  schemaName: string;
56
59
  };
57
60
 
@@ -59,6 +62,9 @@ describe("Sys Schema Resource Tools", () => {
59
62
  expect(result.tableStatistics).toHaveLength(1);
60
63
  expect(result.indexStatistics).toHaveLength(1);
61
64
  expect(result.autoIncrementStatus).toHaveLength(1);
65
+ expect(result.tableStatisticsCount).toBe(1);
66
+ expect(result.indexStatisticsCount).toBe(1);
67
+ expect(result.autoIncrementStatusCount).toBe(1);
62
68
  expect(result.schemaName).toBe("testdb");
63
69
  });
64
70
 
@@ -113,6 +119,9 @@ describe("Sys Schema Resource Tools", () => {
113
119
  expect(result.tableStatistics).toEqual([]);
114
120
  expect(result.indexStatistics).toEqual([]);
115
121
  expect(result.autoIncrementStatus).toEqual([]);
122
+ expect(result.tableStatisticsCount).toBe(0);
123
+ expect(result.indexStatisticsCount).toBe(0);
124
+ expect(result.autoIncrementStatusCount).toBe(0);
116
125
  });
117
126
 
118
127
  it("should return exists: false for nonexistent schema (P154)", async () => {
@@ -144,9 +153,15 @@ describe("Sys Schema Resource Tools", () => {
144
153
  );
145
154
  const result = (await tool.handler({}, mockContext)) as {
146
155
  schemaName: string;
156
+ tableStatisticsCount: number;
157
+ indexStatisticsCount: number;
158
+ autoIncrementStatusCount: number;
147
159
  };
148
160
 
149
161
  expect(result.schemaName).toBe("real_db_name");
162
+ expect(result.tableStatisticsCount).toBe(0);
163
+ expect(result.indexStatisticsCount).toBe(0);
164
+ expect(result.autoIncrementStatusCount).toBe(0);
150
165
  // First call should be SELECT DATABASE()
151
166
  const firstCall = mockAdapter.executeQuery.mock.calls[0][0] as string;
152
167
  expect(firstCall).toContain("SELECT DATABASE()");
@@ -235,11 +250,15 @@ describe("Sys Schema Resource Tools", () => {
235
250
  const result = (await tool.handler({}, mockContext)) as {
236
251
  globalMemory: unknown[];
237
252
  memoryByUser: unknown[];
253
+ globalMemoryCount: number;
254
+ memoryByUserCount: number;
238
255
  };
239
256
 
240
257
  expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(2);
241
258
  expect(result.globalMemory).toHaveLength(1);
242
259
  expect(result.memoryByUser).toHaveLength(1);
260
+ expect(result.globalMemoryCount).toBe(1);
261
+ expect(result.memoryByUserCount).toBe(1);
243
262
  });
244
263
 
245
264
  it("should handle null rows", async () => {
@@ -255,6 +274,8 @@ describe("Sys Schema Resource Tools", () => {
255
274
 
256
275
  expect(result.globalMemory).toEqual([]);
257
276
  expect(result.memoryByUser).toEqual([]);
277
+ expect(result.globalMemoryCount).toBe(0);
278
+ expect(result.memoryByUserCount).toBe(0);
258
279
  });
259
280
  });
260
281
  });
@@ -142,6 +142,9 @@ export function createSysSchemaStatsTool(
142
142
  tableStatistics: tableStats.rows ?? [],
143
143
  indexStatistics: indexStats.rows ?? [],
144
144
  autoIncrementStatus: autoIncStats.rows ?? [],
145
+ tableStatisticsCount: (tableStats.rows ?? []).length,
146
+ indexStatisticsCount: (indexStats.rows ?? []).length,
147
+ autoIncrementStatusCount: (autoIncStats.rows ?? []).length,
145
148
  schemaName: resolvedSchema,
146
149
  };
147
150
  },
@@ -262,6 +265,8 @@ export function createSysMemorySummaryTool(
262
265
  return {
263
266
  globalMemory: globalStats.rows ?? [],
264
267
  memoryByUser: userStats.rows ?? [],
268
+ globalMemoryCount: (globalStats.rows ?? []).length,
269
+ memoryByUserCount: (userStats.rows ?? []).length,
265
270
  };
266
271
  },
267
272
  };
@@ -10,7 +10,12 @@ import type {
10
10
  ToolDefinition,
11
11
  RequestContext,
12
12
  } from "../../../../types/index.js";
13
- import { FulltextCreateSchema, FulltextSearchSchema } from "../../types.js";
13
+ import {
14
+ FulltextCreateSchema,
15
+ FulltextCreateSchemaBase,
16
+ FulltextSearchSchema,
17
+ FulltextSearchSchemaBase,
18
+ } from "../../types.js";
14
19
  import { z } from "zod";
15
20
  import {
16
21
  validateIdentifier,
@@ -75,7 +80,7 @@ export function createFulltextCreateTool(
75
80
  description:
76
81
  "Create a FULLTEXT index on specified columns for fast text search.",
77
82
  group: "fulltext",
78
- inputSchema: FulltextCreateSchema,
83
+ inputSchema: FulltextCreateSchemaBase,
79
84
  requiredScopes: ["write"],
80
85
  annotations: {
81
86
  readOnlyHint: false,
@@ -156,7 +161,7 @@ export function createFulltextDropTool(adapter: MySQLAdapter): ToolDefinition {
156
161
  };
157
162
  }
158
163
 
159
- const FulltextSearchWithTruncateSchema = FulltextSearchSchema.extend({
164
+ const FulltextSearchWithTruncateSchema = FulltextSearchSchemaBase.extend({
160
165
  maxLength: z
161
166
  .number()
162
167
  .optional()
@@ -180,8 +185,11 @@ export function createFulltextSearchTool(
180
185
  idempotentHint: true,
181
186
  },
182
187
  handler: async (params: unknown, _context: RequestContext) => {
183
- const { table, columns, query, mode, maxLength } =
184
- FulltextSearchWithTruncateSchema.parse(params);
188
+ const parsed = FulltextSearchSchema.parse(params);
189
+ const { table, columns, query, mode } = parsed;
190
+ const maxLength = (params as Record<string, unknown>)["maxLength"] as
191
+ | number
192
+ | undefined;
185
193
 
186
194
  // Validate inputs
187
195
  validateQualifiedIdentifier(table, "table");
@@ -12,10 +12,18 @@ import type {
12
12
  } from "../../../../types/index.js";
13
13
  import {
14
14
  RegexpMatchSchema,
15
+ RegexpMatchSchemaBase,
15
16
  LikeSearchSchema,
17
+ LikeSearchSchemaBase,
16
18
  SoundexSchema,
19
+ SoundexSchemaBase,
20
+ SubstringSchema,
21
+ SubstringSchemaBase,
22
+ ConcatSchema,
23
+ ConcatSchemaBase,
24
+ CollationConvertSchema,
25
+ CollationConvertSchemaBase,
17
26
  } from "../../types.js";
18
- import { z } from "zod";
19
27
  import {
20
28
  validateIdentifier,
21
29
  validateQualifiedIdentifier,
@@ -29,7 +37,7 @@ export function createRegexpMatchTool(adapter: MySQLAdapter): ToolDefinition {
29
37
  title: "MySQL REGEXP Match",
30
38
  description: "Find rows where column matches a regular expression pattern.",
31
39
  group: "text",
32
- inputSchema: RegexpMatchSchema,
40
+ inputSchema: RegexpMatchSchemaBase,
33
41
  requiredScopes: ["read"],
34
42
  annotations: {
35
43
  readOnlyHint: true,
@@ -70,7 +78,7 @@ export function createLikeSearchTool(adapter: MySQLAdapter): ToolDefinition {
70
78
  description:
71
79
  "Find rows using LIKE pattern matching with % and _ wildcards.",
72
80
  group: "text",
73
- inputSchema: LikeSearchSchema,
81
+ inputSchema: LikeSearchSchemaBase,
74
82
  requiredScopes: ["read"],
75
83
  annotations: {
76
84
  readOnlyHint: true,
@@ -110,7 +118,7 @@ export function createSoundexTool(adapter: MySQLAdapter): ToolDefinition {
110
118
  title: "MySQL SOUNDEX",
111
119
  description: "Find rows with phonetically similar values using SOUNDEX.",
112
120
  group: "text",
113
- inputSchema: SoundexSchema,
121
+ inputSchema: SoundexSchemaBase,
114
122
  requiredScopes: ["read"],
115
123
  annotations: {
116
124
  readOnlyHint: true,
@@ -145,27 +153,20 @@ export function createSoundexTool(adapter: MySQLAdapter): ToolDefinition {
145
153
  }
146
154
 
147
155
  export function createSubstringTool(adapter: MySQLAdapter): ToolDefinition {
148
- const schema = z.object({
149
- table: z.string(),
150
- column: z.string(),
151
- start: z.number().describe("Starting position (1-indexed)"),
152
- length: z.number().optional().describe("Number of characters"),
153
- where: z.string().optional(),
154
- });
155
-
156
156
  return {
157
157
  name: "mysql_substring",
158
158
  title: "MySQL SUBSTRING",
159
159
  description: "Extract substrings from column values.",
160
160
  group: "text",
161
- inputSchema: schema,
161
+ inputSchema: SubstringSchemaBase,
162
162
  requiredScopes: ["read"],
163
163
  annotations: {
164
164
  readOnlyHint: true,
165
165
  idempotentHint: true,
166
166
  },
167
167
  handler: async (params: unknown, _context: RequestContext) => {
168
- const { table, column, start, length, where } = schema.parse(params);
168
+ const { table, column, start, length, where } =
169
+ SubstringSchema.parse(params);
169
170
 
170
171
  // Validate inputs
171
172
  validateQualifiedIdentifier(table, "table");
@@ -201,35 +202,12 @@ export function createSubstringTool(adapter: MySQLAdapter): ToolDefinition {
201
202
  }
202
203
 
203
204
  export function createConcatTool(adapter: MySQLAdapter): ToolDefinition {
204
- const schema = z.object({
205
- table: z.string(),
206
- columns: z.array(z.string()).describe("Columns to concatenate"),
207
- separator: z
208
- .string()
209
- .optional()
210
- .default(" ")
211
- .describe("Separator between values"),
212
- alias: z
213
- .string()
214
- .optional()
215
- .default("concatenated")
216
- .describe("Result column name"),
217
- where: z.string().optional(),
218
- includeSourceColumns: z
219
- .boolean()
220
- .optional()
221
- .default(true)
222
- .describe(
223
- "Include individual source columns in output (default: true). Set to false for minimal payload.",
224
- ),
225
- });
226
-
227
205
  return {
228
206
  name: "mysql_concat",
229
207
  title: "MySQL CONCAT",
230
208
  description: "Concatenate multiple columns with an optional separator.",
231
209
  group: "text",
232
- inputSchema: schema,
210
+ inputSchema: ConcatSchemaBase,
233
211
  requiredScopes: ["read"],
234
212
  annotations: {
235
213
  readOnlyHint: true,
@@ -237,7 +215,7 @@ export function createConcatTool(adapter: MySQLAdapter): ToolDefinition {
237
215
  },
238
216
  handler: async (params: unknown, _context: RequestContext) => {
239
217
  const { table, columns, separator, alias, where, includeSourceColumns } =
240
- schema.parse(params);
218
+ ConcatSchema.parse(params);
241
219
 
242
220
  // Validate inputs
243
221
  validateQualifiedIdentifier(table, "table");
@@ -278,28 +256,21 @@ export function createConcatTool(adapter: MySQLAdapter): ToolDefinition {
278
256
  export function createCollationConvertTool(
279
257
  adapter: MySQLAdapter,
280
258
  ): ToolDefinition {
281
- const schema = z.object({
282
- table: z.string(),
283
- column: z.string(),
284
- charset: z.string().describe("Target character set (e.g., utf8mb4)"),
285
- collation: z.string().optional().describe("Target collation"),
286
- where: z.string().optional(),
287
- });
288
-
289
259
  return {
290
260
  name: "mysql_collation_convert",
291
261
  title: "MySQL Collation Convert",
292
262
  description:
293
263
  "Convert column values to a different character set or collation.",
294
264
  group: "text",
295
- inputSchema: schema,
265
+ inputSchema: CollationConvertSchemaBase,
296
266
  requiredScopes: ["read"],
297
267
  annotations: {
298
268
  readOnlyHint: true,
299
269
  idempotentHint: true,
300
270
  },
301
271
  handler: async (params: unknown, _context: RequestContext) => {
302
- const { table, column, charset, collation, where } = schema.parse(params);
272
+ const { table, column, charset, collation, where } =
273
+ CollationConvertSchema.parse(params);
303
274
 
304
275
  // Validate inputs
305
276
  validateQualifiedIdentifier(table, "table");