@cmd233/mcp-database-server 1.1.7 → 1.3.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 +92 -0
- package/dist/src/db/mysql-adapter.js +13 -6
- package/dist/src/db/postgresql-adapter.js +14 -5
- package/dist/src/db/sql-validator.js +34 -0
- package/dist/src/db/sqlite-adapter.js +10 -3
- package/dist/src/db/sqlserver-adapter.js +126 -23
- package/dist/src/handlers/toolHandlers.js +527 -27
- package/dist/src/tools/insightTools.js +17 -5
- package/dist/src/tools/queryTools.js +14 -2
- package/dist/src/tools/schemaTools.js +342 -49
- package/package.json +2 -3
|
@@ -3,13 +3,21 @@ import { formatSuccessResponse } from '../utils/formatUtils.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* 添加业务洞察到备忘录
|
|
5
5
|
* @param insight 业务洞察文本
|
|
6
|
-
* @
|
|
6
|
+
* @param confirm 安全确认标志(默认 false,防止误操作)
|
|
7
|
+
* @returns 操作结果,包含新增记录的 ID
|
|
7
8
|
*/
|
|
8
|
-
export async function appendInsight(insight) {
|
|
9
|
+
export async function appendInsight(insight, confirm = false) {
|
|
9
10
|
try {
|
|
10
11
|
if (!insight) {
|
|
11
12
|
throw new Error("洞察内容不能为空");
|
|
12
13
|
}
|
|
14
|
+
// 确认检查:防止误操作
|
|
15
|
+
if (!confirm) {
|
|
16
|
+
return formatSuccessResponse({
|
|
17
|
+
success: false,
|
|
18
|
+
message: "需要安全确认。设置 confirm=true 以继续添加洞察。"
|
|
19
|
+
});
|
|
20
|
+
}
|
|
13
21
|
// 如果 insights 表不存在则创建
|
|
14
22
|
await dbExec(`
|
|
15
23
|
CREATE TABLE IF NOT EXISTS mcp_insights (
|
|
@@ -18,9 +26,13 @@ export async function appendInsight(insight) {
|
|
|
18
26
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
19
27
|
)
|
|
20
28
|
`);
|
|
21
|
-
//
|
|
22
|
-
await dbRun("INSERT INTO mcp_insights (insight) VALUES (?)", [insight]);
|
|
23
|
-
return formatSuccessResponse({
|
|
29
|
+
// 插入洞察记录并获取返回的 ID
|
|
30
|
+
const result = await dbRun("INSERT INTO mcp_insights (insight) VALUES (?)", [insight]);
|
|
31
|
+
return formatSuccessResponse({
|
|
32
|
+
success: true,
|
|
33
|
+
message: "洞察已添加",
|
|
34
|
+
id: result.lastID
|
|
35
|
+
});
|
|
24
36
|
}
|
|
25
37
|
catch (error) {
|
|
26
38
|
throw new Error(`添加洞察失败: ${error.message}`);
|
|
@@ -20,17 +20,29 @@ export async function readQuery(query) {
|
|
|
20
20
|
/**
|
|
21
21
|
* 执行数据修改 SQL 查询
|
|
22
22
|
* @param query 要执行的 SQL 查询
|
|
23
|
+
* @param confirm 安全确认标志(默认 false,防止误操作)
|
|
23
24
|
* @returns 受影响行的信息
|
|
24
25
|
*/
|
|
25
|
-
export async function writeQuery(query) {
|
|
26
|
+
export async function writeQuery(query, confirm = false) {
|
|
26
27
|
try {
|
|
27
28
|
const lowerQuery = query.trim().toLowerCase();
|
|
28
29
|
if (lowerQuery.startsWith("select")) {
|
|
29
30
|
throw new Error("SELECT 操作请使用 read_query");
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
// 支持 INSERT、UPDATE、DELETE 操作
|
|
33
|
+
// 注意:TRUNCATE 操作已被禁用,因为它不可回滚且不触发触发器
|
|
34
|
+
const supportedOperations = ["insert", "update", "delete"];
|
|
35
|
+
const operation = supportedOperations.find(op => lowerQuery.startsWith(op));
|
|
36
|
+
if (!operation) {
|
|
32
37
|
throw new Error("write_query 只允许执行 INSERT、UPDATE 或 DELETE 操作");
|
|
33
38
|
}
|
|
39
|
+
// 确认检查:防止误操作
|
|
40
|
+
if (!confirm) {
|
|
41
|
+
return formatSuccessResponse({
|
|
42
|
+
success: false,
|
|
43
|
+
message: `需要安全确认。设置 confirm=true 以继续执行 ${operation.toUpperCase()} 操作。`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
34
46
|
const result = await dbRun(query);
|
|
35
47
|
return formatSuccessResponse({ affected_rows: result.changes });
|
|
36
48
|
}
|
|
@@ -1,15 +1,106 @@
|
|
|
1
|
-
import { dbAll, dbExec, getListTablesQuery, getDescribeTableQuery } 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
|
+
/**
|
|
4
|
+
* 检查数据库对象是否存在
|
|
5
|
+
* @param objectName 对象名
|
|
6
|
+
* @param objectType 对象类型 ('table' | 'view')
|
|
7
|
+
* @returns 如果存在返回 true,否则返回 false
|
|
8
|
+
*/
|
|
9
|
+
async function checkObjectExists(objectName, objectType) {
|
|
10
|
+
if (objectType === 'table') {
|
|
11
|
+
const query = getListTablesQuery();
|
|
12
|
+
const objects = await dbAll(query);
|
|
13
|
+
return objects.some(obj => obj.name === objectName);
|
|
14
|
+
}
|
|
15
|
+
else if (supportsViews()) {
|
|
16
|
+
const query = getListViewsQuery();
|
|
17
|
+
const objects = await dbAll(query);
|
|
18
|
+
return objects.some(obj => obj.name === objectName);
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
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
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 格式化列结构信息
|
|
37
|
+
* @param columns 原始列数据数组
|
|
38
|
+
* @returns 格式化后的列数组
|
|
39
|
+
*/
|
|
40
|
+
function formatColumns(columns) {
|
|
41
|
+
return columns.map((col) => ({
|
|
42
|
+
name: col.name,
|
|
43
|
+
type: col.type,
|
|
44
|
+
notnull: !!col.notnull,
|
|
45
|
+
default_value: col.dflt_value,
|
|
46
|
+
primary_key: !!col.pk,
|
|
47
|
+
comment: col.comment || null
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 从 SQL 语句中提取表名
|
|
52
|
+
* @param query SQL 语句
|
|
53
|
+
* @param operation SQL 操作类型(CREATE TABLE、ALTER TABLE 等)
|
|
54
|
+
* @returns 提取的表名或 null
|
|
55
|
+
*/
|
|
56
|
+
function extractTableName(query, operation) {
|
|
57
|
+
try {
|
|
58
|
+
const normalizedQuery = query.trim().replace(/\s+/g, ' ');
|
|
59
|
+
const operationPrefix = operation.toLowerCase();
|
|
60
|
+
if (!normalizedQuery.toLowerCase().startsWith(operationPrefix)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
// 移除操作前缀后的剩余部分
|
|
64
|
+
const afterOperation = normalizedQuery.substring(operationPrefix.length).trim();
|
|
65
|
+
// 处理 IF NOT EXISTS 或 IF EXISTS 等子句
|
|
66
|
+
const patterns = [
|
|
67
|
+
/^if\s+not\s+exists\s+([^\s(]+)/i, // CREATE TABLE IF NOT EXISTS tablename
|
|
68
|
+
/^if\s+exists\s+([^\s(]+)/i, // DROP TABLE IF EXISTS tablename
|
|
69
|
+
/^([^\s(]+)/ // CREATE TABLE tablename
|
|
70
|
+
];
|
|
71
|
+
for (const pattern of patterns) {
|
|
72
|
+
const match = afterOperation.match(pattern);
|
|
73
|
+
if (match && match[1]) {
|
|
74
|
+
// 移除引号(如果有)
|
|
75
|
+
return match[1].replace(/^[`"[]|[`"\]]$/g, '');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
3
84
|
/**
|
|
4
85
|
* 在数据库中创建新表
|
|
5
86
|
* @param query CREATE TABLE SQL 语句
|
|
87
|
+
* @param confirm 安全确认标志(默认 false,防止误操作)
|
|
6
88
|
* @returns 操作结果
|
|
7
89
|
*/
|
|
8
|
-
export async function createTable(query) {
|
|
90
|
+
export async function createTable(query, confirm = false) {
|
|
9
91
|
try {
|
|
10
92
|
if (!query.trim().toLowerCase().startsWith("create table")) {
|
|
11
93
|
throw new Error("只允许执行 CREATE TABLE 语句");
|
|
12
94
|
}
|
|
95
|
+
// 确认检查:防止误操作
|
|
96
|
+
if (!confirm) {
|
|
97
|
+
const tableName = extractTableName(query, "CREATE TABLE");
|
|
98
|
+
const tableInfo = tableName ? ` '${tableName}'` : '';
|
|
99
|
+
return formatSuccessResponse({
|
|
100
|
+
success: false,
|
|
101
|
+
message: `需要安全确认。设置 confirm=true 以继续创建表${tableInfo}。`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
13
104
|
await dbExec(query);
|
|
14
105
|
return formatSuccessResponse({ success: true, message: "表创建成功" });
|
|
15
106
|
}
|
|
@@ -20,13 +111,23 @@ export async function createTable(query) {
|
|
|
20
111
|
/**
|
|
21
112
|
* 修改现有表的结构
|
|
22
113
|
* @param query ALTER TABLE SQL 语句
|
|
114
|
+
* @param confirm 安全确认标志(默认 false,防止误操作)
|
|
23
115
|
* @returns 操作结果
|
|
24
116
|
*/
|
|
25
|
-
export async function alterTable(query) {
|
|
117
|
+
export async function alterTable(query, confirm = false) {
|
|
26
118
|
try {
|
|
27
119
|
if (!query.trim().toLowerCase().startsWith("alter table")) {
|
|
28
120
|
throw new Error("只允许执行 ALTER TABLE 语句");
|
|
29
121
|
}
|
|
122
|
+
// 确认检查:防止误操作
|
|
123
|
+
if (!confirm) {
|
|
124
|
+
const tableName = extractTableName(query, "ALTER TABLE");
|
|
125
|
+
const tableInfo = tableName ? ` '${tableName}'` : '';
|
|
126
|
+
return formatSuccessResponse({
|
|
127
|
+
success: false,
|
|
128
|
+
message: `需要安全确认。设置 confirm=true 以继续修改表结构${tableInfo}。`
|
|
129
|
+
});
|
|
130
|
+
}
|
|
30
131
|
await dbExec(query);
|
|
31
132
|
return formatSuccessResponse({ success: true, message: "表结构修改成功" });
|
|
32
133
|
}
|
|
@@ -39,46 +140,33 @@ export async function alterTable(query) {
|
|
|
39
140
|
* @param tableName 要删除的表名
|
|
40
141
|
* @param confirm 安全确认标志
|
|
41
142
|
* @returns 操作结果
|
|
143
|
+
* @deprecated DROP 操作已被禁用,此类操作应由 DBA 在数据库层面处理
|
|
42
144
|
*/
|
|
43
145
|
export async function dropTable(tableName, confirm) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return formatSuccessResponse({
|
|
50
|
-
success: false,
|
|
51
|
-
message: "需要安全确认。设置 confirm=true 以继续删除表。"
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
// First check if table exists by directly querying for tables
|
|
55
|
-
const query = getListTablesQuery();
|
|
56
|
-
const tables = await dbAll(query);
|
|
57
|
-
const tableNames = tables.map(t => t.name);
|
|
58
|
-
if (!tableNames.includes(tableName)) {
|
|
59
|
-
throw new Error(`表 '${tableName}' 不存在`);
|
|
60
|
-
}
|
|
61
|
-
// 删除表
|
|
62
|
-
await dbExec(`DROP TABLE "${tableName}"`);
|
|
63
|
-
return formatSuccessResponse({
|
|
64
|
-
success: true,
|
|
65
|
-
message: `表 '${tableName}' 删除成功`
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
throw new Error(`删除表失败: ${error.message}`);
|
|
70
|
-
}
|
|
146
|
+
// DROP 操作已被禁用
|
|
147
|
+
return formatSuccessResponse({
|
|
148
|
+
success: false,
|
|
149
|
+
message: "DROP 操作已被禁用,此类操作应由 DBA 在数据库层面处理。如需删除表,请联系数据库管理员。"
|
|
150
|
+
});
|
|
71
151
|
}
|
|
72
152
|
/**
|
|
73
153
|
* 列出数据库中的所有表
|
|
154
|
+
* @param includeViews 是否包含视图(默认 false)
|
|
74
155
|
* @returns 表名数组
|
|
75
156
|
*/
|
|
76
|
-
export async function listTables() {
|
|
157
|
+
export async function listTables(includeViews = false) {
|
|
77
158
|
try {
|
|
78
159
|
// 使用适配器特定的查询来列出表
|
|
79
160
|
const query = getListTablesQuery();
|
|
80
161
|
const tables = await dbAll(query);
|
|
81
|
-
|
|
162
|
+
const result = tables.map((t) => ({ name: t.name, type: 'table' }));
|
|
163
|
+
// 如果需要包含视图且数据库支持视图
|
|
164
|
+
if (includeViews && supportsViews()) {
|
|
165
|
+
const viewsQuery = getListViewsQuery();
|
|
166
|
+
const views = await dbAll(viewsQuery);
|
|
167
|
+
result.push(...views.map((v) => ({ name: v.name, type: 'view' })));
|
|
168
|
+
}
|
|
169
|
+
return formatSuccessResponse(result);
|
|
82
170
|
}
|
|
83
171
|
catch (error) {
|
|
84
172
|
throw new Error(`列出表失败: ${error.message}`);
|
|
@@ -86,34 +174,239 @@ export async function listTables() {
|
|
|
86
174
|
}
|
|
87
175
|
/**
|
|
88
176
|
* 获取指定表的结构信息
|
|
89
|
-
*
|
|
90
|
-
* @
|
|
177
|
+
* 支持实体表和视图
|
|
178
|
+
* @param tableName 要描述的表名或视图名
|
|
179
|
+
* @returns 表/视图的列定义
|
|
91
180
|
*/
|
|
92
181
|
export async function describeTable(tableName) {
|
|
93
182
|
try {
|
|
94
183
|
if (!tableName) {
|
|
95
184
|
throw new Error("表名不能为空");
|
|
96
185
|
}
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
186
|
+
// 检查是表还是视图
|
|
187
|
+
let objectType = 'table';
|
|
188
|
+
if (await checkObjectExists(tableName, 'table')) {
|
|
189
|
+
objectType = 'table';
|
|
190
|
+
}
|
|
191
|
+
else if (supportsViews() && await checkObjectExists(tableName, 'view')) {
|
|
192
|
+
objectType = 'view';
|
|
103
193
|
}
|
|
104
|
-
|
|
194
|
+
else {
|
|
195
|
+
// 错误消息不直接包含用户输入,防止日志注入
|
|
196
|
+
throw new Error(supportsViews() ? "指定的表或视图不存在" : "指定的表不存在");
|
|
197
|
+
}
|
|
198
|
+
// 使用适配器特定的查询来描述表/视图结构
|
|
105
199
|
const descQuery = getDescribeTableQuery(tableName);
|
|
106
200
|
const columns = await dbAll(descQuery);
|
|
107
|
-
return formatSuccessResponse(
|
|
108
|
-
name:
|
|
109
|
-
type:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
primary_key: !!col.pk,
|
|
113
|
-
comment: col.comment || null
|
|
114
|
-
})));
|
|
201
|
+
return formatSuccessResponse({
|
|
202
|
+
name: tableName,
|
|
203
|
+
type: objectType,
|
|
204
|
+
columns: formatColumns(columns)
|
|
205
|
+
});
|
|
115
206
|
}
|
|
116
207
|
catch (error) {
|
|
117
208
|
throw new Error(`描述表结构失败: ${error.message}`);
|
|
118
209
|
}
|
|
119
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* 列出数据库中的所有视图
|
|
213
|
+
* 仅支持 SQL Server
|
|
214
|
+
* @returns 视图名数组
|
|
215
|
+
*/
|
|
216
|
+
export async function listViews() {
|
|
217
|
+
try {
|
|
218
|
+
if (!supportsViews()) {
|
|
219
|
+
throw new Error("视图功能仅支持 SQL Server 数据库");
|
|
220
|
+
}
|
|
221
|
+
const query = getListViewsQuery();
|
|
222
|
+
const views = await dbAll(query);
|
|
223
|
+
return formatSuccessResponse(views.map((v) => v.name));
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
throw new Error(`列出视图失败: ${error.message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 获取指定视图的结构信息
|
|
231
|
+
* 仅支持 SQL Server
|
|
232
|
+
* @param viewName 视图名
|
|
233
|
+
* @returns 视图的列定义
|
|
234
|
+
*/
|
|
235
|
+
export async function describeView(viewName) {
|
|
236
|
+
try {
|
|
237
|
+
if (!viewName) {
|
|
238
|
+
throw new Error("视图名不能为空");
|
|
239
|
+
}
|
|
240
|
+
if (!supportsViews()) {
|
|
241
|
+
throw new Error("视图功能仅支持 SQL Server 数据库");
|
|
242
|
+
}
|
|
243
|
+
// 检查视图是否存在
|
|
244
|
+
if (!(await checkObjectExists(viewName, 'view'))) {
|
|
245
|
+
// 错误消息不直接包含用户输入,防止日志注入
|
|
246
|
+
throw new Error("指定的视图不存在");
|
|
247
|
+
}
|
|
248
|
+
// 使用相同的 describe 查询获取视图列结构
|
|
249
|
+
const descQuery = getDescribeTableQuery(viewName);
|
|
250
|
+
const columns = await dbAll(descQuery);
|
|
251
|
+
return formatSuccessResponse({
|
|
252
|
+
name: viewName,
|
|
253
|
+
type: 'view',
|
|
254
|
+
columns: formatColumns(columns)
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
throw new Error(`描述视图结构失败: ${error.message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 获取视图的定义 SQL
|
|
263
|
+
* 仅支持 SQL Server
|
|
264
|
+
* 注意: 使用 WITH ENCRYPTION 创建的视图无法获取定义
|
|
265
|
+
* @param viewName 视图名
|
|
266
|
+
* @returns 视图定义 SQL
|
|
267
|
+
*/
|
|
268
|
+
export async function getViewDefinition(viewName) {
|
|
269
|
+
try {
|
|
270
|
+
if (!viewName) {
|
|
271
|
+
throw new Error("视图名不能为空");
|
|
272
|
+
}
|
|
273
|
+
if (!supportsViews()) {
|
|
274
|
+
throw new Error("视图功能仅支持 SQL Server 数据库");
|
|
275
|
+
}
|
|
276
|
+
// 检查视图是否存在
|
|
277
|
+
if (!(await checkObjectExists(viewName, 'view'))) {
|
|
278
|
+
// 错误消息不直接包含用户输入,防止日志注入
|
|
279
|
+
throw new Error("指定的视图不存在");
|
|
280
|
+
}
|
|
281
|
+
// 获取视图定义
|
|
282
|
+
const defQuery = getViewDefinitionQuery(viewName);
|
|
283
|
+
const result = await dbAll(defQuery);
|
|
284
|
+
if (result.length === 0 || !result[0].definition) {
|
|
285
|
+
return formatSuccessResponse({
|
|
286
|
+
name: viewName,
|
|
287
|
+
definition: null,
|
|
288
|
+
message: "视图定义不可用(可能使用 WITH ENCRYPTION 创建)"
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return formatSuccessResponse({
|
|
292
|
+
name: viewName,
|
|
293
|
+
definition: result[0].definition
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
throw new Error(`获取视图定义失败: ${error.message}`);
|
|
298
|
+
}
|
|
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.3.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": {
|