@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.
@@ -1,222 +0,0 @@
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
- try {
12
- const astOrArray = parser.astify(sql, { database: "mysql" });
13
- const statements = Array.isArray(astOrArray) ? astOrArray : [astOrArray];
14
- for (const stmt of statements) {
15
- if (stmt.type === "use" && stmt.db) {
16
- return stmt.db;
17
- }
18
- const tables = stmt.table || stmt.from;
19
- if (Array.isArray(tables)) {
20
- for (const t of tables) {
21
- if (t.db) {
22
- return t.db;
23
- }
24
- }
25
- }
26
- else if (tables && typeof tables === "object" && tables.db) {
27
- return tables.db;
28
- }
29
- }
30
- }
31
- catch (err) {
32
- log("error", "Failed to parse SQL for schema extraction:", err.message);
33
- }
34
- return defaultSchema;
35
- }
36
- const EXPLAIN_MODIFIER_RE = /^(\s*EXPLAIN\s+)((?:ANALYZE\s+|EXTENDED\s+|PARTITIONS\s+|FORMAT\s*=\s*\w+\s+)+)/i;
37
- function stripExplainModifiers(sql) {
38
- return sql.replace(EXPLAIN_MODIFIER_RE, "$1");
39
- }
40
- async function getQueryTypes(query) {
41
- try {
42
- log("info", "Parsing SQL query: ", query);
43
- const normalised = stripExplainModifiers(query);
44
- const astOrArray = parser.astify(normalised, { database: "mysql" });
45
- const statements = Array.isArray(astOrArray) ? astOrArray : [astOrArray];
46
- return statements.map((stmt) => stmt.type?.toLowerCase() ?? "unknown");
47
- }
48
- catch (err) {
49
- log("error", "sqlParser error, query: ", query);
50
- log("error", "Error parsing SQL query:", err);
51
- throw new Error(`Parsing failed: ${err.message}`);
52
- }
53
- }
54
- function containsSelectStar(sql) {
55
- let astOrArray;
56
- try {
57
- astOrArray = parser.astify(sql, { database: "mysql" });
58
- }
59
- catch {
60
- return false;
61
- }
62
- const statements = Array.isArray(astOrArray) ? astOrArray : [astOrArray];
63
- return statements.some((stmt) => nodeHasColumnStar(stmt));
64
- }
65
- function nodeHasColumnStar(node) {
66
- if (node == null || typeof node !== "object")
67
- return false;
68
- if (node instanceof Date)
69
- return false;
70
- const obj = node;
71
- if (obj.type === "column_ref" && obj.column === "*")
72
- return true;
73
- for (const value of Object.values(obj)) {
74
- if (Array.isArray(value)) {
75
- if (value.some((item) => nodeHasColumnStar(item)))
76
- return true;
77
- }
78
- else if (value && typeof value === "object") {
79
- if (nodeHasColumnStar(value))
80
- return true;
81
- }
82
- }
83
- return false;
84
- }
85
- function findPIIColumnReferences(sql, isPII) {
86
- let astOrArray;
87
- try {
88
- astOrArray = parser.astify(sql, { database: "mysql" });
89
- }
90
- catch {
91
- return [];
92
- }
93
- const statements = Array.isArray(astOrArray) ? astOrArray : [astOrArray];
94
- const seen = new Set();
95
- const hits = [];
96
- for (const stmt of statements) {
97
- collectPIIColumnRefs(stmt, isPII, seen, hits);
98
- }
99
- return hits;
100
- }
101
- function collectPIIColumnRefs(node, isPII, seen, out) {
102
- if (node == null || typeof node !== "object")
103
- return;
104
- if (node instanceof Date)
105
- return;
106
- const obj = node;
107
- if (obj.type === "column_ref" &&
108
- typeof obj.column === "string" &&
109
- obj.column !== "*" &&
110
- isPII(obj.column)) {
111
- const table = typeof obj.table === "string" ? obj.table : null;
112
- const key = `${table ?? ""}.${obj.column}`;
113
- if (!seen.has(key)) {
114
- seen.add(key);
115
- out.push({ table, column: obj.column });
116
- }
117
- }
118
- for (const value of Object.values(obj)) {
119
- if (Array.isArray(value)) {
120
- for (const item of value)
121
- collectPIIColumnRefs(item, isPII, seen, out);
122
- }
123
- else if (value && typeof value === "object") {
124
- collectPIIColumnRefs(value, isPII, seen, out);
125
- }
126
- }
127
- }
128
- const SHOW_INTROSPECTION_RE = /^\s*SHOW\s+(?:FULL\s+)?(COLUMNS|FIELDS|CREATE\s+TABLE|CREATE\s+VIEW|INDEX(?:ES)?|KEYS|TABLE\s+STATUS|TABLES|DATABASES|SCHEMAS|CHARACTER\s+SET|CHARSET|COLLATION)\b/i;
129
- const DESCRIBE_RE = /^\s*(?:DESCRIBE|DESC)\s+/i;
130
- const EXPLAIN_TABLE_RE = /^\s*EXPLAIN\s+(?!SELECT\b|INSERT\b|UPDATE\b|DELETE\b|REPLACE\b|ANALYZE\b|FORMAT\b|FOR\s|EXTENDED\b|PARTITIONS\b|\()[A-Za-z_`]/i;
131
- function isIntrospectionQuery(sql) {
132
- const showMatch = sql.match(SHOW_INTROSPECTION_RE);
133
- if (showMatch) {
134
- const keyword = showMatch[1].toUpperCase().replace(/\s+/g, " ");
135
- if (keyword.startsWith("COLUMNS") || keyword.startsWith("FIELDS")) {
136
- return { kind: "show_columns" };
137
- }
138
- if (keyword.startsWith("CREATE")) {
139
- return { kind: "show_create" };
140
- }
141
- if (keyword.startsWith("INDEX") ||
142
- keyword.startsWith("KEYS")) {
143
- return { kind: "show_index" };
144
- }
145
- if (keyword === "TABLES" ||
146
- keyword.startsWith("TABLE STATUS") ||
147
- keyword === "DATABASES" ||
148
- keyword === "SCHEMAS" ||
149
- keyword.startsWith("CHARACTER") ||
150
- keyword === "CHARSET" ||
151
- keyword === "COLLATION") {
152
- return { kind: "show_passthrough" };
153
- }
154
- return { kind: "show_other" };
155
- }
156
- if (DESCRIBE_RE.test(sql) || EXPLAIN_TABLE_RE.test(sql)) {
157
- return { kind: "describe" };
158
- }
159
- let astOrArray;
160
- try {
161
- astOrArray = parser.astify(sql, { database: "mysql" });
162
- }
163
- catch {
164
- return { kind: null };
165
- }
166
- const statements = Array.isArray(astOrArray) ? astOrArray : [astOrArray];
167
- for (const stmt of statements) {
168
- const kind = findIntrospectionKind(stmt);
169
- if (kind)
170
- return { kind };
171
- }
172
- return { kind: null };
173
- }
174
- function findIntrospectionKind(node) {
175
- if (node == null || typeof node !== "object")
176
- return null;
177
- if (node instanceof Date)
178
- return null;
179
- const obj = node;
180
- if (obj.type === "show") {
181
- const keyword = typeof obj.keyword === "string" ? obj.keyword.toLowerCase() : "";
182
- if (keyword === "columns" || keyword === "fields")
183
- return "show_columns";
184
- if (keyword === "create")
185
- return "show_create";
186
- if (keyword === "index" || keyword === "keys")
187
- return "show_index";
188
- if (keyword === "tables" ||
189
- keyword === "databases" ||
190
- keyword === "character" ||
191
- keyword === "collation") {
192
- return "show_passthrough";
193
- }
194
- return "show_other";
195
- }
196
- if (obj.type === "desc" || obj.type === "describe") {
197
- return "describe";
198
- }
199
- if (typeof obj.db === "string") {
200
- const db = obj.db.toLowerCase();
201
- if (db === "information_schema")
202
- return "information_schema";
203
- if (db === "mysql")
204
- return "mysql_schema";
205
- }
206
- for (const value of Object.values(obj)) {
207
- if (Array.isArray(value)) {
208
- for (const item of value) {
209
- const kind = findIntrospectionKind(item);
210
- if (kind)
211
- return kind;
212
- }
213
- }
214
- else if (value && typeof value === "object") {
215
- const kind = findIntrospectionKind(value);
216
- if (kind)
217
- return kind;
218
- }
219
- }
220
- return null;
221
- }
222
- export { extractSchemaFromQuery, getQueryTypes, containsSelectStar, findPIIColumnReferences, isIntrospectionQuery, };
@@ -1,230 +0,0 @@
1
- import { log } from "../utils/index.js";
2
- export const DEFAULT_PII_COLUMNS = [
3
- "email",
4
- "e_mail",
5
- "ssn",
6
- "social_security",
7
- "phone",
8
- "mobile",
9
- "first_name",
10
- "last_name",
11
- "full_name",
12
- "middle_name",
13
- "given_name",
14
- "family_name",
15
- "surname",
16
- "forename",
17
- "maiden_name",
18
- "preferred_name",
19
- "legal_name",
20
- "address",
21
- "street",
22
- "zip",
23
- "postal_code",
24
- "dob",
25
- "date_of_birth",
26
- "credit_card",
27
- "card_number",
28
- "cc_number",
29
- "cvv",
30
- "password",
31
- "passwd",
32
- "pwd",
33
- "api_key",
34
- "secret",
35
- "token",
36
- "ip_address",
37
- "ipaddress",
38
- ];
39
- const EMAIL_RE = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*@([A-Za-z0-9-])[A-Za-z0-9.-]*\.([A-Za-z]{2,})/g;
40
- const SSN_RE = /(?<!\d)\d{3}-\d{2}-(\d{4})(?!\d)/g;
41
- const US_PHONE_RE = /(?<!\d)(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?(\d{4})(?!\d)/g;
42
- const IPV4_RE = /(?<!\d)(?:\d{1,3}\.){3}(\d{1,3})(?!\d)/g;
43
- const CARD_RE = /(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)/g;
44
- export function maskEmail(value) {
45
- return value.replace(EMAIL_RE, (_m, local, domain, tld) => `${local}***@${domain}***.${tld}`);
46
- }
47
- export function maskPhone(value) {
48
- return value.replace(US_PHONE_RE, (_m, last4) => `***-***-${last4}`);
49
- }
50
- export function maskSSN(value) {
51
- return value.replace(SSN_RE, (_m, last4) => `***-**-${last4}`);
52
- }
53
- export function maskIP(value) {
54
- return value.replace(IPV4_RE, (_m, last) => `***.***.***.${last}`);
55
- }
56
- function isLuhnValid(digits) {
57
- if (digits.length < 13 || digits.length > 19)
58
- return false;
59
- let sum = 0;
60
- let alt = false;
61
- for (let i = digits.length - 1; i >= 0; i--) {
62
- const code = digits.charCodeAt(i);
63
- if (code < 48 || code > 57)
64
- return false;
65
- let n = code - 48;
66
- if (alt) {
67
- n *= 2;
68
- if (n > 9)
69
- n -= 9;
70
- }
71
- sum += n;
72
- alt = !alt;
73
- }
74
- return sum % 10 === 0;
75
- }
76
- export function maskCard(value) {
77
- return value.replace(CARD_RE, (match) => {
78
- const digits = match.replace(/[ -]/g, "");
79
- if (!isLuhnValid(digits))
80
- return match;
81
- const last4 = digits.slice(-4);
82
- return `****-****-****-${last4}`;
83
- });
84
- }
85
- export function maskGeneric(value) {
86
- if (value.length === 0)
87
- return value;
88
- if (value.length === 1)
89
- return "*";
90
- const keep = value.slice(0, 1);
91
- const tailLen = Math.min(value.length - 1, 8);
92
- return keep + "*".repeat(tailLen);
93
- }
94
- export function applyPatternMasks(value) {
95
- let out = value;
96
- out = maskEmail(out);
97
- out = maskCard(out);
98
- out = maskSSN(out);
99
- out = maskPhone(out);
100
- out = maskIP(out);
101
- return out;
102
- }
103
- function matchPIIColumn(key, columns, patterns) {
104
- const lower = key.toLowerCase();
105
- for (const needle of columns) {
106
- if (lower.includes(needle))
107
- return `substring "${needle}"`;
108
- }
109
- for (const pattern of patterns) {
110
- if (pattern.test(lower))
111
- return `pattern ${pattern}`;
112
- }
113
- return null;
114
- }
115
- export function isPIIColumn(key, columns = DEFAULT_PII_COLUMNS, patterns = []) {
116
- return matchPIIColumn(key, columns, patterns) !== null;
117
- }
118
- function maskByColumn(value) {
119
- const patternMasked = applyPatternMasks(value);
120
- if (patternMasked !== value)
121
- return patternMasked;
122
- return maskGeneric(value);
123
- }
124
- export function redactPII(value, options = {}) {
125
- const cleanedExtras = options.extraColumns?.filter((s) => s.length > 0);
126
- const columns = options.columns ??
127
- (cleanedExtras && cleanedExtras.length > 0
128
- ? [...DEFAULT_PII_COLUMNS, ...cleanedExtras]
129
- : DEFAULT_PII_COLUMNS);
130
- const patterns = options.columnPatterns ?? [];
131
- const parseJsonStrings = options.parseJsonStrings !== false;
132
- const stats = { fields: 0, columns: new Set() };
133
- const result = walk(value, columns, patterns, null, stats, parseJsonStrings);
134
- if (stats.fields > 0) {
135
- log("info", `[redact] masked ${stats.fields} field(s) across column(s): ${[...stats.columns].join(", ")}`);
136
- }
137
- else {
138
- log("info", "[redact] no PII fields detected");
139
- }
140
- return result;
141
- }
142
- const SHOW_COLUMNS_KEY = "field";
143
- const SHOW_INDEX_KEY = "column_name";
144
- function findKey(row, target) {
145
- for (const key of Object.keys(row)) {
146
- if (key.toLowerCase() === target)
147
- return key;
148
- }
149
- return null;
150
- }
151
- export function filterIntrospectionRows(rows, kind, isPII) {
152
- if (!Array.isArray(rows))
153
- return null;
154
- if (rows.length === 0)
155
- return rows;
156
- const keyName = kind === "show_index" ? SHOW_INDEX_KEY : SHOW_COLUMNS_KEY;
157
- const sample = rows[0];
158
- if (sample == null || typeof sample !== "object" || Array.isArray(sample)) {
159
- return null;
160
- }
161
- const sampleKey = findKey(sample, keyName);
162
- if (sampleKey === null)
163
- return null;
164
- let hidden = 0;
165
- const out = rows.filter((row) => {
166
- if (row == null || typeof row !== "object" || Array.isArray(row)) {
167
- return true;
168
- }
169
- const obj = row;
170
- const colKey = findKey(obj, keyName);
171
- if (colKey === null)
172
- return true;
173
- const value = obj[colKey];
174
- if (typeof value !== "string")
175
- return true;
176
- if (isPII(value)) {
177
- hidden++;
178
- return false;
179
- }
180
- return true;
181
- });
182
- if (hidden > 0) {
183
- log("info", `[redact] filtered ${hidden} PII column(s) from ${kind} result`);
184
- }
185
- return out;
186
- }
187
- function walk(value, columns, patterns, parentMatch, stats, parseJsonStrings) {
188
- if (value == null)
189
- return value;
190
- if (typeof value === "string") {
191
- if (parentMatch !== null) {
192
- stats.fields++;
193
- return maskByColumn(value);
194
- }
195
- if (parseJsonStrings) {
196
- const trimmed = value.trim();
197
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
198
- try {
199
- const parsed = JSON.parse(trimmed);
200
- if (typeof parsed === "object" && parsed !== null) {
201
- return JSON.stringify(walk(parsed, columns, patterns, null, stats, parseJsonStrings));
202
- }
203
- }
204
- catch {
205
- }
206
- }
207
- }
208
- return applyPatternMasks(value);
209
- }
210
- if (Array.isArray(value)) {
211
- return value.map((item) => walk(item, columns, patterns, parentMatch, stats, parseJsonStrings));
212
- }
213
- if (typeof value === "object") {
214
- if (value instanceof Date)
215
- return value;
216
- if (typeof Buffer !== "undefined" && Buffer.isBuffer(value))
217
- return value;
218
- const out = {};
219
- for (const [key, child] of Object.entries(value)) {
220
- const match = matchPIIColumn(key, columns, patterns);
221
- if (match !== null) {
222
- log("info", `[redact] column "${key}" matched PII rule: ${match}`);
223
- stats.columns.add(key);
224
- }
225
- out[key] = walk(child, columns, patterns, match, stats, parseJsonStrings);
226
- }
227
- return out;
228
- }
229
- return value;
230
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,123 +0,0 @@
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
- }