@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/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # EasyDB MCP Server
2
+
3
+ 一个通过 npx 部署的 MCP 服务器,为 AI 助手提供统一的数据库访问接口。
4
+
5
+ ## 支持的数据库
6
+
7
+ - MySQL (5.7+)
8
+ - PostgreSQL (12+)
9
+ - SQLite (3+)
10
+
11
+ ## 快速开始
12
+
13
+ ```bash
14
+ npx hyqf98@easy_db_mcp_server
15
+ ```
16
+
17
+ ## 配置
18
+
19
+ ### 环境变量
20
+
21
+ | 变量 | 必填 | 说明 |
22
+ |-----|-----|------|
23
+ | `EASYDB_TYPE` | 是 | 数据库类型:`mysql`、`postgresql` 或 `sqlite` |
24
+ | `EASYDB_HOST` | 是* | 数据库主机(SQLite 不需要) |
25
+ | `EASYDB_PORT` | 否 | 数据库端口(默认值:MySQL=3306、PostgreSQL=5432) |
26
+ | `EASYDB_USER` | 是* | 数据库用户(SQLite 不需要) |
27
+ | `EASYDB_PASSWORD` | 是* | 数据库密码(SQLite 不需要) |
28
+ | `EASYDB_DATABASE` | 否 | 默认数据库名或 SQLite 文件路径 |
29
+ | `EASYDB_ALLOW_WRITE` | 否 | 允许写入操作(默认:false) |
30
+ | `EASYDB_ALLOW_DDL` | 否 | 允许 DDL 操作(默认:false) |
31
+
32
+ ### 配置示例
33
+
34
+ **MySQL:**
35
+ ```bash
36
+ export EASYDB_TYPE=mysql
37
+ export EASYDB_HOST=localhost
38
+ export EASYDB_USER=root
39
+ export EASYDB_PASSWORD=secret
40
+ export EASYDB_DATABASE=mydb
41
+ ```
42
+
43
+ **PostgreSQL:**
44
+ ```bash
45
+ export EASYDB_TYPE=postgresql
46
+ export EASYDB_HOST=localhost
47
+ export EASYDB_USER=postgres
48
+ export EASYDB_PASSWORD=secret
49
+ export EASYDB_DATABASE=mydb
50
+ ```
51
+
52
+ **SQLite:**
53
+ ```bash
54
+ export EASYDB_TYPE=sqlite
55
+ export EASYDB_DATABASE=/path/to/database.db
56
+ ```
57
+
58
+ ## Claude Desktop 配置
59
+
60
+ 将以下配置添加到 Claude Desktop 配置文件中:
61
+
62
+ **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
63
+ **Windows:** `%APPDATA%/Claude/claude_desktop_config.json`
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "database": {
69
+ "command": "npx",
70
+ "args": ["hyqf98@easy_db_mcp_server"],
71
+ "env": {
72
+ "EASYDB_TYPE": "mysql",
73
+ "EASYDB_HOST": "localhost",
74
+ "EASYDB_USER": "root",
75
+ "EASYDB_PASSWORD": "your_password",
76
+ "EASYDB_DATABASE": "mydb"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## 可用工具
84
+
85
+ ### list_tables
86
+
87
+ 列出数据库中的所有表。
88
+
89
+ **参数:**
90
+ - `database`(可选):数据库名
91
+
92
+ **示例:**
93
+ ```
94
+ 请显示数据库中的所有表
95
+ ```
96
+
97
+ ### describe_table
98
+
99
+ 获取表的结构,包括列、类型和约束。
100
+
101
+ **参数:**
102
+ - `table`(必填):表名
103
+ - `database`(可选):数据库名
104
+
105
+ **示例:**
106
+ ```
107
+ 显示 users 表的结构
108
+ ```
109
+
110
+ ### execute_query
111
+
112
+ 执行 SELECT 查询(只读)。
113
+
114
+ **参数:**
115
+ - `sql`(必填):SQL SELECT 查询语句
116
+ - `database`(可选):数据库名
117
+
118
+ **示例:**
119
+ ```
120
+ 查找过去 7 天内创建的所有用户
121
+ ```
122
+
123
+ ### execute_sql
124
+
125
+ 执行任意 SQL 语句(需要 `EASYDB_ALLOW_WRITE=true`)。
126
+
127
+ **参数:**
128
+ - `sql`(必填):SQL 语句
129
+ - `database`(可选):数据库名
130
+
131
+ **注意:** DDL 语句(CREATE、DROP、ALTER)还需要 `EASYDB_ALLOW_DDL=true`。
132
+
133
+ ## 安全性
134
+
135
+ 默认情况下,服务器只允许读取操作。要启用写入权限:
136
+
137
+ ```bash
138
+ export EASYDB_ALLOW_WRITE=true
139
+ ```
140
+
141
+ 要启用 DDL 操作(CREATE、DROP、ALTER):
142
+
143
+ ```bash
144
+ export EASYDB_ALLOW_WRITE=true
145
+ export EASYDB_ALLOW_DDL=true
146
+ ```
147
+
148
+ ## 开发
149
+
150
+ ```bash
151
+ # 安装依赖
152
+ npm install
153
+
154
+ # 构建
155
+ npm run build
156
+
157
+ # 开发模式运行
158
+ npm run dev
159
+
160
+ # 运行生产构建
161
+ npm start
162
+ ```
163
+
164
+ ## 许可证
165
+
166
+ MIT
@@ -0,0 +1,12 @@
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
+ export declare function loadConfig(): DatabaseConfig;
12
+ export declare function getDefaultPort(type: DatabaseConfig['type']): number;
package/dist/config.js ADDED
@@ -0,0 +1,58 @@
1
+ export function loadConfig() {
2
+ const type = process.env.EASYDB_TYPE?.trim();
3
+ if (!type) {
4
+ throw new Error('EASYDB_TYPE is required. Must be one of: mysql, postgresql, sqlite');
5
+ }
6
+ const normalizedType = type.toLowerCase();
7
+ if (!['mysql', 'postgresql', 'sqlite'].includes(normalizedType)) {
8
+ throw new Error(`EASYDB_TYPE must be one of: mysql, postgresql, sqlite. Got: ${type}`);
9
+ }
10
+ // Parse and validate port
11
+ let port;
12
+ const portEnv = process.env.EASYDB_PORT?.trim();
13
+ if (portEnv) {
14
+ port = parseInt(portEnv, 10);
15
+ if (isNaN(port) || port < 1 || port > 65535) {
16
+ throw new Error(`EASYDB_PORT must be a valid port number (1-65535). Got: ${portEnv}`);
17
+ }
18
+ }
19
+ const config = {
20
+ type: normalizedType,
21
+ host: process.env.EASYDB_HOST?.trim(),
22
+ port,
23
+ user: process.env.EASYDB_USER?.trim(),
24
+ password: process.env.EASYDB_PASSWORD,
25
+ database: process.env.EASYDB_DATABASE?.trim(),
26
+ allowWrite: process.env.EASYDB_ALLOW_WRITE === 'true',
27
+ allowDDL: process.env.EASYDB_ALLOW_DDL === 'true',
28
+ };
29
+ // Validate required fields based on type
30
+ if (normalizedType !== 'sqlite') {
31
+ if (!config.host || config.host.length === 0) {
32
+ throw new Error('EASYDB_HOST is required for mysql/postgresql');
33
+ }
34
+ if (!config.user || config.user.length === 0) {
35
+ throw new Error('EASYDB_USER is required for mysql/postgresql');
36
+ }
37
+ if (!config.password) {
38
+ throw new Error('EASYDB_PASSWORD is required for mysql/postgresql');
39
+ }
40
+ // database is optional - can be specified in tool calls
41
+ }
42
+ else {
43
+ if (!config.database || config.database.length === 0) {
44
+ throw new Error('EASYDB_DATABASE (file path) is required for sqlite');
45
+ }
46
+ }
47
+ return config;
48
+ }
49
+ export function getDefaultPort(type) {
50
+ switch (type) {
51
+ case 'mysql':
52
+ return 3306;
53
+ case 'postgresql':
54
+ return 5432;
55
+ case 'sqlite':
56
+ return 0;
57
+ }
58
+ }
@@ -0,0 +1,44 @@
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
+ export interface TableInfo {
10
+ name: string;
11
+ rowCount?: number;
12
+ }
13
+ export interface QueryResult {
14
+ rows: Record<string, unknown>[];
15
+ rowCount: number;
16
+ }
17
+ export interface DatabaseAdapter {
18
+ /**
19
+ * Establish connection to the database
20
+ */
21
+ connect(): Promise<void>;
22
+ /**
23
+ * List all tables in the database
24
+ */
25
+ listTables(database?: string): Promise<TableInfo[]>;
26
+ /**
27
+ * Get table structure/column information
28
+ */
29
+ describeTable(table: string, database?: string): Promise<TableColumn[]>;
30
+ /**
31
+ * Execute a SELECT query
32
+ */
33
+ executeQuery(sql: string, database?: string): Promise<QueryResult>;
34
+ /**
35
+ * Execute any SQL statement (INSERT, UPDATE, DELETE, DDL)
36
+ */
37
+ executeSQL(sql: string, database?: string): Promise<QueryResult | {
38
+ affectedRows: number;
39
+ }>;
40
+ /**
41
+ * Close the database connection
42
+ */
43
+ close(): Promise<void>;
44
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { DatabaseAdapter } from './base.js';
2
+ import type { DatabaseConfig } from '../config.js';
3
+ export declare function createAdapter(config: DatabaseConfig): DatabaseAdapter;
@@ -0,0 +1,15 @@
1
+ import { MySQLAdapter } from './mysql.js';
2
+ import { PostgreSQLAdapter } from './postgresql.js';
3
+ import { SQLiteAdapter } from './sqlite.js';
4
+ export function createAdapter(config) {
5
+ switch (config.type) {
6
+ case 'mysql':
7
+ return new MySQLAdapter(config);
8
+ case 'postgresql':
9
+ return new PostgreSQLAdapter(config);
10
+ case 'sqlite':
11
+ return new SQLiteAdapter(config);
12
+ default:
13
+ throw new Error(`Unsupported database type: ${config.type}`);
14
+ }
15
+ }
@@ -0,0 +1,15 @@
1
+ import type { DatabaseAdapter, TableInfo, TableColumn, QueryResult } from './base.js';
2
+ import type { DatabaseConfig } from '../config.js';
3
+ export declare class MySQLAdapter implements DatabaseAdapter {
4
+ private pool?;
5
+ private config;
6
+ constructor(config: DatabaseConfig);
7
+ connect(): Promise<void>;
8
+ listTables(database?: string): Promise<TableInfo[]>;
9
+ describeTable(table: string, database?: string): Promise<TableColumn[]>;
10
+ executeQuery(sql: string, database?: string): Promise<QueryResult>;
11
+ executeSQL(sql: string, database?: string): Promise<QueryResult | {
12
+ affectedRows: number;
13
+ }>;
14
+ close(): Promise<void>;
15
+ }
@@ -0,0 +1,96 @@
1
+ import mysql from 'mysql2/promise';
2
+ export class MySQLAdapter {
3
+ pool;
4
+ config;
5
+ constructor(config) {
6
+ this.config = config;
7
+ }
8
+ async connect() {
9
+ this.pool = mysql.createPool({
10
+ host: this.config.host,
11
+ port: this.config.port || 3306,
12
+ user: this.config.user,
13
+ password: this.config.password,
14
+ database: this.config.database,
15
+ waitForConnections: true,
16
+ connectionLimit: 10,
17
+ });
18
+ // Test connection
19
+ const connection = await this.pool.getConnection();
20
+ connection.release();
21
+ }
22
+ async listTables(database) {
23
+ if (!this.pool)
24
+ throw new Error('Not connected');
25
+ const dbName = database || this.config.database;
26
+ if (!dbName)
27
+ throw new Error('Database name required');
28
+ const [rows] = await this.pool.query('SELECT TABLE_NAME as name, TABLE_ROWS as rowCount FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?', [dbName]);
29
+ return rows.map((row) => ({
30
+ name: row.name,
31
+ rowCount: row.rowCount,
32
+ }));
33
+ }
34
+ async describeTable(table, database) {
35
+ if (!this.pool)
36
+ throw new Error('Not connected');
37
+ const dbName = database || this.config.database;
38
+ if (!dbName)
39
+ throw new Error('Database name required');
40
+ const [rows] = await this.pool.query(`SELECT
41
+ COLUMN_NAME as name,
42
+ DATA_TYPE as type,
43
+ IS_NULLABLE as nullable,
44
+ COLUMN_DEFAULT as defaultValue,
45
+ COLUMN_KEY as primaryKey,
46
+ EXTRA as extra
47
+ FROM INFORMATION_SCHEMA.COLUMNS
48
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
49
+ ORDER BY ORDINAL_POSITION`, [dbName, table]);
50
+ return rows.map((row) => ({
51
+ name: row.name,
52
+ type: row.type,
53
+ nullable: row.nullable === 'YES',
54
+ defaultValue: row.defaultValue,
55
+ primaryKey: row.primaryKey === 'PRI',
56
+ extra: row.extra,
57
+ }));
58
+ }
59
+ async executeQuery(sql, database) {
60
+ if (!this.pool)
61
+ 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
+ const trimmed = sql.trim().toUpperCase();
66
+ if (!trimmed.startsWith('SELECT')) {
67
+ throw new Error('Only SELECT queries allowed in executeQuery');
68
+ }
69
+ const [rows] = await this.pool.query(sql);
70
+ return {
71
+ rows: rows,
72
+ rowCount: Array.isArray(rows) ? rows.length : 0,
73
+ };
74
+ }
75
+ async executeSQL(sql, database) {
76
+ if (!this.pool)
77
+ throw new Error('Not connected');
78
+ // Allow SQL execution with database-specific queries
79
+ const [result] = await this.pool.query(sql);
80
+ // @ts-ignore - MySQL result structure
81
+ if (result.affectedRows !== undefined) {
82
+ // @ts-ignore
83
+ return { affectedRows: result.affectedRows };
84
+ }
85
+ return {
86
+ rows: result,
87
+ rowCount: Array.isArray(result) ? result.length : 0,
88
+ };
89
+ }
90
+ async close() {
91
+ if (this.pool) {
92
+ await this.pool.end();
93
+ this.pool = undefined;
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,15 @@
1
+ import type { DatabaseAdapter, TableInfo, TableColumn, QueryResult } from './base.js';
2
+ import type { DatabaseConfig } from '../config.js';
3
+ export declare class PostgreSQLAdapter implements DatabaseAdapter {
4
+ private pool?;
5
+ private config;
6
+ constructor(config: DatabaseConfig);
7
+ connect(): Promise<void>;
8
+ listTables(database?: string): Promise<TableInfo[]>;
9
+ describeTable(table: string, database?: string): Promise<TableColumn[]>;
10
+ executeQuery(sql: string, database?: string): Promise<QueryResult>;
11
+ executeSQL(sql: string, database?: string): Promise<QueryResult | {
12
+ affectedRows: number;
13
+ }>;
14
+ close(): Promise<void>;
15
+ }
@@ -0,0 +1,97 @@
1
+ import pg from 'pg';
2
+ const { Pool } = pg;
3
+ export class PostgreSQLAdapter {
4
+ pool;
5
+ config;
6
+ constructor(config) {
7
+ this.config = config;
8
+ }
9
+ async connect() {
10
+ this.pool = new Pool({
11
+ host: this.config.host,
12
+ port: this.config.port || 5432,
13
+ user: this.config.user,
14
+ password: this.config.password,
15
+ database: this.config.database,
16
+ max: 10,
17
+ });
18
+ // Test connection
19
+ const client = await this.pool.connect();
20
+ client.release();
21
+ }
22
+ async listTables(database) {
23
+ if (!this.pool)
24
+ throw new Error('Not connected');
25
+ const dbName = database || this.config.database;
26
+ if (!dbName)
27
+ throw new Error('Database name required');
28
+ const result = await this.pool.query(`SELECT tablename as name, n_live_tup as "rowCount"
29
+ FROM pg_stat_user_tables
30
+ WHERE schemaname = 'public'`);
31
+ return result.rows.map((row) => ({
32
+ name: row.name,
33
+ rowCount: row.rowCount,
34
+ }));
35
+ }
36
+ async describeTable(table, database) {
37
+ if (!this.pool)
38
+ throw new Error('Not connected');
39
+ const dbName = database || this.config.database;
40
+ const result = await this.pool.query(`SELECT
41
+ column_name as name,
42
+ data_type as type,
43
+ is_nullable as nullable,
44
+ column_default as "defaultValue",
45
+ COALESCE(pk.primary, false) as "primaryKey"
46
+ FROM information_schema.columns
47
+ LEFT JOIN (
48
+ SELECT a.attname as primary, c.relname
49
+ FROM pg_index i
50
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
51
+ JOIN pg_class c ON c.oid = i.indrelid
52
+ WHERE i.indisprimary
53
+ ) pk ON pk.primary = column_name AND pk.relname = $1
54
+ WHERE table_schema = 'public' AND table_name = $1
55
+ ORDER BY ordinal_position`, [table]);
56
+ return result.rows.map((row) => ({
57
+ name: row.name,
58
+ type: row.type,
59
+ nullable: row.nullable === 'YES',
60
+ defaultValue: row.defaultValue,
61
+ primaryKey: row.primaryKey,
62
+ }));
63
+ }
64
+ async executeQuery(sql, database) {
65
+ if (!this.pool)
66
+ throw new Error('Not connected');
67
+ // Allow queries with fully qualified table names (schema.table)
68
+ const trimmed = sql.trim().toUpperCase();
69
+ if (!trimmed.startsWith('SELECT')) {
70
+ throw new Error('Only SELECT queries allowed in executeQuery');
71
+ }
72
+ const result = await this.pool.query(sql);
73
+ return {
74
+ rows: result.rows,
75
+ rowCount: result.rowCount || 0,
76
+ };
77
+ }
78
+ async executeSQL(sql, database) {
79
+ if (!this.pool)
80
+ throw new Error('Not connected');
81
+ // Allow SQL execution with database-specific queries
82
+ const result = await this.pool.query(sql);
83
+ if (result.rowCount !== null && result.command !== 'SELECT') {
84
+ return { affectedRows: result.rowCount };
85
+ }
86
+ return {
87
+ rows: result.rows,
88
+ rowCount: result.rowCount || 0,
89
+ };
90
+ }
91
+ async close() {
92
+ if (this.pool) {
93
+ await this.pool.end();
94
+ this.pool = undefined;
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,15 @@
1
+ import type { DatabaseAdapter, TableInfo, TableColumn, QueryResult } from './base.js';
2
+ import type { DatabaseConfig } from '../config.js';
3
+ export declare class SQLiteAdapter implements DatabaseAdapter {
4
+ private db?;
5
+ private config;
6
+ constructor(config: DatabaseConfig);
7
+ connect(): Promise<void>;
8
+ listTables(): Promise<TableInfo[]>;
9
+ describeTable(table: string): Promise<TableColumn[]>;
10
+ executeQuery(sql: string): Promise<QueryResult>;
11
+ executeSQL(sql: string): Promise<QueryResult | {
12
+ affectedRows: number;
13
+ }>;
14
+ close(): Promise<void>;
15
+ }
@@ -0,0 +1,69 @@
1
+ import Database from 'better-sqlite3';
2
+ export class SQLiteAdapter {
3
+ db;
4
+ config;
5
+ constructor(config) {
6
+ this.config = config;
7
+ }
8
+ async connect() {
9
+ if (!this.config.database) {
10
+ throw new Error('Database file path required for SQLite');
11
+ }
12
+ this.db = new Database(this.config.database);
13
+ }
14
+ async listTables() {
15
+ if (!this.db)
16
+ throw new Error('Not connected');
17
+ const rows = this.db
18
+ .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`)
19
+ .all();
20
+ return rows.map((row) => ({
21
+ name: row.name,
22
+ }));
23
+ }
24
+ async describeTable(table) {
25
+ if (!this.db)
26
+ throw new Error('Not connected');
27
+ const rows = this.db.prepare(`PRAGMA table_info(${table})`).all();
28
+ return rows.map((row) => ({
29
+ name: row.name,
30
+ type: row.type,
31
+ nullable: row.notnull === 0,
32
+ defaultValue: row.dflt_value,
33
+ primaryKey: row.pk > 0,
34
+ }));
35
+ }
36
+ async executeQuery(sql) {
37
+ if (!this.db)
38
+ throw new Error('Not connected');
39
+ const trimmed = sql.trim().toUpperCase();
40
+ if (!trimmed.startsWith('SELECT')) {
41
+ throw new Error('Only SELECT queries allowed in executeQuery');
42
+ }
43
+ const rows = this.db.prepare(sql).all();
44
+ return {
45
+ rows,
46
+ rowCount: rows.length,
47
+ };
48
+ }
49
+ async executeSQL(sql) {
50
+ if (!this.db)
51
+ throw new Error('Not connected');
52
+ const trimmed = sql.trim().toUpperCase();
53
+ if (trimmed.startsWith('SELECT')) {
54
+ const rows = this.db.prepare(sql).all();
55
+ return {
56
+ rows,
57
+ rowCount: rows.length,
58
+ };
59
+ }
60
+ const result = this.db.prepare(sql).run();
61
+ return { affectedRows: result.changes };
62
+ }
63
+ async close() {
64
+ if (this.db) {
65
+ this.db.close();
66
+ this.db = undefined;
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};