@berthojoris/mcp-mysql-server 1.10.5 → 1.13.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.
@@ -10,6 +10,7 @@ const index_js_2 = require("./index.js");
10
10
  // Layer 2 (Categories): MCP_CATEGORIES (optional, for fine-grained control)
11
11
  const permissions = process.env.MCP_PERMISSIONS || process.env.MCP_CONFIG || "";
12
12
  const categories = process.env.MCP_CATEGORIES || "";
13
+ const preset = process.env.MCP_PRESET || process.env.MCP_PERMISSION_PRESET || "";
13
14
  // Declare the MySQL MCP instance (will be initialized in main())
14
15
  let mysqlMCP;
15
16
  // Define all available tools with their schemas
@@ -35,6 +36,79 @@ const TOOLS = [
35
36
  },
36
37
  },
37
38
  },
39
+ {
40
+ name: "get_database_summary",
41
+ description: "Get a high-level summary of the database (tables, columns, row counts) optimized for AI context.",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ database: {
46
+ type: "string",
47
+ description: "Optional: specific database name",
48
+ },
49
+ },
50
+ },
51
+ },
52
+ {
53
+ name: "get_schema_erd",
54
+ description: "Get a Mermaid.js ER diagram string representing the database schema and relationships.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ database: {
59
+ type: "string",
60
+ description: "Optional: specific database name",
61
+ },
62
+ },
63
+ },
64
+ },
65
+ {
66
+ name: "get_schema_rag_context",
67
+ description: "Return a compact schema-aware context pack (tables, PK/FK, columns, row estimates) optimized for RAG prompts.",
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ database: {
72
+ type: "string",
73
+ description: "Optional: specific database name",
74
+ },
75
+ max_tables: {
76
+ type: "number",
77
+ description: "Optional: maximum number of tables to include (default 50, max 200)",
78
+ },
79
+ max_columns: {
80
+ type: "number",
81
+ description: "Optional: maximum number of columns per table (default 12, max 200)",
82
+ },
83
+ include_relationships: {
84
+ type: "boolean",
85
+ description: "Whether to include FK relationships section (default: true)",
86
+ },
87
+ },
88
+ },
89
+ },
90
+ {
91
+ name: "get_column_statistics",
92
+ description: "Get detailed statistics for a specific column (min, max, avg, distinct counts, nulls).",
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {
96
+ table_name: {
97
+ type: "string",
98
+ description: "Name of the table",
99
+ },
100
+ column_name: {
101
+ type: "string",
102
+ description: "Name of the column",
103
+ },
104
+ database: {
105
+ type: "string",
106
+ description: "Optional: specific database name",
107
+ },
108
+ },
109
+ required: ["table_name", "column_name"],
110
+ },
111
+ },
38
112
  {
39
113
  name: "read_table_schema",
40
114
  description: "Reads the schema of a specified table, including columns, types, keys, and indexes.",
@@ -375,6 +449,10 @@ const TOOLS = [
375
449
  type: "boolean",
376
450
  description: "Whether to use query result caching (default: true)",
377
451
  },
452
+ dry_run: {
453
+ type: "boolean",
454
+ description: "If true, returns query plan and estimated cost without executing (Safe Mode)",
455
+ },
378
456
  },
379
457
  required: ["query"],
380
458
  },
@@ -398,6 +476,24 @@ const TOOLS = [
398
476
  required: ["query"],
399
477
  },
400
478
  },
479
+ {
480
+ name: "repair_query",
481
+ description: "Analyzes a SQL query (and optional error) to suggest repairs or optimizations using EXPLAIN and heuristics.",
482
+ inputSchema: {
483
+ type: "object",
484
+ properties: {
485
+ query: {
486
+ type: "string",
487
+ description: "The SQL query to analyze or repair",
488
+ },
489
+ error_message: {
490
+ type: "string",
491
+ description: "Optional error message received when executing the query",
492
+ },
493
+ },
494
+ required: ["query"],
495
+ },
496
+ },
401
497
  {
402
498
  name: "create_table",
403
499
  description: 'Creates a new table with the specified columns and indexes. Requires "ddl" permission.',
@@ -2563,7 +2659,7 @@ const TOOLS = [
2563
2659
  // Performance Monitoring Tools
2564
2660
  {
2565
2661
  name: "get_performance_metrics",
2566
- description: "Get comprehensive performance metrics including query performance, connection stats, buffer pool metrics, and InnoDB statistics.",
2662
+ description: "Get comprehensive MySQL performance metrics including query performance, connection stats, buffer pool metrics, and InnoDB statistics.",
2567
2663
  inputSchema: {
2568
2664
  type: "object",
2569
2665
  properties: {},
@@ -2687,7 +2783,7 @@ const TOOLS = [
2687
2783
  // Create the MCP server
2688
2784
  const server = new index_js_1.Server({
2689
2785
  name: "mysql-mcp-server",
2690
- version: "1.4.4",
2786
+ version: "1.12.0",
2691
2787
  }, {
2692
2788
  capabilities: {
2693
2789
  tools: {},
@@ -2721,38 +2817,20 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2721
2817
  case "list_tables":
2722
2818
  result = await mysqlMCP.listTables((args || {}));
2723
2819
  break;
2724
- case "read_table_schema":
2725
- result = await mysqlMCP.readTableSchema((args || {}));
2726
- break;
2727
- case "create_record":
2728
- result = await mysqlMCP.createRecord((args || {}));
2729
- break;
2730
- case "read_records":
2731
- result = await mysqlMCP.readRecords((args || {}));
2732
- break;
2733
- case "update_record":
2734
- result = await mysqlMCP.updateRecord((args || {}));
2735
- break;
2736
- case "delete_record":
2737
- result = await mysqlMCP.deleteRecord((args || {}));
2820
+ case "get_database_summary":
2821
+ result = await mysqlMCP.getDatabaseSummary((args || {}));
2738
2822
  break;
2739
- case "bulk_insert":
2740
- result = await mysqlMCP.bulkInsert((args || {}));
2823
+ case "get_schema_erd":
2824
+ result = await mysqlMCP.getSchemaERD((args || {}));
2741
2825
  break;
2742
- case "bulk_update":
2743
- result = await mysqlMCP.bulkUpdate((args || {}));
2826
+ case "get_schema_rag_context":
2827
+ result = await mysqlMCP.getSchemaRagContext((args || {}));
2744
2828
  break;
2745
- case "bulk_delete":
2746
- result = await mysqlMCP.bulkDelete((args || {}));
2829
+ case "get_column_statistics":
2830
+ result = await mysqlMCP.getColumnStatistics((args || {}));
2747
2831
  break;
2748
- case "run_query":
2749
- result = await mysqlMCP.runQuery((args || {}));
2750
- break;
2751
- case "execute_sql":
2752
- result = await mysqlMCP.executeSql((args || {}));
2753
- break;
2754
- case "create_table":
2755
- result = await mysqlMCP.createTable(args || {});
2832
+ case "read_table_schema":
2833
+ result = await mysqlMCP.readTableSchema((args || {}));
2756
2834
  break;
2757
2835
  case "alter_table":
2758
2836
  result = await mysqlMCP.alterTable(args || {});
@@ -3090,6 +3168,9 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
3090
3168
  case "reset_performance_stats":
3091
3169
  result = await mysqlMCP.resetPerformanceStats();
3092
3170
  break;
3171
+ case "repair_query":
3172
+ result = await mysqlMCP.repairQuery((args || {}));
3173
+ break;
3093
3174
  default:
3094
3175
  throw new Error(`Unknown tool: ${name}`);
3095
3176
  }
@@ -3173,20 +3254,20 @@ async function main() {
3173
3254
  await server.connect(transport);
3174
3255
  // Initialize the MySQL MCP instance AFTER transport is connected
3175
3256
  // This ensures the database connection pool is created when the server is ready
3176
- mysqlMCP = new index_js_2.MySQLMCP(permissions, categories);
3257
+ mysqlMCP = new index_js_2.MySQLMCP(permissions, categories, preset);
3177
3258
  // Log the effective filtering configuration to stderr
3178
- if (permissions && categories) {
3179
- console.error(`Active permissions (Layer 1): ${permissions}`);
3180
- console.error(`Active categories (Layer 2): ${categories}`);
3181
- console.error("Filtering mode: Dual-layer");
3259
+ const accessProfile = mysqlMCP.getAccessProfile();
3260
+ if (accessProfile.preset) {
3261
+ console.error(`Preset: ${accessProfile.preset.name} (${accessProfile.preset.description})`);
3182
3262
  }
3183
- else if (permissions) {
3184
- console.error(`Active permissions: ${permissions}`);
3185
- console.error("Filtering mode: Single-layer");
3263
+ else if (preset) {
3264
+ console.error(`Preset requested but not recognized: ${preset}`);
3186
3265
  }
3187
- else {
3188
- console.error("Active permissions: all (default)");
3266
+ console.error(`Permissions (resolved): ${accessProfile.permissions}`);
3267
+ if (accessProfile.categories) {
3268
+ console.error(`Categories (resolved): ${accessProfile.categories}`);
3189
3269
  }
3270
+ console.error(`Filtering mode: ${accessProfile.filteringMode}`);
3190
3271
  // Log to stderr (not stdout, which is used for MCP protocol)
3191
3272
  console.error("MySQL MCP Server running on stdio");
3192
3273
  }
@@ -0,0 +1,21 @@
1
+ export interface ExplainAnalysis {
2
+ complexity: "LOW" | "MEDIUM" | "HIGH";
3
+ issues: string[];
4
+ suggestions: string[];
5
+ summary: string;
6
+ }
7
+ export declare class ExplainAnalyzer {
8
+ private static instance;
9
+ private constructor();
10
+ static getInstance(): ExplainAnalyzer;
11
+ /**
12
+ * Analyze EXPLAIN output (assumed to be in JSON format or standard table format)
13
+ * Note: The server should prefer EXPLAIN FORMAT=JSON for better analysis,
14
+ * but we will handle the standard result set array as well.
15
+ */
16
+ analyze(explainResult: any[]): ExplainAnalysis;
17
+ private analyzeJsonFormat;
18
+ private traverseJsonPlan;
19
+ private analyzeRow;
20
+ private generateSummary;
21
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExplainAnalyzer = void 0;
4
+ class ExplainAnalyzer {
5
+ constructor() { }
6
+ static getInstance() {
7
+ if (!ExplainAnalyzer.instance) {
8
+ ExplainAnalyzer.instance = new ExplainAnalyzer();
9
+ }
10
+ return ExplainAnalyzer.instance;
11
+ }
12
+ /**
13
+ * Analyze EXPLAIN output (assumed to be in JSON format or standard table format)
14
+ * Note: The server should prefer EXPLAIN FORMAT=JSON for better analysis,
15
+ * but we will handle the standard result set array as well.
16
+ */
17
+ analyze(explainResult) {
18
+ const issues = [];
19
+ const suggestions = [];
20
+ let maxComplexity = 0;
21
+ // Check if result is empty
22
+ if (!explainResult || explainResult.length === 0) {
23
+ return {
24
+ complexity: "LOW",
25
+ issues: ["No EXPLAIN output returned"],
26
+ suggestions: [],
27
+ summary: "Query validation failed or produced no execution plan."
28
+ };
29
+ }
30
+ // Detect format (JSON string in first column or standard columns)
31
+ // If using EXPLAIN FORMAT=JSON, output is usually [{ EXPLAIN: '...json...' }]
32
+ const firstRow = explainResult[0];
33
+ if (firstRow && (firstRow['EXPLAIN'] || firstRow['JSON_EXPLAIN'])) {
34
+ return this.analyzeJsonFormat(firstRow['EXPLAIN'] || firstRow['JSON_EXPLAIN']);
35
+ }
36
+ // Analyze standard tabular format
37
+ for (const row of explainResult) {
38
+ this.analyzeRow(row, issues, suggestions);
39
+ // Heuristic for complexity
40
+ if (row.type === 'ALL')
41
+ maxComplexity = Math.max(maxComplexity, 3);
42
+ else if (row.type === 'index')
43
+ maxComplexity = Math.max(maxComplexity, 2);
44
+ else
45
+ maxComplexity = Math.max(maxComplexity, 1);
46
+ }
47
+ let complexity = "LOW";
48
+ if (maxComplexity >= 3)
49
+ complexity = "HIGH";
50
+ else if (maxComplexity === 2)
51
+ complexity = "MEDIUM";
52
+ return {
53
+ complexity,
54
+ issues: Array.from(new Set(issues)),
55
+ suggestions: Array.from(new Set(suggestions)),
56
+ summary: this.generateSummary(complexity, issues)
57
+ };
58
+ }
59
+ analyzeJsonFormat(jsonString) {
60
+ try {
61
+ const data = JSON.parse(jsonString);
62
+ const queryBlock = data.query_block || data; // Root might be query_block directly in some versions
63
+ const issues = [];
64
+ const suggestions = [];
65
+ this.traverseJsonPlan(queryBlock, issues, suggestions);
66
+ const distinctIssues = Array.from(new Set(issues));
67
+ const complexity = distinctIssues.some(i => i.includes("Full Table Scan")) ? "HIGH" : "MEDIUM"; // Simplified
68
+ return {
69
+ complexity,
70
+ issues: distinctIssues,
71
+ suggestions: Array.from(new Set(suggestions)),
72
+ summary: this.generateSummary(complexity, distinctIssues)
73
+ };
74
+ }
75
+ catch (e) {
76
+ return {
77
+ complexity: "LOW",
78
+ issues: ["Failed to parse EXPLAIN JSON output"],
79
+ suggestions: [],
80
+ summary: "Could not analyze the execution plan details."
81
+ };
82
+ }
83
+ }
84
+ traverseJsonPlan(node, issues, suggestions) {
85
+ if (!node)
86
+ return;
87
+ // Check for full table scans
88
+ if (node.access_type === 'ALL') {
89
+ issues.push(`Full Table Scan on table '${node.table_name || 'unknown'}'`);
90
+ suggestions.push(`Consider adding an index on table '${node.table_name || 'unknown'}' for the columns used in WHERE clause.`);
91
+ }
92
+ // Check for filesort
93
+ if (node.filesort || (node.extra && node.extra.includes('Using filesort'))) {
94
+ issues.push("Using Filesort (sorting without index)");
95
+ suggestions.push("Consider adding an index that matches the ORDER BY clause.");
96
+ }
97
+ // Check for temporary tables
98
+ if (node.temporary_table || (node.extra && node.extra.includes('Using temporary'))) {
99
+ issues.push("Using Temporary Table");
100
+ suggestions.push("Optimize query to avoid temporary tables (e.g. check GROUP BY or ORDER BY columns).");
101
+ }
102
+ // Recursive traversal (simplified)
103
+ if (node.nested_loop) {
104
+ for (const child of node.nested_loop) {
105
+ this.traverseJsonPlan(child.table, issues, suggestions);
106
+ }
107
+ }
108
+ // TODO: Add more structural traversal if needed for specific JSON structures
109
+ }
110
+ analyzeRow(row, issues, suggestions) {
111
+ const table = row.table || 'unknown';
112
+ const type = row.type;
113
+ const extra = row.Extra || '';
114
+ const possible_keys = row.possible_keys;
115
+ const key = row.key;
116
+ if (type === 'ALL') {
117
+ issues.push(`Full Table Scan on table '${table}'`);
118
+ suggestions.push(`Add an index to '${table}' to avoid full scan.`);
119
+ }
120
+ if (type === 'index') {
121
+ issues.push(`Full Index Scan on table '${table}'`);
122
+ suggestions.push(`Query scans entire index tree for '${table}'. Consider specific range or key.`);
123
+ }
124
+ if (!key && possible_keys && type !== 'ALL') {
125
+ // Indexes exist but not used? Rare case if not ALL.
126
+ }
127
+ if (extra.includes('Using filesort')) {
128
+ issues.push(`Using Filesort on table '${table}'`);
129
+ suggestions.push(`Add index for ORDER BY clause on '${table}'.`);
130
+ }
131
+ if (extra.includes('Using temporary')) {
132
+ issues.push(`Using Temporary Table for '${table}'`);
133
+ suggestions.push(`Check GROUP BY / ORDER BY columns on '${table}'.`);
134
+ }
135
+ if (extra.includes('Range checked for each record')) {
136
+ issues.push(`Inefficient join buffer usage on '${table}'`);
137
+ suggestions.push(`Check join conditions and indexes on '${table}'.`);
138
+ }
139
+ }
140
+ generateSummary(complexity, issues) {
141
+ if (issues.length === 0) {
142
+ return "Query plan looks good. No obvious performance issues detected.";
143
+ }
144
+ return `Query has ${complexity} complexity with ${issues.length} potential performance issue(s): ${issues.join("; ")}`;
145
+ }
146
+ }
147
+ exports.ExplainAnalyzer = ExplainAnalyzer;
@@ -0,0 +1,22 @@
1
+ import { SecurityLayer } from "../security/securityLayer";
2
+ export declare class AiTools {
3
+ private db;
4
+ private security;
5
+ private analyzer;
6
+ private optimizer;
7
+ constructor(security: SecurityLayer);
8
+ /**
9
+ * guided_query_fixer
10
+ * Analyzes a query (and optional error) to suggest repairs or optimizations using EXPLAIN.
11
+ */
12
+ repairQuery(params: {
13
+ query: string;
14
+ error_message?: string;
15
+ }): Promise<{
16
+ status: string;
17
+ analysis?: any;
18
+ fixed_query?: string;
19
+ suggestions?: string[];
20
+ error?: string;
21
+ }>;
22
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AiTools = void 0;
7
+ const connection_1 = __importDefault(require("../db/connection"));
8
+ const explainAnalyzer_1 = require("../optimization/explainAnalyzer");
9
+ const queryOptimizer_1 = require("../optimization/queryOptimizer");
10
+ class AiTools {
11
+ constructor(security) {
12
+ this.db = connection_1.default.getInstance();
13
+ this.security = security;
14
+ this.analyzer = explainAnalyzer_1.ExplainAnalyzer.getInstance();
15
+ this.optimizer = queryOptimizer_1.QueryOptimizer.getInstance();
16
+ }
17
+ /**
18
+ * guided_query_fixer
19
+ * Analyzes a query (and optional error) to suggest repairs or optimizations using EXPLAIN.
20
+ */
21
+ async repairQuery(params) {
22
+ const { query, error_message } = params;
23
+ // 1. If there is a syntax error provided, we can't run EXPLAIN.
24
+ // We try to provided simple heuristics or just return the analysis of the error.
25
+ if (error_message) {
26
+ return {
27
+ status: "success",
28
+ suggestions: [
29
+ "Check SQL syntax matching your MySQL version.",
30
+ "Verify table and column names using 'list_tables' or 'read_table_schema'.",
31
+ "Ensure string literals are quoted correctly."
32
+ ],
33
+ fixed_query: query, // We can't auto-fix syntax errors reliably without an LLM
34
+ analysis: {
35
+ issue: "Syntax/Execution Error",
36
+ details: error_message
37
+ }
38
+ };
39
+ }
40
+ // 2. Validate query generally (security check)
41
+ // We assume this tool is used by an agent who might have 'read' permissions at least.
42
+ // If the query is unsafe (e.g. injection), we return that.
43
+ const validation = this.security.validateQuery(query, true); // validate with execute permission simulation to check structure
44
+ if (!validation.valid) {
45
+ return {
46
+ status: "error",
47
+ error: `Query rejected by security layer: ${validation.error}`
48
+ };
49
+ }
50
+ // 3. Run EXPLAIN
51
+ try {
52
+ const explainQuery = `EXPLAIN FORMAT=JSON ${query}`;
53
+ // Note: We use the raw connection or executeSql equivalent.
54
+ // But EXPLAIN is safe-ish if the inner query is safe.
55
+ // validation passed, so we try EXPLAIN.
56
+ const explainResult = await this.db.query(explainQuery);
57
+ const analysis = this.analyzer.analyze(explainResult);
58
+ // 4. Try to apply simple fixes based on analysis (e.g. Missing Limit)
59
+ let fixedQuery = query;
60
+ if (analysis.complexity === "HIGH" && !query.toLowerCase().includes("limit")) {
61
+ // Suggest adding LIMIT if not present and complexity is high
62
+ analysis.suggestions.push("Consider adding 'LIMIT 100' to prevent massive data transfer.");
63
+ }
64
+ return {
65
+ status: "success",
66
+ analysis: analysis,
67
+ suggestions: analysis.suggestions,
68
+ fixed_query: fixedQuery
69
+ };
70
+ }
71
+ catch (e) {
72
+ return {
73
+ status: "error",
74
+ error: `Failed to analyze query: ${e.message}`,
75
+ suggestions: ["Verify the query is valid SQL before analyzing."]
76
+ };
77
+ }
78
+ }
79
+ }
80
+ exports.AiTools = AiTools;
@@ -0,0 +1,35 @@
1
+ import { SecurityLayer } from "../security/securityLayer";
2
+ export declare class AnalysisTools {
3
+ private db;
4
+ private security;
5
+ constructor(security: SecurityLayer);
6
+ /**
7
+ * Validate database access - ensures only the connected database can be accessed
8
+ */
9
+ private validateDatabaseAccess;
10
+ /**
11
+ * Get statistics for a specific column
12
+ */
13
+ getColumnStatistics(params: {
14
+ table_name: string;
15
+ column_name: string;
16
+ database?: string;
17
+ }): Promise<{
18
+ status: string;
19
+ data?: any;
20
+ error?: string;
21
+ }>;
22
+ /**
23
+ * Build a compact, schema-aware context pack for RAG (tables, PK/FK, columns, row estimates)
24
+ */
25
+ getSchemaRagContext(params?: {
26
+ database?: string;
27
+ max_tables?: number;
28
+ max_columns?: number;
29
+ include_relationships?: boolean;
30
+ }): Promise<{
31
+ status: string;
32
+ data?: any;
33
+ error?: string;
34
+ }>;
35
+ }