@hyqf98/easy_db_mcp_server 1.0.0 → 2.0.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.
- package/README.md +202 -8
- package/dist/database/base.d.ts +79 -0
- package/dist/database/mysql.d.ts +11 -1
- package/dist/database/mysql.js +336 -18
- package/dist/database/postgresql.d.ts +11 -1
- package/dist/database/postgresql.js +460 -2
- package/dist/database/sqlite.d.ts +11 -1
- package/dist/database/sqlite.js +223 -0
- package/dist/index.js +241 -172
- package/dist/utils/file.d.ts +4 -0
- package/dist/utils/file.js +37 -0
- package/package.json +1 -1
- package/src/database/base.ts +100 -0
- package/src/database/mysql.ts +428 -19
- package/src/database/postgresql.ts +554 -2
- package/src/database/sqlite.ts +282 -0
- package/src/index.ts +310 -195
- package/src/utils/file.ts +46 -0
package/dist/database/mysql.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import mysql from 'mysql2/promise';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { expandTilde, formatFileSize, ensureDirectoryExists, getDefaultFilePath } from '../utils/file.js';
|
|
2
4
|
export class MySQLAdapter {
|
|
3
5
|
pool;
|
|
4
6
|
config;
|
|
@@ -59,33 +61,51 @@ export class MySQLAdapter {
|
|
|
59
61
|
async executeQuery(sql, database) {
|
|
60
62
|
if (!this.pool)
|
|
61
63
|
throw new Error('Not connected');
|
|
62
|
-
// If database is specified and different from config, we need to handle it
|
|
63
|
-
// For now, we'll allow queries to use fully qualified table names
|
|
64
|
-
// Verify it's a SELECT query
|
|
65
64
|
const trimmed = sql.trim().toUpperCase();
|
|
66
65
|
if (!trimmed.startsWith('SELECT')) {
|
|
67
66
|
throw new Error('Only SELECT queries allowed in executeQuery');
|
|
68
67
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
// Get a connection from the pool to handle database switching
|
|
69
|
+
const connection = await this.pool.getConnection();
|
|
70
|
+
try {
|
|
71
|
+
// Switch to the specified database if provided
|
|
72
|
+
if (database && database !== this.config.database) {
|
|
73
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
74
|
+
}
|
|
75
|
+
const [rows] = await connection.query(sql);
|
|
76
|
+
return {
|
|
77
|
+
rows: rows,
|
|
78
|
+
rowCount: Array.isArray(rows) ? rows.length : 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
connection.release();
|
|
83
|
+
}
|
|
74
84
|
}
|
|
75
85
|
async executeSQL(sql, database) {
|
|
76
86
|
if (!this.pool)
|
|
77
87
|
throw new Error('Not connected');
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
// Get a connection from the pool to handle database switching
|
|
89
|
+
const connection = await this.pool.getConnection();
|
|
90
|
+
try {
|
|
91
|
+
// Switch to the specified database if provided
|
|
92
|
+
if (database && database !== this.config.database) {
|
|
93
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
94
|
+
}
|
|
95
|
+
const [result] = await connection.query(sql);
|
|
96
|
+
// @ts-ignore - MySQL result structure
|
|
97
|
+
if (result.affectedRows !== undefined) {
|
|
98
|
+
// @ts-ignore
|
|
99
|
+
return { affectedRows: result.affectedRows };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
rows: result,
|
|
103
|
+
rowCount: Array.isArray(result) ? result.length : 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
connection.release();
|
|
84
108
|
}
|
|
85
|
-
return {
|
|
86
|
-
rows: result,
|
|
87
|
-
rowCount: Array.isArray(result) ? result.length : 0,
|
|
88
|
-
};
|
|
89
109
|
}
|
|
90
110
|
async close() {
|
|
91
111
|
if (this.pool) {
|
|
@@ -93,4 +113,302 @@ export class MySQLAdapter {
|
|
|
93
113
|
this.pool = undefined;
|
|
94
114
|
}
|
|
95
115
|
}
|
|
116
|
+
async executeTransaction(queries, database) {
|
|
117
|
+
if (!this.pool)
|
|
118
|
+
throw new Error('Not connected');
|
|
119
|
+
const connection = await this.pool.getConnection();
|
|
120
|
+
try {
|
|
121
|
+
// Switch to specified database if needed
|
|
122
|
+
if (database && database !== this.config.database) {
|
|
123
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
124
|
+
}
|
|
125
|
+
await connection.beginTransaction();
|
|
126
|
+
const results = [];
|
|
127
|
+
let totalAffectedRows = 0;
|
|
128
|
+
for (const query of queries) {
|
|
129
|
+
const [result] = await connection.query(query);
|
|
130
|
+
// @ts-ignore - MySQL result structure
|
|
131
|
+
if (result.affectedRows !== undefined) {
|
|
132
|
+
// @ts-ignore
|
|
133
|
+
totalAffectedRows += result.affectedRows;
|
|
134
|
+
}
|
|
135
|
+
results.push(result);
|
|
136
|
+
}
|
|
137
|
+
await connection.commit();
|
|
138
|
+
return {
|
|
139
|
+
success: true,
|
|
140
|
+
affectedRows: totalAffectedRows,
|
|
141
|
+
results,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
await connection.rollback();
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
connection.release();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async batchInsert(table, data, database) {
|
|
153
|
+
if (!this.pool)
|
|
154
|
+
throw new Error('Not connected');
|
|
155
|
+
if (data.length === 0) {
|
|
156
|
+
return { insertedRows: 0, duplicateRows: 0 };
|
|
157
|
+
}
|
|
158
|
+
const connection = await this.pool.getConnection();
|
|
159
|
+
try {
|
|
160
|
+
if (database && database !== this.config.database) {
|
|
161
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
162
|
+
}
|
|
163
|
+
// Build bulk insert query
|
|
164
|
+
const columns = Object.keys(data[0]);
|
|
165
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
166
|
+
const columnList = columns.map((c) => mysql.escapeId(c)).join(', ');
|
|
167
|
+
const query = `INSERT INTO ${mysql.escapeId(table)} (${columnList}) VALUES (${placeholders})`;
|
|
168
|
+
let insertedRows = 0;
|
|
169
|
+
let duplicateRows = 0;
|
|
170
|
+
for (const row of data) {
|
|
171
|
+
try {
|
|
172
|
+
const values = columns.map((col) => row[col]);
|
|
173
|
+
await connection.query(query, values);
|
|
174
|
+
insertedRows++;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
// Check for duplicate key error
|
|
178
|
+
if (error.code === 'ER_DUP_ENTRY') {
|
|
179
|
+
duplicateRows++;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { insertedRows, duplicateRows };
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
connection.release();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async batchUpdate(table, updates, database) {
|
|
193
|
+
if (!this.pool)
|
|
194
|
+
throw new Error('Not connected');
|
|
195
|
+
const connection = await this.pool.getConnection();
|
|
196
|
+
try {
|
|
197
|
+
if (database && database !== this.config.database) {
|
|
198
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
199
|
+
}
|
|
200
|
+
// Build SET clause
|
|
201
|
+
const setClause = Object.entries(updates.set)
|
|
202
|
+
.map(([key, value]) => `${mysql.escapeId(key)} = ${this.escapeValue(value)}`)
|
|
203
|
+
.join(', ');
|
|
204
|
+
const query = `UPDATE ${mysql.escapeId(table)} SET ${setClause} WHERE ${updates.where}`;
|
|
205
|
+
const [result] = await connection.query(query);
|
|
206
|
+
// @ts-ignore
|
|
207
|
+
return { affectedRows: result.affectedRows || 0 };
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
connection.release();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
escapeValue(value) {
|
|
214
|
+
if (value === null)
|
|
215
|
+
return 'NULL';
|
|
216
|
+
if (typeof value === 'number')
|
|
217
|
+
return String(value);
|
|
218
|
+
if (typeof value === 'boolean')
|
|
219
|
+
return value ? '1' : '0';
|
|
220
|
+
return mysql.escape(String(value));
|
|
221
|
+
}
|
|
222
|
+
async exportData(table, format, filePath, options, database) {
|
|
223
|
+
if (!this.pool)
|
|
224
|
+
throw new Error('Not connected');
|
|
225
|
+
const connection = await this.pool.getConnection();
|
|
226
|
+
try {
|
|
227
|
+
if (database && database !== this.config.database) {
|
|
228
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
229
|
+
}
|
|
230
|
+
// Build query
|
|
231
|
+
let query = `SELECT * FROM ${mysql.escapeId(table)}`;
|
|
232
|
+
if (options?.where) {
|
|
233
|
+
query += ` WHERE ${options.where}`;
|
|
234
|
+
}
|
|
235
|
+
if (options?.limit) {
|
|
236
|
+
query += ` LIMIT ${options.limit}`;
|
|
237
|
+
}
|
|
238
|
+
const [rows] = await connection.query(query);
|
|
239
|
+
const data = rows;
|
|
240
|
+
// Determine file path
|
|
241
|
+
const targetPath = filePath
|
|
242
|
+
? expandTilde(filePath)
|
|
243
|
+
: getDefaultFilePath(table, format);
|
|
244
|
+
ensureDirectoryExists(targetPath);
|
|
245
|
+
// Export data
|
|
246
|
+
let content;
|
|
247
|
+
if (format === 'json') {
|
|
248
|
+
content = JSON.stringify(data, null, 2);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// CSV format
|
|
252
|
+
if (data.length === 0) {
|
|
253
|
+
content = '';
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const headers = Object.keys(data[0]);
|
|
257
|
+
const csvRows = [
|
|
258
|
+
headers.join(','),
|
|
259
|
+
...data.map((row) => headers.map((h) => {
|
|
260
|
+
const val = row[h];
|
|
261
|
+
if (val === null)
|
|
262
|
+
return '';
|
|
263
|
+
if (typeof val === 'string')
|
|
264
|
+
return '"' + val.replace(/"/g, '""') + '"';
|
|
265
|
+
return String(val);
|
|
266
|
+
}).join(',')),
|
|
267
|
+
];
|
|
268
|
+
content = csvRows.join('\n');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
fs.writeFileSync(targetPath, content, 'utf-8');
|
|
272
|
+
const stats = fs.statSync(targetPath);
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
filePath: targetPath,
|
|
276
|
+
rowCount: data.length,
|
|
277
|
+
fileSize: formatFileSize(stats.size),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
connection.release();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async createTable(table, columns, database) {
|
|
285
|
+
if (!this.pool)
|
|
286
|
+
throw new Error('Not connected');
|
|
287
|
+
const connection = await this.pool.getConnection();
|
|
288
|
+
try {
|
|
289
|
+
if (database && database !== this.config.database) {
|
|
290
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
291
|
+
}
|
|
292
|
+
const columnDefs = columns.map((col) => {
|
|
293
|
+
let def = `${mysql.escapeId(col.name)} ${col.type}`;
|
|
294
|
+
if (col.nullable === false)
|
|
295
|
+
def += ' NOT NULL';
|
|
296
|
+
if (col.primaryKey)
|
|
297
|
+
def += ' PRIMARY KEY';
|
|
298
|
+
if (col.defaultValue !== undefined) {
|
|
299
|
+
def += ` DEFAULT ${this.escapeValue(col.defaultValue)}`;
|
|
300
|
+
}
|
|
301
|
+
return def;
|
|
302
|
+
});
|
|
303
|
+
const query = `CREATE TABLE ${mysql.escapeId(table)} (${columnDefs.join(', ')})`;
|
|
304
|
+
await connection.query(query);
|
|
305
|
+
return { success: true, tableName: table };
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
connection.release();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async dropTable(table, ifExists = false, database) {
|
|
312
|
+
if (!this.pool)
|
|
313
|
+
throw new Error('Not connected');
|
|
314
|
+
const connection = await this.pool.getConnection();
|
|
315
|
+
try {
|
|
316
|
+
if (database && database !== this.config.database) {
|
|
317
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
318
|
+
}
|
|
319
|
+
const query = ifExists
|
|
320
|
+
? `DROP TABLE IF EXISTS ${mysql.escapeId(table)}`
|
|
321
|
+
: `DROP TABLE ${mysql.escapeId(table)}`;
|
|
322
|
+
await connection.query(query);
|
|
323
|
+
return { success: true, tableName: table };
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
connection.release();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async getTableStats(table, database) {
|
|
330
|
+
if (!this.pool)
|
|
331
|
+
throw new Error('Not connected');
|
|
332
|
+
const connection = await this.pool.getConnection();
|
|
333
|
+
try {
|
|
334
|
+
const dbName = database || this.config.database;
|
|
335
|
+
if (!dbName)
|
|
336
|
+
throw new Error('Database name required');
|
|
337
|
+
if (database && database !== this.config.database) {
|
|
338
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
339
|
+
}
|
|
340
|
+
// Get row count
|
|
341
|
+
const [countResult] = await connection.query(`SELECT COUNT(*) as count FROM ${mysql.escapeId(table)}`);
|
|
342
|
+
const rowCount = countResult[0].count;
|
|
343
|
+
// Get column count
|
|
344
|
+
const [colResult] = await connection.query(`SELECT COUNT(*) as count FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`, [dbName, table]);
|
|
345
|
+
const columns = colResult[0].count;
|
|
346
|
+
// Get indexes
|
|
347
|
+
const [indexResult] = await connection.query(`SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME != 'PRIMARY' GROUP BY INDEX_NAME`, [dbName, table]);
|
|
348
|
+
const indexes = indexResult.map((row) => row.INDEX_NAME);
|
|
349
|
+
// Get table size
|
|
350
|
+
const [sizeResult] = await connection.query(`SELECT ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) as size_mb FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`, [dbName, table]);
|
|
351
|
+
const sizeMb = sizeResult[0]?.size_mb || 0;
|
|
352
|
+
const size = sizeMb > 0 ? `${sizeMb} MB` : '< 0.01 MB';
|
|
353
|
+
return {
|
|
354
|
+
tableName: table,
|
|
355
|
+
rowCount,
|
|
356
|
+
columns,
|
|
357
|
+
indexes,
|
|
358
|
+
size,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
connection.release();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async previewData(table, page = 1, pageSize = 50, orderBy, database) {
|
|
366
|
+
if (!this.pool)
|
|
367
|
+
throw new Error('Not connected');
|
|
368
|
+
const connection = await this.pool.getConnection();
|
|
369
|
+
try {
|
|
370
|
+
if (database && database !== this.config.database) {
|
|
371
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
372
|
+
}
|
|
373
|
+
// Get total row count
|
|
374
|
+
const [countResult] = await connection.query(`SELECT COUNT(*) as count FROM ${mysql.escapeId(table)}`);
|
|
375
|
+
const totalRows = countResult[0].count;
|
|
376
|
+
const totalPages = Math.ceil(totalRows / pageSize);
|
|
377
|
+
const offset = (page - 1) * pageSize;
|
|
378
|
+
// Build query
|
|
379
|
+
let query = `SELECT * FROM ${mysql.escapeId(table)}`;
|
|
380
|
+
if (orderBy) {
|
|
381
|
+
query += ` ORDER BY ${mysql.escapeId(orderBy)}`;
|
|
382
|
+
}
|
|
383
|
+
query += ` LIMIT ${pageSize} OFFSET ${offset}`;
|
|
384
|
+
const [rows] = await connection.query(query);
|
|
385
|
+
return {
|
|
386
|
+
rows: rows,
|
|
387
|
+
currentPage: page,
|
|
388
|
+
totalPages,
|
|
389
|
+
totalRows,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
connection.release();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async sampleData(table, count = 10, database) {
|
|
397
|
+
if (!this.pool)
|
|
398
|
+
throw new Error('Not connected');
|
|
399
|
+
const connection = await this.pool.getConnection();
|
|
400
|
+
try {
|
|
401
|
+
if (database && database !== this.config.database) {
|
|
402
|
+
await connection.query(`USE ${mysql.escapeId(database)}`);
|
|
403
|
+
}
|
|
404
|
+
const [rows] = await connection.query(`SELECT * FROM ${mysql.escapeId(table)} ORDER BY RAND() LIMIT ${count}`);
|
|
405
|
+
return {
|
|
406
|
+
rows: rows,
|
|
407
|
+
sampleCount: rows.length,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
connection.release();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
96
414
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DatabaseAdapter, TableInfo, TableColumn, QueryResult } from './base.js';
|
|
1
|
+
import type { DatabaseAdapter, TableInfo, TableColumn, QueryResult, TransactionResult, BatchInsertResult, BatchUpdateResult, BatchUpdateOptions, ExportResult, ExportOptions, CreateTableResult, DropTableResult, TableColumnDef, TableStatsResult, PreviewDataResult, SampleDataResult } from './base.js';
|
|
2
2
|
import type { DatabaseConfig } from '../config.js';
|
|
3
3
|
export declare class PostgreSQLAdapter implements DatabaseAdapter {
|
|
4
4
|
private pool?;
|
|
@@ -12,4 +12,14 @@ export declare class PostgreSQLAdapter implements DatabaseAdapter {
|
|
|
12
12
|
affectedRows: number;
|
|
13
13
|
}>;
|
|
14
14
|
close(): Promise<void>;
|
|
15
|
+
executeTransaction(queries: string[], database?: string): Promise<TransactionResult>;
|
|
16
|
+
batchInsert(table: string, data: Record<string, unknown>[], database?: string): Promise<BatchInsertResult>;
|
|
17
|
+
batchUpdate(table: string, updates: BatchUpdateOptions, database?: string): Promise<BatchUpdateResult>;
|
|
18
|
+
private escapeValue;
|
|
19
|
+
exportData(table: string, format: 'json' | 'csv', filePath?: string, options?: ExportOptions, database?: string): Promise<ExportResult>;
|
|
20
|
+
createTable(table: string, columns: TableColumnDef[], database?: string): Promise<CreateTableResult>;
|
|
21
|
+
dropTable(table: string, ifExists?: boolean, database?: string): Promise<DropTableResult>;
|
|
22
|
+
getTableStats(table: string, database?: string): Promise<TableStatsResult>;
|
|
23
|
+
previewData(table: string, page?: number, pageSize?: number, orderBy?: string, database?: string): Promise<PreviewDataResult>;
|
|
24
|
+
sampleData(table: string, count?: number, database?: string): Promise<SampleDataResult>;
|
|
15
25
|
}
|