@berthojoris/mcp-mysql-server 1.23.0 → 1.24.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.
package/CHANGELOG.md CHANGED
@@ -27,6 +27,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
27
27
  ### Changed
28
28
  - Version increment for implemented features and improvements
29
29
 
30
+ ## [1.24.0] - 2025-12-19
31
+
32
+ ### Added
33
+ - New tool `list_all_tools` for listing all available tools in the MySQL MCP server with their descriptions and schemas
34
+ - Enhanced meta-tool capabilities for AI agents to discover available functionality
35
+
36
+ ### Changed
37
+ - Updated tool count from 144 to 145 in README.md
38
+ - Updated tool count from 157 to 158 in DOCUMENTATIONS.md
39
+ - Added `list_all_tools` to the Utilities section in documentation
40
+ - Updated Last Updated timestamp in README.md
41
+ - Version increment for new tool implementation
42
+
30
43
  ## [Unreleased]
31
44
 
32
45
  ### Security
package/DOCUMENTATIONS.md CHANGED
@@ -9,7 +9,7 @@ This file contains detailed documentation for all features of the MySQL MCP Serv
9
9
  ## Table of Contents
10
10
 
11
11
  1. [Setup & Configuration (Extended)](#setup--configuration-extended) - Permissions + Categories
12
- 2. [🔧 Complete Tools Reference](#🔧-complete-tools-reference) - All 157 tools organized by category
12
+ 2. [🔧 Complete Tools Reference](#🔧-complete-tools-reference) - All 158 tools organized by category
13
13
  3. [DDL Operations](#🏗️-ddl-operations)
14
14
  4. [Data Export Tools](#📤-data-export-tools)
15
15
  5. [Data Import Tools](#📥-data-import-tools)
@@ -201,7 +201,7 @@ Add 'bulk_operations' to the categories argument.
201
201
 
202
202
  ## 🔧 Complete Tools Reference
203
203
 
204
- This section provides a comprehensive reference of all 157 available tools organized by category.
204
+ This section provides a comprehensive reference of all 158 available tools organized by category.
205
205
 
206
206
  ### Database Discovery
207
207
 
@@ -255,6 +255,7 @@ This section provides a comprehensive reference of all 157 available tools organ
255
255
  | `read_changelog` | Read the changelog to see new features/changes |
256
256
  | `export_table_to_csv` | Export table data to CSV format |
257
257
  | `export_query_to_csv` | Export query results to CSV format |
258
+ | `list_all_tools` | List all available tools in this MySQL MCP server |
258
259
 
259
260
  ### Transaction Management
260
261
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **A production-ready Model Context Protocol (MCP) server for MySQL database integration with AI agents**
6
6
 
7
- **Last Updated:** 2025-12-18 13:45:00
7
+ **Last Updated:** 2025-12-19 03:59:00
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/@berthojoris/mcp-mysql-server)](https://www.npmjs.com/package/@berthojoris/mysql-mcp)
10
10
  [![npm downloads](https://img.shields.io/npm/dm/@berthojoris/mysql-mcp)](https://www.npmjs.com/package/@berthojoris/mysql-mcp)
@@ -300,7 +300,7 @@ Common configuration examples are documented in **[DOCUMENTATIONS.md → Categor
300
300
 
301
301
  ## Available Tools
302
302
 
303
- The server exposes **144 tools** organized into categories (CRUD, schema, backups, migrations, perf/monitoring, and AI enhancement).
303
+ The server exposes **145 tools** organized into categories (CRUD, schema, backups, migrations, perf/monitoring, and AI enhancement).
304
304
 
305
305
  - Complete list of tools: **[DOCUMENTATIONS.md → Complete Tools Reference](DOCUMENTATIONS.md#🔧-complete-tools-reference)**
306
306
  - AI enhancement tools overview: **[DOCUMENTATIONS.md → AI Enhancement Tools](DOCUMENTATIONS.md#🤖-ai-enhancement-tools)**
package/dist/index.d.ts CHANGED
@@ -216,6 +216,11 @@ export declare class MySQLMCP {
216
216
  data?: any;
217
217
  error?: string;
218
218
  }>;
219
+ listAllTools(): Promise<{
220
+ status: string;
221
+ data?: any;
222
+ error?: string;
223
+ }>;
219
224
  beginTransaction(params?: {
220
225
  transactionId?: string;
221
226
  }): Promise<import("./tools/transactionTools").TransactionResult | {
package/dist/index.js CHANGED
@@ -270,6 +270,13 @@ class MySQLMCP {
270
270
  }
271
271
  return await this.utilityTools.readChangelog(params);
272
272
  }
273
+ async listAllTools() {
274
+ const check = this.checkToolEnabled("listAllTools");
275
+ if (!check.enabled) {
276
+ return { status: "error", error: check.error };
277
+ }
278
+ return await this.utilityTools.listAllTools();
279
+ }
273
280
  // Transaction Tools
274
281
  async beginTransaction(params) {
275
282
  const check = this.checkToolEnabled("beginTransaction");
@@ -699,7 +699,7 @@ const TOOLS = [
699
699
  },
700
700
  {
701
701
  name: "get_all_tables_relationships",
702
- description: "Gets foreign key relationships for ALL tables in a single call. Processes relationships in memory to avoid multiple queries.",
702
+ description: "Gets all table foreign key relationships in one call with memory-efficient relationship mapping.",
703
703
  inputSchema: {
704
704
  type: "object",
705
705
  properties: {
@@ -60,10 +60,10 @@ class AnalysisTools {
60
60
  return { status: "error", error: "Invalid column name" };
61
61
  }
62
62
  // Check if column exists and get its type
63
- const colCheckQuery = `
64
- SELECT DATA_TYPE
65
- FROM INFORMATION_SCHEMA.COLUMNS
66
- WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?
63
+ const colCheckQuery = `
64
+ SELECT DATA_TYPE
65
+ FROM INFORMATION_SCHEMA.COLUMNS
66
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?
67
67
  `;
68
68
  const colCheck = await this.db.query(colCheckQuery, [
69
69
  database,
@@ -95,34 +95,34 @@ class AnalysisTools {
95
95
  "year",
96
96
  ].includes(dataType);
97
97
  // Build statistics query
98
- let query = `
99
- SELECT
100
- COUNT(*) as total_rows,
101
- COUNT(\`${column_name}\`) as non_null_count,
102
- COUNT(DISTINCT \`${column_name}\`) as distinct_count,
103
- SUM(CASE WHEN \`${column_name}\` IS NULL THEN 1 ELSE 0 END) as null_count
98
+ let query = `
99
+ SELECT
100
+ COUNT(*) as total_rows,
101
+ COUNT(\`${column_name}\`) as non_null_count,
102
+ COUNT(DISTINCT \`${column_name}\`) as distinct_count,
103
+ SUM(CASE WHEN \`${column_name}\` IS NULL THEN 1 ELSE 0 END) as null_count
104
104
  `;
105
105
  if (isNumeric || isDate) {
106
- query += `,
107
- MIN(\`${column_name}\`) as min_value,
108
- MAX(\`${column_name}\`) as max_value
106
+ query += `,
107
+ MIN(\`${column_name}\`) as min_value,
108
+ MAX(\`${column_name}\`) as max_value
109
109
  `;
110
110
  }
111
111
  if (isNumeric) {
112
- query += `,
113
- AVG(\`${column_name}\`) as avg_value
112
+ query += `,
113
+ AVG(\`${column_name}\`) as avg_value
114
114
  `;
115
115
  }
116
116
  query += ` FROM \`${database}\`.\`${table_name}\``;
117
117
  const statsResult = await this.db.query(query);
118
118
  const stats = statsResult[0];
119
119
  // Get top frequent values
120
- const topValuesQuery = `
121
- SELECT \`${column_name}\` as value, COUNT(*) as count
122
- FROM \`${database}\`.\`${table_name}\`
123
- GROUP BY \`${column_name}\`
124
- ORDER BY count DESC
125
- LIMIT 5
120
+ const topValuesQuery = `
121
+ SELECT \`${column_name}\` as value, COUNT(*) as count
122
+ FROM \`${database}\`.\`${table_name}\`
123
+ GROUP BY \`${column_name}\`
124
+ ORDER BY count DESC
125
+ LIMIT 5
126
126
  `;
127
127
  const topValues = await this.db.query(topValuesQuery);
128
128
  return {
@@ -171,12 +171,12 @@ class AnalysisTools {
171
171
  const totalTablesResult = await this.db.query(`SELECT COUNT(*) as total FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?`, [database]);
172
172
  const totalTables = totalTablesResult[0]?.total ?? 0;
173
173
  // Fetch tables limited for context pack
174
- const tables = await this.db.query(`
175
- SELECT TABLE_NAME, TABLE_ROWS
176
- FROM INFORMATION_SCHEMA.TABLES
177
- WHERE TABLE_SCHEMA = ?
178
- ORDER BY TABLE_NAME
179
- LIMIT ?
174
+ const tables = await this.db.query(`
175
+ SELECT TABLE_NAME, TABLE_ROWS
176
+ FROM INFORMATION_SCHEMA.TABLES
177
+ WHERE TABLE_SCHEMA = ?
178
+ ORDER BY TABLE_NAME
179
+ LIMIT ?
180
180
  `, [database, maxTables]);
181
181
  if (!tables.length) {
182
182
  return {
@@ -193,23 +193,23 @@ class AnalysisTools {
193
193
  const tableNames = tables.map((t) => t.TABLE_NAME);
194
194
  const placeholders = tableNames.map(() => "?").join(",");
195
195
  const columnParams = [database, ...tableNames];
196
- const columns = await this.db.query(`
197
- SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY, IS_NULLABLE
198
- FROM INFORMATION_SCHEMA.COLUMNS
199
- WHERE TABLE_SCHEMA = ?
200
- AND TABLE_NAME IN (${placeholders})
201
- ORDER BY TABLE_NAME, ORDINAL_POSITION
196
+ const columns = await this.db.query(`
197
+ SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY, IS_NULLABLE
198
+ FROM INFORMATION_SCHEMA.COLUMNS
199
+ WHERE TABLE_SCHEMA = ?
200
+ AND TABLE_NAME IN (${placeholders})
201
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
202
202
  `, columnParams);
203
203
  let foreignKeys = [];
204
204
  if (includeRelationships) {
205
205
  const fkParams = [database, ...tableNames, ...tableNames];
206
- foreignKeys = await this.db.query(`
207
- SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
208
- FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
209
- WHERE TABLE_SCHEMA = ?
210
- AND TABLE_NAME IN (${placeholders})
211
- AND REFERENCED_TABLE_NAME IN (${placeholders})
212
- AND REFERENCED_TABLE_NAME IS NOT NULL
206
+ foreignKeys = await this.db.query(`
207
+ SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
208
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
209
+ WHERE TABLE_SCHEMA = ?
210
+ AND TABLE_NAME IN (${placeholders})
211
+ AND REFERENCED_TABLE_NAME IN (${placeholders})
212
+ AND REFERENCED_TABLE_NAME IS NOT NULL
213
213
  `, fkParams);
214
214
  }
215
215
  const fkLookup = new Map();
@@ -250,11 +250,11 @@ class IndexRecommendationTools {
250
250
  return false;
251
251
  }
252
252
  async getExistingIndexMap(database) {
253
- const rows = await this.db.query(`
254
- SELECT TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX, COLUMN_NAME
255
- FROM INFORMATION_SCHEMA.STATISTICS
256
- WHERE TABLE_SCHEMA = ?
257
- ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
253
+ const rows = await this.db.query(`
254
+ SELECT TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX, COLUMN_NAME
255
+ FROM INFORMATION_SCHEMA.STATISTICS
256
+ WHERE TABLE_SCHEMA = ?
257
+ ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
258
258
  `, [database]);
259
259
  const byTable = new Map();
260
260
  for (const r of rows) {
@@ -281,21 +281,21 @@ class IndexRecommendationTools {
281
281
  }
282
282
  async getTopSelectDigests(limit) {
283
283
  // Note: performance_schema access may require extra privileges.
284
- const query = `
285
- SELECT
286
- DIGEST_TEXT as query_pattern,
287
- COUNT_STAR as execution_count,
288
- ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
289
- ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
290
- SUM_ROWS_EXAMINED as rows_examined,
291
- SUM_ROWS_SENT as rows_sent,
292
- FIRST_SEEN as first_seen,
293
- LAST_SEEN as last_seen
294
- FROM performance_schema.events_statements_summary_by_digest
295
- WHERE DIGEST_TEXT IS NOT NULL
296
- AND DIGEST_TEXT LIKE 'SELECT %'
297
- ORDER BY SUM_TIMER_WAIT DESC
298
- LIMIT ${limit}
284
+ const query = `
285
+ SELECT
286
+ DIGEST_TEXT as query_pattern,
287
+ COUNT_STAR as execution_count,
288
+ ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
289
+ ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
290
+ SUM_ROWS_EXAMINED as rows_examined,
291
+ SUM_ROWS_SENT as rows_sent,
292
+ FIRST_SEEN as first_seen,
293
+ LAST_SEEN as last_seen
294
+ FROM performance_schema.events_statements_summary_by_digest
295
+ WHERE DIGEST_TEXT IS NOT NULL
296
+ AND DIGEST_TEXT LIKE 'SELECT %'
297
+ ORDER BY SUM_TIMER_WAIT DESC
298
+ LIMIT ${limit}
299
299
  `;
300
300
  const rows = await this.db.query(query);
301
301
  return rows.map((r) => ({
@@ -310,25 +310,25 @@ class IndexRecommendationTools {
310
310
  }));
311
311
  }
312
312
  async getUnusedIndexes() {
313
- const query = `
314
- SELECT
315
- t.TABLE_SCHEMA as table_schema,
316
- t.TABLE_NAME as table_name,
317
- s.INDEX_NAME as index_name
318
- FROM information_schema.STATISTICS s
319
- LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage p
320
- ON s.TABLE_SCHEMA = p.OBJECT_SCHEMA
321
- AND s.TABLE_NAME = p.OBJECT_NAME
322
- AND s.INDEX_NAME = p.INDEX_NAME
323
- JOIN information_schema.TABLES t
324
- ON s.TABLE_SCHEMA = t.TABLE_SCHEMA
325
- AND s.TABLE_NAME = t.TABLE_NAME
326
- WHERE s.TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
327
- AND s.INDEX_NAME != 'PRIMARY'
328
- AND (p.INDEX_NAME IS NULL OR p.COUNT_STAR = 0)
329
- AND t.TABLE_TYPE = 'BASE TABLE'
330
- GROUP BY t.TABLE_SCHEMA, t.TABLE_NAME, s.INDEX_NAME
331
- ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME, s.INDEX_NAME
313
+ const query = `
314
+ SELECT
315
+ t.TABLE_SCHEMA as table_schema,
316
+ t.TABLE_NAME as table_name,
317
+ s.INDEX_NAME as index_name
318
+ FROM information_schema.STATISTICS s
319
+ LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage p
320
+ ON s.TABLE_SCHEMA = p.OBJECT_SCHEMA
321
+ AND s.TABLE_NAME = p.OBJECT_NAME
322
+ AND s.INDEX_NAME = p.INDEX_NAME
323
+ JOIN information_schema.TABLES t
324
+ ON s.TABLE_SCHEMA = t.TABLE_SCHEMA
325
+ AND s.TABLE_NAME = t.TABLE_NAME
326
+ WHERE s.TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
327
+ AND s.INDEX_NAME != 'PRIMARY'
328
+ AND (p.INDEX_NAME IS NULL OR p.COUNT_STAR = 0)
329
+ AND t.TABLE_TYPE = 'BASE TABLE'
330
+ GROUP BY t.TABLE_SCHEMA, t.TABLE_NAME, s.INDEX_NAME
331
+ ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME, s.INDEX_NAME
332
332
  `;
333
333
  const rows = await this.db.query(query);
334
334
  return rows.map((r) => ({
@@ -17,28 +17,28 @@ class PerformanceTools {
17
17
  try {
18
18
  const metrics = {};
19
19
  // Get query performance metrics
20
- const perfQuery = `
21
- SELECT
22
- SUM(TIMER_WAIT) / 1000000000000 as total_execution_time_sec,
23
- SUM(LOCK_TIME) / 1000000000000 as total_lock_time_sec,
24
- SUM(ROWS_EXAMINED) as total_rows_examined,
25
- SUM(ROWS_SENT) as total_rows_sent,
26
- SUM(ROWS_AFFECTED) as total_rows_affected,
27
- SUM(SELECT_FULL_JOIN) as full_table_scans,
28
- SUM(NO_INDEX_USED) as queries_without_indexes,
29
- SUM(NO_GOOD_INDEX_USED) as queries_with_bad_indexes,
30
- COUNT(*) as total_queries
31
- FROM performance_schema.events_statements_summary_global_by_event_name
32
- WHERE EVENT_NAME LIKE 'statement/sql/%'
20
+ const perfQuery = `
21
+ SELECT
22
+ SUM(TIMER_WAIT) / 1000000000000 as total_execution_time_sec,
23
+ SUM(LOCK_TIME) / 1000000000000 as total_lock_time_sec,
24
+ SUM(ROWS_EXAMINED) as total_rows_examined,
25
+ SUM(ROWS_SENT) as total_rows_sent,
26
+ SUM(ROWS_AFFECTED) as total_rows_affected,
27
+ SUM(SELECT_FULL_JOIN) as full_table_scans,
28
+ SUM(NO_INDEX_USED) as queries_without_indexes,
29
+ SUM(NO_GOOD_INDEX_USED) as queries_with_bad_indexes,
30
+ COUNT(*) as total_queries
31
+ FROM performance_schema.events_statements_summary_global_by_event_name
32
+ WHERE EVENT_NAME LIKE 'statement/sql/%'
33
33
  `;
34
34
  const perfResult = await this.db.query(perfQuery);
35
35
  metrics.query_performance = perfResult[0] || {};
36
36
  // Get connection metrics
37
- const connQuery = `
38
- SHOW GLOBAL STATUS WHERE Variable_name IN (
39
- 'Threads_connected', 'Threads_running', 'Threads_created',
40
- 'Connections', 'Max_used_connections', 'Aborted_connects', 'Aborted_clients'
41
- )
37
+ const connQuery = `
38
+ SHOW GLOBAL STATUS WHERE Variable_name IN (
39
+ 'Threads_connected', 'Threads_running', 'Threads_created',
40
+ 'Connections', 'Max_used_connections', 'Aborted_connects', 'Aborted_clients'
41
+ )
42
42
  `;
43
43
  const connResult = await this.db.query(connQuery);
44
44
  metrics.connections = {};
@@ -46,11 +46,11 @@ class PerformanceTools {
46
46
  metrics.connections[row.Variable_name.toLowerCase()] = row.Value;
47
47
  }
48
48
  // Get table cache metrics
49
- const cacheQuery = `
50
- SHOW GLOBAL STATUS WHERE Variable_name IN (
51
- 'Open_tables', 'Opened_tables', 'Table_open_cache_hits',
52
- 'Table_open_cache_misses', 'Table_open_cache_overflows'
53
- )
49
+ const cacheQuery = `
50
+ SHOW GLOBAL STATUS WHERE Variable_name IN (
51
+ 'Open_tables', 'Opened_tables', 'Table_open_cache_hits',
52
+ 'Table_open_cache_misses', 'Table_open_cache_overflows'
53
+ )
54
54
  `;
55
55
  const cacheResult = await this.db.query(cacheQuery);
56
56
  metrics.table_cache = {};
@@ -58,15 +58,15 @@ class PerformanceTools {
58
58
  metrics.table_cache[row.Variable_name.toLowerCase()] = row.Value;
59
59
  }
60
60
  // Get InnoDB metrics
61
- const innodbQuery = `
62
- SHOW GLOBAL STATUS WHERE Variable_name LIKE 'Innodb_%'
63
- AND Variable_name IN (
64
- 'Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests',
65
- 'Innodb_buffer_pool_pages_total', 'Innodb_buffer_pool_pages_free',
66
- 'Innodb_buffer_pool_pages_data', 'Innodb_buffer_pool_pages_dirty',
67
- 'Innodb_row_lock_waits', 'Innodb_row_lock_time', 'Innodb_rows_read',
68
- 'Innodb_rows_inserted', 'Innodb_rows_updated', 'Innodb_rows_deleted'
69
- )
61
+ const innodbQuery = `
62
+ SHOW GLOBAL STATUS WHERE Variable_name LIKE 'Innodb_%'
63
+ AND Variable_name IN (
64
+ 'Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests',
65
+ 'Innodb_buffer_pool_pages_total', 'Innodb_buffer_pool_pages_free',
66
+ 'Innodb_buffer_pool_pages_data', 'Innodb_buffer_pool_pages_dirty',
67
+ 'Innodb_row_lock_waits', 'Innodb_row_lock_time', 'Innodb_rows_read',
68
+ 'Innodb_rows_inserted', 'Innodb_rows_updated', 'Innodb_rows_deleted'
69
+ )
70
70
  `;
71
71
  const innodbResult = await this.db.query(innodbQuery);
72
72
  metrics.innodb = {};
@@ -82,8 +82,8 @@ class PerformanceTools {
82
82
  (((requests - reads) / requests) * 100).toFixed(2) + "%";
83
83
  }
84
84
  // Get slow query metrics
85
- const slowQuery = `
86
- SHOW GLOBAL STATUS WHERE Variable_name IN ('Slow_queries', 'Questions')
85
+ const slowQuery = `
86
+ SHOW GLOBAL STATUS WHERE Variable_name IN ('Slow_queries', 'Questions')
87
87
  `;
88
88
  const slowResult = await this.db.query(slowQuery);
89
89
  metrics.slow_queries = {};
@@ -121,23 +121,23 @@ class PerformanceTools {
121
121
  error: "Limit must be a positive integer between 1 and 100",
122
122
  };
123
123
  }
124
- const query = `
125
- SELECT
126
- DIGEST_TEXT as query_pattern,
127
- COUNT_STAR as execution_count,
128
- ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
129
- ROUND(MAX_TIMER_WAIT / 1000000000000, 6) as max_execution_time_sec,
130
- ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
131
- ROUND(SUM_LOCK_TIME / 1000000000000, 6) as total_lock_time_sec,
132
- SUM_ROWS_EXAMINED as rows_examined,
133
- SUM_ROWS_SENT as rows_sent,
134
- SUM_ROWS_AFFECTED as rows_affected,
135
- FIRST_SEEN,
136
- LAST_SEEN
137
- FROM performance_schema.events_statements_summary_by_digest
138
- WHERE DIGEST_TEXT IS NOT NULL
139
- ORDER BY SUM_TIMER_WAIT DESC
140
- LIMIT ${limit}
124
+ const query = `
125
+ SELECT
126
+ DIGEST_TEXT as query_pattern,
127
+ COUNT_STAR as execution_count,
128
+ ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
129
+ ROUND(MAX_TIMER_WAIT / 1000000000000, 6) as max_execution_time_sec,
130
+ ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
131
+ ROUND(SUM_LOCK_TIME / 1000000000000, 6) as total_lock_time_sec,
132
+ SUM_ROWS_EXAMINED as rows_examined,
133
+ SUM_ROWS_SENT as rows_sent,
134
+ SUM_ROWS_AFFECTED as rows_affected,
135
+ FIRST_SEEN,
136
+ LAST_SEEN
137
+ FROM performance_schema.events_statements_summary_by_digest
138
+ WHERE DIGEST_TEXT IS NOT NULL
139
+ ORDER BY SUM_TIMER_WAIT DESC
140
+ LIMIT ${limit}
141
141
  `;
142
142
  const results = await this.db.query(query);
143
143
  return {
@@ -164,21 +164,21 @@ class PerformanceTools {
164
164
  error: "Limit must be a positive integer between 1 and 100",
165
165
  };
166
166
  }
167
- const query = `
168
- SELECT
169
- DIGEST_TEXT as query_pattern,
170
- COUNT_STAR as execution_count,
171
- ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
172
- ROUND(MAX_TIMER_WAIT / 1000000000000, 6) as max_execution_time_sec,
173
- ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
174
- SUM_ROWS_EXAMINED as rows_examined,
175
- SUM_ROWS_SENT as rows_sent,
176
- FIRST_SEEN,
177
- LAST_SEEN
178
- FROM performance_schema.events_statements_summary_by_digest
179
- WHERE DIGEST_TEXT IS NOT NULL
180
- ORDER BY COUNT_STAR DESC
181
- LIMIT ${limit}
167
+ const query = `
168
+ SELECT
169
+ DIGEST_TEXT as query_pattern,
170
+ COUNT_STAR as execution_count,
171
+ ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
172
+ ROUND(MAX_TIMER_WAIT / 1000000000000, 6) as max_execution_time_sec,
173
+ ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
174
+ SUM_ROWS_EXAMINED as rows_examined,
175
+ SUM_ROWS_SENT as rows_sent,
176
+ FIRST_SEEN,
177
+ LAST_SEEN
178
+ FROM performance_schema.events_statements_summary_by_digest
179
+ WHERE DIGEST_TEXT IS NOT NULL
180
+ ORDER BY COUNT_STAR DESC
181
+ LIMIT ${limit}
182
182
  `;
183
183
  const results = await this.db.query(query);
184
184
  return {
@@ -213,23 +213,23 @@ class PerformanceTools {
213
213
  };
214
214
  }
215
215
  const thresholdPico = thresholdSec * 1000000000000;
216
- const query = `
217
- SELECT
218
- DIGEST_TEXT as query_pattern,
219
- COUNT_STAR as execution_count,
220
- ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
221
- ROUND(MAX_TIMER_WAIT / 1000000000000, 6) as max_execution_time_sec,
222
- ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
223
- ROUND(SUM_LOCK_TIME / 1000000000000, 6) as total_lock_time_sec,
224
- SUM_ROWS_EXAMINED as rows_examined,
225
- SUM_ROWS_SENT as rows_sent,
226
- SUM_NO_INDEX_USED as no_index_used_count,
227
- FIRST_SEEN,
228
- LAST_SEEN
229
- FROM performance_schema.events_statements_summary_by_digest
230
- WHERE DIGEST_TEXT IS NOT NULL AND AVG_TIMER_WAIT > ${thresholdPico}
231
- ORDER BY AVG_TIMER_WAIT DESC
232
- LIMIT ${limit}
216
+ const query = `
217
+ SELECT
218
+ DIGEST_TEXT as query_pattern,
219
+ COUNT_STAR as execution_count,
220
+ ROUND(AVG_TIMER_WAIT / 1000000000000, 6) as avg_execution_time_sec,
221
+ ROUND(MAX_TIMER_WAIT / 1000000000000, 6) as max_execution_time_sec,
222
+ ROUND(SUM_TIMER_WAIT / 1000000000000, 6) as total_execution_time_sec,
223
+ ROUND(SUM_LOCK_TIME / 1000000000000, 6) as total_lock_time_sec,
224
+ SUM_ROWS_EXAMINED as rows_examined,
225
+ SUM_ROWS_SENT as rows_sent,
226
+ SUM_NO_INDEX_USED as no_index_used_count,
227
+ FIRST_SEEN,
228
+ LAST_SEEN
229
+ FROM performance_schema.events_statements_summary_by_digest
230
+ WHERE DIGEST_TEXT IS NOT NULL AND AVG_TIMER_WAIT > ${thresholdPico}
231
+ ORDER BY AVG_TIMER_WAIT DESC
232
+ LIMIT ${limit}
233
233
  `;
234
234
  const results = await this.db.query(query);
235
235
  return {
@@ -257,21 +257,21 @@ class PerformanceTools {
257
257
  error: "Limit must be a positive integer between 1 and 100",
258
258
  };
259
259
  }
260
- let query = `
261
- SELECT
262
- OBJECT_SCHEMA as table_schema,
263
- OBJECT_NAME as table_name,
264
- COUNT_READ as read_operations,
265
- COUNT_WRITE as write_operations,
266
- COUNT_FETCH as fetch_operations,
267
- COUNT_INSERT as insert_operations,
268
- COUNT_UPDATE as update_operations,
269
- COUNT_DELETE as delete_operations,
270
- ROUND(SUM_TIMER_READ / 1000000000000, 6) as total_read_time_sec,
271
- ROUND(SUM_TIMER_WRITE / 1000000000000, 6) as total_write_time_sec,
272
- ROUND(SUM_TIMER_FETCH / 1000000000000, 6) as total_fetch_time_sec
273
- FROM performance_schema.table_io_waits_summary_by_table
274
- WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
260
+ let query = `
261
+ SELECT
262
+ OBJECT_SCHEMA as table_schema,
263
+ OBJECT_NAME as table_name,
264
+ COUNT_READ as read_operations,
265
+ COUNT_WRITE as write_operations,
266
+ COUNT_FETCH as fetch_operations,
267
+ COUNT_INSERT as insert_operations,
268
+ COUNT_UPDATE as update_operations,
269
+ COUNT_DELETE as delete_operations,
270
+ ROUND(SUM_TIMER_READ / 1000000000000, 6) as total_read_time_sec,
271
+ ROUND(SUM_TIMER_WRITE / 1000000000000, 6) as total_write_time_sec,
272
+ ROUND(SUM_TIMER_FETCH / 1000000000000, 6) as total_fetch_time_sec
273
+ FROM performance_schema.table_io_waits_summary_by_table
274
+ WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
275
275
  `;
276
276
  if (schema) {
277
277
  query += ` AND OBJECT_SCHEMA = '${schema.replace(/'/g, "''")}'`;
@@ -303,21 +303,21 @@ class PerformanceTools {
303
303
  error: "Limit must be a positive integer between 1 and 100",
304
304
  };
305
305
  }
306
- let query = `
307
- SELECT
308
- OBJECT_SCHEMA as table_schema,
309
- OBJECT_NAME as table_name,
310
- INDEX_NAME as index_name,
311
- COUNT_STAR as usage_count,
312
- COUNT_READ as read_count,
313
- COUNT_WRITE as write_count,
314
- COUNT_FETCH as fetch_count,
315
- COUNT_INSERT as insert_count,
316
- COUNT_UPDATE as update_count,
317
- COUNT_DELETE as delete_count
318
- FROM performance_schema.table_io_waits_summary_by_index_usage
319
- WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
320
- AND INDEX_NAME IS NOT NULL
306
+ let query = `
307
+ SELECT
308
+ OBJECT_SCHEMA as table_schema,
309
+ OBJECT_NAME as table_name,
310
+ INDEX_NAME as index_name,
311
+ COUNT_STAR as usage_count,
312
+ COUNT_READ as read_count,
313
+ COUNT_WRITE as write_count,
314
+ COUNT_FETCH as fetch_count,
315
+ COUNT_INSERT as insert_count,
316
+ COUNT_UPDATE as update_count,
317
+ COUNT_DELETE as delete_count
318
+ FROM performance_schema.table_io_waits_summary_by_index_usage
319
+ WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
320
+ AND INDEX_NAME IS NOT NULL
321
321
  `;
322
322
  if (schema) {
323
323
  query += ` AND OBJECT_SCHEMA = '${schema.replace(/'/g, "''")}'`;
@@ -342,26 +342,26 @@ class PerformanceTools {
342
342
  async getUnusedIndexes(params) {
343
343
  try {
344
344
  const schema = params?.table_schema;
345
- let query = `
346
- SELECT
347
- t.TABLE_SCHEMA as table_schema,
348
- t.TABLE_NAME as table_name,
349
- s.INDEX_NAME as index_name,
350
- s.COLUMN_NAME as column_name,
351
- s.SEQ_IN_INDEX as sequence_in_index,
352
- s.NON_UNIQUE as is_non_unique
353
- FROM information_schema.STATISTICS s
354
- LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage p
355
- ON s.TABLE_SCHEMA = p.OBJECT_SCHEMA
356
- AND s.TABLE_NAME = p.OBJECT_NAME
357
- AND s.INDEX_NAME = p.INDEX_NAME
358
- JOIN information_schema.TABLES t
359
- ON s.TABLE_SCHEMA = t.TABLE_SCHEMA
360
- AND s.TABLE_NAME = t.TABLE_NAME
361
- WHERE s.TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
362
- AND s.INDEX_NAME != 'PRIMARY'
363
- AND (p.INDEX_NAME IS NULL OR p.COUNT_STAR = 0)
364
- AND t.TABLE_TYPE = 'BASE TABLE'
345
+ let query = `
346
+ SELECT
347
+ t.TABLE_SCHEMA as table_schema,
348
+ t.TABLE_NAME as table_name,
349
+ s.INDEX_NAME as index_name,
350
+ s.COLUMN_NAME as column_name,
351
+ s.SEQ_IN_INDEX as sequence_in_index,
352
+ s.NON_UNIQUE as is_non_unique
353
+ FROM information_schema.STATISTICS s
354
+ LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage p
355
+ ON s.TABLE_SCHEMA = p.OBJECT_SCHEMA
356
+ AND s.TABLE_NAME = p.OBJECT_NAME
357
+ AND s.INDEX_NAME = p.INDEX_NAME
358
+ JOIN information_schema.TABLES t
359
+ ON s.TABLE_SCHEMA = t.TABLE_SCHEMA
360
+ AND s.TABLE_NAME = t.TABLE_NAME
361
+ WHERE s.TABLE_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
362
+ AND s.INDEX_NAME != 'PRIMARY'
363
+ AND (p.INDEX_NAME IS NULL OR p.COUNT_STAR = 0)
364
+ AND t.TABLE_TYPE = 'BASE TABLE'
365
365
  `;
366
366
  if (schema) {
367
367
  query += ` AND s.TABLE_SCHEMA = '${schema.replace(/'/g, "''")}'`;
@@ -385,18 +385,18 @@ class PerformanceTools {
385
385
  */
386
386
  async getConnectionPoolStats() {
387
387
  try {
388
- const statusQuery = `
389
- SHOW GLOBAL STATUS WHERE Variable_name IN (
390
- 'Threads_connected', 'Threads_running', 'Threads_created', 'Threads_cached',
391
- 'Connections', 'Max_used_connections', 'Max_used_connections_time',
392
- 'Aborted_connects', 'Aborted_clients', 'Connection_errors_max_connections'
393
- )
388
+ const statusQuery = `
389
+ SHOW GLOBAL STATUS WHERE Variable_name IN (
390
+ 'Threads_connected', 'Threads_running', 'Threads_created', 'Threads_cached',
391
+ 'Connections', 'Max_used_connections', 'Max_used_connections_time',
392
+ 'Aborted_connects', 'Aborted_clients', 'Connection_errors_max_connections'
393
+ )
394
394
  `;
395
- const variablesQuery = `
396
- SHOW GLOBAL VARIABLES WHERE Variable_name IN (
397
- 'max_connections', 'thread_cache_size', 'connect_timeout',
398
- 'wait_timeout', 'interactive_timeout'
399
- )
395
+ const variablesQuery = `
396
+ SHOW GLOBAL VARIABLES WHERE Variable_name IN (
397
+ 'max_connections', 'thread_cache_size', 'connect_timeout',
398
+ 'wait_timeout', 'interactive_timeout'
399
+ )
400
400
  `;
401
401
  const statusResult = await this.db.query(statusQuery);
402
402
  const variablesResult = await this.db.query(variablesQuery);
@@ -461,8 +461,8 @@ class PerformanceTools {
461
461
  errors: [],
462
462
  };
463
463
  // Check 1: Connection usage
464
- const connQuery = `
465
- SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected', 'Max_used_connections')
464
+ const connQuery = `
465
+ SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected', 'Max_used_connections')
466
466
  `;
467
467
  const maxConnQuery = `SHOW GLOBAL VARIABLES WHERE Variable_name = 'max_connections'`;
468
468
  const connResult = await this.db.query(connQuery);
@@ -498,8 +498,8 @@ class PerformanceTools {
498
498
  health.overall_status = "warning";
499
499
  }
500
500
  // Check 2: Buffer pool hit ratio
501
- const bufferQuery = `
502
- SHOW GLOBAL STATUS WHERE Variable_name IN ('Innodb_buffer_pool_read_requests', 'Innodb_buffer_pool_reads')
501
+ const bufferQuery = `
502
+ SHOW GLOBAL STATUS WHERE Variable_name IN ('Innodb_buffer_pool_read_requests', 'Innodb_buffer_pool_reads')
503
503
  `;
504
504
  const bufferResult = await this.db.query(bufferQuery);
505
505
  let readRequests = 0;
@@ -526,8 +526,8 @@ class PerformanceTools {
526
526
  health.overall_status = "warning";
527
527
  }
528
528
  // Check 3: Aborted connections
529
- const abortQuery = `
530
- SHOW GLOBAL STATUS WHERE Variable_name IN ('Connections', 'Aborted_connects')
529
+ const abortQuery = `
530
+ SHOW GLOBAL STATUS WHERE Variable_name IN ('Connections', 'Aborted_connects')
531
531
  `;
532
532
  const abortResult = await this.db.query(abortQuery);
533
533
  let totalConnections = 0;
@@ -557,8 +557,8 @@ class PerformanceTools {
557
557
  health.overall_status = "warning";
558
558
  }
559
559
  // Check 4: Slow queries
560
- const slowQuery = `
561
- SHOW GLOBAL STATUS WHERE Variable_name IN ('Questions', 'Slow_queries')
560
+ const slowQuery = `
561
+ SHOW GLOBAL STATUS WHERE Variable_name IN ('Questions', 'Slow_queries')
562
562
  `;
563
563
  const slowResult = await this.db.query(slowQuery);
564
564
  let questions = 0;
@@ -217,15 +217,15 @@ class SecurityAuditTools {
217
217
  async tryReadUserAccounts() {
218
218
  try {
219
219
  // We intentionally do NOT return authentication_string or password hashes.
220
- const rows = await this.db.query(`
221
- SELECT
222
- User as user,
223
- Host as host,
224
- account_locked,
225
- password_expired,
226
- plugin
227
- FROM mysql.user
228
- WHERE User NOT IN ('mysql.sys', 'mysql.session', 'mysql.infoschema')
220
+ const rows = await this.db.query(`
221
+ SELECT
222
+ User as user,
223
+ Host as host,
224
+ account_locked,
225
+ password_expired,
226
+ plugin
227
+ FROM mysql.user
228
+ WHERE User NOT IN ('mysql.sys', 'mysql.session', 'mysql.infoschema')
229
229
  `);
230
230
  const findings = [];
231
231
  const anonymous = rows.filter((r) => !r.user);
@@ -282,10 +282,10 @@ class SecurityAuditTools {
282
282
  async tryReadPrivilegeSummaries() {
283
283
  try {
284
284
  // These INFORMATION_SCHEMA views are generally less sensitive than mysql.user.
285
- const schemaPriv = await this.db.query(`
286
- SELECT GRANTEE as grantee, TABLE_SCHEMA as schema_name, PRIVILEGE_TYPE as privilege_type
287
- FROM INFORMATION_SCHEMA.SCHEMA_PRIVILEGES
288
- WHERE TABLE_SCHEMA NOT IN ('mysql','performance_schema','information_schema','sys')
285
+ const schemaPriv = await this.db.query(`
286
+ SELECT GRANTEE as grantee, TABLE_SCHEMA as schema_name, PRIVILEGE_TYPE as privilege_type
287
+ FROM INFORMATION_SCHEMA.SCHEMA_PRIVILEGES
288
+ WHERE TABLE_SCHEMA NOT IN ('mysql','performance_schema','information_schema','sys')
289
289
  `);
290
290
  const findings = [];
291
291
  const anyAll = schemaPriv.filter((r) => (r.privilege_type || "").toUpperCase() === "ALL PRIVILEGES");
@@ -38,6 +38,14 @@ export declare class UtilityTools {
38
38
  data?: any;
39
39
  error?: string;
40
40
  }>;
41
+ /**
42
+ * Lists all available tools in this MySQL MCP server
43
+ */
44
+ listAllTools(): Promise<{
45
+ status: string;
46
+ data?: any;
47
+ error?: string;
48
+ }>;
41
49
  /**
42
50
  * Reads the CHANGELOG.md file from the project root
43
51
  */
@@ -292,6 +292,45 @@ class UtilityTools {
292
292
  };
293
293
  }
294
294
  }
295
+ /**
296
+ * Lists all available tools in this MySQL MCP server
297
+ */
298
+ async listAllTools() {
299
+ try {
300
+ // Read manifest.json to get tool definitions
301
+ const manifestPath = path_1.default.resolve(__dirname, "..", "..", "manifest.json");
302
+ if (!fs_1.default.existsSync(manifestPath)) {
303
+ return {
304
+ status: "error",
305
+ error: "manifest.json not found in project root.",
306
+ };
307
+ }
308
+ const manifestContent = fs_1.default.readFileSync(manifestPath, "utf-8");
309
+ const manifest = JSON.parse(manifestContent);
310
+ const tools = manifest.tools.map((tool) => ({
311
+ name: tool.name,
312
+ description: tool.description,
313
+ input_schema: tool.input_schema,
314
+ output_schema: tool.output_schema
315
+ }));
316
+ return {
317
+ status: "success",
318
+ data: {
319
+ total_tools: tools.length,
320
+ server_name: manifest.name,
321
+ server_version: manifest.version,
322
+ server_description: manifest.description,
323
+ tools: tools
324
+ }
325
+ };
326
+ }
327
+ catch (error) {
328
+ return {
329
+ status: "error",
330
+ error: `Failed to list tools: ${error.message}`,
331
+ };
332
+ }
333
+ }
295
334
  /**
296
335
  * Reads the CHANGELOG.md file from the project root
297
336
  */
package/manifest.json CHANGED
@@ -404,7 +404,7 @@
404
404
  },
405
405
  {
406
406
  "name": "get_all_tables_relationships",
407
- "description": "Gets foreign key relationships for ALL tables in a single call. Processes relationships in memory to avoid multiple queries.",
407
+ "description": "Gets all table foreign key relationships in one call with memory-efficient relationship mapping.",
408
408
  "input_schema": {
409
409
  "type": "object",
410
410
  "properties": {
@@ -784,6 +784,42 @@
784
784
  "error": { "type": ["string", "null"] }
785
785
  }
786
786
  }
787
+ },
788
+ {
789
+ "name": "list_all_tools",
790
+ "description": "Lists all available tools in this MySQL MCP server with their descriptions and schemas.",
791
+ "input_schema": {
792
+ "type": "object",
793
+ "properties": {}
794
+ },
795
+ "output_schema": {
796
+ "type": "object",
797
+ "properties": {
798
+ "status": { "type": "string" },
799
+ "data": {
800
+ "type": "object",
801
+ "properties": {
802
+ "total_tools": { "type": "number" },
803
+ "server_name": { "type": "string" },
804
+ "server_version": { "type": "string" },
805
+ "server_description": { "type": "string" },
806
+ "tools": {
807
+ "type": "array",
808
+ "items": {
809
+ "type": "object",
810
+ "properties": {
811
+ "name": { "type": "string" },
812
+ "description": { "type": "string" },
813
+ "input_schema": { "type": "object" },
814
+ "output_schema": { "type": "object" }
815
+ }
816
+ }
817
+ }
818
+ }
819
+ },
820
+ "error": { "type": ["string", "null"] }
821
+ }
822
+ }
787
823
  }
788
824
  ]
789
825
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@berthojoris/mcp-mysql-server",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Model Context Protocol server for MySQL database integration with dynamic per-project permissions, backup/restore, data import/export, and data migration capabilities",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",