@cmd233/mcp-database-server 1.2.0 → 1.4.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 → README.md} +10 -5
- package/dist/src/db/index.js +58 -3
- package/dist/src/db/mysql-adapter.js +7 -0
- package/dist/src/db/postgresql-adapter.js +7 -0
- package/dist/src/db/sql-validator.js +34 -0
- package/dist/src/db/sqlite-adapter.js +7 -0
- package/dist/src/db/sqlserver-adapter.js +90 -3
- package/dist/src/handlers/toolHandlers.js +108 -42
- package/dist/src/tools/queryTools.js +4 -3
- package/dist/src/tools/schemaTools.js +139 -28
- package/package.json +2 -3
- package/dist/src/utils/cryptoUtils.js +0 -119
package/{readme.md → README.md}
RENAMED
|
@@ -105,6 +105,8 @@ node dist/src/index.js --mysql --host <host-name> --database <database-name> --p
|
|
|
105
105
|
必需参数:
|
|
106
106
|
- `--host`: MySQL 主机名或 IP 地址
|
|
107
107
|
- `--database`: 数据库名称
|
|
108
|
+
|
|
109
|
+
可选参数:
|
|
108
110
|
- `--port`: 端口号(默认: 3306)
|
|
109
111
|
|
|
110
112
|
可选参数:
|
|
@@ -285,15 +287,18 @@ MCP 数据库服务器提供以下可供 Claude 使用的工具:
|
|
|
285
287
|
| 工具 | 描述 | 必需参数 |
|
|
286
288
|
|------|-------------|---------------------|
|
|
287
289
|
| `read_query` | 执行 SELECT 查询以读取数据 | `query`: SQL SELECT 语句 |
|
|
288
|
-
| `write_query` | 执行 INSERT、UPDATE 或
|
|
289
|
-
| `create_table` | 在数据库中创建新表 | `query`: CREATE TABLE
|
|
290
|
-
| `alter_table` | 修改现有表架构 | `query`: ALTER TABLE
|
|
290
|
+
| `write_query` | 执行 INSERT、UPDATE、DELETE 或 TRUNCATE 查询 | `query`: SQL 修改语句<br>`confirm`: 安全标志(必须为 true) |
|
|
291
|
+
| `create_table` | 在数据库中创建新表 | `query`: CREATE TABLE 语句<br>`confirm`: 安全标志(必须为 true) |
|
|
292
|
+
| `alter_table` | 修改现有表架构 | `query`: ALTER TABLE 语句<br>`confirm`: 安全标志(必须为 true) |
|
|
291
293
|
| `drop_table` | 从数据库中删除表 | `table_name`: 表名<br>`confirm`: 安全标志(必须为 true) |
|
|
292
294
|
| `list_tables` | 获取所有表的列表 | 无 |
|
|
293
295
|
| `describe_table` | 查看表的架构信息 | `table_name`: 表名 |
|
|
294
296
|
| `export_query` | 将查询结果导出为 CSV/JSON | `query`: SQL SELECT 语句<br>`format`: "csv" 或 "json" |
|
|
295
|
-
| `append_insight` | 添加业务洞察到备忘录 | `insight`:
|
|
296
|
-
| `list_insights` | 列出所有业务洞察 | 无 |
|
|
297
|
+
| `append_insight` | 添加业务洞察到备忘录 (**仅 SQLite**) | `insight`: 洞察文本<br>`confirm`: 安全标志(必须为 true) |
|
|
298
|
+
| `list_insights` | 列出所有业务洞察 (**仅 SQLite**) | 无 |
|
|
299
|
+
| `list_views` | 列出所有视图 (**仅 SQL Server**) | 无 |
|
|
300
|
+
| `describe_view` | 获取视图结构 (**仅 SQL Server**) | `view_name`: 视图名 |
|
|
301
|
+
| `get_view_definition` | 获取视图定义 SQL (**仅 SQL Server**) | `view_name`: 视图名 |
|
|
297
302
|
|
|
298
303
|
有关如何在 Claude 中使用这些工具的实际示例,请参阅[使用示例](docs/usage-examples.md)。
|
|
299
304
|
|
package/dist/src/db/index.js
CHANGED
|
@@ -95,7 +95,9 @@ export function getDescribeTableQuery(tableName) {
|
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
97
|
* 获取列出视图的数据库特定查询
|
|
98
|
-
* 仅 SQL Server
|
|
98
|
+
* 仅 SQL Server 支持
|
|
99
|
+
* @returns SQL 查询字符串
|
|
100
|
+
* @throws 如果数据库不支持视图功能
|
|
99
101
|
*/
|
|
100
102
|
export function getListViewsQuery() {
|
|
101
103
|
if (!dbAdapter) {
|
|
@@ -104,8 +106,8 @@ export function getListViewsQuery() {
|
|
|
104
106
|
if (dbAdapter.getListViewsQuery) {
|
|
105
107
|
return dbAdapter.getListViewsQuery();
|
|
106
108
|
}
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
+
// 统一错误处理策略:不支持视图时抛出明确错误
|
|
110
|
+
throw new Error("当前数据库不支持视图功能");
|
|
109
111
|
}
|
|
110
112
|
/**
|
|
111
113
|
* 获取视图定义的数据库特定查询
|
|
@@ -130,3 +132,56 @@ export function supportsViews() {
|
|
|
130
132
|
}
|
|
131
133
|
return dbAdapter.supportsViews ? dbAdapter.supportsViews() : false;
|
|
132
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* 检查存储过程功能是否可用
|
|
137
|
+
* @returns 可用的数据库适配器
|
|
138
|
+
* @throws 如果数据库未初始化或不支持存储过程功能
|
|
139
|
+
*/
|
|
140
|
+
function requireProcedureSupport() {
|
|
141
|
+
if (!dbAdapter) {
|
|
142
|
+
throw new Error('数据库未初始化');
|
|
143
|
+
}
|
|
144
|
+
if (!dbAdapter.getListProceduresQuery) {
|
|
145
|
+
throw new Error('当前数据库不支持存储过程功能');
|
|
146
|
+
}
|
|
147
|
+
return dbAdapter;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 获取列出存储过程的查询
|
|
151
|
+
* 仅 SQL Server 支持
|
|
152
|
+
* @returns SQL 查询字符串
|
|
153
|
+
* @throws 如果数据库不支持存储过程功能
|
|
154
|
+
*/
|
|
155
|
+
export function getListProceduresQuery() {
|
|
156
|
+
return requireProcedureSupport().getListProceduresQuery();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 获取存储过程参数信息的查询
|
|
160
|
+
* 仅 SQL Server 支持
|
|
161
|
+
* @param procedureName 存储过程名
|
|
162
|
+
* @returns SQL 查询字符串
|
|
163
|
+
* @throws 如果数据库不支持存储过程功能
|
|
164
|
+
*/
|
|
165
|
+
export function getDescribeProcedureQuery(procedureName) {
|
|
166
|
+
return requireProcedureSupport().getDescribeProcedureQuery(procedureName);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 获取存储过程定义的查询
|
|
170
|
+
* 仅 SQL Server 支持
|
|
171
|
+
* @param procedureName 存储过程名
|
|
172
|
+
* @returns SQL 查询字符串
|
|
173
|
+
* @throws 如果数据库不支持存储过程功能
|
|
174
|
+
*/
|
|
175
|
+
export function getProcedureDefinitionQuery(procedureName) {
|
|
176
|
+
return requireProcedureSupport().getProcedureDefinitionQuery(procedureName);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 检查数据库是否支持存储过程功能
|
|
180
|
+
* @returns 如果支持返回 true,否则返回 false
|
|
181
|
+
*/
|
|
182
|
+
export function supportsProcedures() {
|
|
183
|
+
if (!dbAdapter) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return dbAdapter.supportsProcedures ? dbAdapter.supportsProcedures() : false;
|
|
187
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateForbiddenOperations } from "./sql-validator.js";
|
|
1
2
|
import mysql from "mysql2/promise";
|
|
2
3
|
import { Signer } from "@aws-sdk/rds-signer";
|
|
3
4
|
/**
|
|
@@ -116,6 +117,8 @@ export class MysqlAdapter {
|
|
|
116
117
|
if (!this.connection) {
|
|
117
118
|
throw new Error("数据库未初始化");
|
|
118
119
|
}
|
|
120
|
+
// 验证禁用的操作(防止恶意查询)
|
|
121
|
+
validateForbiddenOperations(query);
|
|
119
122
|
try {
|
|
120
123
|
const [rows] = await this.connection.execute(query, params);
|
|
121
124
|
return Array.isArray(rows) ? rows : [];
|
|
@@ -131,6 +134,8 @@ export class MysqlAdapter {
|
|
|
131
134
|
if (!this.connection) {
|
|
132
135
|
throw new Error("数据库未初始化");
|
|
133
136
|
}
|
|
137
|
+
// 验证禁用的操作
|
|
138
|
+
validateForbiddenOperations(query);
|
|
134
139
|
try {
|
|
135
140
|
const [result] = await this.connection.execute(query, params);
|
|
136
141
|
const changes = result.affectedRows || 0;
|
|
@@ -148,6 +153,8 @@ export class MysqlAdapter {
|
|
|
148
153
|
if (!this.connection) {
|
|
149
154
|
throw new Error("数据库未初始化");
|
|
150
155
|
}
|
|
156
|
+
// 验证禁用的操作
|
|
157
|
+
validateForbiddenOperations(query);
|
|
151
158
|
try {
|
|
152
159
|
await this.connection.query(query);
|
|
153
160
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateForbiddenOperations } from "./sql-validator.js";
|
|
1
2
|
import pg from 'pg';
|
|
2
3
|
/**
|
|
3
4
|
* PostgreSQL 数据库适配器实现
|
|
@@ -52,6 +53,8 @@ export class PostgresqlAdapter {
|
|
|
52
53
|
if (!this.client) {
|
|
53
54
|
throw new Error("数据库未初始化");
|
|
54
55
|
}
|
|
56
|
+
// 验证禁用的操作(防止恶意查询)
|
|
57
|
+
validateForbiddenOperations(query);
|
|
55
58
|
try {
|
|
56
59
|
// PostgreSQL 使用 $1, $2 等作为参数化查询的占位符
|
|
57
60
|
let paramIndex = 0;
|
|
@@ -73,6 +76,8 @@ export class PostgresqlAdapter {
|
|
|
73
76
|
if (!this.client) {
|
|
74
77
|
throw new Error("数据库未初始化");
|
|
75
78
|
}
|
|
79
|
+
// 验证禁用的操作
|
|
80
|
+
validateForbiddenOperations(query);
|
|
76
81
|
try {
|
|
77
82
|
// 将 ? 替换为编号参数
|
|
78
83
|
let paramIndex = 0;
|
|
@@ -108,6 +113,8 @@ export class PostgresqlAdapter {
|
|
|
108
113
|
if (!this.client) {
|
|
109
114
|
throw new Error("数据库未初始化");
|
|
110
115
|
}
|
|
116
|
+
// 验证禁用的操作
|
|
117
|
+
validateForbiddenOperations(query);
|
|
111
118
|
try {
|
|
112
119
|
await this.client.query(query);
|
|
113
120
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL 操作验证工具
|
|
3
|
+
* 集中管理禁用的 SQL 操作,确保数据库安全
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 禁用的 SQL 操作类型及其错误消息
|
|
7
|
+
* 使用更严格的模式,检测注释后的危险语句
|
|
8
|
+
*/
|
|
9
|
+
const FORBIDDEN_OPERATIONS = [
|
|
10
|
+
{
|
|
11
|
+
type: 'DROP',
|
|
12
|
+
// 匹配 DROP,忽略前导空白和单行注释
|
|
13
|
+
pattern: /^(\s*|--[^\n]*\n)*DROP\s/i,
|
|
14
|
+
message: 'DROP 操作已被禁用,此类操作应由 DBA 在数据库层面处理'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: 'TRUNCATE',
|
|
18
|
+
// 匹配 TRUNCATE,忽略前导空白和单行注释
|
|
19
|
+
pattern: /^(\s*|--[^\n]*\n)*TRUNCATE\s/i,
|
|
20
|
+
message: 'TRUNCATE 操作已被禁用,因为它不可回滚且不触发触发器'
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* 验证 SQL 查询是否包含禁用的操作
|
|
25
|
+
* @param query 要验证的 SQL 查询
|
|
26
|
+
* @throws Error 如果查询包含禁用的操作
|
|
27
|
+
*/
|
|
28
|
+
export function validateForbiddenOperations(query) {
|
|
29
|
+
for (const { pattern, message } of FORBIDDEN_OPERATIONS) {
|
|
30
|
+
if (pattern.test(query)) {
|
|
31
|
+
throw new Error(message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sqlite3 from "sqlite3";
|
|
2
|
+
import { validateForbiddenOperations } from "./sql-validator.js";
|
|
2
3
|
/**
|
|
3
4
|
* SQLite 数据库适配器实现
|
|
4
5
|
*/
|
|
@@ -36,6 +37,8 @@ export class SqliteAdapter {
|
|
|
36
37
|
if (!this.db) {
|
|
37
38
|
throw new Error("数据库未初始化");
|
|
38
39
|
}
|
|
40
|
+
// 验证禁用的操作(防止恶意查询)
|
|
41
|
+
validateForbiddenOperations(query);
|
|
39
42
|
return new Promise((resolve, reject) => {
|
|
40
43
|
this.db.all(query, params, (err, rows) => {
|
|
41
44
|
if (err) {
|
|
@@ -57,6 +60,8 @@ export class SqliteAdapter {
|
|
|
57
60
|
if (!this.db) {
|
|
58
61
|
throw new Error("数据库未初始化");
|
|
59
62
|
}
|
|
63
|
+
// 验证禁用的操作
|
|
64
|
+
validateForbiddenOperations(query);
|
|
60
65
|
return new Promise((resolve, reject) => {
|
|
61
66
|
this.db.run(query, params, function (err) {
|
|
62
67
|
if (err) {
|
|
@@ -77,6 +82,8 @@ export class SqliteAdapter {
|
|
|
77
82
|
if (!this.db) {
|
|
78
83
|
throw new Error("数据库未初始化");
|
|
79
84
|
}
|
|
85
|
+
// 验证禁用的操作
|
|
86
|
+
validateForbiddenOperations(query);
|
|
80
87
|
return new Promise((resolve, reject) => {
|
|
81
88
|
this.db.exec(query, (err) => {
|
|
82
89
|
if (err) {
|
|
@@ -1,4 +1,31 @@
|
|
|
1
|
+
import { validateForbiddenOperations } from "./sql-validator.js";
|
|
1
2
|
import sql from 'mssql';
|
|
3
|
+
/**
|
|
4
|
+
* 验证并转义 SQL Server 标识符名称
|
|
5
|
+
* 防止 SQL 注入攻击
|
|
6
|
+
* @param name 标识符名称(表名、视图名、存储过程名等)
|
|
7
|
+
* @returns 转义后的安全标识符
|
|
8
|
+
* @throws 如果标识符包含非法字符
|
|
9
|
+
*/
|
|
10
|
+
function escapeIdentifier(name) {
|
|
11
|
+
// 检查名称是否为空
|
|
12
|
+
if (!name || typeof name !== 'string') {
|
|
13
|
+
throw new Error('标识符名称不能为空');
|
|
14
|
+
}
|
|
15
|
+
// 检查名称长度(SQL Server 限制为 128 字符)
|
|
16
|
+
if (name.length > 128) {
|
|
17
|
+
throw new Error('标识符名称长度超过限制(最大 128 字符)');
|
|
18
|
+
}
|
|
19
|
+
// 检查是否包含危险的 SQL 字符
|
|
20
|
+
// 只允许字母、数字、下划线和常见的安全字符
|
|
21
|
+
const validNamePattern = /^[a-zA-Z_][a-zA-Z0-9_@$#]*$/;
|
|
22
|
+
if (!validNamePattern.test(name)) {
|
|
23
|
+
throw new Error(`标识符名称包含非法字符: ${name.substring(0, 20)}...`);
|
|
24
|
+
}
|
|
25
|
+
// 使用方括号转义标识符
|
|
26
|
+
// 方括号内的右方括号需要转义为两个右方括号
|
|
27
|
+
return `[${name.replace(/]/g, ']]')}]`;
|
|
28
|
+
}
|
|
2
29
|
/**
|
|
3
30
|
* SQL Server 数据库适配器实现
|
|
4
31
|
*/
|
|
@@ -176,6 +203,8 @@ export class SqlServerAdapter {
|
|
|
176
203
|
* @returns 包含查询结果的 Promise
|
|
177
204
|
*/
|
|
178
205
|
async all(query, params = []) {
|
|
206
|
+
// 验证禁用的操作(防止恶意查询)
|
|
207
|
+
validateForbiddenOperations(query);
|
|
179
208
|
return this.executeWithRetry(async (pool) => {
|
|
180
209
|
const request = pool.request();
|
|
181
210
|
// 向请求添加参数
|
|
@@ -196,6 +225,8 @@ export class SqlServerAdapter {
|
|
|
196
225
|
* @returns 包含结果信息的 Promise
|
|
197
226
|
*/
|
|
198
227
|
async run(query, params = []) {
|
|
228
|
+
// 验证禁用的操作
|
|
229
|
+
validateForbiddenOperations(query);
|
|
199
230
|
return this.executeWithRetry(async (pool) => {
|
|
200
231
|
const request = pool.request();
|
|
201
232
|
// 向请求添加参数
|
|
@@ -233,6 +264,8 @@ export class SqlServerAdapter {
|
|
|
233
264
|
* @returns 执行完成后解析的 Promise
|
|
234
265
|
*/
|
|
235
266
|
async exec(query) {
|
|
267
|
+
// 验证禁用的操作
|
|
268
|
+
validateForbiddenOperations(query);
|
|
236
269
|
return this.executeWithRetry(async (pool) => {
|
|
237
270
|
const request = pool.request();
|
|
238
271
|
await request.batch(query);
|
|
@@ -258,8 +291,11 @@ export class SqlServerAdapter {
|
|
|
258
291
|
/**
|
|
259
292
|
* 获取描述表或视图的数据库特定查询
|
|
260
293
|
* @param tableName 表名或视图名
|
|
294
|
+
* @returns SQL 查询字符串
|
|
261
295
|
*/
|
|
262
296
|
getDescribeTableQuery(tableName) {
|
|
297
|
+
// 验证并转义表名,防止 SQL 注入
|
|
298
|
+
const escapedTableName = escapeIdentifier(tableName);
|
|
263
299
|
return `
|
|
264
300
|
SELECT
|
|
265
301
|
c.COLUMN_NAME as name,
|
|
@@ -280,13 +316,13 @@ export class SqlServerAdapter {
|
|
|
280
316
|
SELECT o.object_id
|
|
281
317
|
FROM sys.objects o
|
|
282
318
|
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
|
|
283
|
-
WHERE o.name =
|
|
319
|
+
WHERE o.name = ${escapedTableName} AND s.name = c.TABLE_SCHEMA
|
|
284
320
|
AND o.type IN ('U', 'V')
|
|
285
321
|
)
|
|
286
322
|
AND ep.minor_id = c.ORDINAL_POSITION
|
|
287
323
|
AND ep.name = 'MS_Description'
|
|
288
324
|
WHERE
|
|
289
|
-
c.TABLE_NAME =
|
|
325
|
+
c.TABLE_NAME = ${escapedTableName}
|
|
290
326
|
ORDER BY
|
|
291
327
|
c.ORDINAL_POSITION
|
|
292
328
|
`;
|
|
@@ -300,10 +336,13 @@ export class SqlServerAdapter {
|
|
|
300
336
|
/**
|
|
301
337
|
* 获取视图定义的数据库特定查询
|
|
302
338
|
* @param viewName 视图名
|
|
339
|
+
* @returns SQL 查询字符串
|
|
303
340
|
* 注意: 使用 WITH ENCRYPTION 创建的视图无法获取定义
|
|
304
341
|
*/
|
|
305
342
|
getViewDefinitionQuery(viewName) {
|
|
306
|
-
|
|
343
|
+
// 验证并转义视图名,防止 SQL 注入
|
|
344
|
+
const escapedViewName = escapeIdentifier(viewName);
|
|
345
|
+
return `SELECT VIEW_DEFINITION as definition FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = ${escapedViewName}`;
|
|
307
346
|
}
|
|
308
347
|
/**
|
|
309
348
|
* 检查数据库是否支持视图功能
|
|
@@ -311,4 +350,52 @@ export class SqlServerAdapter {
|
|
|
311
350
|
supportsViews() {
|
|
312
351
|
return true;
|
|
313
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* 检查数据库是否支持存储过程功能
|
|
355
|
+
*/
|
|
356
|
+
supportsProcedures() {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* 获取列出存储过程的数据库特定查询
|
|
361
|
+
*/
|
|
362
|
+
getListProceduresQuery() {
|
|
363
|
+
return "SELECT ROUTINE_NAME as name FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE = 'PROCEDURE' ORDER BY ROUTINE_NAME";
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* 获取存储过程参数信息的查询
|
|
367
|
+
* @param procedureName 存储过程名
|
|
368
|
+
* @returns SQL 查询字符串
|
|
369
|
+
*/
|
|
370
|
+
getDescribeProcedureQuery(procedureName) {
|
|
371
|
+
// 验证并转义存储过程名,防止 SQL 注入
|
|
372
|
+
const escapedProcedureName = escapeIdentifier(procedureName);
|
|
373
|
+
return `
|
|
374
|
+
SELECT
|
|
375
|
+
PARAMETER_NAME as name,
|
|
376
|
+
DATA_TYPE +
|
|
377
|
+
CASE
|
|
378
|
+
WHEN CHARACTER_MAXIMUM_LENGTH IS NOT NULL
|
|
379
|
+
THEN '(' + CAST(CHARACTER_MAXIMUM_LENGTH AS VARCHAR) + ')'
|
|
380
|
+
ELSE ''
|
|
381
|
+
END as type,
|
|
382
|
+
PARAMETER_MODE as direction,
|
|
383
|
+
CASE WHEN PARAMETER_MODE IN ('OUT', 'INOUT') THEN 1 ELSE 0 END as is_output,
|
|
384
|
+
NULL as default_value
|
|
385
|
+
FROM INFORMATION_SCHEMA.PARAMETERS
|
|
386
|
+
WHERE SPECIFIC_NAME = ${escapedProcedureName}
|
|
387
|
+
ORDER BY ORDINAL_POSITION
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 获取存储过程定义的查询
|
|
392
|
+
* @param procedureName 存储过程名
|
|
393
|
+
* @returns SQL 查询字符串
|
|
394
|
+
* 注意: 使用 WITH ENCRYPTION 创建的存储过程无法获取定义
|
|
395
|
+
*/
|
|
396
|
+
getProcedureDefinitionQuery(procedureName) {
|
|
397
|
+
// 验证并转义存储过程名,防止 SQL 注入
|
|
398
|
+
const escapedProcedureName = escapeIdentifier(procedureName);
|
|
399
|
+
return `SELECT ROUTINE_DEFINITION as definition FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE = 'PROCEDURE' AND ROUTINE_NAME = ${escapedProcedureName}`;
|
|
400
|
+
}
|
|
314
401
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatErrorResponse } from '../utils/formatUtils.js';
|
|
2
2
|
// 导入所有工具实现
|
|
3
3
|
import { readQuery, writeQuery, exportQuery } from '../tools/queryTools.js';
|
|
4
|
-
import { createTable, alterTable, dropTable, listTables, describeTable, listViews, describeView, getViewDefinition } from '../tools/schemaTools.js';
|
|
4
|
+
import { createTable, alterTable, dropTable, listTables, describeTable, listViews, describeView, getViewDefinition, listProcedures, describeProcedure, getProcedureDefinition } from '../tools/schemaTools.js';
|
|
5
5
|
import { appendInsight, listInsights } from '../tools/insightTools.js';
|
|
6
6
|
/**
|
|
7
7
|
* 处理列出可用工具的请求
|
|
@@ -48,7 +48,7 @@ export function handleListTools() {
|
|
|
48
48
|
{
|
|
49
49
|
name: "write_query",
|
|
50
50
|
title: "Write Query",
|
|
51
|
-
description: "Execute INSERT, UPDATE,
|
|
51
|
+
description: "Execute INSERT, UPDATE, or DELETE queries to modify database data. " +
|
|
52
52
|
"Returns the number of affected rows. " +
|
|
53
53
|
"Cannot be used for SELECT queries - use read_query instead. " +
|
|
54
54
|
"Supports all database types: SQLite, SQL Server, PostgreSQL, MySQL. " +
|
|
@@ -58,7 +58,7 @@ export function handleListTools() {
|
|
|
58
58
|
properties: {
|
|
59
59
|
query: {
|
|
60
60
|
type: "string",
|
|
61
|
-
description: "The SQL INSERT/UPDATE/DELETE
|
|
61
|
+
description: "The SQL INSERT/UPDATE/DELETE query to execute"
|
|
62
62
|
},
|
|
63
63
|
confirm: {
|
|
64
64
|
type: "boolean",
|
|
@@ -165,45 +165,6 @@ export function handleListTools() {
|
|
|
165
165
|
destructiveHint: true
|
|
166
166
|
}
|
|
167
167
|
},
|
|
168
|
-
{
|
|
169
|
-
name: "drop_table",
|
|
170
|
-
title: "Drop Table",
|
|
171
|
-
description: "Permanently delete a table from the database. " +
|
|
172
|
-
"This operation cannot be undone - all data and structure will be lost. " +
|
|
173
|
-
"Requires confirm=true to execute as a safety measure. " +
|
|
174
|
-
"Validates that the table exists before attempting deletion.",
|
|
175
|
-
inputSchema: {
|
|
176
|
-
type: "object",
|
|
177
|
-
properties: {
|
|
178
|
-
table_name: {
|
|
179
|
-
type: "string",
|
|
180
|
-
description: "Name of the table to delete"
|
|
181
|
-
},
|
|
182
|
-
confirm: {
|
|
183
|
-
type: "boolean",
|
|
184
|
-
description: "Must be set to true to confirm table deletion"
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
required: ["table_name", "confirm"],
|
|
188
|
-
},
|
|
189
|
-
outputSchema: {
|
|
190
|
-
type: "object",
|
|
191
|
-
properties: {
|
|
192
|
-
success: {
|
|
193
|
-
type: "boolean",
|
|
194
|
-
description: "True if the table was dropped successfully"
|
|
195
|
-
},
|
|
196
|
-
message: {
|
|
197
|
-
type: "string",
|
|
198
|
-
description: "Success message with the dropped table name"
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
annotations: {
|
|
203
|
-
readOnlyHint: false,
|
|
204
|
-
destructiveHint: true
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
168
|
{
|
|
208
169
|
name: "export_query",
|
|
209
170
|
title: "Export Query",
|
|
@@ -505,6 +466,105 @@ export function handleListTools() {
|
|
|
505
466
|
idempotentHint: true
|
|
506
467
|
}
|
|
507
468
|
},
|
|
469
|
+
{
|
|
470
|
+
name: "list_procedures",
|
|
471
|
+
title: "List Procedures",
|
|
472
|
+
description: "Retrieve a list of all stored procedure names in the current database. " +
|
|
473
|
+
"Only works with SQL Server databases. " +
|
|
474
|
+
"Returns only procedure names without parameter details. " +
|
|
475
|
+
"Use describe_procedure to get detailed parameter information for a specific procedure.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: "object",
|
|
478
|
+
properties: {},
|
|
479
|
+
},
|
|
480
|
+
outputSchema: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {
|
|
483
|
+
procedures: {
|
|
484
|
+
type: "array",
|
|
485
|
+
items: { type: "string" },
|
|
486
|
+
description: "Array of stored procedure names in the database"
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
annotations: {
|
|
491
|
+
readOnlyHint: true,
|
|
492
|
+
idempotentHint: true
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: "describe_procedure",
|
|
497
|
+
title: "Describe Procedure",
|
|
498
|
+
description: "Get detailed parameter information about a specific stored procedure. " +
|
|
499
|
+
"Only works with SQL Server databases. " +
|
|
500
|
+
"Returns parameter name, data type, direction (IN/OUT/INOUT), and default value. " +
|
|
501
|
+
"The procedure must exist in the database.",
|
|
502
|
+
inputSchema: {
|
|
503
|
+
type: "object",
|
|
504
|
+
properties: {
|
|
505
|
+
procedure_name: {
|
|
506
|
+
type: "string",
|
|
507
|
+
description: "Name of the stored procedure to describe"
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
required: ["procedure_name"],
|
|
511
|
+
},
|
|
512
|
+
outputSchema: {
|
|
513
|
+
type: "object",
|
|
514
|
+
properties: {
|
|
515
|
+
name: { type: "string", description: "Procedure name" },
|
|
516
|
+
type: { type: "string", description: "Always 'procedure'" },
|
|
517
|
+
parameters: {
|
|
518
|
+
type: "array",
|
|
519
|
+
description: "Array of parameter definitions",
|
|
520
|
+
items: {
|
|
521
|
+
type: "object",
|
|
522
|
+
properties: {
|
|
523
|
+
name: { type: "string", description: "Parameter name" },
|
|
524
|
+
type: { type: "string", description: "Data type" },
|
|
525
|
+
direction: { type: "string", enum: ["IN", "OUT", "INOUT"], description: "Parameter direction" },
|
|
526
|
+
default_value: { type: "string", description: "Default value" },
|
|
527
|
+
is_output: { type: "boolean", description: "Whether this is an output parameter" }
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
annotations: {
|
|
534
|
+
readOnlyHint: true,
|
|
535
|
+
idempotentHint: true
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: "get_procedure_definition",
|
|
540
|
+
title: "Get Procedure Definition",
|
|
541
|
+
description: "Retrieve the SQL definition (CREATE PROCEDURE statement) of a specific stored procedure. " +
|
|
542
|
+
"Only works with SQL Server databases. " +
|
|
543
|
+
"Returns the complete CREATE PROCEDURE SQL statement. " +
|
|
544
|
+
"Note: Procedures created WITH ENCRYPTION cannot have their definition retrieved.",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: {
|
|
548
|
+
procedure_name: {
|
|
549
|
+
type: "string",
|
|
550
|
+
description: "Name of the stored procedure to get definition for"
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
required: ["procedure_name"],
|
|
554
|
+
},
|
|
555
|
+
outputSchema: {
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {
|
|
558
|
+
name: { type: "string", description: "Procedure name" },
|
|
559
|
+
definition: { type: "string", description: "CREATE PROCEDURE SQL statement" },
|
|
560
|
+
message: { type: "string", description: "Additional information if definition is unavailable" }
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
annotations: {
|
|
564
|
+
readOnlyHint: true,
|
|
565
|
+
idempotentHint: true
|
|
566
|
+
}
|
|
567
|
+
},
|
|
508
568
|
],
|
|
509
569
|
};
|
|
510
570
|
}
|
|
@@ -539,6 +599,12 @@ export async function handleToolCall(name, args) {
|
|
|
539
599
|
return await describeView(args.view_name);
|
|
540
600
|
case "get_view_definition":
|
|
541
601
|
return await getViewDefinition(args.view_name);
|
|
602
|
+
case "list_procedures":
|
|
603
|
+
return await listProcedures();
|
|
604
|
+
case "describe_procedure":
|
|
605
|
+
return await describeProcedure(args.procedure_name);
|
|
606
|
+
case "get_procedure_definition":
|
|
607
|
+
return await getProcedureDefinition(args.procedure_name);
|
|
542
608
|
case "append_insight":
|
|
543
609
|
return await appendInsight(args.insight, args.confirm);
|
|
544
610
|
case "list_insights":
|
|
@@ -29,11 +29,12 @@ export async function writeQuery(query, confirm = false) {
|
|
|
29
29
|
if (lowerQuery.startsWith("select")) {
|
|
30
30
|
throw new Error("SELECT 操作请使用 read_query");
|
|
31
31
|
}
|
|
32
|
-
// 支持 INSERT、UPDATE、DELETE
|
|
33
|
-
|
|
32
|
+
// 支持 INSERT、UPDATE、DELETE 操作
|
|
33
|
+
// 注意:TRUNCATE 操作已被禁用,因为它不可回滚且不触发触发器
|
|
34
|
+
const supportedOperations = ["insert", "update", "delete"];
|
|
34
35
|
const operation = supportedOperations.find(op => lowerQuery.startsWith(op));
|
|
35
36
|
if (!operation) {
|
|
36
|
-
throw new Error("write_query 只允许执行 INSERT、UPDATE
|
|
37
|
+
throw new Error("write_query 只允许执行 INSERT、UPDATE 或 DELETE 操作");
|
|
37
38
|
}
|
|
38
39
|
// 确认检查:防止误操作
|
|
39
40
|
if (!confirm) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dbAll, dbExec, getListTablesQuery, getDescribeTableQuery, getListViewsQuery, getViewDefinitionQuery, supportsViews } from '../db/index.js';
|
|
1
|
+
import { dbAll, dbExec, getListTablesQuery, getDescribeTableQuery, getListViewsQuery, getViewDefinitionQuery, supportsViews, getListProceduresQuery, getDescribeProcedureQuery, getProcedureDefinitionQuery, supportsProcedures } from '../db/index.js';
|
|
2
2
|
import { formatSuccessResponse } from '../utils/formatUtils.js';
|
|
3
3
|
/**
|
|
4
4
|
* 检查数据库对象是否存在
|
|
@@ -19,6 +19,19 @@ async function checkObjectExists(objectName, objectType) {
|
|
|
19
19
|
}
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* 检查存储过程是否存在
|
|
24
|
+
* @param procedureName 存储过程名
|
|
25
|
+
* @returns 如果存在返回 true,否则返回 false
|
|
26
|
+
*/
|
|
27
|
+
async function checkProcedureExists(procedureName) {
|
|
28
|
+
if (!supportsProcedures()) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const query = getListProceduresQuery();
|
|
32
|
+
const procedures = await dbAll(query);
|
|
33
|
+
return procedures.some(proc => proc.name === procedureName);
|
|
34
|
+
}
|
|
22
35
|
/**
|
|
23
36
|
* 格式化列结构信息
|
|
24
37
|
* @param columns 原始列数据数组
|
|
@@ -127,32 +140,14 @@ export async function alterTable(query, confirm = false) {
|
|
|
127
140
|
* @param tableName 要删除的表名
|
|
128
141
|
* @param confirm 安全确认标志
|
|
129
142
|
* @returns 操作结果
|
|
143
|
+
* @deprecated DROP 操作已被禁用,此类操作应由 DBA 在数据库层面处理
|
|
130
144
|
*/
|
|
131
145
|
export async function dropTable(tableName, confirm) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return formatSuccessResponse({
|
|
138
|
-
success: false,
|
|
139
|
-
message: "需要安全确认。设置 confirm=true 以继续删除表。"
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
// 检查表是否存在
|
|
143
|
-
if (!(await checkObjectExists(tableName, 'table'))) {
|
|
144
|
-
throw new Error(`表 '${tableName}' 不存在`);
|
|
145
|
-
}
|
|
146
|
-
// 删除表
|
|
147
|
-
await dbExec(`DROP TABLE "${tableName}"`);
|
|
148
|
-
return formatSuccessResponse({
|
|
149
|
-
success: true,
|
|
150
|
-
message: `表 '${tableName}' 删除成功`
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
throw new Error(`删除表失败: ${error.message}`);
|
|
155
|
-
}
|
|
146
|
+
// DROP 操作已被禁用
|
|
147
|
+
return formatSuccessResponse({
|
|
148
|
+
success: false,
|
|
149
|
+
message: "DROP 操作已被禁用,此类操作应由 DBA 在数据库层面处理。如需删除表,请联系数据库管理员。"
|
|
150
|
+
});
|
|
156
151
|
}
|
|
157
152
|
/**
|
|
158
153
|
* 列出数据库中的所有表
|
|
@@ -197,7 +192,8 @@ export async function describeTable(tableName) {
|
|
|
197
192
|
objectType = 'view';
|
|
198
193
|
}
|
|
199
194
|
else {
|
|
200
|
-
|
|
195
|
+
// 错误消息不直接包含用户输入,防止日志注入
|
|
196
|
+
throw new Error(supportsViews() ? "指定的表或视图不存在" : "指定的表不存在");
|
|
201
197
|
}
|
|
202
198
|
// 使用适配器特定的查询来描述表/视图结构
|
|
203
199
|
const descQuery = getDescribeTableQuery(tableName);
|
|
@@ -246,7 +242,8 @@ export async function describeView(viewName) {
|
|
|
246
242
|
}
|
|
247
243
|
// 检查视图是否存在
|
|
248
244
|
if (!(await checkObjectExists(viewName, 'view'))) {
|
|
249
|
-
|
|
245
|
+
// 错误消息不直接包含用户输入,防止日志注入
|
|
246
|
+
throw new Error("指定的视图不存在");
|
|
250
247
|
}
|
|
251
248
|
// 使用相同的 describe 查询获取视图列结构
|
|
252
249
|
const descQuery = getDescribeTableQuery(viewName);
|
|
@@ -278,7 +275,8 @@ export async function getViewDefinition(viewName) {
|
|
|
278
275
|
}
|
|
279
276
|
// 检查视图是否存在
|
|
280
277
|
if (!(await checkObjectExists(viewName, 'view'))) {
|
|
281
|
-
|
|
278
|
+
// 错误消息不直接包含用户输入,防止日志注入
|
|
279
|
+
throw new Error("指定的视图不存在");
|
|
282
280
|
}
|
|
283
281
|
// 获取视图定义
|
|
284
282
|
const defQuery = getViewDefinitionQuery(viewName);
|
|
@@ -299,3 +297,116 @@ export async function getViewDefinition(viewName) {
|
|
|
299
297
|
throw new Error(`获取视图定义失败: ${error.message}`);
|
|
300
298
|
}
|
|
301
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* 列出数据库中的所有存储过程
|
|
302
|
+
* 仅支持 SQL Server
|
|
303
|
+
* @returns 存储过程名数组
|
|
304
|
+
*/
|
|
305
|
+
export async function listProcedures() {
|
|
306
|
+
try {
|
|
307
|
+
if (!supportsProcedures()) {
|
|
308
|
+
throw new Error("存储过程功能仅支持 SQL Server 数据库");
|
|
309
|
+
}
|
|
310
|
+
const query = getListProceduresQuery();
|
|
311
|
+
const procedures = await dbAll(query);
|
|
312
|
+
return formatSuccessResponse(procedures.map((p) => p.name));
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
throw new Error(`列出存储过程失败: ${error.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* 验证存储过程名称格式是否合法
|
|
320
|
+
* @param name 存储过程名称
|
|
321
|
+
* @throws 如果名称包含非法字符
|
|
322
|
+
*/
|
|
323
|
+
function validateProcedureNameFormat(name) {
|
|
324
|
+
// 检查名称是否为空
|
|
325
|
+
if (!name || typeof name !== 'string') {
|
|
326
|
+
throw new Error("存储过程名不能为空");
|
|
327
|
+
}
|
|
328
|
+
// 检查名称长度(SQL Server 限制为 128 字符)
|
|
329
|
+
if (name.length > 128) {
|
|
330
|
+
throw new Error("存储过程名称长度超过限制(最大 128 字符)");
|
|
331
|
+
}
|
|
332
|
+
// 检查是否包含危险的 SQL 字符
|
|
333
|
+
// 只允许字母、数字、下划线和常见的安全字符
|
|
334
|
+
const validNamePattern = /^[a-zA-Z_][a-zA-Z0-9_@$#]*$/;
|
|
335
|
+
if (!validNamePattern.test(name)) {
|
|
336
|
+
throw new Error("存储过程名称包含非法字符");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* 验证存储过程操作的前置条件
|
|
341
|
+
* @param procedureName 存储过程名
|
|
342
|
+
*/
|
|
343
|
+
async function validateProcedureOperation(procedureName) {
|
|
344
|
+
// 首先验证名称格式,防止恶意输入
|
|
345
|
+
validateProcedureNameFormat(procedureName);
|
|
346
|
+
if (!supportsProcedures()) {
|
|
347
|
+
throw new Error("存储过程功能仅支持 SQL Server 数据库");
|
|
348
|
+
}
|
|
349
|
+
if (!(await checkProcedureExists(procedureName))) {
|
|
350
|
+
// 错误消息不直接包含用户输入,防止日志注入
|
|
351
|
+
throw new Error("指定的存储过程不存在");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* 获取存储过程的参数信息
|
|
356
|
+
* 仅支持 SQL Server
|
|
357
|
+
* @param procedureName 存储过程名
|
|
358
|
+
* @returns 存储过程的参数定义
|
|
359
|
+
*/
|
|
360
|
+
export async function describeProcedure(procedureName) {
|
|
361
|
+
try {
|
|
362
|
+
await validateProcedureOperation(procedureName);
|
|
363
|
+
// 获取参数信息
|
|
364
|
+
const descQuery = getDescribeProcedureQuery(procedureName);
|
|
365
|
+
const params = await dbAll(descQuery);
|
|
366
|
+
// 格式化参数信息
|
|
367
|
+
const parameters = params.map((param) => ({
|
|
368
|
+
name: param.name,
|
|
369
|
+
type: param.type,
|
|
370
|
+
direction: param.direction,
|
|
371
|
+
default_value: param.default_value,
|
|
372
|
+
is_output: !!param.is_output
|
|
373
|
+
}));
|
|
374
|
+
return formatSuccessResponse({
|
|
375
|
+
name: procedureName,
|
|
376
|
+
type: 'procedure',
|
|
377
|
+
parameters: parameters
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
throw new Error(`描述存储过程失败: ${error.message}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 获取存储过程的定义 SQL
|
|
386
|
+
* 仅支持 SQL Server
|
|
387
|
+
* 注意: 使用 WITH ENCRYPTION 创建的存储过程无法获取定义
|
|
388
|
+
* @param procedureName 存储过程名
|
|
389
|
+
* @returns 存储过程定义 SQL
|
|
390
|
+
*/
|
|
391
|
+
export async function getProcedureDefinition(procedureName) {
|
|
392
|
+
try {
|
|
393
|
+
await validateProcedureOperation(procedureName);
|
|
394
|
+
// 获取存储过程定义
|
|
395
|
+
const defQuery = getProcedureDefinitionQuery(procedureName);
|
|
396
|
+
const result = await dbAll(defQuery);
|
|
397
|
+
if (result.length === 0 || !result[0].definition) {
|
|
398
|
+
return formatSuccessResponse({
|
|
399
|
+
name: procedureName,
|
|
400
|
+
definition: null,
|
|
401
|
+
message: "存储过程定义不可用(可能使用 WITH ENCRYPTION 创建)"
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return formatSuccessResponse({
|
|
405
|
+
name: procedureName,
|
|
406
|
+
definition: result[0].definition
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
throw new Error(`获取存储过程定义失败: ${error.message}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmd233/mcp-database-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for interacting with SQLite, SQL Server, PostgreSQL and MySQL databases (
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "MCP server for interacting with SQLite, SQL Server, PostgreSQL and MySQL databases (Added stored procedure support and enhanced SQL injection protection)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "cmd233",
|
|
7
7
|
"homepage": "https://github.com/cmd233/mcp-database-server",
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"watch": "tsc --watch",
|
|
21
21
|
"start": "node dist/src/index.js",
|
|
22
22
|
"dev": "tsc && node dist/src/index.js",
|
|
23
|
-
"example": "node examples/example.js",
|
|
24
23
|
"clean": "rimraf dist"
|
|
25
24
|
},
|
|
26
25
|
"dependencies": {
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 加密和确认码工具模块
|
|
3
|
-
* 用于两步确认机制,防止 AI 模型自动执行危险操作
|
|
4
|
-
*/
|
|
5
|
-
// 硬编码密钥,用于生成确认码
|
|
6
|
-
const CONFIRM_SECRET_KEY = 'mcp-db-server-confirm-key-v1';
|
|
7
|
-
/**
|
|
8
|
-
* 获取当前分钟时间戳
|
|
9
|
-
* @returns 当前分钟的时间戳字符串
|
|
10
|
-
*/
|
|
11
|
-
function getMinuteTimestamp() {
|
|
12
|
-
const now = Date.now();
|
|
13
|
-
// 转换为分钟级时间戳(去掉秒和毫秒)
|
|
14
|
-
const minuteTimestamp = Math.floor(now / 60000);
|
|
15
|
-
return minuteTimestamp.toString();
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* 生成简单哈希
|
|
19
|
-
* 使用简单的字符串哈希算法生成固定长度的哈希值
|
|
20
|
-
* @param content 要哈希的内容
|
|
21
|
-
* @returns 8位十六进制哈希字符串
|
|
22
|
-
*/
|
|
23
|
-
function generateSimpleHash(content) {
|
|
24
|
-
let hash = 0;
|
|
25
|
-
// 使用简单的字符串哈希算法
|
|
26
|
-
for (let i = 0; i < content.length; i++) {
|
|
27
|
-
const char = content.charCodeAt(i);
|
|
28
|
-
hash = ((hash << 5) - hash) + char;
|
|
29
|
-
hash = hash & hash; // 转换为 32 位整数
|
|
30
|
-
}
|
|
31
|
-
// 转换为无符号整数并转为十六进制,取前 8 位
|
|
32
|
-
const hashStr = Math.abs(hash).toString(16).padStart(8, '0');
|
|
33
|
-
return hashStr.substring(0, 8);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* 标准化操作内容
|
|
37
|
-
* 移除多余的空格,统一大小写,确保相同操作生成相同的确认码
|
|
38
|
-
* @param query 原始 SQL 查询或操作内容
|
|
39
|
-
* @returns 标准化后的操作内容
|
|
40
|
-
*/
|
|
41
|
-
export function normalizeOperationContent(query) {
|
|
42
|
-
// 移除前后空格,将多个连续空格替换为单个空格
|
|
43
|
-
return query.trim().replace(/\s+/g, ' ');
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* 生成确认码
|
|
47
|
-
* 基于操作内容和当前分钟时间戳生成 8 位十六进制确认码
|
|
48
|
-
* @param operationContent 操作内容(SQL 查询或操作描述)
|
|
49
|
-
* @returns 8 位十六进制确认码
|
|
50
|
-
*/
|
|
51
|
-
export function generateConfirmCode(operationContent) {
|
|
52
|
-
const normalized = normalizeOperationContent(operationContent);
|
|
53
|
-
const minuteTimestamp = getMinuteTimestamp();
|
|
54
|
-
const hashInput = normalized + minuteTimestamp + CONFIRM_SECRET_KEY;
|
|
55
|
-
return generateSimpleHash(hashInput);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* 验证确认码
|
|
59
|
-
* 验证确认码是否有效,支持当前分钟和上一分钟的确认码(2 分钟窗口)
|
|
60
|
-
* @param operationContent 操作内容(SQL 查询或操作描述)
|
|
61
|
-
* @param confirmCode 要验证的确认码
|
|
62
|
-
* @returns 确认码是否有效
|
|
63
|
-
*/
|
|
64
|
-
export function verifyConfirmCode(operationContent, confirmCode) {
|
|
65
|
-
if (!confirmCode || confirmCode.length !== 8) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
const normalized = normalizeOperationContent(operationContent);
|
|
69
|
-
const now = Date.now();
|
|
70
|
-
const currentMinute = Math.floor(now / 60000);
|
|
71
|
-
const previousMinute = currentMinute - 1;
|
|
72
|
-
// 验证当前分钟的确认码
|
|
73
|
-
const currentHashInput = normalized + currentMinute.toString() + CONFIRM_SECRET_KEY;
|
|
74
|
-
const currentCode = generateSimpleHash(currentHashInput);
|
|
75
|
-
if (currentCode === confirmCode) {
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
// 验证上一分钟的确认码
|
|
79
|
-
const previousHashInput = normalized + previousMinute.toString() + CONFIRM_SECRET_KEY;
|
|
80
|
-
const previousCode = generateSimpleHash(previousHashInput);
|
|
81
|
-
return previousCode === confirmCode;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* 格式化预览响应
|
|
85
|
-
* 生成预览模式的响应格式,包含确认码和操作说明
|
|
86
|
-
* @param toolName 工具名称
|
|
87
|
-
* @param operationContent 操作内容
|
|
88
|
-
* @param additionalInfo 额外信息(如警告消息)
|
|
89
|
-
* @returns 预览响应对象
|
|
90
|
-
*/
|
|
91
|
-
export function formatPreviewResponse(toolName, operationContent, additionalInfo) {
|
|
92
|
-
const confirmCode = generateConfirmCode(operationContent);
|
|
93
|
-
const response = {
|
|
94
|
-
preview: true,
|
|
95
|
-
tool_name: toolName,
|
|
96
|
-
confirm_code: confirmCode,
|
|
97
|
-
message: "⚠️ 预览模式:此操作需要确认",
|
|
98
|
-
operation_content: normalizeOperationContent(operationContent),
|
|
99
|
-
instructions: [
|
|
100
|
-
"请仔细检查上述操作内容",
|
|
101
|
-
"如确认执行,请重新调用此工具并提供确认码",
|
|
102
|
-
`参数示例: { "preview": false, "confirm_code": "${confirmCode}" }`
|
|
103
|
-
]
|
|
104
|
-
};
|
|
105
|
-
// 添加额外信息
|
|
106
|
-
if (additionalInfo?.warning) {
|
|
107
|
-
response.warning = additionalInfo.warning;
|
|
108
|
-
}
|
|
109
|
-
if (additionalInfo?.operation) {
|
|
110
|
-
response.operation = additionalInfo.operation;
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
content: [{
|
|
114
|
-
type: "text",
|
|
115
|
-
text: JSON.stringify(response, null, 2)
|
|
116
|
-
}],
|
|
117
|
-
isError: false
|
|
118
|
-
};
|
|
119
|
-
}
|