@cainli/mcp-server-mysql 2.0.8

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.
@@ -0,0 +1,267 @@
1
+ import { performance } from "perf_hooks";
2
+ import { isMultiDbMode } from "./../config/index.js";
3
+ import { isDDLAllowedForSchema, isInsertAllowedForSchema, isUpdateAllowedForSchema, isDeleteAllowedForSchema, } from "./permissions.js";
4
+ import { extractSchemaFromQuery, getQueryTypes } from "./utils.js";
5
+ import * as mysql2 from "mysql2/promise";
6
+ import { log } from "./../utils/index.js";
7
+ import { mcpConfig as config, MYSQL_DISABLE_READ_ONLY_TRANSACTIONS } from "./../config/index.js";
8
+ if (isMultiDbMode && process.env.MULTI_DB_WRITE_MODE !== "true") {
9
+ log("error", "Multi-DB mode detected - enabling read-only mode for safety");
10
+ }
11
+ const isTestEnvironment = process.env.NODE_ENV === "test" || process.env.VITEST;
12
+ function safeExit(code) {
13
+ if (!isTestEnvironment) {
14
+ process.exit(code);
15
+ }
16
+ else {
17
+ log("error", `[Test mode] Would have called process.exit(${code})`);
18
+ }
19
+ }
20
+ let poolPromise;
21
+ const getPool = () => {
22
+ if (!poolPromise) {
23
+ poolPromise = new Promise((resolve, reject) => {
24
+ try {
25
+ const pool = mysql2.createPool(config.mysql);
26
+ log("info", "MySQL pool created successfully");
27
+ resolve(pool);
28
+ }
29
+ catch (error) {
30
+ log("error", "Error creating MySQL pool:", error);
31
+ reject(error);
32
+ }
33
+ });
34
+ }
35
+ return poolPromise;
36
+ };
37
+ async function executeQuery(sql, params = []) {
38
+ let connection;
39
+ try {
40
+ const pool = await getPool();
41
+ connection = await pool.getConnection();
42
+ const result = await connection.query(sql, params);
43
+ return (Array.isArray(result) ? result[0] : result);
44
+ }
45
+ catch (error) {
46
+ log("error", "Error executing query:", error);
47
+ throw error;
48
+ }
49
+ finally {
50
+ if (connection) {
51
+ connection.release();
52
+ log("error", "Connection released");
53
+ }
54
+ }
55
+ }
56
+ async function executeWriteQuery(sql) {
57
+ let connection;
58
+ try {
59
+ const pool = await getPool();
60
+ connection = await pool.getConnection();
61
+ log("error", "Write connection acquired");
62
+ const schema = extractSchemaFromQuery(sql);
63
+ await connection.beginTransaction();
64
+ try {
65
+ const startTime = performance.now();
66
+ const result = await connection.query(sql);
67
+ const endTime = performance.now();
68
+ const duration = endTime - startTime;
69
+ const response = Array.isArray(result) ? result[0] : result;
70
+ await connection.commit();
71
+ let responseText;
72
+ const queryTypes = await getQueryTypes(sql);
73
+ const isUpdateOperation = queryTypes.some((type) => ["update"].includes(type));
74
+ const isInsertOperation = queryTypes.some((type) => ["insert"].includes(type));
75
+ const isDeleteOperation = queryTypes.some((type) => ["delete"].includes(type));
76
+ const isDDLOperation = queryTypes.some((type) => ["create", "alter", "drop", "truncate"].includes(type));
77
+ if (isInsertOperation) {
78
+ const resultHeader = response;
79
+ responseText = `Insert successful on schema '${schema || "default"}'. Affected rows: ${resultHeader.affectedRows}, Last insert ID: ${resultHeader.insertId}`;
80
+ }
81
+ else if (isUpdateOperation) {
82
+ const resultHeader = response;
83
+ responseText = `Update successful on schema '${schema || "default"}'. Affected rows: ${resultHeader.affectedRows}, Changed rows: ${resultHeader.changedRows || 0}`;
84
+ }
85
+ else if (isDeleteOperation) {
86
+ const resultHeader = response;
87
+ responseText = `Delete successful on schema '${schema || "default"}'. Affected rows: ${resultHeader.affectedRows}`;
88
+ }
89
+ else if (isDDLOperation) {
90
+ responseText = `DDL operation successful on schema '${schema || "default"}'.`;
91
+ }
92
+ else {
93
+ responseText = JSON.stringify(response, null, 2);
94
+ }
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: responseText,
100
+ },
101
+ {
102
+ type: "text",
103
+ text: `Query execution time: ${duration.toFixed(2)} ms`,
104
+ },
105
+ ],
106
+ isError: false,
107
+ };
108
+ }
109
+ catch (error) {
110
+ log("error", "Error executing write query:", error);
111
+ await connection.rollback();
112
+ return {
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: `Error executing write operation: ${error instanceof Error ? error.message : String(error)}`,
117
+ },
118
+ ],
119
+ isError: true,
120
+ };
121
+ }
122
+ }
123
+ catch (error) {
124
+ log("error", "Error in write operation transaction:", error);
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: `Database connection error: ${error instanceof Error ? error.message : String(error)}`,
130
+ },
131
+ ],
132
+ isError: true,
133
+ };
134
+ }
135
+ finally {
136
+ if (connection) {
137
+ connection.release();
138
+ log("error", "Write connection released");
139
+ }
140
+ }
141
+ }
142
+ async function executeReadOnlyQuery(sql) {
143
+ let connection;
144
+ try {
145
+ const queryTypes = await getQueryTypes(sql);
146
+ const schema = extractSchemaFromQuery(sql);
147
+ const isUpdateOperation = queryTypes.some((type) => ["update"].includes(type));
148
+ const isInsertOperation = queryTypes.some((type) => ["insert"].includes(type));
149
+ const isDeleteOperation = queryTypes.some((type) => ["delete"].includes(type));
150
+ const isDDLOperation = queryTypes.some((type) => ["create", "alter", "drop", "truncate"].includes(type));
151
+ if (isInsertOperation && !isInsertAllowedForSchema(schema)) {
152
+ log("error", `INSERT operations are not allowed for schema '${schema || "default"}'. Configure SCHEMA_INSERT_PERMISSIONS.`);
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: `Error: INSERT operations are not allowed for schema '${schema || "default"}'. Ask the administrator to update SCHEMA_INSERT_PERMISSIONS.`,
158
+ },
159
+ ],
160
+ isError: true,
161
+ };
162
+ }
163
+ if (isUpdateOperation && !isUpdateAllowedForSchema(schema)) {
164
+ log("error", `UPDATE operations are not allowed for schema '${schema || "default"}'. Configure SCHEMA_UPDATE_PERMISSIONS.`);
165
+ return {
166
+ content: [
167
+ {
168
+ type: "text",
169
+ text: `Error: UPDATE operations are not allowed for schema '${schema || "default"}'. Ask the administrator to update SCHEMA_UPDATE_PERMISSIONS.`,
170
+ },
171
+ ],
172
+ isError: true,
173
+ };
174
+ }
175
+ if (isDeleteOperation && !isDeleteAllowedForSchema(schema)) {
176
+ log("error", `DELETE operations are not allowed for schema '${schema || "default"}'. Configure SCHEMA_DELETE_PERMISSIONS.`);
177
+ return {
178
+ content: [
179
+ {
180
+ type: "text",
181
+ text: `Error: DELETE operations are not allowed for schema '${schema || "default"}'. Ask the administrator to update SCHEMA_DELETE_PERMISSIONS.`,
182
+ },
183
+ ],
184
+ isError: true,
185
+ };
186
+ }
187
+ if (isDDLOperation && !isDDLAllowedForSchema(schema)) {
188
+ log("error", `DDL operations are not allowed for schema '${schema || "default"}'. Configure SCHEMA_DDL_PERMISSIONS.`);
189
+ return {
190
+ content: [
191
+ {
192
+ type: "text",
193
+ text: `Error: DDL operations are not allowed for schema '${schema || "default"}'. Ask the administrator to update SCHEMA_DDL_PERMISSIONS.`,
194
+ },
195
+ ],
196
+ isError: true,
197
+ };
198
+ }
199
+ if ((isInsertOperation && isInsertAllowedForSchema(schema)) ||
200
+ (isUpdateOperation && isUpdateAllowedForSchema(schema)) ||
201
+ (isDeleteOperation && isDeleteAllowedForSchema(schema)) ||
202
+ (isDDLOperation && isDDLAllowedForSchema(schema))) {
203
+ return executeWriteQuery(sql);
204
+ }
205
+ const pool = await getPool();
206
+ connection = await pool.getConnection();
207
+ log("error", "Read-only connection acquired");
208
+ if (!MYSQL_DISABLE_READ_ONLY_TRANSACTIONS) {
209
+ await connection.query("SET SESSION TRANSACTION READ ONLY");
210
+ }
211
+ else {
212
+ log("info", "Read-only transactions disabled via MYSQL_DISABLE_READ_ONLY_TRANSACTIONS=true");
213
+ }
214
+ await connection.beginTransaction();
215
+ try {
216
+ const startTime = performance.now();
217
+ const result = await connection.query(sql);
218
+ const endTime = performance.now();
219
+ const duration = endTime - startTime;
220
+ const rows = Array.isArray(result) ? result[0] : result;
221
+ await connection.rollback();
222
+ if (!MYSQL_DISABLE_READ_ONLY_TRANSACTIONS) {
223
+ await connection.query("SET SESSION TRANSACTION READ WRITE");
224
+ }
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: JSON.stringify(rows, null, 2),
230
+ },
231
+ {
232
+ type: "text",
233
+ text: `Query execution time: ${duration.toFixed(2)} ms`,
234
+ },
235
+ ],
236
+ isError: false,
237
+ };
238
+ }
239
+ catch (error) {
240
+ log("error", "Error executing read-only query:", error);
241
+ await connection.rollback();
242
+ throw error;
243
+ }
244
+ }
245
+ catch (error) {
246
+ log("error", "Error in read-only query transaction:", error);
247
+ try {
248
+ if (connection) {
249
+ await connection.rollback();
250
+ if (!MYSQL_DISABLE_READ_ONLY_TRANSACTIONS) {
251
+ await connection.query("SET SESSION TRANSACTION READ WRITE");
252
+ }
253
+ }
254
+ }
255
+ catch (cleanupError) {
256
+ log("error", "Error during cleanup:", cleanupError);
257
+ }
258
+ throw error;
259
+ }
260
+ finally {
261
+ if (connection) {
262
+ connection.release();
263
+ log("error", "Read-only connection released");
264
+ }
265
+ }
266
+ }
267
+ export { isTestEnvironment, safeExit, executeQuery, getPool, executeWriteQuery, executeReadOnlyQuery, poolPromise, };
@@ -0,0 +1,34 @@
1
+ import { ALLOW_DELETE_OPERATION, ALLOW_DDL_OPERATION, ALLOW_INSERT_OPERATION, ALLOW_UPDATE_OPERATION, SCHEMA_DELETE_PERMISSIONS, SCHEMA_DDL_PERMISSIONS, SCHEMA_INSERT_PERMISSIONS, SCHEMA_UPDATE_PERMISSIONS, } from "../config/index.js";
2
+ function isInsertAllowedForSchema(schema) {
3
+ if (!schema) {
4
+ return ALLOW_INSERT_OPERATION;
5
+ }
6
+ return schema in SCHEMA_INSERT_PERMISSIONS
7
+ ? SCHEMA_INSERT_PERMISSIONS[schema]
8
+ : ALLOW_INSERT_OPERATION;
9
+ }
10
+ function isUpdateAllowedForSchema(schema) {
11
+ if (!schema) {
12
+ return ALLOW_UPDATE_OPERATION;
13
+ }
14
+ return schema in SCHEMA_UPDATE_PERMISSIONS
15
+ ? SCHEMA_UPDATE_PERMISSIONS[schema]
16
+ : ALLOW_UPDATE_OPERATION;
17
+ }
18
+ function isDeleteAllowedForSchema(schema) {
19
+ if (!schema) {
20
+ return ALLOW_DELETE_OPERATION;
21
+ }
22
+ return schema in SCHEMA_DELETE_PERMISSIONS
23
+ ? SCHEMA_DELETE_PERMISSIONS[schema]
24
+ : ALLOW_DELETE_OPERATION;
25
+ }
26
+ function isDDLAllowedForSchema(schema) {
27
+ if (!schema) {
28
+ return ALLOW_DDL_OPERATION;
29
+ }
30
+ return schema in SCHEMA_DDL_PERMISSIONS
31
+ ? SCHEMA_DDL_PERMISSIONS[schema]
32
+ : ALLOW_DDL_OPERATION;
33
+ }
34
+ export { isInsertAllowedForSchema, isUpdateAllowedForSchema, isDeleteAllowedForSchema, isDDLAllowedForSchema, };
@@ -0,0 +1,34 @@
1
+ import { isMultiDbMode } from "./../config/index.js";
2
+ import { log } from "./../utils/index.js";
3
+ import SqlParser from "node-sql-parser";
4
+ const { Parser } = SqlParser;
5
+ const parser = new Parser();
6
+ function extractSchemaFromQuery(sql) {
7
+ const defaultSchema = process.env.MYSQL_DB || null;
8
+ if (defaultSchema && !isMultiDbMode) {
9
+ return defaultSchema;
10
+ }
11
+ const useMatch = sql.match(/USE\s+`?([a-zA-Z0-9_]+)`?/i);
12
+ if (useMatch && useMatch[1]) {
13
+ return useMatch[1];
14
+ }
15
+ const dbTableMatch = sql.match(/`?([a-zA-Z0-9_]+)`?\.`?[a-zA-Z0-9_]+`?/i);
16
+ if (dbTableMatch && dbTableMatch[1]) {
17
+ return dbTableMatch[1];
18
+ }
19
+ return defaultSchema;
20
+ }
21
+ async function getQueryTypes(query) {
22
+ try {
23
+ log("info", "Parsing SQL query: ", query);
24
+ const astOrArray = parser.astify(query, { database: "mysql" });
25
+ const statements = Array.isArray(astOrArray) ? astOrArray : [astOrArray];
26
+ return statements.map((stmt) => stmt.type?.toLowerCase() ?? "unknown");
27
+ }
28
+ catch (err) {
29
+ log("error", "sqlParser error, query: ", query);
30
+ log("error", "Error parsing SQL query:", err);
31
+ throw new Error(`Parsing failed: ${err.message}`);
32
+ }
33
+ }
34
+ export { extractSchemaFromQuery, getQueryTypes };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ const ENABLE_LOGGING = process.env.ENABLE_LOGGING === "true" || process.env.ENABLE_LOGGING === "1";
2
+ export function log(type = "info", ...args) {
3
+ if (!ENABLE_LOGGING)
4
+ return;
5
+ switch (type) {
6
+ case "info":
7
+ console.info(...args);
8
+ break;
9
+ case "error":
10
+ console.error(...args);
11
+ break;
12
+ default:
13
+ console.log(...args);
14
+ }
15
+ }
16
+ export function parseSchemaPermissions(permissionsString) {
17
+ const permissions = {};
18
+ if (!permissionsString) {
19
+ return permissions;
20
+ }
21
+ const permissionPairs = permissionsString.split(",");
22
+ for (const pair of permissionPairs) {
23
+ const [schema, value] = pair.split(":");
24
+ if (schema && value) {
25
+ permissions[schema.trim()] = value.trim() === "true";
26
+ }
27
+ }
28
+ return permissions;
29
+ }
30
+ export function parseMySQLConnectionString(connectionString) {
31
+ const config = {};
32
+ let cleanedString = connectionString.trim().replace(/^mysql\s+/, '');
33
+ const tokens = [];
34
+ let currentToken = '';
35
+ let inQuotes = false;
36
+ let quoteChar = null;
37
+ for (let i = 0; i < cleanedString.length; i++) {
38
+ const char = cleanedString[i];
39
+ if ((char === '"' || char === "'") && (!inQuotes || char === quoteChar)) {
40
+ inQuotes = !inQuotes;
41
+ quoteChar = inQuotes ? char : null;
42
+ }
43
+ else if (char === ' ' && !inQuotes) {
44
+ if (currentToken) {
45
+ tokens.push(currentToken);
46
+ currentToken = '';
47
+ }
48
+ }
49
+ else {
50
+ currentToken += char;
51
+ }
52
+ }
53
+ if (currentToken) {
54
+ tokens.push(currentToken);
55
+ }
56
+ for (let i = 0; i < tokens.length; i++) {
57
+ const token = tokens[i];
58
+ if (token.startsWith('-') && !token.startsWith('--')) {
59
+ const flag = token[1];
60
+ let value = token.substring(2);
61
+ if (!value && i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
62
+ value = tokens[i + 1];
63
+ i++;
64
+ }
65
+ switch (flag) {
66
+ case 'h':
67
+ config.host = value;
68
+ break;
69
+ case 'P': {
70
+ const port = parseInt(value, 10);
71
+ if (Number.isNaN(port) || !Number.isFinite(port) || port < 1 || port > 65535) {
72
+ throw new Error(`Invalid port: ${value}`);
73
+ }
74
+ config.port = port;
75
+ break;
76
+ }
77
+ case 'u':
78
+ config.user = value;
79
+ break;
80
+ case 'p':
81
+ config.password = value;
82
+ break;
83
+ case 'S':
84
+ config.socketPath = value;
85
+ break;
86
+ }
87
+ }
88
+ else if (token.startsWith('--')) {
89
+ const [flag, ...valueParts] = token.substring(2).split('=');
90
+ let value = valueParts.join('=');
91
+ if (!value && i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
92
+ value = tokens[i + 1];
93
+ i++;
94
+ }
95
+ switch (flag) {
96
+ case 'host':
97
+ config.host = value;
98
+ break;
99
+ case 'port': {
100
+ const port = parseInt(value, 10);
101
+ if (Number.isNaN(port) || !Number.isFinite(port) || port < 1 || port > 65535) {
102
+ throw new Error(`Invalid port: ${value}`);
103
+ }
104
+ config.port = port;
105
+ break;
106
+ }
107
+ case 'user':
108
+ config.user = value;
109
+ break;
110
+ case 'password':
111
+ config.password = value;
112
+ break;
113
+ case 'socket':
114
+ config.socketPath = value;
115
+ break;
116
+ }
117
+ }
118
+ else if (!token.startsWith('-')) {
119
+ config.database = token;
120
+ }
121
+ }
122
+ return config;
123
+ }
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@cainli/mcp-server-mysql",
3
+ "version": "2.0.8",
4
+ "description": "MCP server for interacting with MySQL databases with write operations support",
5
+ "license": "MIT",
6
+ "author": "cainli (https://github.com/cainli)",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "module": "index.ts",
10
+ "preferGlobal": true,
11
+ "bin": {
12
+ "mcp-server-mysql": "dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "assets"
18
+ ],
19
+ "scripts": {
20
+ "start": "node dist/index.js",
21
+ "dev": "ts-node index.ts",
22
+ "build": "tsc && shx chmod +x dist/*.js",
23
+ "prepare": "npm run build",
24
+ "watch": "tsc --watch",
25
+ "setup:test:db": "tsx scripts/setup-test-db.ts",
26
+ "pretest": "pnpm run setup:test:db",
27
+ "test": "pnpm run setup:test:db && vitest run",
28
+ "test:socket": "pnpm run setup:test:db && vitest run tests/integration/socket-connection.test.ts",
29
+ "test:watch": "pnpm run setup:test:db && vitest",
30
+ "test:coverage": "vitest run --coverage",
31
+ "test:unit": "vitest run --config vitest.unit.config.ts",
32
+ "test:integration": "vitest run --config vitest.integration.config.ts",
33
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
34
+ "stdio": "node dist/index.js --stdio",
35
+ "exec": " pnpm build && npx node --env-file=.env dist/index.js",
36
+ "lint": "npm run lint:eslint && npm run lint:markdown",
37
+ "lint:eslint": "eslint .",
38
+ "lint:markdown": "markdownlint .",
39
+ "lint:fix": "npm run lint:eslint:fix && npm run lint:markdown:fix",
40
+ "lint:eslint:fix": "eslint . --fix",
41
+ "lint:markdown:fix": "markdownlint . --fix"
42
+ },
43
+ "dependencies": {
44
+ "@ai-sdk/openai": "^1.3.22",
45
+ "@modelcontextprotocol/sdk": "1.15.1",
46
+ "dotenv": "^16.5.0",
47
+ "express": "^5.1.0",
48
+ "mcp-evals": "^1.0.18",
49
+ "mysql2": "^3.14.1",
50
+ "node-sql-parser": "^5.3.9",
51
+ "zod": "^3.25.67"
52
+ },
53
+ "devDependencies": {
54
+ "@types/express": "^5.0.3",
55
+ "@types/jest": "^29.5.14",
56
+ "@types/node": "^20.17.50",
57
+ "@typescript-eslint/eslint-plugin": "^8.35.0",
58
+ "@typescript-eslint/parser": "^8.35.0",
59
+ "eslint": "^9.27.0",
60
+ "markdownlint-cli": "^0.45.0",
61
+ "shx": "^0.3.4",
62
+ "ts-node": "^10.9.2",
63
+ "tslib": "^2.8.1",
64
+ "tsx": "^4.19.4",
65
+ "typescript": "^5.8.3",
66
+ "vitest": "^1.6.1"
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "keywords": [
72
+ "node",
73
+ "mcp",
74
+ "ai",
75
+ "cursor",
76
+ "mcp-server",
77
+ "modelcontextprotocol",
78
+ "smithery",
79
+ "mcp-get",
80
+ "mcp-put",
81
+ "mcp-post",
82
+ "mcp-delete",
83
+ "mcp-patch",
84
+ "mcp-options",
85
+ "mcp-head"
86
+ ]
87
+ }