@cmd233/mcp-database-server 1.1.7 → 1.2.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.
@@ -1,15 +1,93 @@
1
- import { dbAll, dbExec, getListTablesQuery, getDescribeTableQuery } from '../db/index.js';
1
+ import { dbAll, dbExec, getListTablesQuery, getDescribeTableQuery, getListViewsQuery, getViewDefinitionQuery, supportsViews } 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 columns 原始列数据数组
25
+ * @returns 格式化后的列数组
26
+ */
27
+ function formatColumns(columns) {
28
+ return columns.map((col) => ({
29
+ name: col.name,
30
+ type: col.type,
31
+ notnull: !!col.notnull,
32
+ default_value: col.dflt_value,
33
+ primary_key: !!col.pk,
34
+ comment: col.comment || null
35
+ }));
36
+ }
37
+ /**
38
+ * 从 SQL 语句中提取表名
39
+ * @param query SQL 语句
40
+ * @param operation SQL 操作类型(CREATE TABLE、ALTER TABLE 等)
41
+ * @returns 提取的表名或 null
42
+ */
43
+ function extractTableName(query, operation) {
44
+ try {
45
+ const normalizedQuery = query.trim().replace(/\s+/g, ' ');
46
+ const operationPrefix = operation.toLowerCase();
47
+ if (!normalizedQuery.toLowerCase().startsWith(operationPrefix)) {
48
+ return null;
49
+ }
50
+ // 移除操作前缀后的剩余部分
51
+ const afterOperation = normalizedQuery.substring(operationPrefix.length).trim();
52
+ // 处理 IF NOT EXISTS 或 IF EXISTS 等子句
53
+ const patterns = [
54
+ /^if\s+not\s+exists\s+([^\s(]+)/i, // CREATE TABLE IF NOT EXISTS tablename
55
+ /^if\s+exists\s+([^\s(]+)/i, // DROP TABLE IF EXISTS tablename
56
+ /^([^\s(]+)/ // CREATE TABLE tablename
57
+ ];
58
+ for (const pattern of patterns) {
59
+ const match = afterOperation.match(pattern);
60
+ if (match && match[1]) {
61
+ // 移除引号(如果有)
62
+ return match[1].replace(/^[`"[]|[`"\]]$/g, '');
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
3
71
  /**
4
72
  * 在数据库中创建新表
5
73
  * @param query CREATE TABLE SQL 语句
74
+ * @param confirm 安全确认标志(默认 false,防止误操作)
6
75
  * @returns 操作结果
7
76
  */
8
- export async function createTable(query) {
77
+ export async function createTable(query, confirm = false) {
9
78
  try {
10
79
  if (!query.trim().toLowerCase().startsWith("create table")) {
11
80
  throw new Error("只允许执行 CREATE TABLE 语句");
12
81
  }
82
+ // 确认检查:防止误操作
83
+ if (!confirm) {
84
+ const tableName = extractTableName(query, "CREATE TABLE");
85
+ const tableInfo = tableName ? ` '${tableName}'` : '';
86
+ return formatSuccessResponse({
87
+ success: false,
88
+ message: `需要安全确认。设置 confirm=true 以继续创建表${tableInfo}。`
89
+ });
90
+ }
13
91
  await dbExec(query);
14
92
  return formatSuccessResponse({ success: true, message: "表创建成功" });
15
93
  }
@@ -20,13 +98,23 @@ export async function createTable(query) {
20
98
  /**
21
99
  * 修改现有表的结构
22
100
  * @param query ALTER TABLE SQL 语句
101
+ * @param confirm 安全确认标志(默认 false,防止误操作)
23
102
  * @returns 操作结果
24
103
  */
25
- export async function alterTable(query) {
104
+ export async function alterTable(query, confirm = false) {
26
105
  try {
27
106
  if (!query.trim().toLowerCase().startsWith("alter table")) {
28
107
  throw new Error("只允许执行 ALTER TABLE 语句");
29
108
  }
109
+ // 确认检查:防止误操作
110
+ if (!confirm) {
111
+ const tableName = extractTableName(query, "ALTER TABLE");
112
+ const tableInfo = tableName ? ` '${tableName}'` : '';
113
+ return formatSuccessResponse({
114
+ success: false,
115
+ message: `需要安全确认。设置 confirm=true 以继续修改表结构${tableInfo}。`
116
+ });
117
+ }
30
118
  await dbExec(query);
31
119
  return formatSuccessResponse({ success: true, message: "表结构修改成功" });
32
120
  }
@@ -51,11 +139,8 @@ export async function dropTable(tableName, confirm) {
51
139
  message: "需要安全确认。设置 confirm=true 以继续删除表。"
52
140
  });
53
141
  }
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)) {
142
+ // 检查表是否存在
143
+ if (!(await checkObjectExists(tableName, 'table'))) {
59
144
  throw new Error(`表 '${tableName}' 不存在`);
60
145
  }
61
146
  // 删除表
@@ -71,14 +156,22 @@ export async function dropTable(tableName, confirm) {
71
156
  }
72
157
  /**
73
158
  * 列出数据库中的所有表
159
+ * @param includeViews 是否包含视图(默认 false)
74
160
  * @returns 表名数组
75
161
  */
76
- export async function listTables() {
162
+ export async function listTables(includeViews = false) {
77
163
  try {
78
164
  // 使用适配器特定的查询来列出表
79
165
  const query = getListTablesQuery();
80
166
  const tables = await dbAll(query);
81
- return formatSuccessResponse(tables.map((t) => t.name));
167
+ const result = tables.map((t) => ({ name: t.name, type: 'table' }));
168
+ // 如果需要包含视图且数据库支持视图
169
+ if (includeViews && supportsViews()) {
170
+ const viewsQuery = getListViewsQuery();
171
+ const views = await dbAll(viewsQuery);
172
+ result.push(...views.map((v) => ({ name: v.name, type: 'view' })));
173
+ }
174
+ return formatSuccessResponse(result);
82
175
  }
83
176
  catch (error) {
84
177
  throw new Error(`列出表失败: ${error.message}`);
@@ -86,34 +179,123 @@ export async function listTables() {
86
179
  }
87
180
  /**
88
181
  * 获取指定表的结构信息
89
- * @param tableName 要描述的表名
90
- * @returns 表的列定义
182
+ * 支持实体表和视图
183
+ * @param tableName 要描述的表名或视图名
184
+ * @returns 表/视图的列定义
91
185
  */
92
186
  export async function describeTable(tableName) {
93
187
  try {
94
188
  if (!tableName) {
95
189
  throw new Error("表名不能为空");
96
190
  }
97
- // 首先通过直接查询来检查表是否存在
98
- const query = getListTablesQuery();
99
- const tables = await dbAll(query);
100
- const tableNames = tables.map(t => t.name);
101
- if (!tableNames.includes(tableName)) {
102
- throw new Error(`Table '${tableName}' does not exist`);
191
+ // 检查是表还是视图
192
+ let objectType = 'table';
193
+ if (await checkObjectExists(tableName, 'table')) {
194
+ objectType = 'table';
103
195
  }
104
- // 使用适配器特定的查询来描述表结构
196
+ else if (supportsViews() && await checkObjectExists(tableName, 'view')) {
197
+ objectType = 'view';
198
+ }
199
+ else {
200
+ throw new Error(supportsViews() ? `表或视图 '${tableName}' 不存在` : `表 '${tableName}' 不存在`);
201
+ }
202
+ // 使用适配器特定的查询来描述表/视图结构
105
203
  const descQuery = getDescribeTableQuery(tableName);
106
204
  const columns = await dbAll(descQuery);
107
- return formatSuccessResponse(columns.map((col) => ({
108
- name: col.name,
109
- type: col.type,
110
- notnull: !!col.notnull,
111
- default_value: col.dflt_value,
112
- primary_key: !!col.pk,
113
- comment: col.comment || null
114
- })));
205
+ return formatSuccessResponse({
206
+ name: tableName,
207
+ type: objectType,
208
+ columns: formatColumns(columns)
209
+ });
115
210
  }
116
211
  catch (error) {
117
212
  throw new Error(`描述表结构失败: ${error.message}`);
118
213
  }
119
214
  }
215
+ /**
216
+ * 列出数据库中的所有视图
217
+ * 仅支持 SQL Server
218
+ * @returns 视图名数组
219
+ */
220
+ export async function listViews() {
221
+ try {
222
+ if (!supportsViews()) {
223
+ throw new Error("视图功能仅支持 SQL Server 数据库");
224
+ }
225
+ const query = getListViewsQuery();
226
+ const views = await dbAll(query);
227
+ return formatSuccessResponse(views.map((v) => v.name));
228
+ }
229
+ catch (error) {
230
+ throw new Error(`列出视图失败: ${error.message}`);
231
+ }
232
+ }
233
+ /**
234
+ * 获取指定视图的结构信息
235
+ * 仅支持 SQL Server
236
+ * @param viewName 视图名
237
+ * @returns 视图的列定义
238
+ */
239
+ export async function describeView(viewName) {
240
+ try {
241
+ if (!viewName) {
242
+ throw new Error("视图名不能为空");
243
+ }
244
+ if (!supportsViews()) {
245
+ throw new Error("视图功能仅支持 SQL Server 数据库");
246
+ }
247
+ // 检查视图是否存在
248
+ if (!(await checkObjectExists(viewName, 'view'))) {
249
+ throw new Error(`视图 '${viewName}' 不存在`);
250
+ }
251
+ // 使用相同的 describe 查询获取视图列结构
252
+ const descQuery = getDescribeTableQuery(viewName);
253
+ const columns = await dbAll(descQuery);
254
+ return formatSuccessResponse({
255
+ name: viewName,
256
+ type: 'view',
257
+ columns: formatColumns(columns)
258
+ });
259
+ }
260
+ catch (error) {
261
+ throw new Error(`描述视图结构失败: ${error.message}`);
262
+ }
263
+ }
264
+ /**
265
+ * 获取视图的定义 SQL
266
+ * 仅支持 SQL Server
267
+ * 注意: 使用 WITH ENCRYPTION 创建的视图无法获取定义
268
+ * @param viewName 视图名
269
+ * @returns 视图定义 SQL
270
+ */
271
+ export async function getViewDefinition(viewName) {
272
+ try {
273
+ if (!viewName) {
274
+ throw new Error("视图名不能为空");
275
+ }
276
+ if (!supportsViews()) {
277
+ throw new Error("视图功能仅支持 SQL Server 数据库");
278
+ }
279
+ // 检查视图是否存在
280
+ if (!(await checkObjectExists(viewName, 'view'))) {
281
+ throw new Error(`视图 '${viewName}' 不存在`);
282
+ }
283
+ // 获取视图定义
284
+ const defQuery = getViewDefinitionQuery(viewName);
285
+ const result = await dbAll(defQuery);
286
+ if (result.length === 0 || !result[0].definition) {
287
+ return formatSuccessResponse({
288
+ name: viewName,
289
+ definition: null,
290
+ message: "视图定义不可用(可能使用 WITH ENCRYPTION 创建)"
291
+ });
292
+ }
293
+ return formatSuccessResponse({
294
+ name: viewName,
295
+ definition: result[0].definition
296
+ });
297
+ }
298
+ catch (error) {
299
+ throw new Error(`获取视图定义失败: ${error.message}`);
300
+ }
301
+ }
@@ -0,0 +1,119 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmd233/mcp-database-server",
3
- "version": "1.1.7",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for interacting with SQLite, SQL Server, PostgreSQL and MySQL databases (Fixed nullable field detection)",
5
5
  "license": "MIT",
6
6
  "author": "cmd233",