@berthojoris/mcp-mysql-server 1.42.2 → 1.43.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.
@@ -95,6 +95,111 @@ const validateSeedFromTemplateArgs = (args) => {
95
95
  validateRowsPerTable(args.rows_per_table, errors);
96
96
  return errors.length ? { valid: false, errors } : { valid: true };
97
97
  };
98
+ const validateDiscoveryKeyword = (value, key, errors) => {
99
+ if (!value || typeof value !== "string" || !value.trim()) {
100
+ errors.push(`${key} is required`);
101
+ return;
102
+ }
103
+ if (value.length > 255) {
104
+ errors.push(`${key} must be 255 characters or fewer`);
105
+ return;
106
+ }
107
+ const validation = (0, inputValidation_js_1.validateValue)(value);
108
+ if (!validation.valid)
109
+ errors.push(validation.error || `Invalid ${key}`);
110
+ };
111
+ const validatePositiveNumber = (value, key, min, max, errors) => {
112
+ if (value === undefined)
113
+ return;
114
+ const numeric = Number(value);
115
+ if (!Number.isFinite(numeric) || numeric < min || numeric > max) {
116
+ errors.push(`${key} must be a number between ${min} and ${max}`);
117
+ }
118
+ };
119
+ const validateColumnList = (value, key, errors) => {
120
+ if (value === undefined)
121
+ return;
122
+ if (!Array.isArray(value)) {
123
+ errors.push(`${key} must be an array`);
124
+ return;
125
+ }
126
+ for (const column of value) {
127
+ const validation = (0, inputValidation_js_1.validateTableName)(column);
128
+ if (!validation.valid) {
129
+ errors.push(`Invalid ${key} column '${column}': ${validation.error}`);
130
+ }
131
+ }
132
+ };
133
+ const validateFindTablesByKeywordArgs = (args) => {
134
+ const errors = [];
135
+ validateDiscoveryKeyword(args.keyword, "keyword", errors);
136
+ if (args.search_in !== undefined && !["table_names", "column_names", "comments", "all"].includes(args.search_in)) {
137
+ errors.push("search_in must be one of table_names, column_names, comments, all");
138
+ }
139
+ if (args.database !== undefined) {
140
+ const validation = (0, inputValidation_js_1.validateValue)(args.database);
141
+ if (!validation.valid)
142
+ errors.push(validation.error || "Invalid database name");
143
+ }
144
+ validatePositiveNumber(args.limit, "limit", 1, 100, errors);
145
+ return errors.length ? { valid: false, errors } : { valid: true };
146
+ };
147
+ const validateSearchDataAcrossTablesArgs = (args) => {
148
+ const errors = [];
149
+ validateDiscoveryKeyword(args.keyword, "keyword", errors);
150
+ if (args.database !== undefined) {
151
+ const validation = (0, inputValidation_js_1.validateValue)(args.database);
152
+ if (!validation.valid)
153
+ errors.push(validation.error || "Invalid database name");
154
+ }
155
+ validateTableList(args.tables, "tables", errors);
156
+ validateColumnList(args.columns, "columns", errors);
157
+ validatePositiveNumber(args.max_tables, "max_tables", 1, 100, errors);
158
+ validatePositiveNumber(args.limit_per_table, "limit_per_table", 1, 20, errors);
159
+ return errors.length ? { valid: false, errors } : { valid: true };
160
+ };
161
+ const validateSearchSchemaArgs = (args) => {
162
+ const errors = [];
163
+ validateDiscoveryKeyword(args.query, "query", errors);
164
+ if (args.database !== undefined) {
165
+ const validation = (0, inputValidation_js_1.validateValue)(args.database);
166
+ if (!validation.valid)
167
+ errors.push(validation.error || "Invalid database name");
168
+ }
169
+ if (args.modes !== undefined) {
170
+ if (!Array.isArray(args.modes) || args.modes.length === 0) {
171
+ errors.push("modes must be a non-empty array");
172
+ }
173
+ else {
174
+ const validModes = ["table_names", "column_names", "comments", "sample_data"];
175
+ for (const mode of args.modes) {
176
+ if (!validModes.includes(mode)) {
177
+ errors.push(`Invalid mode '${mode}'. Must be one of ${validModes.join(", ")}`);
178
+ }
179
+ }
180
+ }
181
+ }
182
+ validateTableList(args.tables, "tables", errors);
183
+ validateColumnList(args.columns, "columns", errors);
184
+ validatePositiveNumber(args.max_results, "max_results", 1, 100, errors);
185
+ validatePositiveNumber(args.max_tables, "max_tables", 1, 100, errors);
186
+ validatePositiveNumber(args.limit_per_table, "limit_per_table", 1, 20, errors);
187
+ return errors.length ? { valid: false, errors } : { valid: true };
188
+ };
189
+ const validateSchemaRagContextArgs = (args) => {
190
+ const errors = [];
191
+ if (args.database !== undefined) {
192
+ const validation = (0, inputValidation_js_1.validateValue)(args.database);
193
+ if (!validation.valid)
194
+ errors.push(validation.error || "Invalid database name");
195
+ }
196
+ if (args.keyword_filter !== undefined) {
197
+ validateDiscoveryKeyword(args.keyword_filter, "keyword_filter", errors);
198
+ }
199
+ validatePositiveNumber(args.max_tables, "max_tables", 1, 200, errors);
200
+ validatePositiveNumber(args.max_columns, "max_columns", 1, 200, errors);
201
+ return errors.length ? { valid: false, errors } : { valid: true };
202
+ };
98
203
  const validatePlanIdArgs = (args) => {
99
204
  if (!args.plan_id || typeof args.plan_id !== "string") {
100
205
  return { valid: false, errors: ["plan_id is required"] };
@@ -138,9 +243,16 @@ function validateToolArguments(name, args) {
138
243
  return validateInferSeedRulesArgs(args);
139
244
  case "seed_from_template":
140
245
  return validateSeedFromTemplateArgs(args);
246
+ case "find_tables_by_keyword":
247
+ return validateFindTablesByKeywordArgs(args);
248
+ case "search_schema":
249
+ return validateSearchSchemaArgs(args);
250
+ case "search_data_across_tables":
251
+ return validateSearchDataAcrossTablesArgs(args);
252
+ case "get_schema_rag_context":
253
+ return validateSchemaRagContextArgs(args);
141
254
  case "list_tables":
142
255
  case "get_schema_erd":
143
- case "get_schema_rag_context":
144
256
  case "get_database_summary":
145
257
  if (args.database !== undefined) {
146
258
  const validation = (0, inputValidation_js_1.validateValue)(args.database);
@@ -347,6 +347,13 @@ class UtilityTools {
347
347
  "get_database_summary",
348
348
  "get_schema_rag_context",
349
349
  ],
350
+ discover_concept_location: [
351
+ "find_tables_by_keyword",
352
+ "search_schema",
353
+ "read_table_schema on likely matches",
354
+ "read_records on likely matches for verification",
355
+ "search_data_across_tables only when the keyword may exist only in row data",
356
+ ],
350
357
  inspect_table: [
351
358
  "read_table_schema",
352
359
  "get_column_statistics",
@@ -375,7 +382,9 @@ class UtilityTools {
375
382
  ],
376
383
  },
377
384
  selection_rules: [
378
- "Use get_schema_rag_context before generating SQL to reduce token usage.",
385
+ "Use get_schema_rag_context before generating SQL to reduce token usage; add keyword_filter for concept-focused context.",
386
+ "Use find_tables_by_keyword or search_schema when users ask which table stores a concept.",
387
+ "Use search_data_across_tables only as a bounded fallback after schema metadata is inconclusive.",
379
388
  "Use run_select_query only for SELECT statements.",
380
389
  "Use execute_write_query for INSERT and UPDATE. DELETE requires the delete permission.",
381
390
  "Use execute_ddl only for CREATE, ALTER, DROP, TRUNCATE, and RENAME.",
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mysql-mcp",
3
3
  "description": "A Model Context Protocol for MySQL database interaction",
4
- "version": "1.42.1",
4
+ "version": "1.43.0",
5
5
  "tools": [
6
6
  {
7
7
  "name": "list_databases",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  {
74
74
  "name": "get_schema_rag_context",
75
- "description": "🎯 AI-OPTIMIZED: Returns ultra-compact schema information (tables, columns, keys, relationships, row estimates) designed specifically for LLM context windows. Use this when you need schema awareness but want to minimize token usage. Configurable limits for tables/columns.",
75
+ "description": "🎯 AI-OPTIMIZED: Returns ultra-compact schema information (tables, columns, keys, relationships, comments, row estimates) designed specifically for LLM context windows. Use keyword_filter for concept-focused schema discovery.",
76
76
  "input_schema": {
77
77
  "type": "object",
78
78
  "properties": {
@@ -91,6 +91,14 @@
91
91
  "include_relationships": {
92
92
  "type": "boolean",
93
93
  "description": "Whether to include FK relationships section (default: true)"
94
+ },
95
+ "include_comments": {
96
+ "type": "boolean",
97
+ "description": "Optional: include TABLE_COMMENT and COLUMN_COMMENT metadata (default: false)"
98
+ },
99
+ "keyword_filter": {
100
+ "type": "string",
101
+ "description": "Optional: only include tables relevant to this keyword, ranked by table names, column names, and comments"
94
102
  }
95
103
  }
96
104
  },
@@ -98,6 +106,150 @@
98
106
  "type": "object"
99
107
  }
100
108
  },
109
+ {
110
+ "name": "find_tables_by_keyword",
111
+ "description": "🔎 Schema discovery: finds candidate tables for a concept or keyword by searching table names, column names, TABLE_COMMENT, and COLUMN_COMMENT metadata. Use when users ask 'which table stores X?' before reading sample data.",
112
+ "input_schema": {
113
+ "type": "object",
114
+ "properties": {
115
+ "keyword": {
116
+ "type": "string",
117
+ "description": "Concept or keyword to find, such as survey, invoice, feedback, or customer"
118
+ },
119
+ "search_in": {
120
+ "type": "string",
121
+ "enum": [
122
+ "table_names",
123
+ "column_names",
124
+ "comments",
125
+ "all"
126
+ ],
127
+ "description": "Where to search (default: all)"
128
+ },
129
+ "database": {
130
+ "type": "string",
131
+ "description": "Optional: specific database name"
132
+ },
133
+ "limit": {
134
+ "type": "number",
135
+ "description": "Optional: maximum number of ranked tables to return (default 20, max 100)"
136
+ }
137
+ },
138
+ "required": [
139
+ "keyword"
140
+ ]
141
+ },
142
+ "output_schema": {
143
+ "type": "object"
144
+ }
145
+ },
146
+ {
147
+ "name": "search_schema",
148
+ "description": "🧭 Unified schema discovery for natural-language 'where is X?' questions. Searches schema metadata by default and can optionally perform a guarded sample-data scan when mode sample_data is requested.",
149
+ "input_schema": {
150
+ "type": "object",
151
+ "properties": {
152
+ "query": {
153
+ "type": "string",
154
+ "description": "Concept or keyword to discover"
155
+ },
156
+ "modes": {
157
+ "type": "array",
158
+ "items": {
159
+ "type": "string",
160
+ "enum": [
161
+ "table_names",
162
+ "column_names",
163
+ "comments",
164
+ "sample_data"
165
+ ]
166
+ },
167
+ "description": "Discovery modes (default: table_names, column_names, comments). sample_data requires read permission."
168
+ },
169
+ "max_results": {
170
+ "type": "number",
171
+ "description": "Optional: maximum combined results to return (default 20, max 100)"
172
+ },
173
+ "database": {
174
+ "type": "string",
175
+ "description": "Optional: specific database name"
176
+ },
177
+ "tables": {
178
+ "type": "array",
179
+ "items": {
180
+ "type": "string"
181
+ },
182
+ "description": "Optional: restrict sample_data mode to these tables"
183
+ },
184
+ "columns": {
185
+ "type": "array",
186
+ "items": {
187
+ "type": "string"
188
+ },
189
+ "description": "Optional: restrict sample_data mode to these columns"
190
+ },
191
+ "max_tables": {
192
+ "type": "number",
193
+ "description": "Optional: max tables scanned in sample_data mode (default 20, max 100)"
194
+ },
195
+ "limit_per_table": {
196
+ "type": "number",
197
+ "description": "Optional: max matching rows returned per table in sample_data mode (default 5, max 20)"
198
+ }
199
+ },
200
+ "required": [
201
+ "query"
202
+ ]
203
+ },
204
+ "output_schema": {
205
+ "type": "object"
206
+ }
207
+ },
208
+ {
209
+ "name": "search_data_across_tables",
210
+ "description": "🔍 Guarded read-only keyword scan across text-like table data. Use only when schema metadata does not reveal where a concept is stored; enforces max table and per-table result limits.",
211
+ "input_schema": {
212
+ "type": "object",
213
+ "properties": {
214
+ "keyword": {
215
+ "type": "string",
216
+ "description": "Keyword to search inside text-like column values"
217
+ },
218
+ "tables": {
219
+ "type": "array",
220
+ "items": {
221
+ "type": "string"
222
+ },
223
+ "description": "Optional: restrict scan to these tables"
224
+ },
225
+ "columns": {
226
+ "type": "array",
227
+ "items": {
228
+ "type": "string"
229
+ },
230
+ "description": "Optional: restrict scan to these column names"
231
+ },
232
+ "database": {
233
+ "type": "string",
234
+ "description": "Optional: specific database name"
235
+ },
236
+ "max_tables": {
237
+ "type": "number",
238
+ "description": "Optional: maximum tables to scan (default 20, max 100)"
239
+ },
240
+ "limit_per_table": {
241
+ "type": "number",
242
+ "description": "Optional: maximum rows returned per table (default 5, max 20)"
243
+ }
244
+ },
245
+ "required": [
246
+ "keyword"
247
+ ]
248
+ },
249
+ "output_schema": {
250
+ "type": "object"
251
+ }
252
+ },
101
253
  {
102
254
  "name": "get_column_statistics",
103
255
  "description": "Returns detailed statistics for a specific column: min/max values, average, distinct count, null percentage, and value distribution. Use to understand data quality and ranges in a column before writing queries.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@berthojoris/mcp-mysql-server",
3
- "version": "1.42.2",
3
+ "version": "1.43.0",
4
4
  "description": "Model Context Protocol server for MySQL database integration with dynamic per-project permissions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",