@dbx-app/mcp-server 0.4.32 → 0.4.36

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.
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import { createRequire } from "node:module";
6
+ import { dirname, join } from "node:path";
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const platformPackages = {
10
+ "darwin-arm64": ["@dbx-app/mcp-darwin-arm64", "dbx-mcp"],
11
+ "darwin-x64": ["@dbx-app/mcp-darwin-x64", "dbx-mcp"],
12
+ "linux-arm64": ["@dbx-app/mcp-linux-arm64-gnu", "dbx-mcp"],
13
+ "linux-x64": ["@dbx-app/mcp-linux-x64-gnu", "dbx-mcp"],
14
+ "win32-arm64": ["@dbx-app/mcp-win32-arm64", "dbx-mcp.exe"],
15
+ "win32-x64": ["@dbx-app/mcp-win32-x64", "dbx-mcp.exe"],
16
+ };
17
+
18
+ function resolveBinary() {
19
+ if (process.env.DBX_MCP_BINARY) {
20
+ return process.env.DBX_MCP_BINARY;
21
+ }
22
+ const platform = `${process.platform}-${process.arch}`;
23
+ const target = platformPackages[platform];
24
+ if (!target) {
25
+ throw new Error(`DBX MCP does not provide a Rust binary for ${platform}.`);
26
+ }
27
+ const [packageName, binaryName] = target;
28
+ let manifest;
29
+ try {
30
+ manifest = require.resolve(`${packageName}/package.json`);
31
+ } catch {
32
+ throw new Error(
33
+ `The optional package ${packageName} was not installed. Reinstall @dbx-app/mcp-server without --no-optional.`,
34
+ );
35
+ }
36
+ const binary = join(dirname(manifest), "bin", binaryName);
37
+ if (!existsSync(binary)) {
38
+ throw new Error(`The DBX MCP binary is missing from ${packageName}.`);
39
+ }
40
+ return binary;
41
+ }
42
+
43
+ try {
44
+ if (process.argv[2] === "--verify-platform") {
45
+ const platform = `${process.platform}-${process.arch}`;
46
+ if (!platformPackages[platform]) {
47
+ throw new Error(`DBX MCP does not provide a Rust binary for ${platform}.`);
48
+ }
49
+ process.exit(0);
50
+ }
51
+ const binary = resolveBinary();
52
+ const child = spawn(binary, process.argv.slice(2), { stdio: "inherit", env: process.env });
53
+ for (const signal of ["SIGINT", "SIGTERM"]) {
54
+ process.on(signal, () => child.kill(signal));
55
+ }
56
+ child.on("error", (error) => {
57
+ console.error(`Failed to start DBX MCP: ${error.message}`);
58
+ process.exit(1);
59
+ });
60
+ child.on("exit", (code, signal) => {
61
+ if (signal) process.kill(process.pid, signal);
62
+ else process.exit(code ?? 1);
63
+ });
64
+ } catch (error) {
65
+ console.error(error instanceof Error ? error.message : String(error));
66
+ process.exit(1);
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbx-app/mcp-server",
3
- "version": "0.4.32",
3
+ "version": "0.4.36",
4
4
  "description": "MCP server for DBX — query databases from Claude Code, Cursor, and other AI agents",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -19,31 +19,39 @@
19
19
  "directory": "packages/mcp-server"
20
20
  },
21
21
  "bin": {
22
- "dbx-mcp-server": "dist/index.js",
23
- "mcp-server": "dist/index.js"
22
+ "dbx-mcp-server": "bin/dbx-mcp-server.js",
23
+ "mcp-server": "bin/dbx-mcp-server.js"
24
24
  },
25
25
  "files": [
26
- "dist",
26
+ "bin",
27
27
  "server.json"
28
28
  ],
29
29
  "type": "module",
30
- "dependencies": {
31
- "@modelcontextprotocol/sdk": "^1.12.1",
32
- "zod": "^3.25.20",
33
- "@dbx-app/node-core": "^0.4.32"
30
+ "optionalDependencies": {
31
+ "@dbx-app/mcp-darwin-arm64": "0.4.36",
32
+ "@dbx-app/mcp-darwin-x64": "0.4.36",
33
+ "@dbx-app/mcp-linux-arm64-gnu": "0.4.36",
34
+ "@dbx-app/mcp-linux-x64-gnu": "0.4.36",
35
+ "@dbx-app/mcp-win32-arm64": "0.4.36",
36
+ "@dbx-app/mcp-win32-x64": "0.4.36"
34
37
  },
35
38
  "devDependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.12.1",
36
40
  "@types/node": "^22.15.21",
37
41
  "tsx": "^4.19.4",
38
- "typescript": "^5.8.3"
42
+ "typescript": "^5.8.3",
43
+ "vitest": "^4.1.8",
44
+ "zod": "^3.25.20",
45
+ "@dbx-app/node-core": "^0.4.36"
39
46
  },
40
47
  "engines": {
41
- "node": ">=22.13.0"
48
+ "node": ">=18.18.0"
42
49
  },
43
50
  "mcpName": "io.github.t8y2/dbx",
44
51
  "scripts": {
45
- "start": "tsx src/index.ts",
46
- "test": "pnpm --filter @dbx-app/mcp-server build && vitest run --config vitest.config.ts",
47
- "build": "pnpm --filter @dbx-app/node-core build && tsc"
52
+ "start": "cargo run -p dbx-mcp --",
53
+ "test": "cargo build -p dbx-mcp && pnpm build:legacy && vitest run --config vitest.config.ts",
54
+ "build": "pnpm build:legacy",
55
+ "build:legacy": "pnpm --filter @dbx-app/node-core build && tsc"
48
56
  }
49
57
  }
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.32",
9
+ "version": "0.4.36",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@dbx-app/mcp-server",
14
- "version": "0.4.32",
14
+ "version": "0.4.36",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }
package/dist/index.d.ts DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
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, rqlite, cloudflare-d1, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, weaviate, chromadb, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, prestosql, hive, spark, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, oscar, tdengine, iotdb, xugu, zookeeper, jdbc, access, mq";
6
- export declare function createDbxMcpServer(backend: Backend, options?: {
7
- isWebMode?: boolean;
8
- }): McpServer;
package/dist/index.js DELETED
@@ -1,467 +0,0 @@
1
- #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { createRequire } from "node:module";
5
- import { z } from "zod";
6
- import { buildSchemaContext, createBackend, evaluateRedisCommandSafety, evaluateMongoAggregateSafety, evaluateSqlSafety, formatCell, formatSchemaContext, isMainModule, mdTable, notifyReload, parseMongoAggregateCommand, assessProductionSql, isLikelyMongoMutation, isProductionDatabase, postBridge, logSqlDiagnostic, sqlSafetyFromEnv, splitSqlStatements, supportsHashLineComments, } 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";
10
- function text(s) {
11
- return { content: [{ type: "text", text: s }] };
12
- }
13
- function toolError(code, message) {
14
- return { ...text(`${code}: ${message}`), isError: true };
15
- }
16
- function withDatabase(config, database) {
17
- return database === undefined ? config : { ...config, database };
18
- }
19
- function metadataScope(config, database, schema) {
20
- if (config.db_type !== "dameng") {
21
- return { config: withDatabase(config, database), schema };
22
- }
23
- // Dameng exposes tables under user-owned schemas rather than separate
24
- // databases. Accept the legacy database argument as a schema, and default to
25
- // the login user when neither argument is provided.
26
- const resolvedSchema = schema?.trim() || database?.trim() || config.username?.trim() || undefined;
27
- return { config, schema: resolvedSchema };
28
- }
29
- function connectionIdentity(config) {
30
- return `${config.name} (${config.id}) [${config.db_type} @ ${config.host}:${config.port}]`;
31
- }
32
- function labeledText(config, body) {
33
- return text(`[${connectionIdentity(config)}]\n${body}`);
34
- }
35
- function formatQueryToolResult(result, title) {
36
- const prefix = title ? `${title}\n` : "";
37
- if (result.columns.length === 0)
38
- return text(`${prefix}Query executed. ${result.row_count} row(s) affected.`);
39
- const rows = result.rows.map((r) => result.columns.map((c) => formatCell(r[c])));
40
- return text(`${prefix}${mdTable(result.columns, rows)}\n\n${result.row_count} row(s)`);
41
- }
42
- function redisDbFromValue(value) {
43
- const trimmed = value?.trim();
44
- if (!trimmed)
45
- return undefined;
46
- const db = Number(trimmed);
47
- return Number.isInteger(db) && db >= 0 ? db : undefined;
48
- }
49
- function defaultRedisDb(config, scope, db) {
50
- return db ?? redisDbFromValue(scope.database) ?? redisDbFromValue(config.database) ?? 0;
51
- }
52
- function formatRedisCommandValue(value) {
53
- if (typeof value === "string")
54
- return value;
55
- return JSON.stringify(value, null, 2) ?? String(value);
56
- }
57
- function formatRedisCommandToolResult(result) {
58
- return text(`Command: ${result.command}\nSafety: ${result.safety}\n\n${formatRedisCommandValue(result.value)}`);
59
- }
60
- export const DBX_CONNECTION_TYPE_DESCRIPTION = "Database type: postgres, mysql, sqlite, rqlite, cloudflare-d1, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, weaviate, chromadb, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, prestosql, hive, spark, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, oscar, tdengine, iotdb, xugu, zookeeper, jdbc, access, mq";
61
- const FILE_CAPABLE_CONNECTION_TYPES = new Set(["sqlite", "duckdb", "access", "h2"]);
62
- function scopedValue(value) {
63
- const trimmed = value?.trim();
64
- return trimmed ? trimmed : undefined;
65
- }
66
- function mcpScopeFromEnv() {
67
- return {
68
- connectionId: scopedValue(process.env.DBX_MCP_SCOPE_CONNECTION_ID),
69
- connectionName: scopedValue(process.env.DBX_MCP_SCOPE_CONNECTION_NAME),
70
- database: scopedValue(process.env.DBX_MCP_SCOPE_DATABASE),
71
- };
72
- }
73
- function scopeEnabled(scope) {
74
- return !!(scope.connectionId || scope.connectionName);
75
- }
76
- function connectionMatchesScope(config, scope) {
77
- return (!!scope.connectionId && config.id === scope.connectionId) || (!!scope.connectionName && config.name === scope.connectionName);
78
- }
79
- async function loadScopedConnections(backend, scope) {
80
- const connections = await backend.loadConnections();
81
- if (!scopeEnabled(scope))
82
- return connections;
83
- return connections.filter((config) => connectionMatchesScope(config, scope));
84
- }
85
- async function resolveConnection(backend, scope, requestedId, requestedName) {
86
- // connection_id takes priority over connection_name when both are provided.
87
- if (requestedId?.trim()) {
88
- const connections = await backend.loadConnections();
89
- const config = connections.find((c) => c.id === requestedId.trim());
90
- if (!config)
91
- return { error: toolError("CONNECTION_NOT_FOUND", `Connection with id "${requestedId}" not found.`) };
92
- // In scoped mode, verify the resolved connection is within the scope.
93
- if (scopeEnabled(scope) && !connectionMatchesScope(config, scope)) {
94
- return { error: toolError("CONNECTION_OUT_OF_SCOPE", `Connection "${requestedId}" is outside this DBX AI session scope.`) };
95
- }
96
- return { config };
97
- }
98
- if (!scopeEnabled(scope)) {
99
- if (!requestedName?.trim())
100
- return { error: toolError("CONNECTION_NOT_FOUND", "Connection name is required.") };
101
- const connections = await backend.loadConnections();
102
- const matching = connections.filter((c) => c.name.toLowerCase() === requestedName.trim().toLowerCase());
103
- if (matching.length === 0)
104
- return { error: toolError("CONNECTION_NOT_FOUND", `Connection "${requestedName}" not found.`) };
105
- if (matching.length > 1) {
106
- const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
107
- return {
108
- error: toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${requestedName}". Please specify connection_id:\n${lines.join("\n")}`),
109
- };
110
- }
111
- return { config: matching[0] };
112
- }
113
- const [scopedConfig] = await loadScopedConnections(backend, scope);
114
- if (!scopedConfig)
115
- return { error: toolError("CONNECTION_NOT_FOUND", "Scoped DBX connection was not found.") };
116
- if (requestedName?.trim() && requestedName !== scopedConfig.name && requestedName !== scopedConfig.id) {
117
- return { error: toolError("CONNECTION_OUT_OF_SCOPE", `Connection "${requestedName}" is outside this DBX AI session scope.`) };
118
- }
119
- return { config: scopedConfig };
120
- }
121
- export function createDbxMcpServer(backend, options = {}) {
122
- const isWebMode = options.isWebMode ?? !!process.env.DBX_WEB_URL;
123
- const scope = mcpScopeFromEnv();
124
- const scoped = scopeEnabled(scope);
125
- const server = new McpServer({
126
- name: "dbx",
127
- version: DBX_MCP_PACKAGE_VERSION,
128
- });
129
- server.tool("dbx_list_connections", "List all database connections configured in DBX", {}, async () => {
130
- const connections = await loadScopedConnections(backend, scope);
131
- if (connections.length === 0)
132
- return text("No connections configured in DBX.");
133
- const rows = connections.map((c) => [c.id, c.name, c.db_type, c.host, String(c.port), c.database || ""]);
134
- return text(mdTable(["ID", "Name", "Type", "Host", "Port", "Database"], rows));
135
- });
136
- server.tool("dbx_list_tables", "List tables and views for a database connection", {
137
- connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
138
- connection_name: z.string().optional().describe("Name of the DBX connection"),
139
- database: z.string().optional().describe("Database name; for Dameng this is also accepted as a schema alias"),
140
- schema: z.string().optional().describe("Schema name (default: public for PostgreSQL, login user for Dameng)"),
141
- }, async ({ connection_id, connection_name, database, schema }) => {
142
- const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
143
- if (error)
144
- return error;
145
- const resolvedConfig = config;
146
- const scopeValue = metadataScope(resolvedConfig, database ?? scope.database, schema);
147
- const tables = await backend.listTables(scopeValue.config, scopeValue.schema);
148
- if (tables.length === 0)
149
- return text("No tables found.");
150
- const rows = tables.map((t) => [t.name, t.type]);
151
- return labeledText(resolvedConfig, mdTable(["Table", "Type"], rows));
152
- });
153
- server.tool("dbx_describe_table", "Get column definitions for a table", {
154
- connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
155
- connection_name: z.string().optional().describe("Name of the DBX connection"),
156
- table: z.string().describe("Table name"),
157
- database: z.string().optional().describe("Database name; for Dameng this is also accepted as a schema alias"),
158
- schema: z.string().optional().describe("Schema name (default: public for PostgreSQL, login user for Dameng)"),
159
- }, async ({ connection_id, connection_name, table, database, schema }) => {
160
- const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
161
- if (error)
162
- return error;
163
- const resolvedConfig = config;
164
- const scopeValue = metadataScope(resolvedConfig, database ?? scope.database, schema);
165
- const columns = await backend.describeTable(scopeValue.config, table, scopeValue.schema);
166
- if (columns.length === 0)
167
- return text("No columns found.");
168
- const rows = columns.map((c) => [c.is_primary_key ? `${c.name} (PK)` : c.name, c.data_type, c.is_nullable ? "YES" : "NO", c.column_default ?? "", c.comment ?? ""]);
169
- return labeledText(resolvedConfig, mdTable(["Column", "Type", "Nullable", "Default", "Comment"], rows));
170
- });
171
- server.tool("dbx_execute_query", "Execute a SQL query on a database connection (max 100 rows returned)", {
172
- connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
173
- connection_name: z.string().optional().describe("Name of the DBX connection"),
174
- database: z.string().optional().describe("Database name"),
175
- sql: z.string().describe("SQL query to execute"),
176
- }, async ({ connection_id, connection_name, database, sql }) => {
177
- logSqlDiagnostic("dbx_execute_query", sql, { connection_id, connection_name, database });
178
- const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
179
- if (error)
180
- return error;
181
- const scopedConfig = config;
182
- if (scopedConfig.db_type === "redis") {
183
- return toolError("REDIS_COMMAND_REQUIRED", "Redis connections do not accept SQL through dbx_execute_query. Use dbx_execute_redis_command with a Redis command such as GET key or INFO.");
184
- }
185
- if (scopedConfig.db_type !== "mongodb") {
186
- const hashLineComments = supportsHashLineComments(scopedConfig.db_type);
187
- const safety = evaluateSqlSafety(sql, { ...sqlSafetyFromEnv(), allowMultipleStatements: true, hashLineComments });
188
- if (!safety.allowed)
189
- return toolError("SQL_BLOCKED", safety.reason ?? "SQL blocked.");
190
- const production = assessProductionSql(sql, scopedConfig, database ?? scope.database ?? scopedConfig.database);
191
- if (production.active && production.isMutation) {
192
- return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot execute writes against a production database. Return the SQL for a user to review and run in DBX.");
193
- }
194
- }
195
- else if (isProductionDatabase(scopedConfig, database ?? scope.database ?? scopedConfig.database) && isLikelyMongoMutation(sql)) {
196
- return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot execute writes against a production database. Return the command for a user to review and run in DBX.");
197
- }
198
- // MongoDB shell commands don't fit the SQL safety evaluator; the backend
199
- // (node-core executeQuery) applies command-aware read/write gating.
200
- try {
201
- const statements = scopedConfig.db_type === "mongodb" ? [sql] : splitSqlStatements(sql, { hashLineComments: supportsHashLineComments(scopedConfig.db_type) });
202
- const results = [];
203
- for (const statement of statements) {
204
- results.push(await backend.executeQuery(withDatabase(scopedConfig, database ?? scope.database), statement));
205
- }
206
- if (results.length === 1)
207
- return labeledText(scopedConfig, formatQueryToolResult(results[0]).content[0].text);
208
- return labeledText(scopedConfig, results.map((result, index) => formatQueryToolResult(result, `Statement ${index + 1}`).content[0].text).join("\n\n"));
209
- }
210
- catch (e) {
211
- const msg = e instanceof Error ? e.message : String(e);
212
- return toolError("QUERY_ERROR", msg);
213
- }
214
- });
215
- server.tool("dbx_execute_redis_command", "Execute a Redis command on a Redis connection", {
216
- connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
217
- connection_name: z.string().optional().describe("Name of the DBX Redis connection"),
218
- db: z.number().int().min(0).optional().describe("Redis logical database number (default: scoped/default database or 0)"),
219
- command: z.string().describe("Redis command to execute, for example: GET mykey, INFO, or DBSIZE"),
220
- }, async ({ connection_id, connection_name, db, command }) => {
221
- const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
222
- if (error)
223
- return error;
224
- const scopedConfig = config;
225
- if (scopedConfig.db_type !== "redis") {
226
- return toolError("INVALID_CONNECTION_TYPE", `Connection "${scopedConfig.name}" is ${scopedConfig.db_type}, not Redis.`);
227
- }
228
- if (!backend.executeRedisCommand) {
229
- return toolError("UNSUPPORTED_BACKEND", "This DBX backend does not support Redis command execution.");
230
- }
231
- const safety = evaluateRedisCommandSafety(command, sqlSafetyFromEnv());
232
- if (!safety.allowed)
233
- return toolError("REDIS_COMMAND_BLOCKED", safety.reason ?? "Redis command blocked.");
234
- if (isProductionDatabase(scopedConfig, String(defaultRedisDb(scopedConfig, scope, db))) && safety.safety !== "allowed") {
235
- return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot execute write or dangerous Redis commands against a production database.");
236
- }
237
- try {
238
- const result = await backend.executeRedisCommand(scopedConfig, defaultRedisDb(scopedConfig, scope, db), command, {
239
- skipSafetyCheck: safety.skipSafetyCheck,
240
- });
241
- return labeledText(scopedConfig, formatRedisCommandToolResult(result).content[0].text);
242
- }
243
- catch (e) {
244
- const msg = e instanceof Error ? e.message : String(e);
245
- return toolError("REDIS_COMMAND_ERROR", msg);
246
- }
247
- });
248
- server.tool("dbx_get_schema_context", "Get compact table and column context for writing SQL", {
249
- connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
250
- connection_name: z.string().optional().describe("Name of the DBX connection"),
251
- database: z.string().optional().describe("Database name"),
252
- schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
253
- tables: z.array(z.string()).optional().describe("Specific table names to include"),
254
- max_tables: z.number().int().min(1).max(20).default(8).describe("Maximum number of tables to include"),
255
- }, async ({ connection_id, connection_name, database, schema, tables, max_tables }) => {
256
- const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
257
- if (error)
258
- return error;
259
- const resolvedConfig = config;
260
- const context = await buildSchemaContext(backend, withDatabase(resolvedConfig, database ?? scope.database), {
261
- schema,
262
- tables,
263
- maxTables: max_tables,
264
- });
265
- if (context.tables.length === 0)
266
- return text("No matching tables found.");
267
- return labeledText(resolvedConfig, formatSchemaContext(context));
268
- });
269
- if (!scoped) {
270
- server.tool("dbx_add_connection", "Add a new database connection to DBX", {
271
- name: z.string().describe("Connection name"),
272
- db_type: z.string().describe(DBX_CONNECTION_TYPE_DESCRIPTION),
273
- host: z.string().describe("Database host; for cloudflare-d1, use the Cloudflare Account ID"),
274
- port: z.number().optional().describe("Database port (TDengine defaults to 6041, IoTDB defaults to 6667, XuguDB defaults to 5138)"),
275
- username: z.string().default("").describe("Username"),
276
- password: z.string().default("").describe("Password; for cloudflare-d1, use the API Token"),
277
- database: z.string().optional().describe("Default database name; for cloudflare-d1, use the D1 Database ID"),
278
- ssl: z.boolean().default(false).describe("Enable SSL"),
279
- driver_profile: z.string().optional().describe("Driver profile (e.g. 'gbase8a', 'gbase8s')"),
280
- }, async ({ name, db_type, host, port, username, password, database, ssl, driver_profile }) => {
281
- const existing = await backend.findConnection(name);
282
- if (existing)
283
- return text(`Connection "${name}" already exists.`);
284
- const DEFAULT_PORTS = {
285
- kwdb: 26257,
286
- rqlite: 4001,
287
- "cloudflare-d1": 443,
288
- tdengine: 6041,
289
- oscar: 2003,
290
- iotdb: 6667,
291
- xugu: 5138,
292
- };
293
- const resolvedPort = port ?? DEFAULT_PORTS[db_type] ?? (FILE_CAPABLE_CONNECTION_TYPES.has(db_type) ? 0 : undefined);
294
- if (resolvedPort === undefined)
295
- return text("Port is required for this database type.");
296
- const config = await backend.addConnection({
297
- name,
298
- db_type,
299
- host,
300
- port: resolvedPort,
301
- username,
302
- password,
303
- database,
304
- ssl,
305
- driver_profile,
306
- ssh_enabled: false,
307
- });
308
- await notifyReload();
309
- return text(`Connection "${config.name}" added (id: ${config.id}).`);
310
- });
311
- server.tool("dbx_remove_connection", "Remove a database connection from DBX", {
312
- connection_name: z.string().describe("Name of the connection to remove"),
313
- connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to remove by id instead of name)"),
314
- }, async ({ connection_name, connection_id }) => {
315
- if (connection_id?.trim()) {
316
- if (backend.removeConnectionById) {
317
- const removed = await backend.removeConnectionById(connection_id.trim());
318
- if (!removed)
319
- return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
320
- await notifyReload();
321
- return text(`Connection with id "${connection_id}" removed.`);
322
- }
323
- // Fallback: resolve by id then remove by name
324
- const connections = await backend.loadConnections();
325
- const config = connections.find((c) => c.id === connection_id.trim());
326
- if (!config)
327
- return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
328
- const removed = await backend.removeConnection(config.name);
329
- if (!removed)
330
- return toolError("CONNECTION_NOT_FOUND", `Connection "${config.name}" could not be removed.`);
331
- await notifyReload();
332
- return text(`Connection "${config.name}" (id: ${config.id}) removed.`);
333
- }
334
- const allConnections = await backend.loadConnections();
335
- const matching = allConnections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
336
- if (matching.length === 0)
337
- return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
338
- if (matching.length > 1) {
339
- const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
340
- return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
341
- }
342
- const removed = await backend.removeConnection(connection_name);
343
- if (!removed)
344
- return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
345
- await notifyReload();
346
- return text(`Connection "${connection_name}" removed.`);
347
- });
348
- }
349
- // Desktop-only tools: open table and execute-and-show require the Tauri bridge
350
- if (!isWebMode && !scoped) {
351
- server.tool("dbx_open_table", "Open a table in DBX desktop app UI. Requires DBX to be running.", {
352
- connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
353
- connection_name: z.string().optional().describe("Name of the DBX connection"),
354
- table: z.string().describe("Table name to open"),
355
- database: z.string().optional().describe("Database name"),
356
- schema: z.string().optional().describe("Schema name"),
357
- }, async ({ connection_id, connection_name, table, database, schema }) => {
358
- let config;
359
- if (connection_id?.trim()) {
360
- const connections = await backend.loadConnections();
361
- config = connections.find((c) => c.id === connection_id.trim());
362
- if (!config)
363
- return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
364
- }
365
- else if (connection_name?.trim()) {
366
- const connections = await backend.loadConnections();
367
- const matching = connections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
368
- if (matching.length === 0)
369
- return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
370
- if (matching.length > 1) {
371
- const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
372
- return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
373
- }
374
- config = matching[0];
375
- }
376
- else {
377
- return toolError("CONNECTION_NOT_FOUND", "Either connection_id or connection_name is required.");
378
- }
379
- return bridgeRequest("/open-table", { connection_id: config.id, connection_name: config.name, table, database, schema }, `Opened ${table} in DBX`);
380
- });
381
- server.tool("dbx_execute_and_show", "Execute a SQL query in DBX desktop app UI and show results there. Requires DBX to be running.", {
382
- connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
383
- connection_name: z.string().optional().describe("Name of the DBX connection"),
384
- sql: z.string().describe("SQL query to execute"),
385
- database: z.string().optional().describe("Database name"),
386
- }, async ({ connection_id, connection_name, sql, database }) => {
387
- let config;
388
- if (connection_id?.trim()) {
389
- const connections = await backend.loadConnections();
390
- config = connections.find((c) => c.id === connection_id.trim());
391
- if (!config)
392
- return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
393
- }
394
- else if (connection_name?.trim()) {
395
- const connections = await backend.loadConnections();
396
- const matching = connections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
397
- if (matching.length === 0)
398
- return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
399
- if (matching.length > 1) {
400
- const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
401
- return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
402
- }
403
- config = matching[0];
404
- }
405
- else {
406
- return toolError("CONNECTION_NOT_FOUND", "Either connection_id or connection_name is required.");
407
- }
408
- const safetyOptions = sqlSafetyFromEnv();
409
- if (config?.db_type === "mongodb") {
410
- const aggregate = parseMongoAggregateCommand(sql);
411
- if (aggregate) {
412
- const safety = evaluateMongoAggregateSafety(aggregate, safetyOptions);
413
- if (!safety.allowed)
414
- return toolError("SQL_BLOCKED", safety.reason ?? "Query blocked.");
415
- }
416
- }
417
- else {
418
- const hashLineComments = supportsHashLineComments(config?.db_type);
419
- const safety = evaluateSqlSafety(sql, { ...safetyOptions, allowMultipleStatements: true, hashLineComments });
420
- if (!safety.allowed)
421
- return toolError("SQL_BLOCKED", safety.reason ?? "SQL blocked.");
422
- }
423
- if (config?.db_type === "mongodb") {
424
- if (isProductionDatabase(config, database ?? scope.database ?? config.database) && isLikelyMongoMutation(sql)) {
425
- return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot send writes against a production database to DBX.");
426
- }
427
- }
428
- else {
429
- const production = assessProductionSql(sql, config, database ?? scope.database ?? config.database);
430
- if (production.active && production.isMutation) {
431
- return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot send writes against a production database to DBX.");
432
- }
433
- }
434
- // MongoDB shell commands bypass the SQL safety evaluator; pass MCP
435
- // safety flags to the desktop executor for command-aware gating.
436
- logSqlDiagnostic("dbx_execute_in_app", sql, { connection_id: config.id, connection_name: config.name, database });
437
- return bridgeRequest("/execute-query", {
438
- connection_id: config.id,
439
- connection_name: config.name,
440
- sql,
441
- database,
442
- allow_writes: safetyOptions.allowWrites,
443
- allow_dangerous: safetyOptions.allowDangerous,
444
- }, "Query sent to DBX");
445
- });
446
- }
447
- return server;
448
- }
449
- async function bridgeRequest(path, body, successMsg) {
450
- const res = await postBridge(path, body);
451
- if (res.ok)
452
- return text(successMsg);
453
- const message = res.text.startsWith("DBX is not running") ? res.text : `Failed: ${res.text}`;
454
- return toolError("DBX_NOT_RUNNING", message);
455
- }
456
- async function main() {
457
- const backend = await createBackend();
458
- const server = createDbxMcpServer(backend);
459
- const transport = new StdioServerTransport();
460
- await server.connect(transport);
461
- }
462
- if (isMainModule(import.meta.url, process.argv[1])) {
463
- main().catch((e) => {
464
- console.error("MCP Server failed to start:", e);
465
- process.exit(1);
466
- });
467
- }