@xingyuchen/mysql-mcp-server 3.0.0 → 3.1.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 +316 -240
- package/dist/httpServer.js +847 -0
- package/dist/index.js +1 -1
- package/package.json +81 -75
@@ -0,0 +1,847 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
import express from "express";
|
3
|
+
import cors from "cors";
|
4
|
+
import { randomUUID } from "node:crypto";
|
5
|
+
import "dotenv/config";
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
8
|
+
import { ConnectionManager } from "./connection-manager.js";
|
9
|
+
import { logger } from "./logger.js";
|
10
|
+
const sessions = new Map();
|
11
|
+
// 从 headers 中提取数据库配置
|
12
|
+
function extractDatabaseConfigFromHeaders(req) {
|
13
|
+
const host = req.headers['x-mysql-host'];
|
14
|
+
const port = req.headers['x-mysql-port'];
|
15
|
+
const user = req.headers['x-mysql-user'];
|
16
|
+
const password = req.headers['x-mysql-password'];
|
17
|
+
const database = req.headers['x-mysql-database'];
|
18
|
+
// 如果没有任何数据库配置,返回 null
|
19
|
+
if (!host && !user && !database) {
|
20
|
+
return null;
|
21
|
+
}
|
22
|
+
return {
|
23
|
+
host: host?.trim(),
|
24
|
+
port: port ? parseInt(port) : undefined,
|
25
|
+
user: user?.trim(),
|
26
|
+
password: password?.trim(),
|
27
|
+
database: database?.trim()
|
28
|
+
};
|
29
|
+
}
|
30
|
+
// 创建 MCP Server (每个会话一个实例)
|
31
|
+
function createMCPServer(connectionManager) {
|
32
|
+
const server = new Server({
|
33
|
+
name: "mysql-mcp-server",
|
34
|
+
version: "3.1.0"
|
35
|
+
}, {
|
36
|
+
capabilities: {
|
37
|
+
tools: {}
|
38
|
+
}
|
39
|
+
});
|
40
|
+
// 辅助函数:获取数据库管理器
|
41
|
+
function getTargetManager(connection_id) {
|
42
|
+
const targetManager = connection_id
|
43
|
+
? connectionManager.getConnection(connection_id)
|
44
|
+
: connectionManager.getActiveConnection();
|
45
|
+
if (!targetManager || !targetManager.isConnected()) {
|
46
|
+
const errorMsg = connection_id
|
47
|
+
? `❌ 连接 '${connection_id}' 不存在或未连接`
|
48
|
+
: "❌ 没有活跃的数据库连接,请先使用 connect_database 工具连接到数据库";
|
49
|
+
throw new McpError(ErrorCode.InvalidRequest, errorMsg);
|
50
|
+
}
|
51
|
+
return targetManager;
|
52
|
+
}
|
53
|
+
// 列出可用工具
|
54
|
+
const tools = [
|
55
|
+
{
|
56
|
+
name: "connect_database",
|
57
|
+
description: "连接到MySQL数据库",
|
58
|
+
inputSchema: {
|
59
|
+
type: "object",
|
60
|
+
properties: {
|
61
|
+
host: {
|
62
|
+
type: "string",
|
63
|
+
description: "数据库主机地址(例如:localhost 或 127.0.0.1)",
|
64
|
+
},
|
65
|
+
port: {
|
66
|
+
type: "number",
|
67
|
+
description: "数据库端口号(默认:3306)",
|
68
|
+
default: 3306,
|
69
|
+
},
|
70
|
+
user: {
|
71
|
+
type: "string",
|
72
|
+
description: "数据库用户名",
|
73
|
+
},
|
74
|
+
password: {
|
75
|
+
type: "string",
|
76
|
+
description: "数据库密码",
|
77
|
+
},
|
78
|
+
database: {
|
79
|
+
type: "string",
|
80
|
+
description: "要连接的数据库名称",
|
81
|
+
},
|
82
|
+
connection_id: {
|
83
|
+
type: "string",
|
84
|
+
description: "连接标识符(可选,用于管理多个数据库连接)",
|
85
|
+
},
|
86
|
+
},
|
87
|
+
required: ["host", "user", "password", "database"],
|
88
|
+
},
|
89
|
+
},
|
90
|
+
{
|
91
|
+
name: "execute_query",
|
92
|
+
description: "执行SQL查询语句(支持增删改查所有操作)",
|
93
|
+
inputSchema: {
|
94
|
+
type: "object",
|
95
|
+
properties: {
|
96
|
+
query: {
|
97
|
+
type: "string",
|
98
|
+
description: "要执行的SQL查询语句",
|
99
|
+
},
|
100
|
+
params: {
|
101
|
+
type: "array",
|
102
|
+
description: "SQL参数(可选,用于参数化查询)",
|
103
|
+
items: {
|
104
|
+
type: "string"
|
105
|
+
}
|
106
|
+
},
|
107
|
+
connection_id: {
|
108
|
+
type: "string",
|
109
|
+
description: "连接标识符(可选,不指定则使用当前活跃连接)",
|
110
|
+
},
|
111
|
+
},
|
112
|
+
required: ["query"],
|
113
|
+
},
|
114
|
+
},
|
115
|
+
{
|
116
|
+
name: "begin_transaction",
|
117
|
+
description: "开始数据库事务",
|
118
|
+
inputSchema: {
|
119
|
+
type: "object",
|
120
|
+
properties: {
|
121
|
+
connection_id: {
|
122
|
+
type: "string",
|
123
|
+
description: "连接标识符(可选,不指定则使用当前活跃连接)",
|
124
|
+
},
|
125
|
+
},
|
126
|
+
},
|
127
|
+
},
|
128
|
+
{
|
129
|
+
name: "commit_transaction",
|
130
|
+
description: "提交数据库事务",
|
131
|
+
inputSchema: {
|
132
|
+
type: "object",
|
133
|
+
properties: {
|
134
|
+
connection_id: {
|
135
|
+
type: "string",
|
136
|
+
description: "连接标识符(可选,不指定则使用当前活跃连接)",
|
137
|
+
},
|
138
|
+
},
|
139
|
+
},
|
140
|
+
},
|
141
|
+
{
|
142
|
+
name: "rollback_transaction",
|
143
|
+
description: "回滚数据库事务",
|
144
|
+
inputSchema: {
|
145
|
+
type: "object",
|
146
|
+
properties: {
|
147
|
+
connection_id: {
|
148
|
+
type: "string",
|
149
|
+
description: "连接标识符(可选,不指定则使用当前活跃连接)",
|
150
|
+
},
|
151
|
+
},
|
152
|
+
},
|
153
|
+
},
|
154
|
+
{
|
155
|
+
name: "show_transaction_history",
|
156
|
+
description: "显示当前事务的操作历史",
|
157
|
+
inputSchema: {
|
158
|
+
type: "object",
|
159
|
+
properties: {
|
160
|
+
connection_id: {
|
161
|
+
type: "string",
|
162
|
+
description: "连接标识符(可选,不指定则使用当前活跃连接)",
|
163
|
+
},
|
164
|
+
},
|
165
|
+
},
|
166
|
+
},
|
167
|
+
{
|
168
|
+
name: "rollback_to_step",
|
169
|
+
description: "回滚到指定的操作步骤",
|
170
|
+
inputSchema: {
|
171
|
+
type: "object",
|
172
|
+
properties: {
|
173
|
+
step_number: {
|
174
|
+
type: "number",
|
175
|
+
description: "要回滚到的步骤号(从操作历史中选择)",
|
176
|
+
},
|
177
|
+
connection_id: {
|
178
|
+
type: "string",
|
179
|
+
description: "连接标识符(可选,不指定则使用当前活跃连接)",
|
180
|
+
},
|
181
|
+
},
|
182
|
+
required: ["step_number"],
|
183
|
+
},
|
184
|
+
},
|
185
|
+
{
|
186
|
+
name: "full_rollback",
|
187
|
+
description: "完全回滚当前事务的所有操作",
|
188
|
+
inputSchema: {
|
189
|
+
type: "object",
|
190
|
+
properties: {
|
191
|
+
connection_id: {
|
192
|
+
type: "string",
|
193
|
+
description: "连接标识符(可选,不指定则使用当前活跃连接)",
|
194
|
+
},
|
195
|
+
},
|
196
|
+
},
|
197
|
+
},
|
198
|
+
{
|
199
|
+
name: "show_tables",
|
200
|
+
description: "显示数据库中的所有表及其结构信息",
|
201
|
+
inputSchema: {
|
202
|
+
type: "object",
|
203
|
+
properties: {
|
204
|
+
connection_id: {
|
205
|
+
type: "string",
|
206
|
+
description: "连接标识符(可选,不指定则使用当前活跃连接)",
|
207
|
+
},
|
208
|
+
},
|
209
|
+
},
|
210
|
+
},
|
211
|
+
{
|
212
|
+
name: "describe_table",
|
213
|
+
description: "显示指定表的详细结构信息和样本数据",
|
214
|
+
inputSchema: {
|
215
|
+
type: "object",
|
216
|
+
properties: {
|
217
|
+
table_name: {
|
218
|
+
type: "string",
|
219
|
+
description: "要查看结构的表名",
|
220
|
+
},
|
221
|
+
connection_id: {
|
222
|
+
type: "string",
|
223
|
+
description: "连接标识符(可选,不指定则使用当前活跃连接)",
|
224
|
+
},
|
225
|
+
},
|
226
|
+
required: ["table_name"],
|
227
|
+
},
|
228
|
+
},
|
229
|
+
{
|
230
|
+
name: "disconnect_database",
|
231
|
+
description: "断开数据库连接",
|
232
|
+
inputSchema: {
|
233
|
+
type: "object",
|
234
|
+
properties: {
|
235
|
+
connection_id: {
|
236
|
+
type: "string",
|
237
|
+
description: "要断开的连接标识符(可选,不指定则断开当前活跃连接)",
|
238
|
+
},
|
239
|
+
},
|
240
|
+
},
|
241
|
+
},
|
242
|
+
{
|
243
|
+
name: "list_connections",
|
244
|
+
description: "列出所有数据库连接",
|
245
|
+
inputSchema: {
|
246
|
+
type: "object",
|
247
|
+
properties: {},
|
248
|
+
},
|
249
|
+
},
|
250
|
+
{
|
251
|
+
name: "switch_active_connection",
|
252
|
+
description: "切换当前活跃的数据库连接",
|
253
|
+
inputSchema: {
|
254
|
+
type: "object",
|
255
|
+
properties: {
|
256
|
+
connection_id: {
|
257
|
+
type: "string",
|
258
|
+
description: "要切换到的连接标识符",
|
259
|
+
},
|
260
|
+
},
|
261
|
+
required: ["connection_id"],
|
262
|
+
},
|
263
|
+
},
|
264
|
+
{
|
265
|
+
name: "remove_connection",
|
266
|
+
description: "移除指定的数据库连接",
|
267
|
+
inputSchema: {
|
268
|
+
type: "object",
|
269
|
+
properties: {
|
270
|
+
connection_id: {
|
271
|
+
type: "string",
|
272
|
+
description: "要移除的连接标识符",
|
273
|
+
},
|
274
|
+
},
|
275
|
+
required: ["connection_id"],
|
276
|
+
},
|
277
|
+
},
|
278
|
+
];
|
279
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
280
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
281
|
+
const { name, arguments: args } = request.params;
|
282
|
+
// 记录工具调用
|
283
|
+
logger.info(`工具调用开始 (HTTP)`, { tool: name, args });
|
284
|
+
try {
|
285
|
+
switch (name) {
|
286
|
+
case "connect_database": {
|
287
|
+
const { host, port = 3306, user, password, database, connection_id } = args;
|
288
|
+
// 生成连接ID(如果未提供)
|
289
|
+
const connId = connection_id || `${host}_${database}_${Date.now()}`;
|
290
|
+
// 添加新连接
|
291
|
+
await connectionManager.addConnection(connId, { host, port, user, password, database });
|
292
|
+
const totalConnections = connectionManager.getConnectionCount();
|
293
|
+
const isActive = connectionManager.getActiveConnectionId() === connId;
|
294
|
+
return {
|
295
|
+
content: [
|
296
|
+
{
|
297
|
+
type: "text",
|
298
|
+
text: `✅ 成功连接到MySQL数据库!\n📍 连接ID: ${connId}\n📍 主机: ${host}:${port}\n🗄️ 数据库: ${database}\n👤 用户: ${user}\n🎯 活跃连接: ${isActive ? '是' : '否'}\n📊 总连接数: ${totalConnections}`,
|
299
|
+
},
|
300
|
+
],
|
301
|
+
};
|
302
|
+
}
|
303
|
+
case "execute_query": {
|
304
|
+
const { query, params = [], connection_id } = args;
|
305
|
+
// 获取目标数据库管理器
|
306
|
+
const targetManager = connection_id
|
307
|
+
? connectionManager.getConnection(connection_id)
|
308
|
+
: connectionManager.getActiveConnection();
|
309
|
+
if (!targetManager || !targetManager.isConnected()) {
|
310
|
+
const errorMsg = connection_id
|
311
|
+
? `❌ 连接 '${connection_id}' 不存在或未连接`
|
312
|
+
: "❌ 没有活跃的数据库连接,请先使用 connect_database 工具连接到数据库";
|
313
|
+
throw new McpError(ErrorCode.InvalidRequest, errorMsg);
|
314
|
+
}
|
315
|
+
const result = await targetManager.executeQuery(query, params);
|
316
|
+
const activeConnId = connectionManager.getActiveConnectionId();
|
317
|
+
const usedConnId = connection_id || activeConnId;
|
318
|
+
return {
|
319
|
+
content: [
|
320
|
+
{
|
321
|
+
type: "text",
|
322
|
+
text: `✅ SQL执行成功!\n🔗 使用连接: ${usedConnId}\n📊 操作类型: ${result.type}\n⏱️ 执行时间: ${result.duration}ms\n\n📋 结果:\n${JSON.stringify(result, null, 2)}`,
|
323
|
+
},
|
324
|
+
],
|
325
|
+
};
|
326
|
+
}
|
327
|
+
case "begin_transaction": {
|
328
|
+
const { connection_id } = args;
|
329
|
+
const targetManager = getTargetManager(connection_id);
|
330
|
+
await targetManager.beginTransaction();
|
331
|
+
return {
|
332
|
+
content: [
|
333
|
+
{
|
334
|
+
type: "text",
|
335
|
+
text: `✅ 事务已开始!\n🔗 连接: ${connection_id || connectionManager.getActiveConnectionId()}\n\n⚠️ 请记得在操作完成后提交或回滚事务`,
|
336
|
+
},
|
337
|
+
],
|
338
|
+
};
|
339
|
+
}
|
340
|
+
case "commit_transaction": {
|
341
|
+
const { connection_id } = args;
|
342
|
+
const targetManager = getTargetManager(connection_id);
|
343
|
+
const transactionManager = targetManager.getTransactionManager();
|
344
|
+
const result = await transactionManager.commitTransaction(async () => {
|
345
|
+
return await targetManager.commitTransaction();
|
346
|
+
});
|
347
|
+
return {
|
348
|
+
content: [
|
349
|
+
{
|
350
|
+
type: "text",
|
351
|
+
text: result,
|
352
|
+
},
|
353
|
+
],
|
354
|
+
};
|
355
|
+
}
|
356
|
+
case "rollback_transaction": {
|
357
|
+
const { connection_id } = args;
|
358
|
+
const targetManager = getTargetManager(connection_id);
|
359
|
+
const transactionManager = targetManager.getTransactionManager();
|
360
|
+
const result = await transactionManager.fullRollback(async (query, params) => {
|
361
|
+
return await targetManager.executeQuery(query, params || []);
|
362
|
+
});
|
363
|
+
return {
|
364
|
+
content: [
|
365
|
+
{
|
366
|
+
type: "text",
|
367
|
+
text: result,
|
368
|
+
},
|
369
|
+
],
|
370
|
+
};
|
371
|
+
}
|
372
|
+
case "show_transaction_history": {
|
373
|
+
const { connection_id } = args;
|
374
|
+
const targetManager = getTargetManager(connection_id);
|
375
|
+
const transactionManager = targetManager.getTransactionManager();
|
376
|
+
const historyText = transactionManager.getRollbackOptions();
|
377
|
+
return {
|
378
|
+
content: [
|
379
|
+
{
|
380
|
+
type: "text",
|
381
|
+
text: `📋 事务操作历史\n🔗 连接: ${connection_id || connectionManager.getActiveConnectionId()}\n\n${historyText}`,
|
382
|
+
},
|
383
|
+
],
|
384
|
+
};
|
385
|
+
}
|
386
|
+
case "rollback_to_step": {
|
387
|
+
const { step_number, connection_id } = args;
|
388
|
+
const targetManager = getTargetManager(connection_id);
|
389
|
+
const transactionManager = targetManager.getTransactionManager();
|
390
|
+
const result = await transactionManager.rollbackToStep(step_number, async (query, params) => {
|
391
|
+
return await targetManager.executeQuery(query, params || []);
|
392
|
+
});
|
393
|
+
return {
|
394
|
+
content: [
|
395
|
+
{
|
396
|
+
type: "text",
|
397
|
+
text: result,
|
398
|
+
},
|
399
|
+
],
|
400
|
+
};
|
401
|
+
}
|
402
|
+
case "full_rollback": {
|
403
|
+
const { connection_id } = args;
|
404
|
+
const targetManager = getTargetManager(connection_id);
|
405
|
+
const transactionManager = targetManager.getTransactionManager();
|
406
|
+
const result = await transactionManager.fullRollback(async (query, params) => {
|
407
|
+
return await targetManager.executeQuery(query, params || []);
|
408
|
+
});
|
409
|
+
return {
|
410
|
+
content: [
|
411
|
+
{
|
412
|
+
type: "text",
|
413
|
+
text: result,
|
414
|
+
},
|
415
|
+
],
|
416
|
+
};
|
417
|
+
}
|
418
|
+
case "show_tables": {
|
419
|
+
const { connection_id } = args;
|
420
|
+
const targetManager = getTargetManager(connection_id);
|
421
|
+
const tables = await targetManager.showTables();
|
422
|
+
let result = `📋 数据库概览\n🔗 连接: ${connection_id || connectionManager.getActiveConnectionId()}\n\n`;
|
423
|
+
if (tables.length === 0) {
|
424
|
+
result += "🔍 数据库中没有找到任何表";
|
425
|
+
}
|
426
|
+
else {
|
427
|
+
result += `📊 总共找到 ${tables.length} 个表:\n\n`;
|
428
|
+
for (const table of tables) {
|
429
|
+
const tableName = Object.values(table)[0];
|
430
|
+
try {
|
431
|
+
// 获取表的行数
|
432
|
+
const countResult = await targetManager.executeQuery(`SELECT COUNT(*) as count FROM \`${tableName}\``);
|
433
|
+
const rowCount = countResult.data[0]?.count || 0;
|
434
|
+
// 获取表结构(只显示列名和类型)
|
435
|
+
const structure = await targetManager.describeTable(tableName);
|
436
|
+
const columnInfo = structure.map((col) => `${col.Field}(${col.Type})`).slice(0, 5).join(', ');
|
437
|
+
const moreColumns = structure.length > 5 ? `... +${structure.length - 5}列` : '';
|
438
|
+
result += `🗂️ **${tableName}**\n`;
|
439
|
+
result += ` 📊 行数: ${rowCount}\n`;
|
440
|
+
result += ` 🏗️ 列: ${columnInfo}${moreColumns}\n\n`;
|
441
|
+
}
|
442
|
+
catch (error) {
|
443
|
+
result += `🗂️ **${tableName}**\n`;
|
444
|
+
result += ` ⚠️ 无法获取详细信息\n\n`;
|
445
|
+
}
|
446
|
+
}
|
447
|
+
result += `💡 提示: 使用 describe_table 工具查看具体表的详细结构和样本数据`;
|
448
|
+
}
|
449
|
+
return {
|
450
|
+
content: [
|
451
|
+
{
|
452
|
+
type: "text",
|
453
|
+
text: result,
|
454
|
+
},
|
455
|
+
],
|
456
|
+
};
|
457
|
+
}
|
458
|
+
case "describe_table": {
|
459
|
+
const { table_name, connection_id } = args;
|
460
|
+
const targetManager = getTargetManager(connection_id);
|
461
|
+
// 获取表结构
|
462
|
+
const structure = await targetManager.describeTable(table_name);
|
463
|
+
// 获取表的行数
|
464
|
+
const countResult = await targetManager.executeQuery(`SELECT COUNT(*) as count FROM \`${table_name}\``);
|
465
|
+
const totalRows = countResult.data[0]?.count || 0;
|
466
|
+
// 获取样本数据(最多5行)
|
467
|
+
let sampleData = [];
|
468
|
+
if (totalRows > 0) {
|
469
|
+
const sampleResult = await targetManager.executeQuery(`SELECT * FROM \`${table_name}\` LIMIT 5`);
|
470
|
+
sampleData = sampleResult.data;
|
471
|
+
}
|
472
|
+
// 格式化表结构
|
473
|
+
const structureText = structure
|
474
|
+
.map((col) => `${col.Field.padEnd(20)} | ${col.Type.padEnd(15)} | ${col.Null.padEnd(8)} | ${col.Key.padEnd(8)} | ${(col.Default || 'NULL').toString().padEnd(10)} | ${col.Extra || ''}`)
|
475
|
+
.join("\n");
|
476
|
+
let result = `🔍 表 "${table_name}" 的详细信息\n\n`;
|
477
|
+
result += `📊 基本信息:\n`;
|
478
|
+
result += ` 总行数: ${totalRows}\n`;
|
479
|
+
result += ` 总列数: ${structure.length}\n\n`;
|
480
|
+
result += `🏗️ 表结构:\n`;
|
481
|
+
result += `${"=".repeat(80)}\n`;
|
482
|
+
result += `字段名 | 类型 | 可为空 | 键 | 默认值 | 额外信息\n`;
|
483
|
+
result += `${"=".repeat(80)}\n`;
|
484
|
+
result += `${structureText}\n\n`;
|
485
|
+
if (sampleData.length > 0) {
|
486
|
+
result += `📄 样本数据 (前${sampleData.length}行):\n`;
|
487
|
+
result += `${"=".repeat(80)}\n`;
|
488
|
+
result += JSON.stringify(sampleData, null, 2);
|
489
|
+
}
|
490
|
+
else {
|
491
|
+
result += `📄 样本数据:\n`;
|
492
|
+
result += ` 表中暂无数据`;
|
493
|
+
}
|
494
|
+
result += `\n\n💡 提示: 使用 execute_query 工具可以执行更复杂的查询操作`;
|
495
|
+
return {
|
496
|
+
content: [
|
497
|
+
{
|
498
|
+
type: "text",
|
499
|
+
text: result,
|
500
|
+
},
|
501
|
+
],
|
502
|
+
};
|
503
|
+
}
|
504
|
+
case "disconnect_database": {
|
505
|
+
const { connection_id } = args;
|
506
|
+
if (connection_id) {
|
507
|
+
// 移除指定连接
|
508
|
+
await connectionManager.removeConnection(connection_id);
|
509
|
+
}
|
510
|
+
else if (connectionManager.hasActiveConnection()) {
|
511
|
+
// 移除活跃连接
|
512
|
+
const activeId = connectionManager.getActiveConnectionId();
|
513
|
+
if (activeId) {
|
514
|
+
await connectionManager.removeConnection(activeId);
|
515
|
+
}
|
516
|
+
}
|
517
|
+
return {
|
518
|
+
content: [
|
519
|
+
{
|
520
|
+
type: "text",
|
521
|
+
text: "✅ 数据库连接已断开",
|
522
|
+
},
|
523
|
+
],
|
524
|
+
};
|
525
|
+
}
|
526
|
+
case "list_connections": {
|
527
|
+
const connections = connectionManager.listConnections();
|
528
|
+
if (connections.length === 0) {
|
529
|
+
return {
|
530
|
+
content: [
|
531
|
+
{
|
532
|
+
type: "text",
|
533
|
+
text: `📋 数据库连接列表\n\n🔍 当前没有任何数据库连接`,
|
534
|
+
},
|
535
|
+
],
|
536
|
+
};
|
537
|
+
}
|
538
|
+
let result = `📋 数据库连接列表\n\n📊 总连接数: ${connections.length}\n\n`;
|
539
|
+
connections.forEach((conn, index) => {
|
540
|
+
// 判断是否是通过 Header 创建的连接
|
541
|
+
const isHeaderConnection = conn.id.startsWith('header_connection_');
|
542
|
+
const connectionSource = isHeaderConnection ? '🔐(Header预配置)' : '🔧(工具参数)';
|
543
|
+
result += `${index + 1}. 🔗 **${conn.id}** ${connectionSource}${conn.isActive ? ' 🎯(活跃)' : ''}\n`;
|
544
|
+
result += ` 📍 主机: ${conn.host}:${conn.port}\n`;
|
545
|
+
result += ` 🗄️ 数据库: ${conn.database}\n`;
|
546
|
+
result += ` 👤 用户: ${conn.user}\n`;
|
547
|
+
result += ` ⏰ 连接时间: ${new Date(conn.connectedAt).toLocaleString()}\n\n`;
|
548
|
+
});
|
549
|
+
return {
|
550
|
+
content: [
|
551
|
+
{
|
552
|
+
type: "text",
|
553
|
+
text: result,
|
554
|
+
},
|
555
|
+
],
|
556
|
+
};
|
557
|
+
}
|
558
|
+
case "switch_active_connection": {
|
559
|
+
const { connection_id } = args;
|
560
|
+
await connectionManager.switchActiveConnection(connection_id);
|
561
|
+
const connection = connectionManager.listConnections().find(c => c.id === connection_id);
|
562
|
+
return {
|
563
|
+
content: [
|
564
|
+
{
|
565
|
+
type: "text",
|
566
|
+
text: `✅ 已切换活跃连接到: ${connection_id}\n📍 数据库: ${connection?.database}\n📊 当前总连接数: ${connectionManager.getConnectionCount()}`,
|
567
|
+
},
|
568
|
+
],
|
569
|
+
};
|
570
|
+
}
|
571
|
+
case "remove_connection": {
|
572
|
+
const { connection_id } = args;
|
573
|
+
await connectionManager.removeConnection(connection_id);
|
574
|
+
return {
|
575
|
+
content: [
|
576
|
+
{
|
577
|
+
type: "text",
|
578
|
+
text: `✅ 已移除连接: ${connection_id}\n📊 剩余连接数: ${connectionManager.getConnectionCount()}`,
|
579
|
+
},
|
580
|
+
],
|
581
|
+
};
|
582
|
+
}
|
583
|
+
default:
|
584
|
+
throw new McpError(ErrorCode.MethodNotFound, `未知的工具: ${name}`);
|
585
|
+
}
|
586
|
+
}
|
587
|
+
catch (error) {
|
588
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
589
|
+
// 记录错误
|
590
|
+
logger.error(`工具调用失败 (HTTP)`, {
|
591
|
+
tool: name,
|
592
|
+
args,
|
593
|
+
error: err.message,
|
594
|
+
stack: err.stack
|
595
|
+
});
|
596
|
+
throw new McpError(ErrorCode.InternalError, `❌ 工具执行失败: ${err.message}`);
|
597
|
+
}
|
598
|
+
finally {
|
599
|
+
// 记录工具调用结束
|
600
|
+
logger.info(`工具调用结束 (HTTP)`, { tool: name });
|
601
|
+
}
|
602
|
+
});
|
603
|
+
return server;
|
604
|
+
}
|
605
|
+
const app = express();
|
606
|
+
const PORT = Number(process.env.PORT) || 3000;
|
607
|
+
// CORS 配置 - 允许自定义 Header
|
608
|
+
app.use(cors({
|
609
|
+
origin: '*',
|
610
|
+
methods: ['GET', 'POST', 'OPTIONS'],
|
611
|
+
allowedHeaders: [
|
612
|
+
'Content-Type',
|
613
|
+
'Accept',
|
614
|
+
'Authorization',
|
615
|
+
'Mcp-Session-Id',
|
616
|
+
'X-MySQL-Host',
|
617
|
+
'X-MySQL-Port',
|
618
|
+
'X-MySQL-User',
|
619
|
+
'X-MySQL-Password',
|
620
|
+
'X-MySQL-Database'
|
621
|
+
],
|
622
|
+
exposedHeaders: ['Content-Type', 'Mcp-Session-Id']
|
623
|
+
}));
|
624
|
+
app.use(express.json({ limit: "10mb" }));
|
625
|
+
// 健康检查
|
626
|
+
app.get("/health", (_req, res) => {
|
627
|
+
res.json({
|
628
|
+
status: "healthy",
|
629
|
+
transport: "streamable-http",
|
630
|
+
activeSessions: sessions.size,
|
631
|
+
version: "3.1.0"
|
632
|
+
});
|
633
|
+
});
|
634
|
+
// Streamable HTTP 主端点:POST /mcp(JSON-RPC)
|
635
|
+
app.all("/mcp", async (req, res) => {
|
636
|
+
const sessionIdHeader = req.headers["mcp-session-id"];
|
637
|
+
const method = req.method.toUpperCase();
|
638
|
+
if (method === "POST") {
|
639
|
+
const body = req.body;
|
640
|
+
if (!body) {
|
641
|
+
return res.status(400).json({
|
642
|
+
jsonrpc: "2.0",
|
643
|
+
error: { code: -32600, message: "Empty body" },
|
644
|
+
id: null
|
645
|
+
});
|
646
|
+
}
|
647
|
+
// 忽略通知(如 notifications/initialized)
|
648
|
+
const isNotification = (body.id === undefined || body.id === null) &&
|
649
|
+
typeof body.method === "string" &&
|
650
|
+
body.method.startsWith("notifications/");
|
651
|
+
if (isNotification) {
|
652
|
+
if (sessionIdHeader && sessions.has(sessionIdHeader)) {
|
653
|
+
sessions.get(sessionIdHeader).lastActivity = new Date();
|
654
|
+
}
|
655
|
+
return res.status(204).end();
|
656
|
+
}
|
657
|
+
// 初始化/会话管理
|
658
|
+
const isInit = body.method === "initialize";
|
659
|
+
let session;
|
660
|
+
if (sessionIdHeader && sessions.has(sessionIdHeader)) {
|
661
|
+
session = sessions.get(sessionIdHeader);
|
662
|
+
session.lastActivity = new Date();
|
663
|
+
}
|
664
|
+
else if (isInit) {
|
665
|
+
const newId = randomUUID();
|
666
|
+
const connectionManager = new ConnectionManager();
|
667
|
+
const server = createMCPServer(connectionManager);
|
668
|
+
session = {
|
669
|
+
id: newId,
|
670
|
+
server,
|
671
|
+
connectionManager,
|
672
|
+
headerConnectionId: null,
|
673
|
+
createdAt: new Date(),
|
674
|
+
lastActivity: new Date()
|
675
|
+
};
|
676
|
+
sessions.set(newId, session);
|
677
|
+
res.setHeader("Mcp-Session-Id", newId);
|
678
|
+
logger.info("新会话已创建", { sessionId: newId });
|
679
|
+
}
|
680
|
+
else {
|
681
|
+
return res.status(400).json({
|
682
|
+
jsonrpc: "2.0",
|
683
|
+
error: { code: -32000, message: "No session and not initialize" },
|
684
|
+
id: null
|
685
|
+
});
|
686
|
+
}
|
687
|
+
// 检查并处理 Header 中的数据库配置
|
688
|
+
if (session) {
|
689
|
+
const dbConfig = extractDatabaseConfigFromHeaders(req);
|
690
|
+
if (dbConfig && dbConfig.host && dbConfig.user && dbConfig.database) {
|
691
|
+
// 验证配置是否完整
|
692
|
+
if (!dbConfig.password) {
|
693
|
+
logger.warn("Header 数据库配置不完整,缺少密码", { sessionId: session.id });
|
694
|
+
}
|
695
|
+
else {
|
696
|
+
// 如果 header 中有数据库配置,自动建立连接
|
697
|
+
const headerConnId = `header_connection_${session.id}`;
|
698
|
+
try {
|
699
|
+
// 检查是否已经创建了这个连接
|
700
|
+
if (!session.headerConnectionId ||
|
701
|
+
!session.connectionManager.getConnection(session.headerConnectionId)) {
|
702
|
+
await session.connectionManager.addConnection(headerConnId, {
|
703
|
+
host: dbConfig.host,
|
704
|
+
port: dbConfig.port || 3306,
|
705
|
+
user: dbConfig.user,
|
706
|
+
password: dbConfig.password,
|
707
|
+
database: dbConfig.database
|
708
|
+
});
|
709
|
+
session.headerConnectionId = headerConnId;
|
710
|
+
logger.info("从 Header 自动创建数据库连接", {
|
711
|
+
sessionId: session.id,
|
712
|
+
connectionId: headerConnId,
|
713
|
+
host: dbConfig.host,
|
714
|
+
database: dbConfig.database
|
715
|
+
});
|
716
|
+
}
|
717
|
+
}
|
718
|
+
catch (error) {
|
719
|
+
logger.error("从 Header 创建数据库连接失败", {
|
720
|
+
sessionId: session.id,
|
721
|
+
error: error instanceof Error ? error.message : String(error)
|
722
|
+
});
|
723
|
+
}
|
724
|
+
}
|
725
|
+
}
|
726
|
+
}
|
727
|
+
// 处理核心方法
|
728
|
+
if (body.method === "initialize") {
|
729
|
+
return res.json({
|
730
|
+
jsonrpc: "2.0",
|
731
|
+
result: {
|
732
|
+
protocolVersion: "2024-11-05",
|
733
|
+
capabilities: { tools: {} },
|
734
|
+
serverInfo: { name: "mysql-mcp-server", version: "3.1.0" }
|
735
|
+
},
|
736
|
+
id: body.id
|
737
|
+
});
|
738
|
+
}
|
739
|
+
if (body.method === "tools/list") {
|
740
|
+
const tools = [
|
741
|
+
{
|
742
|
+
name: "connect_database",
|
743
|
+
description: "连接到MySQL数据库",
|
744
|
+
inputSchema: {
|
745
|
+
type: "object",
|
746
|
+
properties: {
|
747
|
+
host: { type: "string", description: "数据库主机地址" },
|
748
|
+
port: { type: "number", description: "数据库端口号(默认:3306)", default: 3306 },
|
749
|
+
user: { type: "string", description: "数据库用户名" },
|
750
|
+
password: { type: "string", description: "数据库密码" },
|
751
|
+
database: { type: "string", description: "要连接的数据库名称" },
|
752
|
+
connection_id: { type: "string", description: "连接标识符(可选)" },
|
753
|
+
},
|
754
|
+
required: ["host", "user", "password", "database"],
|
755
|
+
},
|
756
|
+
},
|
757
|
+
{
|
758
|
+
name: "execute_query",
|
759
|
+
description: "执行SQL查询语句(支持增删改查所有操作)",
|
760
|
+
inputSchema: {
|
761
|
+
type: "object",
|
762
|
+
properties: {
|
763
|
+
query: { type: "string", description: "要执行的SQL查询语句" },
|
764
|
+
params: { type: "array", description: "SQL参数(可选)", items: { type: "string" } },
|
765
|
+
connection_id: { type: "string", description: "连接标识符(可选)" },
|
766
|
+
},
|
767
|
+
required: ["query"],
|
768
|
+
},
|
769
|
+
},
|
770
|
+
// ... 其他工具的定义可以类似添加
|
771
|
+
];
|
772
|
+
return res.json({ jsonrpc: "2.0", result: { tools }, id: body.id });
|
773
|
+
}
|
774
|
+
if (body.method === "tools/call" && session) {
|
775
|
+
const { name, arguments: args } = body.params;
|
776
|
+
try {
|
777
|
+
const result = await session.server.request({ method: "tools/call", params: { name, arguments: args } }, CallToolRequestSchema);
|
778
|
+
return res.json({ jsonrpc: "2.0", result, id: body.id });
|
779
|
+
}
|
780
|
+
catch (error) {
|
781
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
782
|
+
return res.status(500).json({
|
783
|
+
jsonrpc: "2.0",
|
784
|
+
error: { code: -32000, message: err.message },
|
785
|
+
id: body.id
|
786
|
+
});
|
787
|
+
}
|
788
|
+
}
|
789
|
+
return res.status(400).json({
|
790
|
+
jsonrpc: "2.0",
|
791
|
+
error: { code: -32601, message: `Method not found: ${body.method}` },
|
792
|
+
id: body.id
|
793
|
+
});
|
794
|
+
}
|
795
|
+
return res.status(405).json({
|
796
|
+
jsonrpc: "2.0",
|
797
|
+
error: { code: -32600, message: "Method Not Allowed" },
|
798
|
+
id: null
|
799
|
+
});
|
800
|
+
});
|
801
|
+
// 启动服务器
|
802
|
+
app.listen(PORT, () => {
|
803
|
+
logger.info(`StreamableHTTP MCP Server 已启动`, { port: PORT });
|
804
|
+
console.log(`🚀 StreamableHTTP MCP Server 已启动`);
|
805
|
+
console.log(`📡 MCP endpoint: http://localhost:${PORT}/mcp`);
|
806
|
+
console.log(`💚 Health: http://localhost:${PORT}/health`);
|
807
|
+
console.log(`\n📋 支持的 Header 配置:`);
|
808
|
+
console.log(` - X-MySQL-Host: 数据库主机地址`);
|
809
|
+
console.log(` - X-MySQL-Port: 数据库端口号`);
|
810
|
+
console.log(` - X-MySQL-User: 数据库用户名`);
|
811
|
+
console.log(` - X-MySQL-Password: 数据库密码`);
|
812
|
+
console.log(` - X-MySQL-Database: 数据库名称`);
|
813
|
+
});
|
814
|
+
// 优雅关闭处理
|
815
|
+
process.on("SIGINT", async () => {
|
816
|
+
logger.info("接收到SIGINT信号,正在关闭服务器...");
|
817
|
+
// 断开所有会话的连接
|
818
|
+
for (const [sessionId, session] of sessions.entries()) {
|
819
|
+
try {
|
820
|
+
await session.connectionManager.disconnectAll();
|
821
|
+
logger.info(`会话 ${sessionId} 的连接已断开`);
|
822
|
+
}
|
823
|
+
catch (error) {
|
824
|
+
logger.error(`断开会话 ${sessionId} 连接失败`, {
|
825
|
+
error: error instanceof Error ? error.message : String(error)
|
826
|
+
});
|
827
|
+
}
|
828
|
+
}
|
829
|
+
logger.info("服务器已关闭");
|
830
|
+
process.exit(0);
|
831
|
+
});
|
832
|
+
process.on("SIGTERM", async () => {
|
833
|
+
logger.info("接收到SIGTERM信号,正在关闭服务器...");
|
834
|
+
// 断开所有会话的连接
|
835
|
+
for (const [sessionId, session] of sessions.entries()) {
|
836
|
+
try {
|
837
|
+
await session.connectionManager.disconnectAll();
|
838
|
+
}
|
839
|
+
catch (error) {
|
840
|
+
logger.error(`断开会话 ${sessionId} 连接失败`, {
|
841
|
+
error: error instanceof Error ? error.message : String(error)
|
842
|
+
});
|
843
|
+
}
|
844
|
+
}
|
845
|
+
logger.info("服务器已关闭");
|
846
|
+
process.exit(0);
|
847
|
+
});
|