@xingyuchen/mysql-mcp-server 3.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 +562 -0
- package/README_ENHANCED.md +276 -0
- package/SMITHERY_DEPLOY.md +391 -0
- package/dist/connection-manager.js +159 -0
- package/dist/database.js +400 -0
- package/dist/index.js +632 -0
- package/dist/log-viewer.js +249 -0
- package/dist/logger.js +122 -0
- package/dist/transaction-manager.js +247 -0
- package/package.json +75 -0
- package/smithery.yaml +18 -0
@@ -0,0 +1,249 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
import fs from 'fs/promises';
|
3
|
+
import path from 'path';
|
4
|
+
import { fileURLToPath } from 'url';
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
6
|
+
const logDir = path.join(__dirname, '..', 'logs');
|
7
|
+
class LogViewer {
|
8
|
+
/**
|
9
|
+
* 获取所有日志文件
|
10
|
+
*/
|
11
|
+
async getLogFiles() {
|
12
|
+
try {
|
13
|
+
const files = await fs.readdir(logDir);
|
14
|
+
return files.filter(file => file.endsWith('.log')).sort();
|
15
|
+
}
|
16
|
+
catch (error) {
|
17
|
+
console.error('无法读取日志目录:', error);
|
18
|
+
return [];
|
19
|
+
}
|
20
|
+
}
|
21
|
+
/**
|
22
|
+
* 读取指定日志文件
|
23
|
+
*/
|
24
|
+
async readLogFile(filename) {
|
25
|
+
try {
|
26
|
+
const filePath = path.join(logDir, filename);
|
27
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
28
|
+
const lines = content.split('\n').filter(line => line.trim());
|
29
|
+
return lines.map(line => {
|
30
|
+
try {
|
31
|
+
// 解析日志行格式: [timestamp] LEVEL: message
|
32
|
+
const match = line.match(/^\[([^\]]+)\]\s+(\w+):\s+(.+)$/);
|
33
|
+
if (match) {
|
34
|
+
const [, timestamp, level, messageWithData] = match;
|
35
|
+
// 尝试解析JSON数据
|
36
|
+
let message = messageWithData;
|
37
|
+
let data = undefined;
|
38
|
+
try {
|
39
|
+
const jsonMatch = messageWithData.match(/^(.+?)\s+(\{.+\})$/);
|
40
|
+
if (jsonMatch) {
|
41
|
+
message = jsonMatch[1];
|
42
|
+
data = JSON.parse(jsonMatch[2]);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
catch {
|
46
|
+
// 如果不是JSON格式,保持原样
|
47
|
+
}
|
48
|
+
return { timestamp, level, message, data };
|
49
|
+
}
|
50
|
+
return { timestamp: '', level: 'INFO', message: line };
|
51
|
+
}
|
52
|
+
catch {
|
53
|
+
return { timestamp: '', level: 'INFO', message: line };
|
54
|
+
}
|
55
|
+
});
|
56
|
+
}
|
57
|
+
catch (error) {
|
58
|
+
console.error(`无法读取日志文件 ${filename}:`, error);
|
59
|
+
return [];
|
60
|
+
}
|
61
|
+
}
|
62
|
+
/**
|
63
|
+
* 按级别过滤日志
|
64
|
+
*/
|
65
|
+
filterByLevel(logs, level) {
|
66
|
+
return logs.filter(log => log.level.toUpperCase() === level.toUpperCase());
|
67
|
+
}
|
68
|
+
/**
|
69
|
+
* 按时间范围过滤日志
|
70
|
+
*/
|
71
|
+
filterByTimeRange(logs, startTime, endTime) {
|
72
|
+
return logs.filter(log => {
|
73
|
+
const logTime = new Date(log.timestamp);
|
74
|
+
const start = new Date(startTime);
|
75
|
+
const end = new Date(endTime);
|
76
|
+
return logTime >= start && logTime <= end;
|
77
|
+
});
|
78
|
+
}
|
79
|
+
/**
|
80
|
+
* 搜索日志内容
|
81
|
+
*/
|
82
|
+
searchLogs(logs, keyword) {
|
83
|
+
const lowerKeyword = keyword.toLowerCase();
|
84
|
+
return logs.filter(log => log.message.toLowerCase().includes(lowerKeyword) ||
|
85
|
+
(log.data && JSON.stringify(log.data).toLowerCase().includes(lowerKeyword)));
|
86
|
+
}
|
87
|
+
/**
|
88
|
+
* 格式化输出日志
|
89
|
+
*/
|
90
|
+
formatLog(log) {
|
91
|
+
const colorMap = {
|
92
|
+
ERROR: '\x1b[31m', // 红色
|
93
|
+
WARN: '\x1b[33m', // 黄色
|
94
|
+
INFO: '\x1b[36m', // 青色
|
95
|
+
DEBUG: '\x1b[37m', // 白色
|
96
|
+
};
|
97
|
+
const reset = '\x1b[0m';
|
98
|
+
const color = colorMap[log.level] || '';
|
99
|
+
let output = `${color}[${log.timestamp}] ${log.level}${reset}: ${log.message}`;
|
100
|
+
if (log.data) {
|
101
|
+
output += '\n' + JSON.stringify(log.data, null, 2);
|
102
|
+
}
|
103
|
+
return output;
|
104
|
+
}
|
105
|
+
/**
|
106
|
+
* 显示日志统计
|
107
|
+
*/
|
108
|
+
showStats(logs) {
|
109
|
+
const stats = logs.reduce((acc, log) => {
|
110
|
+
acc[log.level] = (acc[log.level] || 0) + 1;
|
111
|
+
return acc;
|
112
|
+
}, {});
|
113
|
+
console.log('\n📊 日志统计:');
|
114
|
+
console.log('='.repeat(30));
|
115
|
+
Object.entries(stats).forEach(([level, count]) => {
|
116
|
+
console.log(`${level}: ${count}`);
|
117
|
+
});
|
118
|
+
console.log(`总计: ${logs.length}`);
|
119
|
+
}
|
120
|
+
/**
|
121
|
+
* 获取最近的错误
|
122
|
+
*/
|
123
|
+
getRecentErrors(logs, count = 10) {
|
124
|
+
return logs
|
125
|
+
.filter(log => log.level === 'ERROR')
|
126
|
+
.slice(-count)
|
127
|
+
.reverse();
|
128
|
+
}
|
129
|
+
/**
|
130
|
+
* 分析数据库操作
|
131
|
+
*/
|
132
|
+
analyzeDatabaseOperations(logs) {
|
133
|
+
const dbOps = logs.filter(log => log.message.includes('SQL操作') ||
|
134
|
+
log.message.includes('数据库连接') ||
|
135
|
+
log.data?.operation);
|
136
|
+
console.log('\n🗄️ 数据库操作分析:');
|
137
|
+
console.log('='.repeat(40));
|
138
|
+
const opStats = dbOps.reduce((acc, log) => {
|
139
|
+
if (log.data?.operation) {
|
140
|
+
acc[log.data.operation] = (acc[log.data.operation] || 0) + 1;
|
141
|
+
}
|
142
|
+
return acc;
|
143
|
+
}, {});
|
144
|
+
Object.entries(opStats).forEach(([op, count]) => {
|
145
|
+
console.log(`${op}: ${count} 次`);
|
146
|
+
});
|
147
|
+
// 显示最近的数据库错误
|
148
|
+
const dbErrors = dbOps.filter(log => log.level === 'ERROR').slice(-5);
|
149
|
+
if (dbErrors.length > 0) {
|
150
|
+
console.log('\n🚨 最近的数据库错误:');
|
151
|
+
dbErrors.forEach(error => {
|
152
|
+
console.log(this.formatLog(error));
|
153
|
+
});
|
154
|
+
}
|
155
|
+
}
|
156
|
+
}
|
157
|
+
// 命令行接口
|
158
|
+
async function main() {
|
159
|
+
const viewer = new LogViewer();
|
160
|
+
const args = process.argv.slice(2);
|
161
|
+
if (args.length === 0) {
|
162
|
+
console.log(`
|
163
|
+
MySQL MCP Server 日志查看器
|
164
|
+
|
165
|
+
用法:
|
166
|
+
node log-viewer.js list # 列出所有日志文件
|
167
|
+
node log-viewer.js view <filename> # 查看指定日志文件
|
168
|
+
node log-viewer.js errors [filename] # 查看错误日志
|
169
|
+
node log-viewer.js search <keyword> # 搜索日志内容
|
170
|
+
node log-viewer.js stats [filename] # 显示日志统计
|
171
|
+
node log-viewer.js db-analysis [filename] # 数据库操作分析
|
172
|
+
node log-viewer.js tail [filename] [lines] # 查看最新日志
|
173
|
+
|
174
|
+
示例:
|
175
|
+
node log-viewer.js view combined-2024-01-15.log
|
176
|
+
node log-viewer.js errors
|
177
|
+
node log-viewer.js search "连接失败"
|
178
|
+
`);
|
179
|
+
return;
|
180
|
+
}
|
181
|
+
const command = args[0];
|
182
|
+
switch (command) {
|
183
|
+
case 'list': {
|
184
|
+
const files = await viewer.getLogFiles();
|
185
|
+
console.log('📁 可用的日志文件:');
|
186
|
+
files.forEach(file => console.log(` ${file}`));
|
187
|
+
break;
|
188
|
+
}
|
189
|
+
case 'view': {
|
190
|
+
const filename = args[1];
|
191
|
+
if (!filename) {
|
192
|
+
console.error('请指定日志文件名');
|
193
|
+
return;
|
194
|
+
}
|
195
|
+
const logs = await viewer.readLogFile(filename);
|
196
|
+
logs.forEach(log => console.log(viewer.formatLog(log)));
|
197
|
+
break;
|
198
|
+
}
|
199
|
+
case 'errors': {
|
200
|
+
const filename = args[1] || 'combined-' + new Date().toISOString().split('T')[0] + '.log';
|
201
|
+
const logs = await viewer.readLogFile(filename);
|
202
|
+
const errors = viewer.filterByLevel(logs, 'ERROR');
|
203
|
+
console.log(`🚨 错误日志 (${errors.length} 条):`);
|
204
|
+
errors.forEach(log => console.log(viewer.formatLog(log)));
|
205
|
+
break;
|
206
|
+
}
|
207
|
+
case 'search': {
|
208
|
+
const keyword = args[1];
|
209
|
+
if (!keyword) {
|
210
|
+
console.error('请指定搜索关键词');
|
211
|
+
return;
|
212
|
+
}
|
213
|
+
const files = await viewer.getLogFiles();
|
214
|
+
const allLogs = [];
|
215
|
+
for (const file of files) {
|
216
|
+
const logs = await viewer.readLogFile(file);
|
217
|
+
allLogs.push(...logs);
|
218
|
+
}
|
219
|
+
const results = viewer.searchLogs(allLogs, keyword);
|
220
|
+
console.log(`🔍 搜索结果 "${keyword}" (${results.length} 条):`);
|
221
|
+
results.forEach(log => console.log(viewer.formatLog(log)));
|
222
|
+
break;
|
223
|
+
}
|
224
|
+
case 'stats': {
|
225
|
+
const filename = args[1] || 'combined-' + new Date().toISOString().split('T')[0] + '.log';
|
226
|
+
const logs = await viewer.readLogFile(filename);
|
227
|
+
viewer.showStats(logs);
|
228
|
+
break;
|
229
|
+
}
|
230
|
+
case 'db-analysis': {
|
231
|
+
const filename = args[1] || 'database-' + new Date().toISOString().split('T')[0] + '.log';
|
232
|
+
const logs = await viewer.readLogFile(filename);
|
233
|
+
viewer.analyzeDatabaseOperations(logs);
|
234
|
+
break;
|
235
|
+
}
|
236
|
+
case 'tail': {
|
237
|
+
const filename = args[1] || 'combined-' + new Date().toISOString().split('T')[0] + '.log';
|
238
|
+
const lines = parseInt(args[2]) || 20;
|
239
|
+
const logs = await viewer.readLogFile(filename);
|
240
|
+
const recent = logs.slice(-lines);
|
241
|
+
console.log(`📋 最新 ${lines} 条日志:`);
|
242
|
+
recent.forEach(log => console.log(viewer.formatLog(log)));
|
243
|
+
break;
|
244
|
+
}
|
245
|
+
default:
|
246
|
+
console.error(`未知命令: ${command}`);
|
247
|
+
}
|
248
|
+
}
|
249
|
+
main().catch(console.error);
|
package/dist/logger.js
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
import winston from 'winston';
|
2
|
+
import DailyRotateFile from 'winston-daily-rotate-file';
|
3
|
+
import path from 'path';
|
4
|
+
// 创建日志目录(如果不存在)
|
5
|
+
const logDir = 'logs';
|
6
|
+
// 定义日志格式
|
7
|
+
const logFormat = winston.format.combine(winston.format.timestamp({
|
8
|
+
format: 'YYYY-MM-DD HH:mm:ss'
|
9
|
+
}), winston.format.errors({ stack: true }), winston.format.printf(({ timestamp, level, message, stack }) => {
|
10
|
+
return `[${timestamp}] ${level.toUpperCase()}: ${message}${stack ? '\n' + stack : ''}`;
|
11
|
+
}));
|
12
|
+
// 创建日志传输器
|
13
|
+
const transports = [
|
14
|
+
// 控制台输出
|
15
|
+
new winston.transports.Console({
|
16
|
+
format: winston.format.combine(winston.format.colorize(), logFormat),
|
17
|
+
level: 'info'
|
18
|
+
}),
|
19
|
+
// 错误日志文件(按日期轮转)
|
20
|
+
new DailyRotateFile({
|
21
|
+
filename: path.join(logDir, 'error-%DATE%.log'),
|
22
|
+
datePattern: 'YYYY-MM-DD',
|
23
|
+
level: 'error',
|
24
|
+
format: logFormat,
|
25
|
+
maxSize: '20m',
|
26
|
+
maxFiles: '14d',
|
27
|
+
zippedArchive: true
|
28
|
+
}),
|
29
|
+
// 所有日志文件(按日期轮转)
|
30
|
+
new DailyRotateFile({
|
31
|
+
filename: path.join(logDir, 'combined-%DATE%.log'),
|
32
|
+
datePattern: 'YYYY-MM-DD',
|
33
|
+
format: logFormat,
|
34
|
+
maxSize: '20m',
|
35
|
+
maxFiles: '30d',
|
36
|
+
zippedArchive: true
|
37
|
+
}),
|
38
|
+
// 数据库操作专用日志
|
39
|
+
new DailyRotateFile({
|
40
|
+
filename: path.join(logDir, 'database-%DATE%.log'),
|
41
|
+
datePattern: 'YYYY-MM-DD',
|
42
|
+
format: winston.format.combine(logFormat, winston.format.label({ label: 'DATABASE' })),
|
43
|
+
maxSize: '20m',
|
44
|
+
maxFiles: '30d',
|
45
|
+
zippedArchive: true
|
46
|
+
})
|
47
|
+
];
|
48
|
+
// 创建logger实例
|
49
|
+
export const logger = winston.createLogger({
|
50
|
+
level: 'info',
|
51
|
+
format: logFormat,
|
52
|
+
transports,
|
53
|
+
// 处理未捕获的异常
|
54
|
+
exceptionHandlers: [
|
55
|
+
new DailyRotateFile({
|
56
|
+
filename: path.join(logDir, 'exceptions-%DATE%.log'),
|
57
|
+
datePattern: 'YYYY-MM-DD',
|
58
|
+
maxSize: '20m',
|
59
|
+
maxFiles: '14d'
|
60
|
+
})
|
61
|
+
],
|
62
|
+
// 处理未处理的Promise拒绝
|
63
|
+
rejectionHandlers: [
|
64
|
+
new DailyRotateFile({
|
65
|
+
filename: path.join(logDir, 'rejections-%DATE%.log'),
|
66
|
+
datePattern: 'YYYY-MM-DD',
|
67
|
+
maxSize: '20m',
|
68
|
+
maxFiles: '14d'
|
69
|
+
})
|
70
|
+
]
|
71
|
+
});
|
72
|
+
// 数据库专用logger
|
73
|
+
export const dbLogger = winston.createLogger({
|
74
|
+
level: 'debug',
|
75
|
+
format: winston.format.combine(winston.format.label({ label: 'DB' }), logFormat),
|
76
|
+
transports: [
|
77
|
+
new winston.transports.Console({
|
78
|
+
format: winston.format.combine(winston.format.colorize(), winston.format.label({ label: 'DB' }), logFormat),
|
79
|
+
level: 'info'
|
80
|
+
}),
|
81
|
+
new DailyRotateFile({
|
82
|
+
filename: path.join(logDir, 'database-%DATE%.log'),
|
83
|
+
datePattern: 'YYYY-MM-DD',
|
84
|
+
maxSize: '20m',
|
85
|
+
maxFiles: '30d'
|
86
|
+
})
|
87
|
+
]
|
88
|
+
});
|
89
|
+
// 工具函数:记录SQL操作
|
90
|
+
export function logSqlOperation(operation, query, params, duration, error) {
|
91
|
+
const logData = {
|
92
|
+
operation,
|
93
|
+
query: query.replace(/\s+/g, ' ').trim(),
|
94
|
+
params: params ? JSON.stringify(params) : undefined,
|
95
|
+
duration: duration ? `${duration}ms` : undefined,
|
96
|
+
timestamp: new Date().toISOString()
|
97
|
+
};
|
98
|
+
if (error) {
|
99
|
+
dbLogger.error(`SQL操作失败`, { ...logData, error: error.message });
|
100
|
+
}
|
101
|
+
else {
|
102
|
+
dbLogger.info(`SQL操作成功`, logData);
|
103
|
+
}
|
104
|
+
}
|
105
|
+
// 工具函数:记录连接操作
|
106
|
+
export function logConnection(action, config, error) {
|
107
|
+
const logData = {
|
108
|
+
action,
|
109
|
+
host: config?.host,
|
110
|
+
port: config?.port,
|
111
|
+
database: config?.database,
|
112
|
+
user: config?.user,
|
113
|
+
timestamp: new Date().toISOString()
|
114
|
+
};
|
115
|
+
if (error) {
|
116
|
+
dbLogger.error(`数据库${action === 'connect' ? '连接' : '断开'}失败`, { ...logData, error: error.message });
|
117
|
+
}
|
118
|
+
else {
|
119
|
+
dbLogger.info(`数据库${action === 'connect' ? '连接' : '断开'}成功`, logData);
|
120
|
+
}
|
121
|
+
}
|
122
|
+
export default logger;
|
@@ -0,0 +1,247 @@
|
|
1
|
+
import { dbLogger } from "./logger.js";
|
2
|
+
export class TransactionManager {
|
3
|
+
isTransactionActive = false;
|
4
|
+
operationHistory = [];
|
5
|
+
currentTransactionId = null;
|
6
|
+
/**
|
7
|
+
* 检查是否有活跃的事务
|
8
|
+
*/
|
9
|
+
isActive() {
|
10
|
+
return this.isTransactionActive;
|
11
|
+
}
|
12
|
+
/**
|
13
|
+
* 获取当前事务ID
|
14
|
+
*/
|
15
|
+
getCurrentTransactionId() {
|
16
|
+
return this.currentTransactionId;
|
17
|
+
}
|
18
|
+
/**
|
19
|
+
* 开始新事务
|
20
|
+
*/
|
21
|
+
async startTransaction() {
|
22
|
+
const transactionId = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
23
|
+
this.currentTransactionId = transactionId;
|
24
|
+
this.isTransactionActive = true;
|
25
|
+
this.operationHistory = [];
|
26
|
+
dbLogger.info("自动开始新事务", {
|
27
|
+
transactionId,
|
28
|
+
timestamp: new Date().toISOString()
|
29
|
+
});
|
30
|
+
return transactionId;
|
31
|
+
}
|
32
|
+
/**
|
33
|
+
* 记录操作到历史
|
34
|
+
*/
|
35
|
+
recordOperation(operation) {
|
36
|
+
if (!this.isTransactionActive) {
|
37
|
+
throw new Error("没有活跃的事务");
|
38
|
+
}
|
39
|
+
const operationWithId = {
|
40
|
+
...operation,
|
41
|
+
id: `op_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
|
42
|
+
timestamp: new Date()
|
43
|
+
};
|
44
|
+
this.operationHistory.push(operationWithId);
|
45
|
+
dbLogger.info("记录事务操作", {
|
46
|
+
transactionId: this.currentTransactionId,
|
47
|
+
operationId: operationWithId.id,
|
48
|
+
type: operationWithId.type,
|
49
|
+
tableName: operationWithId.tableName,
|
50
|
+
description: operationWithId.description
|
51
|
+
});
|
52
|
+
}
|
53
|
+
/**
|
54
|
+
* 获取操作历史
|
55
|
+
*/
|
56
|
+
getOperationHistory() {
|
57
|
+
return [...this.operationHistory];
|
58
|
+
}
|
59
|
+
/**
|
60
|
+
* 获取可回滚的操作列表(格式化显示)
|
61
|
+
*/
|
62
|
+
getRollbackOptions() {
|
63
|
+
if (this.operationHistory.length === 0) {
|
64
|
+
return "📝 当前事务中没有任何操作";
|
65
|
+
}
|
66
|
+
let result = "🔄 可回滚的操作历史:\n\n";
|
67
|
+
this.operationHistory.forEach((op, index) => {
|
68
|
+
const timeStr = op.timestamp.toLocaleString('zh-CN');
|
69
|
+
result += `${index + 1}. [${timeStr}] ${op.type} - ${op.description}\n`;
|
70
|
+
result += ` 表: ${op.tableName}`;
|
71
|
+
if (op.affectedRows !== undefined) {
|
72
|
+
result += ` | 影响行数: ${op.affectedRows}`;
|
73
|
+
}
|
74
|
+
result += `\n\n`;
|
75
|
+
});
|
76
|
+
result += "💡 提示:\n";
|
77
|
+
result += "- 选择 '提交事务' 将永久保存所有修改\n";
|
78
|
+
result += "- 选择 '回滚到指定步骤' 可以撤销部分操作\n";
|
79
|
+
result += "- 选择 '完全回滚' 将撤销所有操作";
|
80
|
+
return result;
|
81
|
+
}
|
82
|
+
/**
|
83
|
+
* 回滚到指定操作步骤
|
84
|
+
*/
|
85
|
+
async rollbackToStep(stepNumber, executeRollback) {
|
86
|
+
if (!this.isTransactionActive) {
|
87
|
+
throw new Error("没有活跃的事务");
|
88
|
+
}
|
89
|
+
if (stepNumber < 1 || stepNumber > this.operationHistory.length) {
|
90
|
+
throw new Error(`无效的步骤号。请选择 1-${this.operationHistory.length} 之间的数字`);
|
91
|
+
}
|
92
|
+
// 获取需要回滚的操作(从最后一个操作开始,回滚到指定步骤)
|
93
|
+
const operationsToRollback = this.operationHistory.slice(stepNumber).reverse();
|
94
|
+
let rollbackCount = 0;
|
95
|
+
const rollbackResults = [];
|
96
|
+
for (const operation of operationsToRollback) {
|
97
|
+
if (operation.rollbackQuery) {
|
98
|
+
try {
|
99
|
+
await executeRollback(operation.rollbackQuery, operation.rollbackParams);
|
100
|
+
rollbackCount++;
|
101
|
+
rollbackResults.push(`✅ 已回滚: ${operation.description}`);
|
102
|
+
dbLogger.info("执行操作回滚", {
|
103
|
+
transactionId: this.currentTransactionId,
|
104
|
+
operationId: operation.id,
|
105
|
+
rollbackQuery: operation.rollbackQuery
|
106
|
+
});
|
107
|
+
}
|
108
|
+
catch (error) {
|
109
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
110
|
+
rollbackResults.push(`❌ 回滚失败: ${operation.description} - ${err.message}`);
|
111
|
+
dbLogger.error("操作回滚失败", {
|
112
|
+
transactionId: this.currentTransactionId,
|
113
|
+
operationId: operation.id,
|
114
|
+
error: err.message
|
115
|
+
});
|
116
|
+
}
|
117
|
+
}
|
118
|
+
else {
|
119
|
+
rollbackResults.push(`⚠️ 无法回滚: ${operation.description} (无回滚查询)`);
|
120
|
+
}
|
121
|
+
}
|
122
|
+
// 更新操作历史,只保留到指定步骤
|
123
|
+
this.operationHistory = this.operationHistory.slice(0, stepNumber);
|
124
|
+
const result = `🔄 回滚操作完成!\n\n` +
|
125
|
+
`📊 回滚统计:\n` +
|
126
|
+
`- 尝试回滚: ${operationsToRollback.length} 个操作\n` +
|
127
|
+
`- 成功回滚: ${rollbackCount} 个操作\n` +
|
128
|
+
`- 剩余操作: ${this.operationHistory.length} 个\n\n` +
|
129
|
+
`📋 回滚详情:\n${rollbackResults.join('\n')}`;
|
130
|
+
return result;
|
131
|
+
}
|
132
|
+
/**
|
133
|
+
* 完全回滚事务
|
134
|
+
*/
|
135
|
+
async fullRollback(executeRollback) {
|
136
|
+
if (!this.isTransactionActive) {
|
137
|
+
throw new Error("没有活跃的事务");
|
138
|
+
}
|
139
|
+
const totalOperations = this.operationHistory.length;
|
140
|
+
if (totalOperations === 0) {
|
141
|
+
this.endTransaction();
|
142
|
+
return "📝 当前事务中没有任何操作,事务已结束";
|
143
|
+
}
|
144
|
+
// 执行数据库级别的回滚
|
145
|
+
await executeRollback("ROLLBACK");
|
146
|
+
dbLogger.info("执行完全事务回滚", {
|
147
|
+
transactionId: this.currentTransactionId,
|
148
|
+
operationsCount: totalOperations
|
149
|
+
});
|
150
|
+
this.endTransaction();
|
151
|
+
return `🔄 事务完全回滚成功!\n\n` +
|
152
|
+
`📊 统计信息:\n` +
|
153
|
+
`- 已撤销 ${totalOperations} 个操作\n` +
|
154
|
+
`- 数据库已恢复到事务开始前的状态\n` +
|
155
|
+
`- 事务已结束`;
|
156
|
+
}
|
157
|
+
/**
|
158
|
+
* 提交事务
|
159
|
+
*/
|
160
|
+
async commitTransaction(executeCommit) {
|
161
|
+
if (!this.isTransactionActive) {
|
162
|
+
throw new Error("没有活跃的事务");
|
163
|
+
}
|
164
|
+
const totalOperations = this.operationHistory.length;
|
165
|
+
if (totalOperations === 0) {
|
166
|
+
this.endTransaction();
|
167
|
+
return "📝 当前事务中没有任何操作,事务已结束";
|
168
|
+
}
|
169
|
+
// 执行数据库级别的提交
|
170
|
+
await executeCommit();
|
171
|
+
dbLogger.info("提交事务成功", {
|
172
|
+
transactionId: this.currentTransactionId,
|
173
|
+
operationsCount: totalOperations,
|
174
|
+
operations: this.operationHistory.map(op => ({
|
175
|
+
type: op.type,
|
176
|
+
tableName: op.tableName,
|
177
|
+
description: op.description
|
178
|
+
}))
|
179
|
+
});
|
180
|
+
const operationsSummary = this.operationHistory.map((op, index) => `${index + 1}. ${op.type} - ${op.description} (${op.tableName})`).join('\n');
|
181
|
+
this.endTransaction();
|
182
|
+
return `✅ 事务提交成功!所有修改已永久保存到数据库\n\n` +
|
183
|
+
`📊 提交统计:\n` +
|
184
|
+
`- 总操作数: ${totalOperations}\n` +
|
185
|
+
`- 事务ID: ${this.currentTransactionId}\n\n` +
|
186
|
+
`📋 已提交的操作:\n${operationsSummary}`;
|
187
|
+
}
|
188
|
+
/**
|
189
|
+
* 结束事务
|
190
|
+
*/
|
191
|
+
endTransaction() {
|
192
|
+
this.isTransactionActive = false;
|
193
|
+
this.operationHistory = [];
|
194
|
+
this.currentTransactionId = null;
|
195
|
+
}
|
196
|
+
/**
|
197
|
+
* 生成回滚查询
|
198
|
+
*/
|
199
|
+
generateRollbackQuery(operation) {
|
200
|
+
switch (operation.type) {
|
201
|
+
case 'INSERT':
|
202
|
+
// INSERT的回滚是DELETE
|
203
|
+
if (operation.whereClause) {
|
204
|
+
return {
|
205
|
+
query: `DELETE FROM \`${operation.tableName}\` WHERE ${operation.whereClause}`,
|
206
|
+
params: operation.whereParams
|
207
|
+
};
|
208
|
+
}
|
209
|
+
return null;
|
210
|
+
case 'UPDATE':
|
211
|
+
// UPDATE的回滚是恢复原始数据
|
212
|
+
if (operation.originalData && operation.whereClause) {
|
213
|
+
const columns = Object.keys(operation.originalData);
|
214
|
+
const setClause = columns.map(col => `\`${col}\` = ?`).join(', ');
|
215
|
+
const values = Object.values(operation.originalData);
|
216
|
+
return {
|
217
|
+
query: `UPDATE \`${operation.tableName}\` SET ${setClause} WHERE ${operation.whereClause}`,
|
218
|
+
params: [...values, ...operation.whereParams || []]
|
219
|
+
};
|
220
|
+
}
|
221
|
+
return null;
|
222
|
+
case 'DELETE':
|
223
|
+
// DELETE的回滚是INSERT原始数据
|
224
|
+
if (operation.originalData) {
|
225
|
+
const columns = Object.keys(operation.originalData);
|
226
|
+
const placeholders = columns.map(() => '?').join(', ');
|
227
|
+
const values = Object.values(operation.originalData);
|
228
|
+
return {
|
229
|
+
query: `INSERT INTO \`${operation.tableName}\` (\`${columns.join('`, `')}\`) VALUES (${placeholders})`,
|
230
|
+
params: values
|
231
|
+
};
|
232
|
+
}
|
233
|
+
return null;
|
234
|
+
case 'DROP_TABLE':
|
235
|
+
// DROP TABLE的回滚需要重新创建表(这个比较复杂,暂时不支持)
|
236
|
+
return null;
|
237
|
+
case 'CREATE_TABLE':
|
238
|
+
// CREATE TABLE的回滚是DROP TABLE
|
239
|
+
return {
|
240
|
+
query: `DROP TABLE \`${operation.tableName}\``,
|
241
|
+
params: []
|
242
|
+
};
|
243
|
+
default:
|
244
|
+
return null;
|
245
|
+
}
|
246
|
+
}
|
247
|
+
}
|
package/package.json
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
{
|
2
|
+
"name": "@xingyuchen/mysql-mcp-server",
|
3
|
+
"version": "3.0.0",
|
4
|
+
"type": "module",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"bin": {
|
7
|
+
"guangxiang-mysql-mcp": "dist/index.js"
|
8
|
+
},
|
9
|
+
"files": [
|
10
|
+
"dist/**/*",
|
11
|
+
"README.md",
|
12
|
+
"README_ENHANCED.md",
|
13
|
+
"SMITHERY_DEPLOY.md",
|
14
|
+
"smithery.yaml"
|
15
|
+
],
|
16
|
+
"engines": {
|
17
|
+
"node": ">=18.0.0"
|
18
|
+
},
|
19
|
+
"publishConfig": {
|
20
|
+
"access": "public"
|
21
|
+
},
|
22
|
+
"scripts": {
|
23
|
+
"build": "tsc",
|
24
|
+
"dev": "ts-node --esm src/index.ts",
|
25
|
+
"start": "node dist/index.js",
|
26
|
+
"start-gateway": "npm run build && npx supergateway --stdio \"node dist/index.js\" --port 3101",
|
27
|
+
"install-global": "npm run build && npm install -g .",
|
28
|
+
"quick-setup": "npm install && npm run build && echo \"✅ 安装完成!请查看 README.md 了解配置方法\"",
|
29
|
+
"prepublishOnly": "npm run build",
|
30
|
+
"logs": "npm run build && node dist/log-viewer.js",
|
31
|
+
"logs:errors": "npm run build && node dist/log-viewer.js errors",
|
32
|
+
"logs:stats": "npm run build && node dist/log-viewer.js stats",
|
33
|
+
"logs:db": "npm run build && node dist/log-viewer.js db-analysis",
|
34
|
+
"logs:tail": "npm run build && node dist/log-viewer.js tail",
|
35
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
36
|
+
},
|
37
|
+
"keywords": [
|
38
|
+
"mcp",
|
39
|
+
"mysql",
|
40
|
+
"database",
|
41
|
+
"model-context-protocol",
|
42
|
+
"ai-assistant",
|
43
|
+
"vscode",
|
44
|
+
"cline",
|
45
|
+
"sql",
|
46
|
+
"crud",
|
47
|
+
"transaction"
|
48
|
+
],
|
49
|
+
"author": {
|
50
|
+
"name": "guangxiangdebizi",
|
51
|
+
"url": "https://github.com/guangxiangdebizi"
|
52
|
+
},
|
53
|
+
"license": "MIT",
|
54
|
+
"description": "功能强大的MySQL数据库MCP服务器,支持多数据库连接、完整CRUD操作、智能事务管理和回滚功能,专为AI助手设计",
|
55
|
+
"homepage": "https://github.com/guangxiangdebizi/MySQL_MCP#readme",
|
56
|
+
"repository": {
|
57
|
+
"type": "git",
|
58
|
+
"url": "https://github.com/guangxiangdebizi/MySQL_MCP.git"
|
59
|
+
},
|
60
|
+
"bugs": {
|
61
|
+
"url": "https://github.com/guangxiangdebizi/MySQL_MCP/issues"
|
62
|
+
},
|
63
|
+
"dependencies": {
|
64
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
65
|
+
"mysql2": "^3.6.5",
|
66
|
+
"winston": "^3.13.0",
|
67
|
+
"winston-daily-rotate-file": "^5.0.0"
|
68
|
+
},
|
69
|
+
"devDependencies": {
|
70
|
+
"@types/node": "^22.15.21",
|
71
|
+
"supergateway": "^2.8.3",
|
72
|
+
"ts-node": "^10.9.2",
|
73
|
+
"typescript": "^5.8.3"
|
74
|
+
}
|
75
|
+
}
|