@ecubelabs/mysql-mcp 0.0.0-stage → 0.0.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.
@@ -0,0 +1,264 @@
1
+ import SqlParser from 'node-sql-parser';
2
+ import { MYSQL_DB } from './../config/index.js';
3
+ import { log } from './../utils/index.js';
4
+ const { Parser } = SqlParser;
5
+ const parser = new Parser();
6
+ const READ_STATEMENT_TYPES = new Set(['select', 'show', 'desc', 'explain']);
7
+ const WRITE_STATEMENT_TYPES = new Set(['insert', 'update', 'delete', 'create', 'alter', 'drop', 'truncate']);
8
+ function findUnsupportedStatementType(queryTypes) {
9
+ return queryTypes.find((type) => !READ_STATEMENT_TYPES.has(type) && !WRITE_STATEMENT_TYPES.has(type)) ?? null;
10
+ }
11
+ function findFileExport(sql) {
12
+ let astOrArray;
13
+ try {
14
+ astOrArray = parser.astify(stripExplainModifiers(sql), { database: 'mysql' });
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ const visit = (node) => {
20
+ if (!isRecord(node)) {
21
+ return null;
22
+ }
23
+ const into = node.into;
24
+ const keyword = typeof into?.keyword === 'string' ? into.keyword.toUpperCase() : null;
25
+ if (keyword === 'OUTFILE' || keyword === 'DUMPFILE') {
26
+ return `INTO ${keyword}`;
27
+ }
28
+ for (const value of Object.values(node)) {
29
+ const items = Array.isArray(value) ? value : [value];
30
+ for (const item of items) {
31
+ const found = visit(item);
32
+ if (found) {
33
+ return found;
34
+ }
35
+ }
36
+ }
37
+ return null;
38
+ };
39
+ return visit(astOrArray);
40
+ }
41
+ function databaseStatementName(node) {
42
+ if (typeof node.name === 'string') {
43
+ return node.name;
44
+ }
45
+ const schema = node.database?.schema?.[0]?.value;
46
+ return typeof schema === 'string' ? schema : null;
47
+ }
48
+ function extractReferencedSchemas(sql) {
49
+ let astOrArray;
50
+ try {
51
+ astOrArray = parser.astify(sql, { database: 'mysql' });
52
+ }
53
+ catch (err) {
54
+ log('error', 'Failed to parse SQL for schema extraction:', err.message);
55
+ return null;
56
+ }
57
+ const defaultSchema = MYSQL_DB ?? null;
58
+ const schemas = new Set();
59
+ const qualified = new Map();
60
+ const unqualified = [];
61
+ const visit = (node) => {
62
+ if (!isRecord(node)) {
63
+ return;
64
+ }
65
+ if ((node.keyword === 'database' || node.keyword === 'schema') && typeof node.type === 'string') {
66
+ schemas.add(databaseStatementName(node));
67
+ }
68
+ const name = typeof node.table === 'string' ? node.table : typeof node.view === 'string' ? node.view : null;
69
+ if (node.type !== 'column_ref' && name && (typeof node.db === 'string' || node.db === null)) {
70
+ if (typeof node.db === 'string' && node.db) {
71
+ schemas.add(node.db);
72
+ qualified.set(name.toLowerCase(), node.db);
73
+ if (typeof node.as === 'string' && node.as) {
74
+ qualified.set(node.as.toLowerCase(), node.db);
75
+ }
76
+ }
77
+ else {
78
+ unqualified.push(name.toLowerCase());
79
+ }
80
+ }
81
+ for (const value of Object.values(node)) {
82
+ if (Array.isArray(value)) {
83
+ for (const item of value) {
84
+ visit(item);
85
+ }
86
+ }
87
+ else {
88
+ visit(value);
89
+ }
90
+ }
91
+ };
92
+ visit(astOrArray);
93
+ for (const name of unqualified) {
94
+ schemas.add(qualified.get(name) ?? defaultSchema);
95
+ }
96
+ if (schemas.size === 0) {
97
+ schemas.add(null);
98
+ }
99
+ return [...schemas];
100
+ }
101
+ const EXPLAIN_MODIFIER_RE = /^(\s*EXPLAIN\s+)((?:ANALYZE\s+|EXTENDED\s+|PARTITIONS\s+|FORMAT\s*=\s*\w+\s+)+)/i;
102
+ function stripExplainModifiers(sql) {
103
+ return sql.replace(EXPLAIN_MODIFIER_RE, '$1');
104
+ }
105
+ async function getQueryTypes(query) {
106
+ try {
107
+ log('info', `Parsing SQL query (${query.length} chars)`);
108
+ const normalised = stripExplainModifiers(query);
109
+ const astOrArray = parser.astify(normalised, { database: 'mysql' });
110
+ const statements = Array.isArray(astOrArray) ? astOrArray : [astOrArray];
111
+ return statements.map((stmt) => stmt.type?.toLowerCase() ?? 'unknown');
112
+ }
113
+ catch (err) {
114
+ log('error', 'Error parsing SQL query:', err.message);
115
+ throw new Error(`Parsing failed: ${err.message}`);
116
+ }
117
+ }
118
+ 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;
119
+ const DESCRIBE_RE = /^\s*(?:DESCRIBE|DESC)\s+/i;
120
+ 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;
121
+ function isIntrospectionQuery(sql) {
122
+ const showMatch = sql.match(SHOW_INTROSPECTION_RE);
123
+ if (showMatch) {
124
+ const keyword = showMatch[1].toUpperCase().replace(/\s+/g, ' ');
125
+ if (keyword.startsWith('COLUMNS') || keyword.startsWith('FIELDS')) {
126
+ return { kind: 'show_columns' };
127
+ }
128
+ if (keyword.startsWith('CREATE')) {
129
+ return { kind: 'show_create' };
130
+ }
131
+ if (keyword.startsWith('INDEX') || keyword.startsWith('KEYS')) {
132
+ return { kind: 'show_index' };
133
+ }
134
+ if (keyword === 'TABLES' ||
135
+ keyword.startsWith('TABLE STATUS') ||
136
+ keyword === 'DATABASES' ||
137
+ keyword === 'SCHEMAS' ||
138
+ keyword.startsWith('CHARACTER') ||
139
+ keyword === 'CHARSET' ||
140
+ keyword === 'COLLATION') {
141
+ return { kind: 'show_passthrough' };
142
+ }
143
+ return { kind: 'show_other' };
144
+ }
145
+ if (DESCRIBE_RE.test(sql) || EXPLAIN_TABLE_RE.test(sql)) {
146
+ return { kind: 'describe' };
147
+ }
148
+ let astOrArray;
149
+ try {
150
+ astOrArray = parser.astify(sql, { database: 'mysql' });
151
+ }
152
+ catch {
153
+ return { kind: null };
154
+ }
155
+ const statements = Array.isArray(astOrArray) ? astOrArray : [astOrArray];
156
+ for (const stmt of statements) {
157
+ const kind = findIntrospectionKind(stmt);
158
+ if (kind) {
159
+ return { kind };
160
+ }
161
+ }
162
+ return { kind: null };
163
+ }
164
+ function findIntrospectionKind(node) {
165
+ if (node == null || typeof node !== 'object') {
166
+ return null;
167
+ }
168
+ if (node instanceof Date) {
169
+ return null;
170
+ }
171
+ const obj = node;
172
+ if (obj.type === 'show') {
173
+ const keyword = typeof obj.keyword === 'string' ? obj.keyword.toLowerCase() : '';
174
+ if (keyword === 'columns' || keyword === 'fields') {
175
+ return 'show_columns';
176
+ }
177
+ if (keyword === 'create') {
178
+ return 'show_create';
179
+ }
180
+ if (keyword === 'index' || keyword === 'keys') {
181
+ return 'show_index';
182
+ }
183
+ if (keyword === 'tables' || keyword === 'databases' || keyword === 'character' || keyword === 'collation') {
184
+ return 'show_passthrough';
185
+ }
186
+ return 'show_other';
187
+ }
188
+ if (obj.type === 'desc' || obj.type === 'describe') {
189
+ return 'describe';
190
+ }
191
+ if (typeof obj.db === 'string') {
192
+ const db = obj.db.toLowerCase();
193
+ if (db === 'information_schema') {
194
+ return 'information_schema';
195
+ }
196
+ if (db === 'mysql') {
197
+ return 'mysql_schema';
198
+ }
199
+ }
200
+ for (const value of Object.values(obj)) {
201
+ if (Array.isArray(value)) {
202
+ for (const item of value) {
203
+ const kind = findIntrospectionKind(item);
204
+ if (kind) {
205
+ return kind;
206
+ }
207
+ }
208
+ }
209
+ else if (value && typeof value === 'object') {
210
+ const kind = findIntrospectionKind(value);
211
+ if (kind) {
212
+ return kind;
213
+ }
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ function isRecord(node) {
219
+ return node != null && typeof node === 'object' && !(node instanceof Date);
220
+ }
221
+ function extractQualifiers(sql) {
222
+ const map = new Map();
223
+ let astOrArray;
224
+ try {
225
+ astOrArray = parser.astify(stripExplainModifiers(sql), { database: 'mysql' });
226
+ }
227
+ catch {
228
+ return map;
229
+ }
230
+ const visit = (node) => {
231
+ if (!isRecord(node)) {
232
+ return;
233
+ }
234
+ if (node.type !== 'column_ref' &&
235
+ typeof node.table === 'string' &&
236
+ (typeof node.db === 'string' || node.db === null)) {
237
+ const reference = {
238
+ schema: typeof node.db === 'string' && node.db ? node.db : null,
239
+ table: node.table,
240
+ };
241
+ map.set(node.table.toLowerCase(), reference);
242
+ if (typeof node.as === 'string' && node.as) {
243
+ map.set(node.as.toLowerCase(), reference);
244
+ }
245
+ }
246
+ for (const value of Object.values(node)) {
247
+ if (Array.isArray(value)) {
248
+ for (const item of value) {
249
+ visit(item);
250
+ }
251
+ }
252
+ else {
253
+ visit(value);
254
+ }
255
+ }
256
+ };
257
+ visit(astOrArray);
258
+ return map;
259
+ }
260
+ function isUnparseableIntrospection(sql) {
261
+ const kind = isIntrospectionQuery(sql).kind;
262
+ return kind !== null && kind !== 'information_schema' && kind !== 'mysql_schema';
263
+ }
264
+ export { findUnsupportedStatementType, findFileExport, extractReferencedSchemas, getQueryTypes, isIntrospectionQuery, isUnparseableIntrospection, extractQualifiers, stripExplainModifiers, };
@@ -0,0 +1,137 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ export function expandHome(p) {
5
+ if (p === '~') {
6
+ return os.homedir();
7
+ }
8
+ if (p.startsWith('~/')) {
9
+ return path.join(os.homedir(), p.slice(2));
10
+ }
11
+ return p;
12
+ }
13
+ function splitDirective(line) {
14
+ const trimmed = line.trim();
15
+ if (!trimmed || trimmed.startsWith('#')) {
16
+ return null;
17
+ }
18
+ const eq = trimmed.indexOf('=');
19
+ const ws = trimmed.search(/\s/);
20
+ let sepIndex;
21
+ if (eq !== -1 && (ws === -1 || eq < ws)) {
22
+ sepIndex = eq;
23
+ }
24
+ else if (ws !== -1) {
25
+ sepIndex = ws;
26
+ }
27
+ else {
28
+ return null;
29
+ }
30
+ const keyword = trimmed.slice(0, sepIndex).trim().toLowerCase();
31
+ const value = trimmed
32
+ .slice(sepIndex + 1)
33
+ .trim()
34
+ .replace(/^=/, '')
35
+ .trim();
36
+ if (!keyword || !value) {
37
+ return null;
38
+ }
39
+ return [keyword, value];
40
+ }
41
+ function parseLocalForward(value) {
42
+ const parts = value.split(/\s+/).filter(Boolean);
43
+ if (parts.length < 2) {
44
+ return null;
45
+ }
46
+ const localRaw = parts[0];
47
+ const localPortStr = localRaw.includes(':') ? localRaw.slice(localRaw.lastIndexOf(':') + 1) : localRaw;
48
+ const localPort = Number(localPortStr);
49
+ if (!Number.isInteger(localPort) || localPort < 0 || localPort > 65535) {
50
+ return null;
51
+ }
52
+ let remoteHost;
53
+ let remotePortStr;
54
+ const remoteRaw = parts[1];
55
+ const lastColon = remoteRaw.lastIndexOf(':');
56
+ if (lastColon > 0) {
57
+ remoteHost = remoteRaw.slice(0, lastColon);
58
+ remotePortStr = remoteRaw.slice(lastColon + 1);
59
+ }
60
+ else if (parts.length >= 3) {
61
+ remoteHost = remoteRaw;
62
+ remotePortStr = parts[2];
63
+ }
64
+ else {
65
+ return null;
66
+ }
67
+ const remotePort = Number(remotePortStr);
68
+ if (!remoteHost || !Number.isInteger(remotePort) || remotePort <= 0) {
69
+ return null;
70
+ }
71
+ return { localPort, remoteHost, remotePort };
72
+ }
73
+ export function readSSHConfigHost(alias, configPath = path.join(os.homedir(), '.ssh', 'config')) {
74
+ if (!fs.existsSync(configPath)) {
75
+ return null;
76
+ }
77
+ const wanted = alias.trim().toLowerCase();
78
+ let raw;
79
+ try {
80
+ raw = fs.readFileSync(configPath, 'utf8');
81
+ }
82
+ catch (err) {
83
+ throw new Error(`Failed to read SSH config at ${configPath}: ${err.message}`);
84
+ }
85
+ let inBlock = false;
86
+ const entry = {};
87
+ let found = false;
88
+ for (const line of raw.split(/\r?\n/)) {
89
+ const directive = splitDirective(line);
90
+ if (!directive) {
91
+ continue;
92
+ }
93
+ const [keyword, value] = directive;
94
+ if (keyword === 'host') {
95
+ inBlock = value.split(/\s+/).some((token) => token.toLowerCase() === wanted);
96
+ if (inBlock) {
97
+ found = true;
98
+ }
99
+ continue;
100
+ }
101
+ if (keyword === 'match') {
102
+ inBlock = false;
103
+ continue;
104
+ }
105
+ if (!inBlock) {
106
+ continue;
107
+ }
108
+ switch (keyword) {
109
+ case 'hostname':
110
+ entry.hostName ??= value;
111
+ break;
112
+ case 'user':
113
+ entry.user ??= value;
114
+ break;
115
+ case 'port': {
116
+ const port = Number(value);
117
+ if (Number.isInteger(port) && port > 0 && port <= 65535) {
118
+ entry.port ??= port;
119
+ }
120
+ break;
121
+ }
122
+ case 'identityfile':
123
+ entry.identityFile ??= expandHome(value.replace(/^"|"$/g, ''));
124
+ break;
125
+ case 'localforward': {
126
+ const forward = parseLocalForward(value);
127
+ if (forward && !entry.localForward) {
128
+ entry.localForward = forward;
129
+ }
130
+ break;
131
+ }
132
+ default:
133
+ break;
134
+ }
135
+ }
136
+ return found ? entry : null;
137
+ }