@devzhou/mcp-mysql 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/bin/mcp-mysql.js +3 -0
- package/index.js +258 -0
- package/package.json +37 -0
package/bin/mcp-mysql.js
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import mysql from "mysql2/promise";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
// ─── 命令行参数解析 ───
|
|
13
|
+
|
|
14
|
+
let connectionString = "";
|
|
15
|
+
let skipConfirm = false;
|
|
16
|
+
|
|
17
|
+
function parseArgs() {
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const arg = args[i];
|
|
21
|
+
const argLower = arg.toLowerCase();
|
|
22
|
+
|
|
23
|
+
// 支持 ConnectionString=xxx 和 ConnectionString xxx(忽略大小写和前导 -)
|
|
24
|
+
if (argLower.replace(/^-+/, "").startsWith("connectionstring")) {
|
|
25
|
+
const stripped = arg.replace(/^-+/, "");
|
|
26
|
+
const idx = stripped.indexOf("=");
|
|
27
|
+
if (idx !== -1) {
|
|
28
|
+
connectionString = stripped.substring(idx + 1);
|
|
29
|
+
} else if (i + 1 < args.length) {
|
|
30
|
+
connectionString = args[++i];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 支持 SkipConfirm 和 SkipConfirm=true(忽略大小写和前导 -)
|
|
35
|
+
if (argLower.replace(/^-+/, "").startsWith("skipconfirm")) {
|
|
36
|
+
const stripped = arg.replace(/^-+/, "");
|
|
37
|
+
const idx = stripped.indexOf("=");
|
|
38
|
+
if (idx !== -1) {
|
|
39
|
+
skipConfirm = stripped.substring(idx + 1).toLowerCase() === "true";
|
|
40
|
+
} else {
|
|
41
|
+
skipConfirm = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── 配置与连接字符串 ───
|
|
48
|
+
|
|
49
|
+
function getConfigPath(fileName) {
|
|
50
|
+
// 优先查找当前工作目录,再查找可执行文件目录
|
|
51
|
+
const cwdPath = path.join(process.cwd(), fileName);
|
|
52
|
+
if (fs.existsSync(cwdPath)) return cwdPath;
|
|
53
|
+
return path.join(__dirname, "..", fileName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readConfig(configPath) {
|
|
57
|
+
const data = fs.readFileSync(configPath, "utf8");
|
|
58
|
+
const cfg = JSON.parse(data);
|
|
59
|
+
return cfg.database;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getADOParam(params, ...keys) {
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
if (params[key.toLowerCase()] !== undefined) {
|
|
65
|
+
return params[key.toLowerCase()];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 将 ADO.NET 格式连接字符串转换为 mysql2 兼容的连接配置对象
|
|
73
|
+
* 格式: Server=host;Port=3306;Database=dbname;User=root;Password=123456
|
|
74
|
+
*/
|
|
75
|
+
function parseAdoNetCS(cs) {
|
|
76
|
+
cs = cs.trim();
|
|
77
|
+
const params = {};
|
|
78
|
+
cs.split(";").forEach((pair) => {
|
|
79
|
+
pair = pair.trim();
|
|
80
|
+
if (!pair) return;
|
|
81
|
+
const eqIdx = pair.indexOf("=");
|
|
82
|
+
if (eqIdx === -1) return;
|
|
83
|
+
const key = pair.substring(0, eqIdx).trim().toLowerCase();
|
|
84
|
+
const value = pair.substring(eqIdx + 1).trim();
|
|
85
|
+
params[key] = value;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const config = {};
|
|
89
|
+
config.user = getADOParam(params, "user", "uid", "username") || undefined;
|
|
90
|
+
config.password = getADOParam(params, "password", "pwd") || undefined;
|
|
91
|
+
config.host = getADOParam(params, "server", "host", "data source", "datasource") || "127.0.0.1";
|
|
92
|
+
const portStr = getADOParam(params, "port");
|
|
93
|
+
config.port = portStr ? parseInt(portStr, 10) : 3306;
|
|
94
|
+
config.database = getADOParam(params, "database", "initial catalog") || undefined;
|
|
95
|
+
|
|
96
|
+
// 额外参数
|
|
97
|
+
const charset = getADOParam(params, "charset");
|
|
98
|
+
if (charset) config.charset = charset;
|
|
99
|
+
|
|
100
|
+
return config;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildDBConfig() {
|
|
104
|
+
if (connectionString) {
|
|
105
|
+
return parseAdoNetCS(connectionString);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const configPath = getConfigPath("config.json");
|
|
109
|
+
if (fs.existsSync(configPath)) {
|
|
110
|
+
const dbCfg = readConfig(configPath);
|
|
111
|
+
return {
|
|
112
|
+
user: dbCfg.user,
|
|
113
|
+
password: dbCfg.password,
|
|
114
|
+
host: dbCfg.host,
|
|
115
|
+
port: dbCfg.port,
|
|
116
|
+
database: dbCfg.dbname,
|
|
117
|
+
charset: "utf8mb4",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new Error("未提供数据库连接信息:请通过 -ConnectionString 参数或 config.json 配置文件提供");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── 数据库操作 ───
|
|
125
|
+
|
|
126
|
+
async function createConnection() {
|
|
127
|
+
const config = buildDBConfig();
|
|
128
|
+
const connection = await mysql.createConnection(config);
|
|
129
|
+
await connection.ping();
|
|
130
|
+
return connection;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function executeSelect(sql) {
|
|
134
|
+
const connection = await createConnection();
|
|
135
|
+
try {
|
|
136
|
+
const [rows] = await connection.query(sql);
|
|
137
|
+
|
|
138
|
+
// 处理 Buffer 类型,转换为字符串
|
|
139
|
+
const results = (rows || []).map((row) => {
|
|
140
|
+
const newRow = {};
|
|
141
|
+
for (const [key, value] of Object.entries(row)) {
|
|
142
|
+
newRow[key] = Buffer.isBuffer(value) ? value.toString("utf8") : value;
|
|
143
|
+
}
|
|
144
|
+
return newRow;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (results.length === 0) return "[]";
|
|
148
|
+
return JSON.stringify(results);
|
|
149
|
+
} finally {
|
|
150
|
+
await connection.end();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function executeExec(sql) {
|
|
155
|
+
const connection = await createConnection();
|
|
156
|
+
try {
|
|
157
|
+
const [result] = await connection.execute(sql);
|
|
158
|
+
const rowsAffected = result.affectedRows || 0;
|
|
159
|
+
return `${rowsAffected} rows affected`;
|
|
160
|
+
} finally {
|
|
161
|
+
await connection.end();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── MCP 服务器 ───
|
|
166
|
+
|
|
167
|
+
async function main() {
|
|
168
|
+
parseArgs();
|
|
169
|
+
|
|
170
|
+
const mcpServer = new McpServer({
|
|
171
|
+
name: "mcp-mysql",
|
|
172
|
+
version: "1.0.0",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// 注册 select 工具
|
|
176
|
+
mcpServer.tool(
|
|
177
|
+
"select",
|
|
178
|
+
"执行查询sql",
|
|
179
|
+
{
|
|
180
|
+
sql: z.string().describe("执行的sql语句"),
|
|
181
|
+
},
|
|
182
|
+
async ({ sql }) => {
|
|
183
|
+
try {
|
|
184
|
+
const result = await executeSelect(sql);
|
|
185
|
+
return { content: [{ type: "text", text: result }] };
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return { content: [{ type: "text", text: `查询执行失败: ${err.message}` }] };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// 注册 exec 工具(带确认机制)
|
|
193
|
+
mcpServer.tool(
|
|
194
|
+
"exec",
|
|
195
|
+
"执行sql",
|
|
196
|
+
{
|
|
197
|
+
sql: z.string().describe("执行的sql语句"),
|
|
198
|
+
},
|
|
199
|
+
async ({ sql }) => {
|
|
200
|
+
try {
|
|
201
|
+
// 如果跳过确认,直接执行
|
|
202
|
+
if (skipConfirm) {
|
|
203
|
+
const result = await executeExec(sql);
|
|
204
|
+
return { content: [{ type: "text", text: result }] };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 尝试向客户端发送确认请求(Elicitation)
|
|
208
|
+
try {
|
|
209
|
+
const elicitationResult = await mcpServer.server.elicitInput({
|
|
210
|
+
mode: "form",
|
|
211
|
+
message: `即将执行以下SQL语句,请确认是否继续执行:\n\n${sql}`,
|
|
212
|
+
requestedSchema: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
confirm: {
|
|
216
|
+
type: "boolean",
|
|
217
|
+
description: "是否确认执行此SQL语句",
|
|
218
|
+
default: false,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
required: ["confirm"],
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
switch (elicitationResult.action) {
|
|
226
|
+
case "accept":
|
|
227
|
+
if (elicitationResult.content?.confirm === true) {
|
|
228
|
+
const result = await executeExec(sql);
|
|
229
|
+
return { content: [{ type: "text", text: result }] };
|
|
230
|
+
}
|
|
231
|
+
return { content: [{ type: "text", text: "用户取消了执行" }] };
|
|
232
|
+
case "decline":
|
|
233
|
+
return { content: [{ type: "text", text: "用户拒绝了执行" }] };
|
|
234
|
+
case "cancel":
|
|
235
|
+
return { content: [{ type: "text", text: "用户取消了执行" }] };
|
|
236
|
+
default:
|
|
237
|
+
return { content: [{ type: "text", text: `未知的用户响应: ${elicitationResult.action}` }] };
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
// 客户端不支持 elicitation,直接执行
|
|
241
|
+
const result = await executeExec(sql);
|
|
242
|
+
return { content: [{ type: "text", text: result }] };
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
return { content: [{ type: "text", text: err.message }] };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// 启动 STDIO 传输
|
|
251
|
+
const transport = new StdioServerTransport();
|
|
252
|
+
await mcpServer.connect(transport);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
main().catch((err) => {
|
|
256
|
+
console.error("服务器启动失败:", err);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devzhou/mcp-mysql",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP MySQL Server - 通过MCP协议连接MySQL数据库",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-mysql": "./bin/mcp-mysql.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"bin/"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node bin/mcp-mysql.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"mysql",
|
|
20
|
+
"ai",
|
|
21
|
+
"database",
|
|
22
|
+
"model-context-protocol"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/rainy99/mcp-mysql-node.git"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
34
|
+
"mysql2": "^3.11.0",
|
|
35
|
+
"zod": "^3.25.0"
|
|
36
|
+
}
|
|
37
|
+
}
|