@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.
@@ -0,0 +1,37 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const homedir = os.homedir();
5
+ export function expandTilde(filePath) {
6
+ if (filePath.startsWith('~')) {
7
+ return path.join(homedir, filePath.slice(1));
8
+ }
9
+ return filePath;
10
+ }
11
+ export function formatFileSize(bytes) {
12
+ const units = ['B', 'KB', 'MB', 'GB'];
13
+ let size = bytes;
14
+ let unitIndex = 0;
15
+ while (size >= 1024 && unitIndex < units.length - 1) {
16
+ size /= 1024;
17
+ unitIndex++;
18
+ }
19
+ return size.toFixed(2) + ' ' + units[unitIndex];
20
+ }
21
+ export function ensureDirectoryExists(filePath) {
22
+ const dir = path.dirname(filePath);
23
+ if (!fs.existsSync(dir)) {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ }
27
+ export function getDefaultFilePath(baseName, extension) {
28
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
29
+ const time = new Date().toTimeString().split(' ')[0].replace(/:/g, '');
30
+ let fileName = baseName + '.' + extension;
31
+ // Check if file exists, add timestamp if it does
32
+ const fullPath = path.join(homedir, fileName);
33
+ if (fs.existsSync(fullPath)) {
34
+ fileName = baseName + '_' + timestamp + '_' + time + '.' + extension;
35
+ }
36
+ return path.join(homedir, fileName);
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyqf98/easy_db_mcp_server",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "MCP server for database access (MySQL, PostgreSQL, SQLite)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,16 +5,99 @@ export interface TableColumn {
5
5
  defaultValue: string | null;
6
6
  primaryKey: boolean;
7
7
  extra?: string;
8
+ [key: string]: unknown; // Index signature for structuredContent
8
9
  }
9
10
 
10
11
  export interface TableInfo {
11
12
  name: string;
12
13
  rowCount?: number;
14
+ [key: string]: unknown; // Index signature for structuredContent
13
15
  }
14
16
 
15
17
  export interface QueryResult {
16
18
  rows: Record<string, unknown>[];
17
19
  rowCount: number;
20
+ [key: string]: unknown; // Index signature for structuredContent
21
+ }
22
+
23
+ // New type definitions for v2 features
24
+ export interface BatchInsertResult {
25
+ insertedRows: number;
26
+ duplicateRows: number;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export interface BatchUpdateOptions {
31
+ set: Record<string, unknown>;
32
+ where: string;
33
+ }
34
+
35
+ export interface BatchUpdateResult {
36
+ affectedRows: number;
37
+ [key: string]: unknown;
38
+ }
39
+
40
+ export interface TransactionResult {
41
+ success: boolean;
42
+ affectedRows: number;
43
+ results: unknown[];
44
+ [key: string]: unknown;
45
+ }
46
+
47
+ export interface ExportOptions {
48
+ limit?: number;
49
+ where?: string;
50
+ }
51
+
52
+ export interface ExportResult {
53
+ success: boolean;
54
+ filePath: string;
55
+ rowCount: number;
56
+ fileSize: string;
57
+ [key: string]: unknown;
58
+ }
59
+
60
+ export interface TableColumnDef {
61
+ name: string;
62
+ type: string;
63
+ nullable?: boolean;
64
+ primaryKey?: boolean;
65
+ defaultValue?: unknown;
66
+ }
67
+
68
+ export interface CreateTableResult {
69
+ success: boolean;
70
+ tableName: string;
71
+ [key: string]: unknown;
72
+ }
73
+
74
+ export interface DropTableResult {
75
+ success: boolean;
76
+ tableName: string;
77
+ [key: string]: unknown;
78
+ }
79
+
80
+ export interface TableStatsResult {
81
+ tableName: string;
82
+ rowCount: number;
83
+ columns: number;
84
+ indexes: string[];
85
+ size: string;
86
+ [key: string]: unknown;
87
+ }
88
+
89
+ export interface PreviewDataResult {
90
+ rows: Record<string, unknown>[];
91
+ currentPage: number;
92
+ totalPages: number;
93
+ totalRows: number;
94
+ [key: string]: unknown;
95
+ }
96
+
97
+ export interface SampleDataResult {
98
+ rows: Record<string, unknown>[];
99
+ sampleCount: number;
100
+ [key: string]: unknown;
18
101
  }
19
102
 
20
103
  export interface DatabaseAdapter {
@@ -47,4 +130,21 @@ export interface DatabaseAdapter {
47
130
  * Close the database connection
48
131
  */
49
132
  close(): Promise<void>;
133
+
134
+ // New methods - batch operations
135
+ batchInsert(table: string, data: Record<string, unknown>[], database?: string): Promise<BatchInsertResult>;
136
+ batchUpdate(table: string, updates: BatchUpdateOptions, database?: string): Promise<BatchUpdateResult>;
137
+ executeTransaction(queries: string[], database?: string): Promise<TransactionResult>;
138
+
139
+ // New methods - data export
140
+ exportData(table: string, format: 'json' | 'csv', filePath?: string, options?: ExportOptions, database?: string): Promise<ExportResult>;
141
+
142
+ // New methods - table management
143
+ createTable(table: string, columns: TableColumnDef[], database?: string): Promise<CreateTableResult>;
144
+ dropTable(table: string, ifExists?: boolean, database?: string): Promise<DropTableResult>;
145
+
146
+ // New methods - analytics
147
+ getTableStats(table: string, database?: string): Promise<TableStatsResult>;
148
+ previewData(table: string, page?: number, pageSize?: number, orderBy?: string, database?: string): Promise<PreviewDataResult>;
149
+ sampleData(table: string, count?: number, database?: string): Promise<SampleDataResult>;
50
150
  }
@@ -1,11 +1,25 @@
1
1
  import mysql from 'mysql2/promise';
2
+ import fs from 'fs';
2
3
  import type {
3
4
  DatabaseAdapter,
4
5
  TableInfo,
5
6
  TableColumn,
6
7
  QueryResult,
8
+ TransactionResult,
9
+ BatchInsertResult,
10
+ BatchUpdateResult,
11
+ BatchUpdateOptions,
12
+ ExportResult,
13
+ ExportOptions,
14
+ CreateTableResult,
15
+ DropTableResult,
16
+ TableColumnDef,
17
+ TableStatsResult,
18
+ PreviewDataResult,
19
+ SampleDataResult,
7
20
  } from './base.js';
8
21
  import type { DatabaseConfig } from '../config.js';
22
+ import { expandTilde, formatFileSize, ensureDirectoryExists, getDefaultFilePath } from '../utils/file.js';
9
23
 
10
24
  export class MySQLAdapter implements DatabaseAdapter {
11
25
  private pool?: mysql.Pool;
@@ -81,38 +95,58 @@ export class MySQLAdapter implements DatabaseAdapter {
81
95
  async executeQuery(sql: string, database?: string): Promise<QueryResult> {
82
96
  if (!this.pool) throw new Error('Not connected');
83
97
 
84
- // If database is specified and different from config, we need to handle it
85
- // For now, we'll allow queries to use fully qualified table names
86
- // Verify it's a SELECT query
87
98
  const trimmed = sql.trim().toUpperCase();
88
99
  if (!trimmed.startsWith('SELECT')) {
89
100
  throw new Error('Only SELECT queries allowed in executeQuery');
90
101
  }
91
102
 
92
- const [rows] = await this.pool.query(sql);
103
+ // Get a connection from the pool to handle database switching
104
+ const connection = await this.pool.getConnection();
105
+
106
+ try {
107
+ // Switch to the specified database if provided
108
+ if (database && database !== this.config.database) {
109
+ await connection.query(`USE ${mysql.escapeId(database)}`);
110
+ }
93
111
 
94
- return {
95
- rows: rows as Record<string, unknown>[],
96
- rowCount: Array.isArray(rows) ? rows.length : 0,
97
- };
112
+ const [rows] = await connection.query(sql);
113
+
114
+ return {
115
+ rows: rows as Record<string, unknown>[],
116
+ rowCount: Array.isArray(rows) ? rows.length : 0,
117
+ };
118
+ } finally {
119
+ connection.release();
120
+ }
98
121
  }
99
122
 
100
123
  async executeSQL(sql: string, database?: string): Promise<QueryResult | { affectedRows: number }> {
101
124
  if (!this.pool) throw new Error('Not connected');
102
125
 
103
- // Allow SQL execution with database-specific queries
104
- const [result] = await this.pool.query(sql);
126
+ // Get a connection from the pool to handle database switching
127
+ const connection = await this.pool.getConnection();
105
128
 
106
- // @ts-ignore - MySQL result structure
107
- if (result.affectedRows !== undefined) {
108
- // @ts-ignore
109
- return { affectedRows: result.affectedRows };
110
- }
129
+ try {
130
+ // Switch to the specified database if provided
131
+ if (database && database !== this.config.database) {
132
+ await connection.query(`USE ${mysql.escapeId(database)}`);
133
+ }
134
+
135
+ const [result] = await connection.query(sql);
111
136
 
112
- return {
113
- rows: result as Record<string, unknown>[],
114
- rowCount: Array.isArray(result) ? result.length : 0,
115
- };
137
+ // @ts-ignore - MySQL result structure
138
+ if (result.affectedRows !== undefined) {
139
+ // @ts-ignore
140
+ return { affectedRows: result.affectedRows };
141
+ }
142
+
143
+ return {
144
+ rows: result as Record<string, unknown>[],
145
+ rowCount: Array.isArray(result) ? result.length : 0,
146
+ };
147
+ } finally {
148
+ connection.release();
149
+ }
116
150
  }
117
151
 
118
152
  async close(): Promise<void> {
@@ -121,4 +155,379 @@ export class MySQLAdapter implements DatabaseAdapter {
121
155
  this.pool = undefined;
122
156
  }
123
157
  }
158
+
159
+ async executeTransaction(queries: string[], database?: string): Promise<TransactionResult> {
160
+ if (!this.pool) throw new Error('Not connected');
161
+
162
+ const connection = await this.pool.getConnection();
163
+
164
+ try {
165
+ // Switch to specified database if needed
166
+ if (database && database !== this.config.database) {
167
+ await connection.query(`USE ${mysql.escapeId(database)}`);
168
+ }
169
+
170
+ await connection.beginTransaction();
171
+
172
+ const results: unknown[] = [];
173
+ let totalAffectedRows = 0;
174
+
175
+ for (const query of queries) {
176
+ const [result] = await connection.query(query);
177
+
178
+ // @ts-ignore - MySQL result structure
179
+ if (result.affectedRows !== undefined) {
180
+ // @ts-ignore
181
+ totalAffectedRows += result.affectedRows;
182
+ }
183
+
184
+ results.push(result);
185
+ }
186
+
187
+ await connection.commit();
188
+
189
+ return {
190
+ success: true,
191
+ affectedRows: totalAffectedRows,
192
+ results,
193
+ };
194
+ } catch (error) {
195
+ await connection.rollback();
196
+ throw error;
197
+ } finally {
198
+ connection.release();
199
+ }
200
+ }
201
+
202
+ async batchInsert(
203
+ table: string,
204
+ data: Record<string, unknown>[],
205
+ database?: string
206
+ ): Promise<BatchInsertResult> {
207
+ if (!this.pool) throw new Error('Not connected');
208
+
209
+ if (data.length === 0) {
210
+ return { insertedRows: 0, duplicateRows: 0 };
211
+ }
212
+
213
+ const connection = await this.pool.getConnection();
214
+
215
+ try {
216
+ if (database && database !== this.config.database) {
217
+ await connection.query(`USE ${mysql.escapeId(database)}`);
218
+ }
219
+
220
+ // Build bulk insert query
221
+ const columns = Object.keys(data[0]);
222
+ const placeholders = columns.map(() => '?').join(', ');
223
+ const columnList = columns.map((c) => mysql.escapeId(c)).join(', ');
224
+
225
+ const query = `INSERT INTO ${mysql.escapeId(table)} (${columnList}) VALUES (${placeholders})`;
226
+
227
+ let insertedRows = 0;
228
+ let duplicateRows = 0;
229
+
230
+ for (const row of data) {
231
+ try {
232
+ const values = columns.map((col) => row[col]);
233
+ await connection.query(query, values);
234
+ insertedRows++;
235
+ } catch (error: any) {
236
+ // Check for duplicate key error
237
+ if (error.code === 'ER_DUP_ENTRY') {
238
+ duplicateRows++;
239
+ } else {
240
+ throw error;
241
+ }
242
+ }
243
+ }
244
+
245
+ return { insertedRows, duplicateRows };
246
+ } finally {
247
+ connection.release();
248
+ }
249
+ }
250
+
251
+ async batchUpdate(
252
+ table: string,
253
+ updates: BatchUpdateOptions,
254
+ database?: string
255
+ ): Promise<BatchUpdateResult> {
256
+ if (!this.pool) throw new Error('Not connected');
257
+
258
+ const connection = await this.pool.getConnection();
259
+
260
+ try {
261
+ if (database && database !== this.config.database) {
262
+ await connection.query(`USE ${mysql.escapeId(database)}`);
263
+ }
264
+
265
+ // Build SET clause
266
+ const setClause = Object.entries(updates.set)
267
+ .map(([key, value]) => `${mysql.escapeId(key)} = ${this.escapeValue(value)}`)
268
+ .join(', ');
269
+
270
+ const query = `UPDATE ${mysql.escapeId(table)} SET ${setClause} WHERE ${updates.where}`;
271
+
272
+ const [result] = await connection.query(query);
273
+
274
+ // @ts-ignore
275
+ return { affectedRows: result.affectedRows || 0 };
276
+ } finally {
277
+ connection.release();
278
+ }
279
+ }
280
+
281
+ private escapeValue(value: unknown): string {
282
+ if (value === null) return 'NULL';
283
+ if (typeof value === 'number') return String(value);
284
+ if (typeof value === 'boolean') return value ? '1' : '0';
285
+ return mysql.escape(String(value));
286
+ }
287
+
288
+ async exportData(
289
+ table: string,
290
+ format: 'json' | 'csv',
291
+ filePath?: string,
292
+ options?: ExportOptions,
293
+ database?: string
294
+ ): Promise<ExportResult> {
295
+ if (!this.pool) throw new Error('Not connected');
296
+
297
+ const connection = await this.pool.getConnection();
298
+
299
+ try {
300
+ if (database && database !== this.config.database) {
301
+ await connection.query(`USE ${mysql.escapeId(database)}`);
302
+ }
303
+
304
+ // Build query
305
+ let query = `SELECT * FROM ${mysql.escapeId(table)}`;
306
+ if (options?.where) {
307
+ query += ` WHERE ${options.where}`;
308
+ }
309
+ if (options?.limit) {
310
+ query += ` LIMIT ${options.limit}`;
311
+ }
312
+
313
+ const [rows] = await connection.query(query);
314
+ const data = rows as Record<string, unknown>[];
315
+
316
+ // Determine file path
317
+ const targetPath = filePath
318
+ ? expandTilde(filePath)
319
+ : getDefaultFilePath(table, format);
320
+
321
+ ensureDirectoryExists(targetPath);
322
+
323
+ // Export data
324
+ let content: string;
325
+ if (format === 'json') {
326
+ content = JSON.stringify(data, null, 2);
327
+ } else {
328
+ // CSV format
329
+ if (data.length === 0) {
330
+ content = '';
331
+ } else {
332
+ const headers = Object.keys(data[0]);
333
+ const csvRows = [
334
+ headers.join(','),
335
+ ...data.map((row) =>
336
+ headers.map((h) => {
337
+ const val = row[h];
338
+ if (val === null) return '';
339
+ if (typeof val === 'string') return '"' + val.replace(/"/g, '""') + '"';
340
+ return String(val);
341
+ }).join(',')
342
+ ),
343
+ ];
344
+ content = csvRows.join('\n');
345
+ }
346
+ }
347
+
348
+ fs.writeFileSync(targetPath, content, 'utf-8');
349
+ const stats = fs.statSync(targetPath);
350
+
351
+ return {
352
+ success: true,
353
+ filePath: targetPath,
354
+ rowCount: data.length,
355
+ fileSize: formatFileSize(stats.size),
356
+ };
357
+ } finally {
358
+ connection.release();
359
+ }
360
+ }
361
+
362
+ async createTable(table: string, columns: TableColumnDef[], database?: string): Promise<CreateTableResult> {
363
+ if (!this.pool) throw new Error('Not connected');
364
+
365
+ const connection = await this.pool.getConnection();
366
+
367
+ try {
368
+ if (database && database !== this.config.database) {
369
+ await connection.query(`USE ${mysql.escapeId(database)}`);
370
+ }
371
+
372
+ const columnDefs = columns.map((col) => {
373
+ let def = `${mysql.escapeId(col.name)} ${col.type}`;
374
+ if (col.nullable === false) def += ' NOT NULL';
375
+ if (col.primaryKey) def += ' PRIMARY KEY';
376
+ if (col.defaultValue !== undefined) {
377
+ def += ` DEFAULT ${this.escapeValue(col.defaultValue)}`;
378
+ }
379
+ return def;
380
+ });
381
+
382
+ const query = `CREATE TABLE ${mysql.escapeId(table)} (${columnDefs.join(', ')})`;
383
+ await connection.query(query);
384
+
385
+ return { success: true, tableName: table };
386
+ } finally {
387
+ connection.release();
388
+ }
389
+ }
390
+
391
+ async dropTable(table: string, ifExists = false, database?: string): Promise<DropTableResult> {
392
+ if (!this.pool) throw new Error('Not connected');
393
+
394
+ const connection = await this.pool.getConnection();
395
+
396
+ try {
397
+ if (database && database !== this.config.database) {
398
+ await connection.query(`USE ${mysql.escapeId(database)}`);
399
+ }
400
+
401
+ const query = ifExists
402
+ ? `DROP TABLE IF EXISTS ${mysql.escapeId(table)}`
403
+ : `DROP TABLE ${mysql.escapeId(table)}`;
404
+
405
+ await connection.query(query);
406
+
407
+ return { success: true, tableName: table };
408
+ } finally {
409
+ connection.release();
410
+ }
411
+ }
412
+
413
+ async getTableStats(table: string, database?: string): Promise<TableStatsResult> {
414
+ if (!this.pool) throw new Error('Not connected');
415
+
416
+ const connection = await this.pool.getConnection();
417
+
418
+ try {
419
+ const dbName = database || this.config.database;
420
+ if (!dbName) throw new Error('Database name required');
421
+
422
+ if (database && database !== this.config.database) {
423
+ await connection.query(`USE ${mysql.escapeId(database)}`);
424
+ }
425
+
426
+ // Get row count
427
+ const [countResult] = await connection.query(
428
+ `SELECT COUNT(*) as count FROM ${mysql.escapeId(table)}`
429
+ );
430
+ const rowCount = (countResult as any)[0].count;
431
+
432
+ // Get column count
433
+ const [colResult] = await connection.query(
434
+ `SELECT COUNT(*) as count FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
435
+ [dbName, table]
436
+ );
437
+ const columns = (colResult as any)[0].count;
438
+
439
+ // Get indexes
440
+ const [indexResult] = await connection.query(
441
+ `SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME != 'PRIMARY' GROUP BY INDEX_NAME`,
442
+ [dbName, table]
443
+ );
444
+ const indexes = (indexResult as any[]).map((row) => row.INDEX_NAME);
445
+
446
+ // Get table size
447
+ const [sizeResult] = await connection.query(
448
+ `SELECT ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) as size_mb FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
449
+ [dbName, table]
450
+ );
451
+ const sizeMb = (sizeResult as any)[0]?.size_mb || 0;
452
+ const size = sizeMb > 0 ? `${sizeMb} MB` : '< 0.01 MB';
453
+
454
+ return {
455
+ tableName: table,
456
+ rowCount,
457
+ columns,
458
+ indexes,
459
+ size,
460
+ };
461
+ } finally {
462
+ connection.release();
463
+ }
464
+ }
465
+
466
+ async previewData(
467
+ table: string,
468
+ page = 1,
469
+ pageSize = 50,
470
+ orderBy?: string,
471
+ database?: string
472
+ ): Promise<PreviewDataResult> {
473
+ if (!this.pool) throw new Error('Not connected');
474
+
475
+ const connection = await this.pool.getConnection();
476
+
477
+ try {
478
+ if (database && database !== this.config.database) {
479
+ await connection.query(`USE ${mysql.escapeId(database)}`);
480
+ }
481
+
482
+ // Get total row count
483
+ const [countResult] = await connection.query(
484
+ `SELECT COUNT(*) as count FROM ${mysql.escapeId(table)}`
485
+ );
486
+ const totalRows = (countResult as any)[0].count;
487
+
488
+ const totalPages = Math.ceil(totalRows / pageSize);
489
+ const offset = (page - 1) * pageSize;
490
+
491
+ // Build query
492
+ let query = `SELECT * FROM ${mysql.escapeId(table)}`;
493
+ if (orderBy) {
494
+ query += ` ORDER BY ${mysql.escapeId(orderBy)}`;
495
+ }
496
+ query += ` LIMIT ${pageSize} OFFSET ${offset}`;
497
+
498
+ const [rows] = await connection.query(query);
499
+
500
+ return {
501
+ rows: rows as Record<string, unknown>[],
502
+ currentPage: page,
503
+ totalPages,
504
+ totalRows,
505
+ };
506
+ } finally {
507
+ connection.release();
508
+ }
509
+ }
510
+
511
+ async sampleData(table: string, count = 10, database?: string): Promise<SampleDataResult> {
512
+ if (!this.pool) throw new Error('Not connected');
513
+
514
+ const connection = await this.pool.getConnection();
515
+
516
+ try {
517
+ if (database && database !== this.config.database) {
518
+ await connection.query(`USE ${mysql.escapeId(database)}`);
519
+ }
520
+
521
+ const [rows] = await connection.query(
522
+ `SELECT * FROM ${mysql.escapeId(table)} ORDER BY RAND() LIMIT ${count}`
523
+ );
524
+
525
+ return {
526
+ rows: rows as Record<string, unknown>[],
527
+ sampleCount: (rows as any[]).length,
528
+ };
529
+ } finally {
530
+ connection.release();
531
+ }
532
+ }
124
533
  }