@dbx-app/mcp-server 0.4.3 → 0.4.5

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 CHANGED
@@ -92,7 +92,7 @@ See the [DBX CLI README](../cli/README.md) for command details.
92
92
 
93
93
  ## SQL Safety
94
94
 
95
- `dbx_execute_query` is read-only by default. To allow write statements such as `INSERT` or `UPDATE`, set:
95
+ `dbx_execute_query` accepts multiple SQL statements and executes them one at a time after checking each statement. It is read-only by default. To allow write statements such as `INSERT` or `UPDATE`, set:
96
96
 
97
97
  ```bash
98
98
  DBX_MCP_ALLOW_WRITES=1
@@ -127,7 +127,7 @@ PostgreSQL, MySQL, SQLite, Doris, StarRocks, and Redshift queries run directly f
127
127
  ## Requirements
128
128
 
129
129
  - [DBX](https://github.com/t8y2/dbx) installed with at least one connection configured
130
- - Node.js 18+
130
+ - Node.js 22.13.0 或更高版本
131
131
 
132
132
  ## License
133
133
 
@@ -213,7 +213,7 @@ dbx query local "select 1" --json
213
213
 
214
214
  ### SQL 安全
215
215
 
216
- `dbx_execute_query` 默认只读。若要允许 `INSERT`、`UPDATE` 等写操作,设置:
216
+ `dbx_execute_query` 支持多条 SQL 语句,会逐条完成安全检查并依次执行。默认只读。若要允许 `INSERT`、`UPDATE` 等写操作,设置:
217
217
 
218
218
  ```bash
219
219
  DBX_MCP_ALLOW_WRITES=1
@@ -242,4 +242,4 @@ PostgreSQL、MySQL、SQLite、Doris、StarRocks、Redshift 查询可由 MCP Serv
242
242
  ### 系统要求
243
243
 
244
244
  - 已安装 [DBX](https://github.com/t8y2/dbx) 并配置了至少一个数据库连接
245
- - Node.js 18+
245
+ - Node.js 22.13.0 or newer
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { type Backend } from "@dbx-app/node-core";
4
+ export declare const DBX_MCP_PACKAGE_VERSION: string;
5
+ export declare const DBX_CONNECTION_TYPE_DESCRIPTION = "Database type: postgres, mysql, sqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, doris, starrocks, redshift, dameng, kingbase, highgo, vastbase, goldendb, gaussdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, gbase, h2, snowflake, trino, hive, db2, informix, neo4j, cassandra, bigquery, kylin, sundb, tdengine, xugu, jdbc, access";
4
6
  export declare function createDbxMcpServer(backend: Backend, options?: {
5
7
  isWebMode?: boolean;
6
8
  }): McpServer;
package/dist/index.js CHANGED
@@ -1,24 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { pathToFileURL } from "node:url";
4
+ import { createRequire } from "node:module";
5
5
  import { z } from "zod";
6
- import { buildSchemaContext, createBackend, evaluateSqlSafety, formatSchemaContext, notifyReload, postBridge, sqlSafetyFromEnv, } from "@dbx-app/node-core";
6
+ import { buildSchemaContext, createBackend, evaluateMongoAggregateSafety, evaluateSqlSafety, formatCell, formatSchemaContext, isMainModule, mdTable, notifyReload, parseMongoAggregateCommand, postBridge, sqlSafetyFromEnv, splitSqlStatements, } from "@dbx-app/node-core";
7
+ const require = createRequire(import.meta.url);
8
+ const packageJson = require("../package.json");
9
+ export const DBX_MCP_PACKAGE_VERSION = packageJson.version ?? "0.0.0";
7
10
  function text(s) {
8
11
  return { content: [{ type: "text", text: s }] };
9
12
  }
10
- function mdTable(headers, rows) {
11
- const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length), 3));
12
- const header = `| ${headers.map((h, i) => h.padEnd(widths[i])).join(" | ")} |`;
13
- const sep = `| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`;
14
- const body = rows.map((r) => `| ${r.map((c, i) => (c || "").padEnd(widths[i])).join(" | ")} |`).join("\n");
15
- return `${header}\n${sep}\n${body}`;
13
+ function toolError(code, message) {
14
+ return { ...text(`${code}: ${message}`), isError: true };
16
15
  }
16
+ function withDatabase(config, database) {
17
+ return database === undefined ? config : { ...config, database };
18
+ }
19
+ function formatQueryToolResult(result, title) {
20
+ const prefix = title ? `${title}\n` : "";
21
+ if (result.columns.length === 0)
22
+ return text(`${prefix}Query executed. ${result.row_count} row(s) affected.`);
23
+ const rows = result.rows.map((r) => result.columns.map((c) => formatCell(r[c])));
24
+ return text(`${prefix}${mdTable(result.columns, rows)}\n\n${result.row_count} row(s)`);
25
+ }
26
+ export const DBX_CONNECTION_TYPE_DESCRIPTION = "Database type: postgres, mysql, sqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, doris, starrocks, redshift, dameng, kingbase, highgo, vastbase, goldendb, gaussdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, gbase, h2, snowflake, trino, hive, db2, informix, neo4j, cassandra, bigquery, kylin, sundb, tdengine, xugu, jdbc, access";
17
27
  export function createDbxMcpServer(backend, options = {}) {
18
28
  const isWebMode = options.isWebMode ?? !!process.env.DBX_WEB_URL;
19
29
  const server = new McpServer({
20
30
  name: "dbx",
21
- version: "0.4.2",
31
+ version: DBX_MCP_PACKAGE_VERSION,
22
32
  });
23
33
  server.tool("dbx_list_connections", "List all database connections configured in DBX", {}, async () => {
24
34
  const connections = await backend.loadConnections();
@@ -29,12 +39,13 @@ export function createDbxMcpServer(backend, options = {}) {
29
39
  });
30
40
  server.tool("dbx_list_tables", "List tables and views for a database connection", {
31
41
  connection_name: z.string().describe("Name of the DBX connection"),
42
+ database: z.string().optional().describe("Database name"),
32
43
  schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
33
- }, async ({ connection_name, schema }) => {
44
+ }, async ({ connection_name, database, schema }) => {
34
45
  const config = await backend.findConnection(connection_name);
35
46
  if (!config)
36
- return text(`Connection "${connection_name}" not found`);
37
- const tables = await backend.listTables(config, schema);
47
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
48
+ const tables = await backend.listTables(withDatabase(config, database), schema);
38
49
  if (tables.length === 0)
39
50
  return text("No tables found.");
40
51
  const rows = tables.map((t) => [t.name, t.type]);
@@ -43,12 +54,13 @@ export function createDbxMcpServer(backend, options = {}) {
43
54
  server.tool("dbx_describe_table", "Get column definitions for a table", {
44
55
  connection_name: z.string().describe("Name of the DBX connection"),
45
56
  table: z.string().describe("Table name"),
57
+ database: z.string().optional().describe("Database name"),
46
58
  schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
47
- }, async ({ connection_name, table, schema }) => {
59
+ }, async ({ connection_name, table, database, schema }) => {
48
60
  const config = await backend.findConnection(connection_name);
49
61
  if (!config)
50
- return text(`Connection "${connection_name}" not found`);
51
- const columns = await backend.describeTable(config, table, schema);
62
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
63
+ const columns = await backend.describeTable(withDatabase(config, database), table, schema);
52
64
  if (columns.length === 0)
53
65
  return text("No columns found.");
54
66
  const rows = columns.map((c) => [
@@ -62,47 +74,58 @@ export function createDbxMcpServer(backend, options = {}) {
62
74
  });
63
75
  server.tool("dbx_execute_query", "Execute a SQL query on a database connection (max 100 rows returned)", {
64
76
  connection_name: z.string().describe("Name of the DBX connection"),
77
+ database: z.string().optional().describe("Database name"),
65
78
  sql: z.string().describe("SQL query to execute"),
66
- }, async ({ connection_name, sql }) => {
79
+ }, async ({ connection_name, database, sql }) => {
67
80
  const config = await backend.findConnection(connection_name);
68
81
  if (!config)
69
- return text(`Connection "${connection_name}" not found`);
70
- const safety = evaluateSqlSafety(sql, sqlSafetyFromEnv());
71
- if (!safety.allowed)
72
- return text(`Query blocked: ${safety.reason}`);
82
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
83
+ if (config.db_type !== "mongodb") {
84
+ const safety = evaluateSqlSafety(sql, { ...sqlSafetyFromEnv(), allowMultipleStatements: true });
85
+ if (!safety.allowed)
86
+ return toolError("SQL_BLOCKED", safety.reason ?? "SQL blocked.");
87
+ }
88
+ // MongoDB shell commands don't fit the SQL safety evaluator; the backend
89
+ // (node-core executeQuery) applies command-aware read/write gating.
73
90
  try {
74
- const result = await backend.executeQuery(config, sql);
75
- if (result.columns.length === 0)
76
- return text(`Query executed. ${result.row_count} row(s) affected.`);
77
- const rows = result.rows.map((r) => result.columns.map((c) => formatCell(r[c])));
78
- return text(`${mdTable(result.columns, rows)}\n\n${result.row_count} row(s)`);
91
+ const statements = config.db_type === "mongodb" ? [sql] : splitSqlStatements(sql);
92
+ const results = [];
93
+ for (const statement of statements) {
94
+ results.push(await backend.executeQuery(withDatabase(config, database), statement));
95
+ }
96
+ if (results.length === 1)
97
+ return formatQueryToolResult(results[0]);
98
+ return text(results.map((result, index) => formatQueryToolResult(result, `Statement ${index + 1}`).content[0].text).join("\n\n"));
79
99
  }
80
100
  catch (e) {
81
101
  const msg = e instanceof Error ? e.message : String(e);
82
- return text(`Query error: ${msg}`);
102
+ return toolError("QUERY_ERROR", msg);
83
103
  }
84
104
  });
85
105
  server.tool("dbx_get_schema_context", "Get compact table and column context for writing SQL", {
86
106
  connection_name: z.string().describe("Name of the DBX connection"),
107
+ database: z.string().optional().describe("Database name"),
87
108
  schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
88
109
  tables: z.array(z.string()).optional().describe("Specific table names to include"),
89
110
  max_tables: z.number().int().min(1).max(20).default(8).describe("Maximum number of tables to include"),
90
- }, async ({ connection_name, schema, tables, max_tables }) => {
111
+ }, async ({ connection_name, database, schema, tables, max_tables }) => {
91
112
  const config = await backend.findConnection(connection_name);
92
113
  if (!config)
93
- return text(`Connection "${connection_name}" not found`);
94
- const context = await buildSchemaContext(backend, config, { schema, tables, maxTables: max_tables });
114
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
115
+ const context = await buildSchemaContext(backend, withDatabase(config, database), {
116
+ schema,
117
+ tables,
118
+ maxTables: max_tables,
119
+ });
95
120
  if (context.tables.length === 0)
96
121
  return text("No matching tables found.");
97
122
  return text(formatSchemaContext(context));
98
123
  });
99
124
  server.tool("dbx_add_connection", "Add a new database connection to DBX", {
100
125
  name: z.string().describe("Connection name"),
101
- db_type: z
102
- .string()
103
- .describe("Database type: postgres, mysql, sqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, doris, starrocks, redshift, dameng, kingbase, highgo, vastbase, goldendb, gaussdb, h2, snowflake, trino, hive, db2, informix, neo4j, cassandra, bigquery, kylin, sundb, tdengine, jdbc, access"),
126
+ db_type: z.string().describe(DBX_CONNECTION_TYPE_DESCRIPTION),
104
127
  host: z.string().describe("Database host"),
105
- port: z.number().optional().describe("Database port (TDengine defaults to 6041)"),
128
+ port: z.number().optional().describe("Database port (TDengine defaults to 6041, XuguDB defaults to 5138)"),
106
129
  username: z.string().default("").describe("Username"),
107
130
  password: z.string().default("").describe("Password"),
108
131
  database: z.string().optional().describe("Default database name"),
@@ -112,7 +135,8 @@ export function createDbxMcpServer(backend, options = {}) {
112
135
  if (existing)
113
136
  return text(`Connection "${name}" already exists.`);
114
137
  const FILE_BASED_TYPES = new Set(["sqlite", "duckdb", "access"]);
115
- const resolvedPort = port ?? (db_type === "tdengine" ? 6041 : FILE_BASED_TYPES.has(db_type) ? 0 : undefined);
138
+ const DEFAULT_PORTS = { tdengine: 6041, xugu: 5138 };
139
+ const resolvedPort = port ?? DEFAULT_PORTS[db_type] ?? (FILE_BASED_TYPES.has(db_type) ? 0 : undefined);
116
140
  if (resolvedPort === undefined)
117
141
  return text("Port is required for this database type.");
118
142
  const config = await backend.addConnection({
@@ -134,7 +158,7 @@ export function createDbxMcpServer(backend, options = {}) {
134
158
  }, async ({ connection_name }) => {
135
159
  const removed = await backend.removeConnection(connection_name);
136
160
  if (!removed)
137
- return text(`Connection "${connection_name}" not found.`);
161
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
138
162
  await notifyReload();
139
163
  return text(`Connection "${connection_name}" removed.`);
140
164
  });
@@ -146,6 +170,9 @@ export function createDbxMcpServer(backend, options = {}) {
146
170
  database: z.string().optional().describe("Database name"),
147
171
  schema: z.string().optional().describe("Schema name"),
148
172
  }, async ({ connection_name, table, database, schema }) => {
173
+ const config = await backend.findConnection(connection_name);
174
+ if (!config)
175
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
149
176
  return bridgeRequest("/open-table", { connection_name, table, database, schema }, `Opened ${table} in DBX`);
150
177
  });
151
178
  server.tool("dbx_execute_and_show", "Execute a SQL query in DBX desktop app UI and show results there. Requires DBX to be running.", {
@@ -153,26 +180,42 @@ export function createDbxMcpServer(backend, options = {}) {
153
180
  sql: z.string().describe("SQL query to execute"),
154
181
  database: z.string().optional().describe("Database name"),
155
182
  }, async ({ connection_name, sql, database }) => {
156
- const safety = evaluateSqlSafety(sql, sqlSafetyFromEnv());
157
- if (!safety.allowed)
158
- return text(`Query blocked: ${safety.reason}`);
159
- return bridgeRequest("/execute-query", { connection_name, sql, database }, "Query sent to DBX");
183
+ const config = await backend.findConnection(connection_name);
184
+ if (!config)
185
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
186
+ const safetyOptions = sqlSafetyFromEnv();
187
+ if (config?.db_type === "mongodb") {
188
+ const aggregate = parseMongoAggregateCommand(sql);
189
+ if (aggregate) {
190
+ const safety = evaluateMongoAggregateSafety(aggregate, safetyOptions);
191
+ if (!safety.allowed)
192
+ return toolError("SQL_BLOCKED", safety.reason ?? "Query blocked.");
193
+ }
194
+ }
195
+ else {
196
+ const safety = evaluateSqlSafety(sql, { ...safetyOptions, allowMultipleStatements: true });
197
+ if (!safety.allowed)
198
+ return toolError("SQL_BLOCKED", safety.reason ?? "SQL blocked.");
199
+ }
200
+ // MongoDB shell commands bypass the SQL safety evaluator; pass MCP
201
+ // safety flags to the desktop executor for command-aware gating.
202
+ return bridgeRequest("/execute-query", {
203
+ connection_name,
204
+ sql,
205
+ database,
206
+ allow_writes: safetyOptions.allowWrites,
207
+ allow_dangerous: safetyOptions.allowDangerous,
208
+ }, "Query sent to DBX");
160
209
  });
161
210
  }
162
211
  return server;
163
212
  }
164
- function formatCell(value) {
165
- if (value === null || value === undefined)
166
- return "NULL";
167
- if (typeof value === "object")
168
- return JSON.stringify(value);
169
- return String(value);
170
- }
171
213
  async function bridgeRequest(path, body, successMsg) {
172
214
  const res = await postBridge(path, body);
173
215
  if (res.ok)
174
216
  return text(successMsg);
175
- return text(res.text.startsWith("DBX is not running") ? res.text : `Failed: ${res.text}`);
217
+ const message = res.text.startsWith("DBX is not running") ? res.text : `Failed: ${res.text}`;
218
+ return toolError("DBX_NOT_RUNNING", message);
176
219
  }
177
220
  async function main() {
178
221
  const backend = await createBackend();
@@ -180,7 +223,7 @@ async function main() {
180
223
  const transport = new StdioServerTransport();
181
224
  await server.connect(transport);
182
225
  }
183
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
226
+ if (isMainModule(import.meta.url, process.argv[1])) {
184
227
  main().catch((e) => {
185
228
  console.error("MCP Server failed to start:", e);
186
229
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbx-app/mcp-server",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "mcpName": "io.github.t8y2/dbx",
5
5
  "description": "MCP server for DBX — query databases from Claude Code, Cursor, and other AI agents",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@modelcontextprotocol/sdk": "^1.12.1",
36
36
  "zod": "^3.25.20",
37
- "@dbx-app/node-core": "^0.4.3"
37
+ "@dbx-app/node-core": "^0.4.5"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.15.21",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/t8y2/dbx",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.4.3",
9
+ "version": "0.4.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@dbx-app/mcp-server",
14
- "version": "0.4.3",
14
+ "version": "0.4.5",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }