@dexical30/mcp-server-mysql 2.1.0 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexical30/mcp-server-mysql",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "MCP server for interacting with MySQL databases with write operations support",
5
5
  "license": "MIT",
6
6
  "author": "Ben Borla (https://github.com/benborla)",
@@ -21,9 +21,9 @@
21
21
  "scripts": {
22
22
  "start": "node dist/index.js",
23
23
  "dev": "ts-node index.ts",
24
- "build": "tsc && shx chmod +x dist/*.js",
24
+ "build": "tsc --noEmit && node scripts/build.mjs",
25
25
  "prepare": "npm run build",
26
- "watch": "tsc --watch",
26
+ "watch": "tsc --noEmit --watch",
27
27
  "setup:test:db": "tsx scripts/setup-test-db.ts",
28
28
  "pretest": "pnpm run setup:test:db",
29
29
  "test": "pnpm run setup:test:db && vitest run",
@@ -42,30 +42,26 @@
42
42
  "lint:eslint:fix": "eslint . --fix",
43
43
  "lint:markdown:fix": "markdownlint . --fix"
44
44
  },
45
- "dependencies": {
46
- "@ai-sdk/openai": "^1.3.22",
47
- "@modelcontextprotocol/sdk": "1.15.1",
48
- "dotenv": "^16.5.0",
49
- "express": "^5.1.0",
50
- "mcp-evals": "^1.0.18",
51
- "mysql2": "^3.14.1",
52
- "node-sql-parser": "^5.3.9",
53
- "zod": "^3.25.67"
54
- },
55
45
  "devDependencies": {
56
- "@types/express": "^5.0.3",
57
- "@types/jest": "^29.5.14",
58
- "@types/node": "^20.17.50",
59
- "@typescript-eslint/eslint-plugin": "^8.35.0",
60
- "@typescript-eslint/parser": "^8.35.0",
61
- "eslint": "^9.27.0",
62
- "markdownlint-cli": "^0.45.0",
63
- "shx": "^0.3.4",
64
- "ts-node": "^10.9.2",
65
- "tslib": "^2.8.1",
66
- "tsx": "^4.19.4",
67
- "typescript": "^5.8.3",
68
- "vitest": "^1.6.1"
46
+ "@modelcontextprotocol/sdk": "1.32.2",
47
+ "@types/express": "5.0.6",
48
+ "@types/jest": "29.5.14",
49
+ "@types/node": "20.19.43",
50
+ "@typescript-eslint/eslint-plugin": "8.72.1",
51
+ "@typescript-eslint/parser": "8.72.1",
52
+ "dotenv": "16.6.1",
53
+ "esbuild": "0.28.2",
54
+ "eslint": "9.39.5",
55
+ "express": "5.3.0",
56
+ "markdownlint-cli": "0.45.0",
57
+ "mysql2": "3.24.6",
58
+ "node-sql-parser": "5.4.0",
59
+ "ts-node": "10.9.2",
60
+ "tslib": "2.8.1",
61
+ "tsx": "4.23.15",
62
+ "typescript": "5.9.3",
63
+ "vitest": "1.6.1",
64
+ "zod": "4.6.7"
69
65
  },
70
66
  "publishConfig": {
71
67
  "access": "public"
package/dist/evals.js DELETED
@@ -1,32 +0,0 @@
1
- import { openai } from "@ai-sdk/openai";
2
- import { grade } from "mcp-evals";
3
- const mysqlQueryToolEval = {
4
- name: 'mysql_query Tool Evaluation',
5
- description: 'Evaluates the MySQL query execution functionality',
6
- run: async () => {
7
- const result = await grade(openai("gpt-4"), "Please execute the following SQL query and return the results: SELECT * FROM employees WHERE status='ACTIVE';");
8
- return JSON.parse(result);
9
- }
10
- };
11
- const mysqlQueryGenerationEval = {
12
- name: 'mysql_query Tool Generation Evaluation',
13
- description: 'Evaluates the MySQL query tool for correct SQL generation and execution',
14
- run: async () => {
15
- const result = await grade(openai("gpt-4"), "Use the mysql_query tool to select all rows from the 'users' table where isActive = 1. Provide the SQL query in the correct format.");
16
- return JSON.parse(result);
17
- }
18
- };
19
- const mysqlQueryColumnsEval = {
20
- name: 'mysql_query Columns Evaluation',
21
- description: 'Evaluates the mysql_query tool for column selection',
22
- run: async () => {
23
- const result = await grade(openai("gpt-4"), "Please provide a SQL query to retrieve the id, name, and email columns for all records in the users table.");
24
- return JSON.parse(result);
25
- }
26
- };
27
- const config = {
28
- model: openai("gpt-4"),
29
- evals: [mysqlQueryToolEval, mysqlQueryGenerationEval, mysqlQueryColumnsEval]
30
- };
31
- export default config;
32
- export const evals = [mysqlQueryToolEval, mysqlQueryGenerationEval, mysqlQueryColumnsEval];
@@ -1,166 +0,0 @@
1
- import * as dotenv from "dotenv";
2
- import * as fs from "fs";
3
- import { execFileSync } from "child_process";
4
- import { parseSchemaPermissions, parseMySQLConnectionString } from "../utils/index.js";
5
- function readSSLFile(filePath, label) {
6
- try {
7
- if (!fs.existsSync(filePath)) {
8
- throw new Error(`SSL ${label} file not found: ${filePath}`);
9
- }
10
- const data = fs.readFileSync(filePath);
11
- if (data.length === 0) {
12
- throw new Error(`SSL ${label} file is empty: ${filePath}`);
13
- }
14
- return data;
15
- }
16
- catch (error) {
17
- if (error instanceof Error) {
18
- if (error.message.startsWith('SSL ')) {
19
- throw error;
20
- }
21
- throw new Error(`Failed to read SSL ${label}: ${error.message}`);
22
- }
23
- throw error;
24
- }
25
- }
26
- function readCACertificate(filePath) {
27
- return readSSLFile(filePath, 'CA certificate');
28
- }
29
- function readPasswordFromOp(reference, opBin = "op") {
30
- if (!reference.startsWith("op://")) {
31
- throw new Error(`1Password secret reference must start with op://, got: ${reference}`);
32
- }
33
- try {
34
- return execFileSync(opBin, ["read", "--no-newline", reference], {
35
- encoding: "utf8",
36
- stdio: ["ignore", "pipe", "pipe"],
37
- timeout: 60000,
38
- });
39
- }
40
- catch (error) {
41
- const err = error;
42
- const detail = err.stderr?.trim() || err.message;
43
- throw new Error(`Failed to read MySQL password from 1Password (${reference}): ${detail}`);
44
- }
45
- }
46
- export const MCP_VERSION = "2.1.0";
47
- dotenv.config();
48
- const connectionStringConfig = process.env.MYSQL_CONNECTION_STRING
49
- ? parseMySQLConnectionString(process.env.MYSQL_CONNECTION_STRING)
50
- : {};
51
- const rawMysqlPass = process.env.MYSQL_PASS ?? "";
52
- const mysqlPassword = connectionStringConfig.password !== undefined
53
- ? connectionStringConfig.password
54
- : rawMysqlPass.startsWith("op://")
55
- ? readPasswordFromOp(rawMysqlPass, process.env.MYSQL_OP_PATH || "op")
56
- : rawMysqlPass;
57
- if (process.env.NODE_ENV === "test" && !process.env.MYSQL_DB) {
58
- process.env.MYSQL_DB = "mcp_test_db";
59
- }
60
- export const ALLOW_INSERT_OPERATION = process.env.ALLOW_INSERT_OPERATION === "true";
61
- export const ALLOW_UPDATE_OPERATION = process.env.ALLOW_UPDATE_OPERATION === "true";
62
- export const ALLOW_DELETE_OPERATION = process.env.ALLOW_DELETE_OPERATION === "true";
63
- export const ALLOW_DDL_OPERATION = process.env.ALLOW_DDL_OPERATION === "true";
64
- export const MYSQL_DISABLE_READ_ONLY_TRANSACTIONS = process.env.MYSQL_DISABLE_READ_ONLY_TRANSACTIONS === "true";
65
- export const ENABLE_PII_REDACTION = process.env.ENABLE_PII_REDACTION === "true";
66
- export const PII_EXTRA_COLUMNS = (process.env.PII_EXTRA_COLUMNS ?? "")
67
- .split(",")
68
- .map((s) => s.trim().toLowerCase())
69
- .filter((s) => s.length > 0);
70
- function parseColumnPatterns(raw) {
71
- if (!raw)
72
- return [];
73
- const out = [];
74
- for (const entry of raw.split(";")) {
75
- const trimmed = entry.trim();
76
- if (!trimmed)
77
- continue;
78
- try {
79
- out.push(new RegExp(trimmed, "i"));
80
- }
81
- catch (err) {
82
- console.error(`[config] ignoring invalid PII_EXTRA_COLUMN_PATTERNS entry "${trimmed}": ${err.message}`);
83
- }
84
- }
85
- return out;
86
- }
87
- export const PII_EXTRA_COLUMN_PATTERNS = parseColumnPatterns(process.env.PII_EXTRA_COLUMN_PATTERNS);
88
- export const PII_REDACT_JSON_STRINGS = process.env.PII_REDACT_JSON_STRINGS !== "false";
89
- export const PII_ALLOW_SELECT_STAR = process.env.PII_ALLOW_SELECT_STAR === "true";
90
- export const PII_ALLOW_REFERENCES = process.env.PII_ALLOW_REFERENCES === "true";
91
- export const PII_ALLOW_INTROSPECTION = process.env.PII_ALLOW_INTROSPECTION === "true";
92
- export const PII_BLOCK_INTROSPECTION = process.env.PII_BLOCK_INTROSPECTION === "true";
93
- export const SCHEMA_INSERT_PERMISSIONS = parseSchemaPermissions(process.env.SCHEMA_INSERT_PERMISSIONS);
94
- export const SCHEMA_UPDATE_PERMISSIONS = parseSchemaPermissions(process.env.SCHEMA_UPDATE_PERMISSIONS);
95
- export const SCHEMA_DELETE_PERMISSIONS = parseSchemaPermissions(process.env.SCHEMA_DELETE_PERMISSIONS);
96
- export const SCHEMA_DDL_PERMISSIONS = parseSchemaPermissions(process.env.SCHEMA_DDL_PERMISSIONS);
97
- export const IS_REMOTE_MCP = process.env.IS_REMOTE_MCP === "true";
98
- export const REMOTE_SECRET_KEY = process.env.REMOTE_SECRET_KEY || "";
99
- export const PORT = process.env.PORT || 3000;
100
- const dbFromEnvOrConnString = connectionStringConfig.database || process.env.MYSQL_DB;
101
- export const isMultiDbMode = !dbFromEnvOrConnString || dbFromEnvOrConnString.trim() === "";
102
- export const mcpConfig = {
103
- server: {
104
- name: "@dexical30/mcp-server-mysql",
105
- version: MCP_VERSION,
106
- connectionTypes: ["stdio", "streamableHttp"],
107
- },
108
- mysql: {
109
- ...(connectionStringConfig.socketPath || process.env.MYSQL_SOCKET_PATH
110
- ? {
111
- socketPath: connectionStringConfig.socketPath || process.env.MYSQL_SOCKET_PATH,
112
- }
113
- : {
114
- host: connectionStringConfig.host || process.env.MYSQL_HOST || "127.0.0.1",
115
- port: connectionStringConfig.port || Number(process.env.MYSQL_PORT || "3306"),
116
- }),
117
- user: connectionStringConfig.user || process.env.MYSQL_USER || "root",
118
- password: mysqlPassword,
119
- database: connectionStringConfig.database || process.env.MYSQL_DB || undefined,
120
- connectionLimit: 10,
121
- waitForConnections: true,
122
- queueLimit: process.env.MYSQL_QUEUE_LIMIT ? parseInt(process.env.MYSQL_QUEUE_LIMIT, 10) : 100,
123
- enableKeepAlive: true,
124
- keepAliveInitialDelay: 0,
125
- connectTimeout: process.env.MYSQL_CONNECT_TIMEOUT ? parseInt(process.env.MYSQL_CONNECT_TIMEOUT, 10) : 10000,
126
- authPlugins: {
127
- mysql_clear_password: () => () => Buffer.from(mysqlPassword),
128
- },
129
- ...(process.env.MYSQL_SSL === "true"
130
- ? {
131
- ssl: {
132
- rejectUnauthorized: process.env.MYSQL_SSL_REJECT_UNAUTHORIZED === "true",
133
- ...(process.env.MYSQL_SSL_CA
134
- ? { ca: readCACertificate(process.env.MYSQL_SSL_CA) }
135
- : {}),
136
- ...(process.env.MYSQL_SSL_CERT
137
- ? { cert: readSSLFile(process.env.MYSQL_SSL_CERT, 'client certificate') }
138
- : {}),
139
- ...(process.env.MYSQL_SSL_KEY
140
- ? { key: readSSLFile(process.env.MYSQL_SSL_KEY, 'client private key') }
141
- : {}),
142
- },
143
- }
144
- : {}),
145
- ...(process.env.MYSQL_TIMEZONE
146
- ? {
147
- timezone: process.env.MYSQL_TIMEZONE,
148
- }
149
- : {}),
150
- ...(process.env.MYSQL_DATE_STRINGS === "true"
151
- ? {
152
- dateStrings: true,
153
- }
154
- : {}),
155
- ...(process.env.MYSQL_BIG_NUMBER_STRINGS === "true"
156
- ? {
157
- supportBigNumbers: true,
158
- bigNumberStrings: true,
159
- }
160
- : {}),
161
- },
162
- paths: {
163
- schema: "schema",
164
- },
165
- };
166
- export { readCACertificate, readSSLFile, readPasswordFromOp };
@@ -1,375 +0,0 @@
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, containsSelectStar, findPIIColumnReferences, isIntrospectionQuery, } 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, ENABLE_PII_REDACTION, PII_EXTRA_COLUMNS, PII_EXTRA_COLUMN_PATTERNS, PII_REDACT_JSON_STRINGS, PII_ALLOW_SELECT_STAR, PII_ALLOW_REFERENCES, PII_ALLOW_INTROSPECTION, PII_BLOCK_INTROSPECTION, } from "./../config/index.js";
8
- import { redactPII, isPIIColumn, DEFAULT_PII_COLUMNS, filterIntrospectionRows, } from "./../security/redact.js";
9
- if (isMultiDbMode && process.env.MULTI_DB_WRITE_MODE !== "true") {
10
- log("error", "Multi-DB mode detected - enabling read-only mode for safety");
11
- }
12
- const isTestEnvironment = process.env.NODE_ENV === "test" || process.env.VITEST;
13
- function safeExit(code) {
14
- if (!isTestEnvironment) {
15
- process.exit(code);
16
- }
17
- else {
18
- log("error", `[Test mode] Would have called process.exit(${code})`);
19
- }
20
- }
21
- let poolPromise;
22
- const getPool = () => {
23
- if (!poolPromise) {
24
- poolPromise = new Promise((resolve, reject) => {
25
- try {
26
- const pool = mysql2.createPool(config.mysql);
27
- log("info", "MySQL pool created successfully");
28
- resolve(pool);
29
- }
30
- catch (error) {
31
- log("error", "Error creating MySQL pool:", error);
32
- reject(error);
33
- }
34
- });
35
- }
36
- return poolPromise;
37
- };
38
- async function executeQuery(sql, params = []) {
39
- let connection;
40
- try {
41
- const pool = await getPool();
42
- connection = await pool.getConnection();
43
- const result = await connection.query(sql, params);
44
- return (Array.isArray(result) ? result[0] : result);
45
- }
46
- catch (error) {
47
- log("error", "Error executing query:", error);
48
- throw error;
49
- }
50
- finally {
51
- if (connection) {
52
- connection.release();
53
- log("error", "Connection released");
54
- }
55
- }
56
- }
57
- async function executeWriteQuery(sql) {
58
- let connection;
59
- try {
60
- const pool = await getPool();
61
- connection = await pool.getConnection();
62
- log("error", "Write connection acquired");
63
- const schema = extractSchemaFromQuery(sql);
64
- await connection.beginTransaction();
65
- try {
66
- const startTime = performance.now();
67
- const result = await connection.query(sql);
68
- const endTime = performance.now();
69
- const duration = endTime - startTime;
70
- const response = Array.isArray(result) ? result[0] : result;
71
- await connection.commit();
72
- let responseText;
73
- const queryTypes = await getQueryTypes(sql);
74
- const isUpdateOperation = queryTypes.some((type) => ["update"].includes(type));
75
- const isInsertOperation = queryTypes.some((type) => ["insert"].includes(type));
76
- const isDeleteOperation = queryTypes.some((type) => ["delete"].includes(type));
77
- const isDDLOperation = queryTypes.some((type) => ["create", "alter", "drop", "truncate"].includes(type));
78
- if (isInsertOperation) {
79
- const resultHeader = response;
80
- responseText = `Insert successful on schema '${schema || "default"}'. Affected rows: ${resultHeader.affectedRows}, Last insert ID: ${resultHeader.insertId}`;
81
- }
82
- else if (isUpdateOperation) {
83
- const resultHeader = response;
84
- responseText = `Update successful on schema '${schema || "default"}'. Affected rows: ${resultHeader.affectedRows}, Changed rows: ${resultHeader.changedRows || 0}`;
85
- }
86
- else if (isDeleteOperation) {
87
- const resultHeader = response;
88
- responseText = `Delete successful on schema '${schema || "default"}'. Affected rows: ${resultHeader.affectedRows}`;
89
- }
90
- else if (isDDLOperation) {
91
- responseText = `DDL operation successful on schema '${schema || "default"}'.`;
92
- }
93
- else {
94
- responseText = JSON.stringify(response, null, 2);
95
- }
96
- return {
97
- content: [
98
- {
99
- type: "text",
100
- text: responseText,
101
- },
102
- {
103
- type: "text",
104
- text: `Query execution time: ${duration.toFixed(2)} ms`,
105
- },
106
- ],
107
- isError: false,
108
- };
109
- }
110
- catch (error) {
111
- log("error", "Error executing write query:", error);
112
- await connection.rollback();
113
- return {
114
- content: [
115
- {
116
- type: "text",
117
- text: `Error executing write operation: ${error instanceof Error ? error.message : String(error)}`,
118
- },
119
- ],
120
- isError: true,
121
- };
122
- }
123
- }
124
- catch (error) {
125
- log("error", "Error in write operation transaction:", error);
126
- return {
127
- content: [
128
- {
129
- type: "text",
130
- text: `Database connection error: ${error instanceof Error ? error.message : String(error)}`,
131
- },
132
- ],
133
- isError: true,
134
- };
135
- }
136
- finally {
137
- if (connection) {
138
- connection.release();
139
- log("error", "Write connection released");
140
- }
141
- }
142
- }
143
- async function executeReadOnlyQuery(sql) {
144
- let connection;
145
- try {
146
- if (ENABLE_PII_REDACTION &&
147
- !PII_ALLOW_SELECT_STAR &&
148
- containsSelectStar(sql)) {
149
- log("error", "Refusing query with SELECT * while PII redaction is enabled; project explicit columns instead.");
150
- return {
151
- content: [
152
- {
153
- type: "text",
154
- text: "Error: SELECT * (and `table.*`) is not permitted while PII redaction is enabled. " +
155
- "Project an explicit column list (e.g. SELECT col1, col2 FROM ...) so redacted columns are not accidentally returned. " +
156
- "Set PII_ALLOW_SELECT_STAR=true to override this policy.",
157
- },
158
- ],
159
- isError: true,
160
- };
161
- }
162
- let introspectionFilterKind = null;
163
- let isIntrospectionPassThrough = false;
164
- if (ENABLE_PII_REDACTION && !PII_ALLOW_INTROSPECTION) {
165
- const intro = isIntrospectionQuery(sql);
166
- if (intro.kind) {
167
- const filterable = intro.kind === "show_columns" ||
168
- intro.kind === "describe" ||
169
- intro.kind === "show_index"
170
- ? intro.kind
171
- : null;
172
- const passthrough = intro.kind === "show_passthrough";
173
- if (PII_BLOCK_INTROSPECTION || (!filterable && !passthrough)) {
174
- log("error", `Refusing introspection query (${intro.kind}) while PII redaction is enabled.`);
175
- return {
176
- content: [
177
- {
178
- type: "text",
179
- text: `Error: SQL introspection (${intro.kind}) is not permitted while PII redaction is enabled. ` +
180
- `Use the mysql://tables and mysql://tables/{name} MCP resources to inspect schemas (PII columns are filtered there), ` +
181
- `or use SHOW COLUMNS / DESCRIBE / SHOW INDEX (PII columns will be filtered from the result). ` +
182
- `Set PII_ALLOW_INTROSPECTION=true to bypass this policy entirely.`,
183
- },
184
- ],
185
- isError: true,
186
- };
187
- }
188
- if (filterable) {
189
- introspectionFilterKind = filterable;
190
- }
191
- else if (passthrough) {
192
- isIntrospectionPassThrough = true;
193
- }
194
- }
195
- }
196
- if (ENABLE_PII_REDACTION && !PII_ALLOW_REFERENCES) {
197
- const piiList = [...DEFAULT_PII_COLUMNS, ...PII_EXTRA_COLUMNS];
198
- const hits = findPIIColumnReferences(sql, (col) => isPIIColumn(col, piiList, PII_EXTRA_COLUMN_PATTERNS));
199
- if (hits.length > 0) {
200
- const names = hits
201
- .map((h) => (h.table ? `${h.table}.${h.column}` : h.column))
202
- .join(", ");
203
- log("error", `Refusing query referencing redacted column(s): ${names}.`);
204
- return {
205
- content: [
206
- {
207
- type: "text",
208
- text: `Error: query references redacted column(s): ${names}. ` +
209
- `These columns are protected by PII redaction policy and cannot be projected, ` +
210
- `filtered, joined, or ordered on. Choose a different projection, ` +
211
- `or set PII_ALLOW_REFERENCES=true to override this policy.`,
212
- },
213
- ],
214
- isError: true,
215
- };
216
- }
217
- }
218
- let queryTypes = [];
219
- let schema = null;
220
- let isUpdateOperation = false;
221
- let isInsertOperation = false;
222
- let isDeleteOperation = false;
223
- let isDDLOperation = false;
224
- if (!introspectionFilterKind && !isIntrospectionPassThrough) {
225
- queryTypes = await getQueryTypes(sql);
226
- schema = extractSchemaFromQuery(sql);
227
- isUpdateOperation = queryTypes.some((type) => ["update"].includes(type));
228
- isInsertOperation = queryTypes.some((type) => ["insert"].includes(type));
229
- isDeleteOperation = queryTypes.some((type) => ["delete"].includes(type));
230
- isDDLOperation = queryTypes.some((type) => ["create", "alter", "drop", "truncate"].includes(type));
231
- }
232
- if (isInsertOperation && !isInsertAllowedForSchema(schema)) {
233
- log("error", `INSERT operations are not allowed for schema '${schema || "default"}'. Configure SCHEMA_INSERT_PERMISSIONS.`);
234
- return {
235
- content: [
236
- {
237
- type: "text",
238
- text: `Error: INSERT operations are not allowed for schema '${schema || "default"}'. Ask the administrator to update SCHEMA_INSERT_PERMISSIONS.`,
239
- },
240
- ],
241
- isError: true,
242
- };
243
- }
244
- if (isUpdateOperation && !isUpdateAllowedForSchema(schema)) {
245
- log("error", `UPDATE operations are not allowed for schema '${schema || "default"}'. Configure SCHEMA_UPDATE_PERMISSIONS.`);
246
- return {
247
- content: [
248
- {
249
- type: "text",
250
- text: `Error: UPDATE operations are not allowed for schema '${schema || "default"}'. Ask the administrator to update SCHEMA_UPDATE_PERMISSIONS.`,
251
- },
252
- ],
253
- isError: true,
254
- };
255
- }
256
- if (isDeleteOperation && !isDeleteAllowedForSchema(schema)) {
257
- log("error", `DELETE operations are not allowed for schema '${schema || "default"}'. Configure SCHEMA_DELETE_PERMISSIONS.`);
258
- return {
259
- content: [
260
- {
261
- type: "text",
262
- text: `Error: DELETE operations are not allowed for schema '${schema || "default"}'. Ask the administrator to update SCHEMA_DELETE_PERMISSIONS.`,
263
- },
264
- ],
265
- isError: true,
266
- };
267
- }
268
- if (isDDLOperation && !isDDLAllowedForSchema(schema)) {
269
- log("error", `DDL operations are not allowed for schema '${schema || "default"}'. Configure SCHEMA_DDL_PERMISSIONS.`);
270
- return {
271
- content: [
272
- {
273
- type: "text",
274
- text: `Error: DDL operations are not allowed for schema '${schema || "default"}'. Ask the administrator to update SCHEMA_DDL_PERMISSIONS.`,
275
- },
276
- ],
277
- isError: true,
278
- };
279
- }
280
- if ((isInsertOperation && isInsertAllowedForSchema(schema)) ||
281
- (isUpdateOperation && isUpdateAllowedForSchema(schema)) ||
282
- (isDeleteOperation && isDeleteAllowedForSchema(schema)) ||
283
- (isDDLOperation && isDDLAllowedForSchema(schema))) {
284
- return executeWriteQuery(sql);
285
- }
286
- const pool = await getPool();
287
- connection = await pool.getConnection();
288
- log("error", "Read-only connection acquired");
289
- if (!MYSQL_DISABLE_READ_ONLY_TRANSACTIONS) {
290
- await connection.query("SET SESSION TRANSACTION READ ONLY");
291
- }
292
- else {
293
- log("info", "Read-only transactions disabled via MYSQL_DISABLE_READ_ONLY_TRANSACTIONS=true");
294
- }
295
- await connection.beginTransaction();
296
- try {
297
- const startTime = performance.now();
298
- const result = await connection.query(sql);
299
- const endTime = performance.now();
300
- const duration = endTime - startTime;
301
- const rows = Array.isArray(result) ? result[0] : result;
302
- await connection.rollback();
303
- if (!MYSQL_DISABLE_READ_ONLY_TRANSACTIONS) {
304
- await connection.query("SET SESSION TRANSACTION READ WRITE");
305
- }
306
- let intermediate = rows;
307
- if (introspectionFilterKind) {
308
- const piiList = [...DEFAULT_PII_COLUMNS, ...PII_EXTRA_COLUMNS];
309
- const filtered = filterIntrospectionRows(rows, introspectionFilterKind, (col) => isPIIColumn(col, piiList, PII_EXTRA_COLUMN_PATTERNS));
310
- if (filtered === null) {
311
- log("error", `Refusing introspection result (${introspectionFilterKind}): unrecognised row shape, cannot filter PII safely.`);
312
- return {
313
- content: [
314
- {
315
- type: "text",
316
- text: `Error: could not safely filter introspection result for kind '${introspectionFilterKind}'. ` +
317
- `Use SHOW COLUMNS / DESCRIBE / SHOW INDEX against a real table, ` +
318
- `or set PII_ALLOW_INTROSPECTION=true to bypass filtering.`,
319
- },
320
- ],
321
- isError: true,
322
- };
323
- }
324
- intermediate = filtered;
325
- }
326
- const payload = ENABLE_PII_REDACTION
327
- ? redactPII(intermediate, {
328
- extraColumns: PII_EXTRA_COLUMNS,
329
- columnPatterns: PII_EXTRA_COLUMN_PATTERNS,
330
- parseJsonStrings: PII_REDACT_JSON_STRINGS,
331
- })
332
- : intermediate;
333
- return {
334
- content: [
335
- {
336
- type: "text",
337
- text: JSON.stringify(payload, null, 2),
338
- },
339
- {
340
- type: "text",
341
- text: `Query execution time: ${duration.toFixed(2)} ms`,
342
- },
343
- ],
344
- isError: false,
345
- };
346
- }
347
- catch (error) {
348
- log("error", "Error executing read-only query:", error);
349
- await connection.rollback();
350
- throw error;
351
- }
352
- }
353
- catch (error) {
354
- log("error", "Error in read-only query transaction:", error);
355
- try {
356
- if (connection) {
357
- await connection.rollback();
358
- if (!MYSQL_DISABLE_READ_ONLY_TRANSACTIONS) {
359
- await connection.query("SET SESSION TRANSACTION READ WRITE");
360
- }
361
- }
362
- }
363
- catch (cleanupError) {
364
- log("error", "Error during cleanup:", cleanupError);
365
- }
366
- throw error;
367
- }
368
- finally {
369
- if (connection) {
370
- connection.release();
371
- log("error", "Read-only connection released");
372
- }
373
- }
374
- }
375
- export { isTestEnvironment, safeExit, executeQuery, getPool, executeWriteQuery, executeReadOnlyQuery, poolPromise, };
@@ -1,34 +0,0 @@
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, };