@hyqf98/easy_db_mcp_server 1.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/dist/index.js ADDED
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { loadConfig } from './config.js';
6
+ import { createAdapter } from './database/factory.js';
7
+ // Create server instance
8
+ const server = new Server({
9
+ name: 'hyqf98@easy_db_mcp_server',
10
+ version: '1.0.0',
11
+ }, {
12
+ capabilities: {
13
+ tools: {},
14
+ },
15
+ });
16
+ // Load configuration and create adapter
17
+ let adapter;
18
+ try {
19
+ const config = loadConfig();
20
+ adapter = createAdapter(config);
21
+ await adapter.connect();
22
+ console.error(`Connected to ${config.type} database`);
23
+ }
24
+ catch (error) {
25
+ console.error('Failed to connect to database:', error);
26
+ process.exit(1);
27
+ }
28
+ // List available tools
29
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
30
+ return {
31
+ tools: [
32
+ {
33
+ name: 'list_tables',
34
+ description: 'List all tables in the database. If EASYDB_DATABASE is not set, the database parameter must be provided.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ database: {
39
+ type: 'string',
40
+ description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
41
+ },
42
+ },
43
+ },
44
+ },
45
+ {
46
+ name: 'describe_table',
47
+ description: 'Get the structure of a table including columns, types, and constraints. If EASYDB_DATABASE is not set, the database parameter must be provided.',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ table: {
52
+ type: 'string',
53
+ description: 'Table name',
54
+ },
55
+ database: {
56
+ type: 'string',
57
+ description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
58
+ },
59
+ },
60
+ required: ['table'],
61
+ },
62
+ },
63
+ {
64
+ name: 'execute_query',
65
+ description: 'Execute a SELECT query (read-only). If EASYDB_DATABASE is not set, the database parameter must be provided.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ sql: {
70
+ type: 'string',
71
+ description: 'SQL SELECT query to execute',
72
+ },
73
+ database: {
74
+ type: 'string',
75
+ description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
76
+ },
77
+ },
78
+ required: ['sql'],
79
+ },
80
+ },
81
+ {
82
+ name: 'execute_sql',
83
+ description: 'Execute any SQL statement (requires EASYDB_ALLOW_WRITE=true). If EASYDB_DATABASE is not set, the database parameter must be provided.',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ sql: {
88
+ type: 'string',
89
+ description: 'SQL statement to execute (INSERT, UPDATE, DELETE, DDL, etc.)',
90
+ },
91
+ database: {
92
+ type: 'string',
93
+ description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
94
+ },
95
+ },
96
+ required: ['sql'],
97
+ },
98
+ },
99
+ ],
100
+ };
101
+ });
102
+ // Handle tool calls
103
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
104
+ const { name, arguments: args } = request.params;
105
+ const config = loadConfig();
106
+ // Helper function to validate database parameter
107
+ const getDatabase = (paramDb) => {
108
+ if (paramDb) {
109
+ return paramDb;
110
+ }
111
+ if (config.database) {
112
+ return config.database;
113
+ }
114
+ throw new Error('Database name is required. Either set EASYDB_DATABASE environment variable or pass the database parameter in the tool call.');
115
+ };
116
+ try {
117
+ switch (name) {
118
+ case 'list_tables': {
119
+ const database = getDatabase(args?.database);
120
+ const tables = await adapter.listTables(database);
121
+ return {
122
+ content: [
123
+ {
124
+ type: 'text',
125
+ text: JSON.stringify(tables, null, 2),
126
+ },
127
+ ],
128
+ };
129
+ }
130
+ case 'describe_table': {
131
+ const database = getDatabase(args?.database);
132
+ const columns = await adapter.describeTable(args?.table, database);
133
+ return {
134
+ content: [
135
+ {
136
+ type: 'text',
137
+ text: JSON.stringify(columns, null, 2),
138
+ },
139
+ ],
140
+ };
141
+ }
142
+ case 'execute_query': {
143
+ const database = getDatabase(args?.database);
144
+ const result = await adapter.executeQuery(args?.sql, database);
145
+ return {
146
+ content: [
147
+ {
148
+ type: 'text',
149
+ text: JSON.stringify(result, null, 2),
150
+ },
151
+ ],
152
+ };
153
+ }
154
+ case 'execute_sql': {
155
+ if (!config.allowWrite) {
156
+ throw new Error('SQL execution requires EASYDB_ALLOW_WRITE=true for safety. Please enable this environment variable if you want to allow write operations.');
157
+ }
158
+ const sql = args?.sql;
159
+ const trimmed = sql.trim().toUpperCase();
160
+ // Check for DDL statements
161
+ const isDDL = trimmed.startsWith('CREATE ') ||
162
+ trimmed.startsWith('DROP ') ||
163
+ trimmed.startsWith('ALTER ') ||
164
+ trimmed.startsWith('TRUNCATE ');
165
+ if (isDDL && !config.allowDDL) {
166
+ throw new Error('DDL statements require EASYDB_ALLOW_DDL=true for safety. Please enable this environment variable if you want to allow DDL operations.');
167
+ }
168
+ const database = getDatabase(args?.database);
169
+ const result = await adapter.executeSQL(sql, database);
170
+ return {
171
+ content: [
172
+ {
173
+ type: 'text',
174
+ text: JSON.stringify(result, null, 2),
175
+ },
176
+ ],
177
+ };
178
+ }
179
+ default:
180
+ throw new Error(`Unknown tool: ${name}`);
181
+ }
182
+ }
183
+ catch (error) {
184
+ return {
185
+ content: [
186
+ {
187
+ type: 'text',
188
+ text: JSON.stringify({
189
+ success: false,
190
+ error: error instanceof Error ? error.message : String(error),
191
+ }, null, 2),
192
+ },
193
+ ],
194
+ isError: true,
195
+ };
196
+ }
197
+ });
198
+ // Start server
199
+ async function main() {
200
+ const transport = new StdioServerTransport();
201
+ await server.connect(transport);
202
+ console.error('EasyDB MCP Server running');
203
+ }
204
+ main().catch((error) => {
205
+ console.error('Server error:', error);
206
+ process.exit(1);
207
+ });
208
+ // Cleanup on exit
209
+ process.on('SIGINT', async () => {
210
+ console.error('\\nShutting down...');
211
+ await adapter.close();
212
+ process.exit(0);
213
+ });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@hyqf98/easy_db_mcp_server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for database access (MySQL, PostgreSQL, SQLite)",
5
+ "type": "module",
6
+ "bin": {
7
+ "hyqf98@easy_db_mcp_server": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/index.ts",
12
+ "start": "node dist/index.js",
13
+ "test": "echo \"Tests not yet implemented\""
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "database",
18
+ "mysql",
19
+ "postgresql",
20
+ "sqlite"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.0.4",
29
+ "mysql2": "^3.11.0",
30
+ "pg": "^8.13.1",
31
+ "better-sqlite3": "^11.7.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.10.2",
35
+ "@types/pg": "^8.11.10",
36
+ "@types/better-sqlite3": "^7.6.12",
37
+ "typescript": "^5.7.2",
38
+ "tsx": "^4.19.2"
39
+ }
40
+ }
package/src/config.ts ADDED
@@ -0,0 +1,75 @@
1
+ export interface DatabaseConfig {
2
+ type: 'mysql' | 'postgresql' | 'sqlite';
3
+ host?: string;
4
+ port?: number;
5
+ user?: string;
6
+ password?: string;
7
+ database?: string;
8
+ allowWrite: boolean;
9
+ allowDDL: boolean;
10
+ }
11
+
12
+ export function loadConfig(): DatabaseConfig {
13
+ const type = process.env.EASYDB_TYPE?.trim();
14
+
15
+ if (!type) {
16
+ throw new Error('EASYDB_TYPE is required. Must be one of: mysql, postgresql, sqlite');
17
+ }
18
+
19
+ const normalizedType = type.toLowerCase();
20
+ if (!['mysql', 'postgresql', 'sqlite'].includes(normalizedType)) {
21
+ throw new Error(`EASYDB_TYPE must be one of: mysql, postgresql, sqlite. Got: ${type}`);
22
+ }
23
+
24
+ // Parse and validate port
25
+ let port: number | undefined;
26
+ const portEnv = process.env.EASYDB_PORT?.trim();
27
+ if (portEnv) {
28
+ port = parseInt(portEnv, 10);
29
+ if (isNaN(port) || port < 1 || port > 65535) {
30
+ throw new Error(`EASYDB_PORT must be a valid port number (1-65535). Got: ${portEnv}`);
31
+ }
32
+ }
33
+
34
+ const config: DatabaseConfig = {
35
+ type: normalizedType as DatabaseConfig['type'],
36
+ host: process.env.EASYDB_HOST?.trim(),
37
+ port,
38
+ user: process.env.EASYDB_USER?.trim(),
39
+ password: process.env.EASYDB_PASSWORD,
40
+ database: process.env.EASYDB_DATABASE?.trim(),
41
+ allowWrite: process.env.EASYDB_ALLOW_WRITE === 'true',
42
+ allowDDL: process.env.EASYDB_ALLOW_DDL === 'true',
43
+ };
44
+
45
+ // Validate required fields based on type
46
+ if (normalizedType !== 'sqlite') {
47
+ if (!config.host || config.host.length === 0) {
48
+ throw new Error('EASYDB_HOST is required for mysql/postgresql');
49
+ }
50
+ if (!config.user || config.user.length === 0) {
51
+ throw new Error('EASYDB_USER is required for mysql/postgresql');
52
+ }
53
+ if (!config.password) {
54
+ throw new Error('EASYDB_PASSWORD is required for mysql/postgresql');
55
+ }
56
+ // database is optional - can be specified in tool calls
57
+ } else {
58
+ if (!config.database || config.database.length === 0) {
59
+ throw new Error('EASYDB_DATABASE (file path) is required for sqlite');
60
+ }
61
+ }
62
+
63
+ return config;
64
+ }
65
+
66
+ export function getDefaultPort(type: DatabaseConfig['type']): number {
67
+ switch (type) {
68
+ case 'mysql':
69
+ return 3306;
70
+ case 'postgresql':
71
+ return 5432;
72
+ case 'sqlite':
73
+ return 0;
74
+ }
75
+ }
@@ -0,0 +1,50 @@
1
+ export interface TableColumn {
2
+ name: string;
3
+ type: string;
4
+ nullable: boolean;
5
+ defaultValue: string | null;
6
+ primaryKey: boolean;
7
+ extra?: string;
8
+ }
9
+
10
+ export interface TableInfo {
11
+ name: string;
12
+ rowCount?: number;
13
+ }
14
+
15
+ export interface QueryResult {
16
+ rows: Record<string, unknown>[];
17
+ rowCount: number;
18
+ }
19
+
20
+ export interface DatabaseAdapter {
21
+ /**
22
+ * Establish connection to the database
23
+ */
24
+ connect(): Promise<void>;
25
+
26
+ /**
27
+ * List all tables in the database
28
+ */
29
+ listTables(database?: string): Promise<TableInfo[]>;
30
+
31
+ /**
32
+ * Get table structure/column information
33
+ */
34
+ describeTable(table: string, database?: string): Promise<TableColumn[]>;
35
+
36
+ /**
37
+ * Execute a SELECT query
38
+ */
39
+ executeQuery(sql: string, database?: string): Promise<QueryResult>;
40
+
41
+ /**
42
+ * Execute any SQL statement (INSERT, UPDATE, DELETE, DDL)
43
+ */
44
+ executeSQL(sql: string, database?: string): Promise<QueryResult | { affectedRows: number }>;
45
+
46
+ /**
47
+ * Close the database connection
48
+ */
49
+ close(): Promise<void>;
50
+ }
@@ -0,0 +1,18 @@
1
+ import { MySQLAdapter } from './mysql.js';
2
+ import { PostgreSQLAdapter } from './postgresql.js';
3
+ import { SQLiteAdapter } from './sqlite.js';
4
+ import type { DatabaseAdapter } from './base.js';
5
+ import type { DatabaseConfig } from '../config.js';
6
+
7
+ export function createAdapter(config: DatabaseConfig): DatabaseAdapter {
8
+ switch (config.type) {
9
+ case 'mysql':
10
+ return new MySQLAdapter(config);
11
+ case 'postgresql':
12
+ return new PostgreSQLAdapter(config);
13
+ case 'sqlite':
14
+ return new SQLiteAdapter(config);
15
+ default:
16
+ throw new Error(`Unsupported database type: ${config.type}`);
17
+ }
18
+ }
@@ -0,0 +1,124 @@
1
+ import mysql from 'mysql2/promise';
2
+ import type {
3
+ DatabaseAdapter,
4
+ TableInfo,
5
+ TableColumn,
6
+ QueryResult,
7
+ } from './base.js';
8
+ import type { DatabaseConfig } from '../config.js';
9
+
10
+ export class MySQLAdapter implements DatabaseAdapter {
11
+ private pool?: mysql.Pool;
12
+ private config: DatabaseConfig;
13
+
14
+ constructor(config: DatabaseConfig) {
15
+ this.config = config;
16
+ }
17
+
18
+ async connect(): Promise<void> {
19
+ this.pool = mysql.createPool({
20
+ host: this.config.host,
21
+ port: this.config.port || 3306,
22
+ user: this.config.user,
23
+ password: this.config.password,
24
+ database: this.config.database,
25
+ waitForConnections: true,
26
+ connectionLimit: 10,
27
+ });
28
+
29
+ // Test connection
30
+ const connection = await this.pool.getConnection();
31
+ connection.release();
32
+ }
33
+
34
+ async listTables(database?: string): Promise<TableInfo[]> {
35
+ if (!this.pool) throw new Error('Not connected');
36
+
37
+ const dbName = database || this.config.database;
38
+ if (!dbName) throw new Error('Database name required');
39
+
40
+ const [rows] = await this.pool.query<mysql.RowDataPacket[]>(
41
+ 'SELECT TABLE_NAME as name, TABLE_ROWS as rowCount FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?',
42
+ [dbName]
43
+ );
44
+
45
+ return rows.map((row: any) => ({
46
+ name: row.name,
47
+ rowCount: row.rowCount,
48
+ }));
49
+ }
50
+
51
+ async describeTable(table: string, database?: string): Promise<TableColumn[]> {
52
+ if (!this.pool) throw new Error('Not connected');
53
+
54
+ const dbName = database || this.config.database;
55
+ if (!dbName) throw new Error('Database name required');
56
+
57
+ const [rows] = await this.pool.query<mysql.RowDataPacket[]>(
58
+ `SELECT
59
+ COLUMN_NAME as name,
60
+ DATA_TYPE as type,
61
+ IS_NULLABLE as nullable,
62
+ COLUMN_DEFAULT as defaultValue,
63
+ COLUMN_KEY as primaryKey,
64
+ EXTRA as extra
65
+ FROM INFORMATION_SCHEMA.COLUMNS
66
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
67
+ ORDER BY ORDINAL_POSITION`,
68
+ [dbName, table]
69
+ );
70
+
71
+ return rows.map((row: any) => ({
72
+ name: row.name,
73
+ type: row.type,
74
+ nullable: row.nullable === 'YES',
75
+ defaultValue: row.defaultValue,
76
+ primaryKey: row.primaryKey === 'PRI',
77
+ extra: row.extra,
78
+ }));
79
+ }
80
+
81
+ async executeQuery(sql: string, database?: string): Promise<QueryResult> {
82
+ if (!this.pool) throw new Error('Not connected');
83
+
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
+ const trimmed = sql.trim().toUpperCase();
88
+ if (!trimmed.startsWith('SELECT')) {
89
+ throw new Error('Only SELECT queries allowed in executeQuery');
90
+ }
91
+
92
+ const [rows] = await this.pool.query(sql);
93
+
94
+ return {
95
+ rows: rows as Record<string, unknown>[],
96
+ rowCount: Array.isArray(rows) ? rows.length : 0,
97
+ };
98
+ }
99
+
100
+ async executeSQL(sql: string, database?: string): Promise<QueryResult | { affectedRows: number }> {
101
+ if (!this.pool) throw new Error('Not connected');
102
+
103
+ // Allow SQL execution with database-specific queries
104
+ const [result] = await this.pool.query(sql);
105
+
106
+ // @ts-ignore - MySQL result structure
107
+ if (result.affectedRows !== undefined) {
108
+ // @ts-ignore
109
+ return { affectedRows: result.affectedRows };
110
+ }
111
+
112
+ return {
113
+ rows: result as Record<string, unknown>[],
114
+ rowCount: Array.isArray(result) ? result.length : 0,
115
+ };
116
+ }
117
+
118
+ async close(): Promise<void> {
119
+ if (this.pool) {
120
+ await this.pool.end();
121
+ this.pool = undefined;
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,126 @@
1
+ import pg from 'pg';
2
+ import type {
3
+ DatabaseAdapter,
4
+ TableInfo,
5
+ TableColumn,
6
+ QueryResult,
7
+ } from './base.js';
8
+ import type { DatabaseConfig } from '../config.js';
9
+
10
+ const { Pool } = pg;
11
+
12
+ export class PostgreSQLAdapter implements DatabaseAdapter {
13
+ private pool?: pg.Pool;
14
+ private config: DatabaseConfig;
15
+
16
+ constructor(config: DatabaseConfig) {
17
+ this.config = config;
18
+ }
19
+
20
+ async connect(): Promise<void> {
21
+ this.pool = new Pool({
22
+ host: this.config.host,
23
+ port: this.config.port || 5432,
24
+ user: this.config.user,
25
+ password: this.config.password,
26
+ database: this.config.database,
27
+ max: 10,
28
+ });
29
+
30
+ // Test connection
31
+ const client = await this.pool.connect();
32
+ client.release();
33
+ }
34
+
35
+ async listTables(database?: string): Promise<TableInfo[]> {
36
+ if (!this.pool) throw new Error('Not connected');
37
+
38
+ const dbName = database || this.config.database;
39
+ if (!dbName) throw new Error('Database name required');
40
+
41
+ const result = await this.pool.query(
42
+ `SELECT tablename as name, n_live_tup as "rowCount"
43
+ FROM pg_stat_user_tables
44
+ WHERE schemaname = 'public'`
45
+ );
46
+
47
+ return result.rows.map((row: any) => ({
48
+ name: row.name,
49
+ rowCount: row.rowCount,
50
+ }));
51
+ }
52
+
53
+ async describeTable(table: string, database?: string): Promise<TableColumn[]> {
54
+ if (!this.pool) throw new Error('Not connected');
55
+
56
+ const dbName = database || this.config.database;
57
+
58
+ const result = await this.pool.query(
59
+ `SELECT
60
+ column_name as name,
61
+ data_type as type,
62
+ is_nullable as nullable,
63
+ column_default as "defaultValue",
64
+ COALESCE(pk.primary, false) as "primaryKey"
65
+ FROM information_schema.columns
66
+ LEFT JOIN (
67
+ SELECT a.attname as primary, c.relname
68
+ FROM pg_index i
69
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
70
+ JOIN pg_class c ON c.oid = i.indrelid
71
+ WHERE i.indisprimary
72
+ ) pk ON pk.primary = column_name AND pk.relname = $1
73
+ WHERE table_schema = 'public' AND table_name = $1
74
+ ORDER BY ordinal_position`,
75
+ [table]
76
+ );
77
+
78
+ return result.rows.map((row: any) => ({
79
+ name: row.name,
80
+ type: row.type,
81
+ nullable: row.nullable === 'YES',
82
+ defaultValue: row.defaultValue,
83
+ primaryKey: row.primaryKey,
84
+ }));
85
+ }
86
+
87
+ async executeQuery(sql: string, database?: string): Promise<QueryResult> {
88
+ if (!this.pool) throw new Error('Not connected');
89
+
90
+ // Allow queries with fully qualified table names (schema.table)
91
+ const trimmed = sql.trim().toUpperCase();
92
+ if (!trimmed.startsWith('SELECT')) {
93
+ throw new Error('Only SELECT queries allowed in executeQuery');
94
+ }
95
+
96
+ const result = await this.pool.query(sql);
97
+
98
+ return {
99
+ rows: result.rows,
100
+ rowCount: result.rowCount || 0,
101
+ };
102
+ }
103
+
104
+ async executeSQL(sql: string, database?: string): Promise<QueryResult | { affectedRows: number }> {
105
+ if (!this.pool) throw new Error('Not connected');
106
+
107
+ // Allow SQL execution with database-specific queries
108
+ const result = await this.pool.query(sql);
109
+
110
+ if (result.rowCount !== null && result.command !== 'SELECT') {
111
+ return { affectedRows: result.rowCount };
112
+ }
113
+
114
+ return {
115
+ rows: result.rows,
116
+ rowCount: result.rowCount || 0,
117
+ };
118
+ }
119
+
120
+ async close(): Promise<void> {
121
+ if (this.pool) {
122
+ await this.pool.end();
123
+ this.pool = undefined;
124
+ }
125
+ }
126
+ }