@cmd233/mcp-database-server 1.2.0 → 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.
@@ -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 或 DELETE 查询 | `query`: SQL 修改语句 |
289
- | `create_table` | 在数据库中创建新表 | `query`: CREATE TABLE 语句 |
290
- | `alter_table` | 修改现有表架构 | `query`: ALTER TABLE 语句 |
290
+ | `write_query` | 执行 INSERT、UPDATE、DELETETRUNCATE 查询 | `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
 
@@ -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
- return "SELECT 1 as name WHERE 1=0";
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 = '${tableName}' AND s.name = c.TABLE_SCHEMA
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 = '${tableName}'
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
- return `SELECT VIEW_DEFINITION as definition FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '${viewName}'`;
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, DELETE, or TRUNCATE queries to modify database data. " +
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/TRUNCATE query to execute"
61
+ description: "The SQL INSERT/UPDATE/DELETE query to execute"
62
62
  },
63
63
  confirm: {
64
64
  type: "boolean",
@@ -169,9 +169,9 @@ export function handleListTools() {
169
169
  name: "drop_table",
170
170
  title: "Drop Table",
171
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.",
172
+ "This operation has been DISABLED for security reasons. " +
173
+ "DROP operations should be handled by DBA at the database level. " +
174
+ "Contact your database administrator if you need to delete a table.",
175
175
  inputSchema: {
176
176
  type: "object",
177
177
  properties: {
@@ -505,6 +505,105 @@ export function handleListTools() {
505
505
  idempotentHint: true
506
506
  }
507
507
  },
508
+ {
509
+ name: "list_procedures",
510
+ title: "List Procedures",
511
+ description: "Retrieve a list of all stored procedure names in the current database. " +
512
+ "Only works with SQL Server databases. " +
513
+ "Returns only procedure names without parameter details. " +
514
+ "Use describe_procedure to get detailed parameter information for a specific procedure.",
515
+ inputSchema: {
516
+ type: "object",
517
+ properties: {},
518
+ },
519
+ outputSchema: {
520
+ type: "object",
521
+ properties: {
522
+ procedures: {
523
+ type: "array",
524
+ items: { type: "string" },
525
+ description: "Array of stored procedure names in the database"
526
+ }
527
+ }
528
+ },
529
+ annotations: {
530
+ readOnlyHint: true,
531
+ idempotentHint: true
532
+ }
533
+ },
534
+ {
535
+ name: "describe_procedure",
536
+ title: "Describe Procedure",
537
+ description: "Get detailed parameter information about a specific stored procedure. " +
538
+ "Only works with SQL Server databases. " +
539
+ "Returns parameter name, data type, direction (IN/OUT/INOUT), and default value. " +
540
+ "The procedure must exist in the database.",
541
+ inputSchema: {
542
+ type: "object",
543
+ properties: {
544
+ procedure_name: {
545
+ type: "string",
546
+ description: "Name of the stored procedure to describe"
547
+ },
548
+ },
549
+ required: ["procedure_name"],
550
+ },
551
+ outputSchema: {
552
+ type: "object",
553
+ properties: {
554
+ name: { type: "string", description: "Procedure name" },
555
+ type: { type: "string", description: "Always 'procedure'" },
556
+ parameters: {
557
+ type: "array",
558
+ description: "Array of parameter definitions",
559
+ items: {
560
+ type: "object",
561
+ properties: {
562
+ name: { type: "string", description: "Parameter name" },
563
+ type: { type: "string", description: "Data type" },
564
+ direction: { type: "string", enum: ["IN", "OUT", "INOUT"], description: "Parameter direction" },
565
+ default_value: { type: "string", description: "Default value" },
566
+ is_output: { type: "boolean", description: "Whether this is an output parameter" }
567
+ }
568
+ }
569
+ }
570
+ }
571
+ },
572
+ annotations: {
573
+ readOnlyHint: true,
574
+ idempotentHint: true
575
+ }
576
+ },
577
+ {
578
+ name: "get_procedure_definition",
579
+ title: "Get Procedure Definition",
580
+ description: "Retrieve the SQL definition (CREATE PROCEDURE statement) of a specific stored procedure. " +
581
+ "Only works with SQL Server databases. " +
582
+ "Returns the complete CREATE PROCEDURE SQL statement. " +
583
+ "Note: Procedures created WITH ENCRYPTION cannot have their definition retrieved.",
584
+ inputSchema: {
585
+ type: "object",
586
+ properties: {
587
+ procedure_name: {
588
+ type: "string",
589
+ description: "Name of the stored procedure to get definition for"
590
+ },
591
+ },
592
+ required: ["procedure_name"],
593
+ },
594
+ outputSchema: {
595
+ type: "object",
596
+ properties: {
597
+ name: { type: "string", description: "Procedure name" },
598
+ definition: { type: "string", description: "CREATE PROCEDURE SQL statement" },
599
+ message: { type: "string", description: "Additional information if definition is unavailable" }
600
+ }
601
+ },
602
+ annotations: {
603
+ readOnlyHint: true,
604
+ idempotentHint: true
605
+ }
606
+ },
508
607
  ],
509
608
  };
510
609
  }
@@ -539,6 +638,12 @@ export async function handleToolCall(name, args) {
539
638
  return await describeView(args.view_name);
540
639
  case "get_view_definition":
541
640
  return await getViewDefinition(args.view_name);
641
+ case "list_procedures":
642
+ return await listProcedures();
643
+ case "describe_procedure":
644
+ return await describeProcedure(args.procedure_name);
645
+ case "get_procedure_definition":
646
+ return await getProcedureDefinition(args.procedure_name);
542
647
  case "append_insight":
543
648
  return await appendInsight(args.insight, args.confirm);
544
649
  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 和 TRUNCATE 操作
33
- const supportedOperations = ["insert", "update", "delete", "truncate"];
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、DELETETRUNCATE 操作");
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
- try {
133
- if (!tableName) {
134
- throw new Error("表名不能为空");
135
- }
136
- if (!confirm) {
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
- throw new Error(supportsViews() ? `表或视图 '${tableName}' 不存在` : `表 '${tableName}' 不存在`);
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
- throw new Error(`视图 '${viewName}' 不存在`);
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
- throw new Error(`视图 '${viewName}' 不存在`);
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.2.0",
4
- "description": "MCP server for interacting with SQLite, SQL Server, PostgreSQL and MySQL databases (Fixed nullable field detection)",
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": {
@@ -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
- }