@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/src/index.ts CHANGED
@@ -1,19 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import {
5
- CallToolRequestSchema,
6
- ListToolsRequestSchema,
7
- } from '@modelcontextprotocol/sdk/types.js';
4
+ import { z } from 'zod';
8
5
  import { loadConfig } from './config.js';
9
6
  import { createAdapter } from './database/factory.js';
10
7
  import type { DatabaseAdapter } from './database/base.js';
11
8
 
12
- // Create server instance
13
- const server = new Server(
9
+ // Create server instance with new API
10
+ const server = new McpServer(
14
11
  {
15
12
  name: 'hyqf98@easy_db_mcp_server',
16
- version: '1.0.0',
13
+ version: '2.0.0',
17
14
  },
18
15
  {
19
16
  capabilities: {
@@ -24,223 +21,341 @@ const server = new Server(
24
21
 
25
22
  // Load configuration and create adapter
26
23
  let adapter: DatabaseAdapter;
24
+ let cachedConfig: ReturnType<typeof loadConfig>;
27
25
 
28
26
  try {
29
- const config = loadConfig();
30
- adapter = createAdapter(config);
27
+ cachedConfig = loadConfig();
28
+ adapter = createAdapter(cachedConfig);
31
29
  await adapter.connect();
32
- console.error(`Connected to ${config.type} database`);
30
+ console.error(`Connected to ${cachedConfig.type} database`);
33
31
  } catch (error) {
34
32
  console.error('Failed to connect to database:', error);
35
33
  process.exit(1);
36
34
  }
37
35
 
38
- // List available tools
39
- server.setRequestHandler(ListToolsRequestSchema, async () => {
40
- return {
41
- tools: [
42
- {
43
- name: 'list_tables',
44
- description: 'List all tables in the database. If EASYDB_DATABASE is not set, the database parameter must be provided.',
45
- inputSchema: {
46
- type: 'object',
47
- properties: {
48
- database: {
49
- type: 'string',
50
- description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
51
- },
52
- },
53
- },
54
- },
55
- {
56
- name: 'describe_table',
57
- description: 'Get the structure of a table including columns, types, and constraints. If EASYDB_DATABASE is not set, the database parameter must be provided.',
58
- inputSchema: {
59
- type: 'object',
60
- properties: {
61
- table: {
62
- type: 'string',
63
- description: 'Table name',
64
- },
65
- database: {
66
- type: 'string',
67
- description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
68
- },
69
- },
70
- required: ['table'],
71
- },
72
- },
73
- {
74
- name: 'execute_query',
75
- description: 'Execute a SELECT query (read-only). If EASYDB_DATABASE is not set, the database parameter must be provided.',
76
- inputSchema: {
77
- type: 'object',
78
- properties: {
79
- sql: {
80
- type: 'string',
81
- description: 'SQL SELECT query to execute',
82
- },
83
- database: {
84
- type: 'string',
85
- description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
86
- },
87
- },
88
- required: ['sql'],
89
- },
90
- },
91
- {
92
- name: 'execute_sql',
93
- description: 'Execute any SQL statement (requires EASYDB_ALLOW_WRITE=true). If EASYDB_DATABASE is not set, the database parameter must be provided.',
94
- inputSchema: {
95
- type: 'object',
96
- properties: {
97
- sql: {
98
- type: 'string',
99
- description: 'SQL statement to execute (INSERT, UPDATE, DELETE, DDL, etc.)',
100
- },
101
- database: {
102
- type: 'string',
103
- description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
104
- },
105
- },
106
- required: ['sql'],
107
- },
108
- },
109
- ],
110
- };
111
- });
36
+ // Helper function to validate database parameter
37
+ const getDatabase = (paramDb?: string): string => {
38
+ if (paramDb) {
39
+ return paramDb;
40
+ }
41
+ if (cachedConfig.database) {
42
+ return cachedConfig.database;
43
+ }
44
+ throw new Error(
45
+ 'Database name is required. Either set EASYDB_DATABASE environment variable or pass the database parameter in the tool call.'
46
+ );
47
+ };
112
48
 
113
- // Handle tool calls
114
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
115
- const { name, arguments: args } = request.params;
116
- const config = loadConfig();
49
+ // Register existing tools with new API
50
+ server.registerTool(
51
+ 'list_tables',
52
+ {
53
+ title: 'List Tables',
54
+ description: 'List all tables in the database. If EASYDB_DATABASE is not set, the database parameter must be provided.',
55
+ inputSchema: {
56
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set, otherwise required)'),
57
+ },
58
+ },
59
+ async ({ database }) => {
60
+ const dbName = getDatabase(database);
61
+ const tables = await adapter.listTables(dbName);
62
+ return {
63
+ content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }],
64
+ };
65
+ }
66
+ );
117
67
 
118
- // Helper function to validate database parameter
119
- const getDatabase = (paramDb?: string): string => {
120
- if (paramDb) {
121
- return paramDb;
68
+ server.registerTool(
69
+ 'describe_table',
70
+ {
71
+ title: 'Describe Table',
72
+ description: 'Get the structure of a table including columns, types, and constraints. If EASYDB_DATABASE is not set, the database parameter must be provided.',
73
+ inputSchema: {
74
+ table: z.string().describe('Table name'),
75
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set, otherwise required)'),
76
+ },
77
+ },
78
+ async ({ table, database }) => {
79
+ const dbName = getDatabase(database);
80
+ const columns = await adapter.describeTable(table, dbName);
81
+ return {
82
+ content: [{ type: 'text', text: JSON.stringify(columns, null, 2) }],
83
+ };
84
+ }
85
+ );
86
+
87
+ server.registerTool(
88
+ 'execute_query',
89
+ {
90
+ title: 'Execute Query',
91
+ description: 'Execute a SELECT query (read-only). If EASYDB_DATABASE is not set, the database parameter must be provided.',
92
+ inputSchema: {
93
+ sql: z.string().describe('SQL SELECT query to execute'),
94
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set, otherwise required)'),
95
+ },
96
+ },
97
+ async ({ sql, database }) => {
98
+ const dbName = getDatabase(database);
99
+ const result = await adapter.executeQuery(sql, dbName);
100
+ return {
101
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
102
+ };
103
+ }
104
+ );
105
+
106
+ server.registerTool(
107
+ 'execute_sql',
108
+ {
109
+ title: 'Execute SQL',
110
+ description: 'Execute any SQL statement (requires EASYDB_ALLOW_WRITE=true). If EASYDB_DATABASE is not set, the database parameter must be provided.',
111
+ inputSchema: {
112
+ sql: z.string().describe('SQL statement to execute (INSERT, UPDATE, DELETE, DDL, etc.)'),
113
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set, otherwise required)'),
114
+ },
115
+ },
116
+ async ({ sql, database }) => {
117
+ if (!cachedConfig.allowWrite) {
118
+ throw new Error(
119
+ 'SQL execution requires EASYDB_ALLOW_WRITE=true for safety. Please enable this environment variable if you want to allow write operations.'
120
+ );
122
121
  }
123
- if (config.database) {
124
- return config.database;
122
+
123
+ // Improved DDL detection using regex to handle various formats
124
+ const trimmed = sql.trim().toUpperCase();
125
+ const ddlPattern = /^(CREATE|DROP|ALTER|TRUNCATE|RENAME)\s+(TABLE|INDEX|DATABASE|SCHEMA|VIEW|PROCEDURE|FUNCTION|TRIGGER)/;
126
+ const isDDL = ddlPattern.test(trimmed);
127
+
128
+ if (isDDL && !cachedConfig.allowDDL) {
129
+ throw new Error(
130
+ 'DDL statements require EASYDB_ALLOW_DDL=true for safety. Please enable this environment variable if you want to allow DDL operations.'
131
+ );
132
+ }
133
+
134
+ const dbName = getDatabase(database);
135
+ const result = await adapter.executeSQL(sql, dbName);
136
+ return {
137
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
138
+ };
139
+ }
140
+ );
141
+
142
+ server.registerTool(
143
+ 'execute_transaction',
144
+ {
145
+ title: 'Execute Transaction',
146
+ description: 'Execute multiple SQL statements as a transaction. All queries will succeed or all will be rolled back.',
147
+ inputSchema: {
148
+ sql: z.array(z.string()).max(100).describe('Array of SQL statements to execute in transaction (max 100)'),
149
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set)'),
150
+ },
151
+ },
152
+ async ({ sql, database }) => {
153
+ if (!cachedConfig.allowWrite) {
154
+ throw new Error('Transaction execution requires EASYDB_ALLOW_WRITE=true');
125
155
  }
126
- throw new Error(
127
- 'Database name is required. Either set EASYDB_DATABASE environment variable or pass the database parameter in the tool call.'
128
- );
129
- };
130
156
 
131
- try {
132
- switch (name) {
133
- case 'list_tables': {
134
- const database = getDatabase(args?.database as string | undefined);
135
- const tables = await adapter.listTables(database);
136
- return {
137
- content: [
138
- {
139
- type: 'text',
140
- text: JSON.stringify(tables, null, 2),
141
- },
142
- ],
143
- };
144
- }
157
+ const dbName = getDatabase(database);
158
+ const result = await adapter.executeTransaction(sql, dbName);
159
+ return {
160
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
161
+ };
162
+ }
163
+ );
145
164
 
146
- case 'describe_table': {
147
- const database = getDatabase(args?.database as string | undefined);
148
- const columns = await adapter.describeTable(
149
- args?.table as string,
150
- database
151
- );
152
- return {
153
- content: [
154
- {
155
- type: 'text',
156
- text: JSON.stringify(columns, null, 2),
157
- },
158
- ],
159
- };
160
- }
165
+ server.registerTool(
166
+ 'batch_insert',
167
+ {
168
+ title: 'Batch Insert',
169
+ description: 'Insert multiple rows into a table efficiently',
170
+ inputSchema: {
171
+ table: z.string().describe('Table name'),
172
+ data: z.array(z.record(z.string(), z.any())).describe('Array of row data objects'),
173
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set)'),
174
+ },
175
+ },
176
+ async ({ table, data, database }) => {
177
+ if (!cachedConfig.allowWrite) {
178
+ throw new Error('Batch insert requires EASYDB_ALLOW_WRITE=true');
179
+ }
161
180
 
162
- case 'execute_query': {
163
- const database = getDatabase(args?.database as string | undefined);
164
- const result = await adapter.executeQuery(
165
- args?.sql as string,
166
- database
167
- );
168
- return {
169
- content: [
170
- {
171
- type: 'text',
172
- text: JSON.stringify(result, null, 2),
173
- },
174
- ],
175
- };
176
- }
181
+ const dbName = getDatabase(database);
182
+ const result = await adapter.batchInsert(table, data, dbName);
183
+ return {
184
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
185
+ };
186
+ }
187
+ );
177
188
 
178
- case 'execute_sql': {
179
- if (!config.allowWrite) {
180
- throw new Error(
181
- 'SQL execution requires EASYDB_ALLOW_WRITE=true for safety. Please enable this environment variable if you want to allow write operations.'
182
- );
183
- }
189
+ server.registerTool(
190
+ 'batch_update',
191
+ {
192
+ title: 'Batch Update',
193
+ description: 'Update multiple rows in a table based on conditions',
194
+ inputSchema: {
195
+ table: z.string().describe('Table name'),
196
+ updates: z.object({
197
+ set: z.record(z.string(), z.any()).describe('Column values to set'),
198
+ where: z.string().describe('WHERE clause condition'),
199
+ }),
200
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set)'),
201
+ },
202
+ },
203
+ async ({ table, updates, database }) => {
204
+ if (!cachedConfig.allowWrite) {
205
+ throw new Error('Batch update requires EASYDB_ALLOW_WRITE=true');
206
+ }
184
207
 
185
- const sql = args?.sql as string;
186
- const trimmed = sql.trim().toUpperCase();
208
+ const dbName = getDatabase(database);
209
+ const result = await adapter.batchUpdate(table, updates, dbName);
210
+ return {
211
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
212
+ };
213
+ }
214
+ );
187
215
 
188
- // Check for DDL statements
189
- const isDDL =
190
- trimmed.startsWith('CREATE ') ||
191
- trimmed.startsWith('DROP ') ||
192
- trimmed.startsWith('ALTER ') ||
193
- trimmed.startsWith('TRUNCATE ');
216
+ server.registerTool(
217
+ 'export_data',
218
+ {
219
+ title: 'Export Data',
220
+ description: 'Export table data to JSON or CSV file. Default saves to ~/table_name.json',
221
+ inputSchema: {
222
+ table: z.string().describe('Table name to export'),
223
+ format: z.enum(['json', 'csv']).describe('Export format'),
224
+ filePath: z.string().optional().describe('Optional file path (default: ~/table_name.format)'),
225
+ limit: z.number().optional().describe('Optional row limit'),
226
+ where: z.string().optional().describe('Optional WHERE clause'),
227
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set)'),
228
+ },
229
+ },
230
+ async ({ table, format, filePath, limit, where, database }) => {
231
+ const dbName = getDatabase(database);
232
+ const result = await adapter.exportData(table, format, filePath, { limit, where }, dbName);
233
+ return {
234
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
235
+ };
236
+ }
237
+ );
194
238
 
195
- if (isDDL && !config.allowDDL) {
196
- throw new Error(
197
- 'DDL statements require EASYDB_ALLOW_DDL=true for safety. Please enable this environment variable if you want to allow DDL operations.'
198
- );
199
- }
239
+ server.registerTool(
240
+ 'create_table',
241
+ {
242
+ title: 'Create Table',
243
+ description: 'Create a new table with specified columns',
244
+ inputSchema: {
245
+ table: z.string().describe('Table name'),
246
+ columns: z.array(z.object({
247
+ name: z.string().describe('Column name'),
248
+ type: z.string().describe('Column data type'),
249
+ nullable: z.boolean().optional().describe('Whether column allows NULL'),
250
+ primaryKey: z.boolean().optional().describe('Whether column is primary key'),
251
+ defaultValue: z.any().optional().describe('Default value'),
252
+ })).describe('Column definitions'),
253
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set)'),
254
+ },
255
+ },
256
+ async ({ table, columns, database }) => {
257
+ if (!cachedConfig.allowDDL) {
258
+ throw new Error('Create table requires EASYDB_ALLOW_DDL=true');
259
+ }
200
260
 
201
- const database = getDatabase(args?.database as string | undefined);
202
- const result = await adapter.executeSQL(
203
- sql,
204
- database
205
- );
206
- return {
207
- content: [
208
- {
209
- type: 'text',
210
- text: JSON.stringify(result, null, 2),
211
- },
212
- ],
213
- };
214
- }
261
+ const dbName = getDatabase(database);
262
+ const result = await adapter.createTable(table, columns, dbName);
263
+ return {
264
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
265
+ };
266
+ }
267
+ );
215
268
 
216
- default:
217
- throw new Error(`Unknown tool: ${name}`);
269
+ server.registerTool(
270
+ 'drop_table',
271
+ {
272
+ title: 'Drop Table',
273
+ description: 'Delete a table from the database',
274
+ inputSchema: {
275
+ table: z.string().describe('Table name to drop'),
276
+ ifExists: z.boolean().optional().describe('Use IF EXISTS to avoid error if table does not exist'),
277
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set)'),
278
+ },
279
+ },
280
+ async ({ table, ifExists, database }) => {
281
+ if (!cachedConfig.allowDDL) {
282
+ throw new Error('Drop table requires EASYDB_ALLOW_DDL=true');
218
283
  }
219
- } catch (error) {
284
+
285
+ const dbName = getDatabase(database);
286
+ const result = await adapter.dropTable(table, ifExists, dbName);
220
287
  return {
221
- content: [
222
- {
223
- type: 'text',
224
- text: JSON.stringify(
225
- {
226
- success: false,
227
- error: error instanceof Error ? error.message : String(error),
228
- },
229
- null,
230
- 2
231
- ),
232
- },
233
- ],
234
- isError: true,
288
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
235
289
  };
236
290
  }
237
- });
291
+ );
292
+
293
+ server.registerTool(
294
+ 'get_table_stats',
295
+ {
296
+ title: 'Get Table Statistics',
297
+ description: 'Get detailed statistics about a table including row count, columns, indexes, and size',
298
+ inputSchema: {
299
+ table: z.string().describe('Table name'),
300
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set)'),
301
+ },
302
+ },
303
+ async ({ table, database }) => {
304
+ const dbName = getDatabase(database);
305
+ const result = await adapter.getTableStats(table, dbName);
306
+ return {
307
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
308
+ };
309
+ }
310
+ );
311
+
312
+ server.registerTool(
313
+ 'preview_data',
314
+ {
315
+ title: 'Preview Data',
316
+ description: 'Preview table data with pagination',
317
+ inputSchema: {
318
+ table: z.string().describe('Table name'),
319
+ page: z.number().optional().describe('Page number (default: 1)'),
320
+ pageSize: z.number().optional().describe('Rows per page (default: 50)'),
321
+ orderBy: z.string().optional().describe('Order by column name'),
322
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set)'),
323
+ },
324
+ },
325
+ async ({ table, page = 1, pageSize = 50, orderBy, database }) => {
326
+ const dbName = getDatabase(database);
327
+ const result = await adapter.previewData(table, page, pageSize, orderBy, dbName);
328
+ return {
329
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
330
+ };
331
+ }
332
+ );
333
+
334
+ server.registerTool(
335
+ 'sample_data',
336
+ {
337
+ title: 'Sample Data',
338
+ description: 'Get a random sample of rows from a table',
339
+ inputSchema: {
340
+ table: z.string().describe('Table name'),
341
+ count: z.number().optional().describe('Number of rows to sample (default: 10)'),
342
+ database: z.string().optional().describe('Database name (optional if EASYDB_DATABASE is set)'),
343
+ },
344
+ },
345
+ async ({ table, count = 10, database }) => {
346
+ const dbName = getDatabase(database);
347
+ const result = await adapter.sampleData(table, count, dbName);
348
+ return {
349
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
350
+ };
351
+ }
352
+ );
238
353
 
239
354
  // Start server
240
355
  async function main() {
241
356
  const transport = new StdioServerTransport();
242
357
  await server.connect(transport);
243
- console.error('EasyDB MCP Server running');
358
+ console.error('EasyDB MCP Server v2.0.0 running');
244
359
  }
245
360
 
246
361
  main().catch((error) => {
@@ -0,0 +1,46 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const homedir = os.homedir();
6
+
7
+ export function expandTilde(filePath: string): string {
8
+ if (filePath.startsWith('~')) {
9
+ return path.join(homedir, filePath.slice(1));
10
+ }
11
+ return filePath;
12
+ }
13
+
14
+ export function formatFileSize(bytes: number): string {
15
+ const units = ['B', 'KB', 'MB', 'GB'];
16
+ let size = bytes;
17
+ let unitIndex = 0;
18
+
19
+ while (size >= 1024 && unitIndex < units.length - 1) {
20
+ size /= 1024;
21
+ unitIndex++;
22
+ }
23
+
24
+ return size.toFixed(2) + ' ' + units[unitIndex];
25
+ }
26
+
27
+ export function ensureDirectoryExists(filePath: string): void {
28
+ const dir = path.dirname(filePath);
29
+ if (!fs.existsSync(dir)) {
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ export function getDefaultFilePath(baseName: string, extension: string): string {
35
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
36
+ const time = new Date().toTimeString().split(' ')[0].replace(/:/g, '');
37
+ let fileName = baseName + '.' + extension;
38
+
39
+ // Check if file exists, add timestamp if it does
40
+ const fullPath = path.join(homedir, fileName);
41
+ if (fs.existsSync(fullPath)) {
42
+ fileName = baseName + '_' + timestamp + '_' + time + '.' + extension;
43
+ }
44
+
45
+ return path.join(homedir, fileName);
46
+ }