@dbx-app/mcp-server 0.4.25 → 0.4.27
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/dist/index.js +177 -42
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { buildSchemaContext, createBackend, evaluateRedisCommandSafety, evaluateMongoAggregateSafety, evaluateSqlSafety, formatCell, formatSchemaContext, isMainModule, mdTable, notifyReload, parseMongoAggregateCommand, postBridge, sqlSafetyFromEnv, splitSqlStatements, } from "@dbx-app/node-core";
|
|
6
|
+
import { buildSchemaContext, createBackend, evaluateRedisCommandSafety, evaluateMongoAggregateSafety, evaluateSqlSafety, formatCell, formatSchemaContext, isMainModule, mdTable, notifyReload, parseMongoAggregateCommand, assessProductionSql, isLikelyMongoMutation, isProductionDatabase, postBridge, sqlSafetyFromEnv, splitSqlStatements, } from "@dbx-app/node-core";
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const packageJson = require("../package.json");
|
|
9
9
|
export const DBX_MCP_PACKAGE_VERSION = packageJson.version ?? "0.0.0";
|
|
@@ -16,6 +16,22 @@ function toolError(code, message) {
|
|
|
16
16
|
function withDatabase(config, database) {
|
|
17
17
|
return database === undefined ? config : { ...config, database };
|
|
18
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
|
+
}
|
|
19
35
|
function formatQueryToolResult(result, title) {
|
|
20
36
|
const prefix = title ? `${title}\n` : "";
|
|
21
37
|
if (result.columns.length === 0)
|
|
@@ -66,12 +82,33 @@ async function loadScopedConnections(backend, scope) {
|
|
|
66
82
|
return connections;
|
|
67
83
|
return connections.filter((config) => connectionMatchesScope(config, scope));
|
|
68
84
|
}
|
|
69
|
-
async function resolveConnection(backend, scope, requestedName) {
|
|
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
|
+
}
|
|
70
98
|
if (!scopeEnabled(scope)) {
|
|
71
99
|
if (!requestedName?.trim())
|
|
72
100
|
return { error: toolError("CONNECTION_NOT_FOUND", "Connection name is required.") };
|
|
73
|
-
const
|
|
74
|
-
|
|
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] };
|
|
75
112
|
}
|
|
76
113
|
const [scopedConfig] = await loadScopedConnections(backend, scope);
|
|
77
114
|
if (!scopedConfig)
|
|
@@ -93,44 +130,51 @@ export function createDbxMcpServer(backend, options = {}) {
|
|
|
93
130
|
const connections = await loadScopedConnections(backend, scope);
|
|
94
131
|
if (connections.length === 0)
|
|
95
132
|
return text("No connections configured in DBX.");
|
|
96
|
-
const rows = connections.map((c) => [c.name, c.db_type, c.host, String(c.port), c.database || ""]);
|
|
97
|
-
return text(mdTable(["Name", "Type", "Host", "Port", "Database"], rows));
|
|
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));
|
|
98
135
|
});
|
|
99
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)"),
|
|
100
138
|
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
101
|
-
database: z.string().optional().describe("Database name"),
|
|
102
|
-
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
|
|
103
|
-
}, async ({ connection_name, database, schema }) => {
|
|
104
|
-
const { config, error } = await resolveConnection(backend, scope, connection_name);
|
|
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);
|
|
105
143
|
if (error)
|
|
106
144
|
return error;
|
|
107
|
-
const
|
|
145
|
+
const resolvedConfig = config;
|
|
146
|
+
const scopeValue = metadataScope(resolvedConfig, database ?? scope.database, schema);
|
|
147
|
+
const tables = await backend.listTables(scopeValue.config, scopeValue.schema);
|
|
108
148
|
if (tables.length === 0)
|
|
109
149
|
return text("No tables found.");
|
|
110
150
|
const rows = tables.map((t) => [t.name, t.type]);
|
|
111
|
-
return
|
|
151
|
+
return labeledText(resolvedConfig, mdTable(["Table", "Type"], rows));
|
|
112
152
|
});
|
|
113
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)"),
|
|
114
155
|
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
115
156
|
table: z.string().describe("Table name"),
|
|
116
|
-
database: z.string().optional().describe("Database name"),
|
|
117
|
-
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
|
|
118
|
-
}, async ({ connection_name, table, database, schema }) => {
|
|
119
|
-
const { config, error } = await resolveConnection(backend, scope, connection_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);
|
|
120
161
|
if (error)
|
|
121
162
|
return error;
|
|
122
|
-
const
|
|
163
|
+
const resolvedConfig = config;
|
|
164
|
+
const scopeValue = metadataScope(resolvedConfig, database ?? scope.database, schema);
|
|
165
|
+
const columns = await backend.describeTable(scopeValue.config, table, scopeValue.schema);
|
|
123
166
|
if (columns.length === 0)
|
|
124
167
|
return text("No columns found.");
|
|
125
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 ?? ""]);
|
|
126
|
-
return
|
|
169
|
+
return labeledText(resolvedConfig, mdTable(["Column", "Type", "Nullable", "Default", "Comment"], rows));
|
|
127
170
|
});
|
|
128
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)"),
|
|
129
173
|
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
130
174
|
database: z.string().optional().describe("Database name"),
|
|
131
175
|
sql: z.string().describe("SQL query to execute"),
|
|
132
|
-
}, async ({ connection_name, database, sql }) => {
|
|
133
|
-
const { config, error } = await resolveConnection(backend, scope, connection_name);
|
|
176
|
+
}, async ({ connection_id, connection_name, database, sql }) => {
|
|
177
|
+
const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
|
|
134
178
|
if (error)
|
|
135
179
|
return error;
|
|
136
180
|
const scopedConfig = config;
|
|
@@ -141,6 +185,13 @@ export function createDbxMcpServer(backend, options = {}) {
|
|
|
141
185
|
const safety = evaluateSqlSafety(sql, { ...sqlSafetyFromEnv(), allowMultipleStatements: true });
|
|
142
186
|
if (!safety.allowed)
|
|
143
187
|
return toolError("SQL_BLOCKED", safety.reason ?? "SQL blocked.");
|
|
188
|
+
const production = assessProductionSql(sql, scopedConfig, database ?? scope.database ?? scopedConfig.database);
|
|
189
|
+
if (production.active && production.isMutation) {
|
|
190
|
+
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.");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (isProductionDatabase(scopedConfig, database ?? scope.database ?? scopedConfig.database) && isLikelyMongoMutation(sql)) {
|
|
194
|
+
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.");
|
|
144
195
|
}
|
|
145
196
|
// MongoDB shell commands don't fit the SQL safety evaluator; the backend
|
|
146
197
|
// (node-core executeQuery) applies command-aware read/write gating.
|
|
@@ -151,8 +202,8 @@ export function createDbxMcpServer(backend, options = {}) {
|
|
|
151
202
|
results.push(await backend.executeQuery(withDatabase(scopedConfig, database ?? scope.database), statement));
|
|
152
203
|
}
|
|
153
204
|
if (results.length === 1)
|
|
154
|
-
return formatQueryToolResult(results[0]);
|
|
155
|
-
return
|
|
205
|
+
return labeledText(scopedConfig, formatQueryToolResult(results[0]).content[0].text);
|
|
206
|
+
return labeledText(scopedConfig, results.map((result, index) => formatQueryToolResult(result, `Statement ${index + 1}`).content[0].text).join("\n\n"));
|
|
156
207
|
}
|
|
157
208
|
catch (e) {
|
|
158
209
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -160,11 +211,12 @@ export function createDbxMcpServer(backend, options = {}) {
|
|
|
160
211
|
}
|
|
161
212
|
});
|
|
162
213
|
server.tool("dbx_execute_redis_command", "Execute a Redis command on a Redis connection", {
|
|
214
|
+
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
163
215
|
connection_name: z.string().optional().describe("Name of the DBX Redis connection"),
|
|
164
216
|
db: z.number().int().min(0).optional().describe("Redis logical database number (default: scoped/default database or 0)"),
|
|
165
217
|
command: z.string().describe("Redis command to execute, for example: GET mykey, INFO, or DBSIZE"),
|
|
166
|
-
}, async ({ connection_name, db, command }) => {
|
|
167
|
-
const { config, error } = await resolveConnection(backend, scope, connection_name);
|
|
218
|
+
}, async ({ connection_id, connection_name, db, command }) => {
|
|
219
|
+
const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
|
|
168
220
|
if (error)
|
|
169
221
|
return error;
|
|
170
222
|
const scopedConfig = config;
|
|
@@ -177,11 +229,14 @@ export function createDbxMcpServer(backend, options = {}) {
|
|
|
177
229
|
const safety = evaluateRedisCommandSafety(command, sqlSafetyFromEnv());
|
|
178
230
|
if (!safety.allowed)
|
|
179
231
|
return toolError("REDIS_COMMAND_BLOCKED", safety.reason ?? "Redis command blocked.");
|
|
232
|
+
if (isProductionDatabase(scopedConfig, String(defaultRedisDb(scopedConfig, scope, db))) && safety.safety !== "allowed") {
|
|
233
|
+
return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot execute write or dangerous Redis commands against a production database.");
|
|
234
|
+
}
|
|
180
235
|
try {
|
|
181
236
|
const result = await backend.executeRedisCommand(scopedConfig, defaultRedisDb(scopedConfig, scope, db), command, {
|
|
182
237
|
skipSafetyCheck: safety.skipSafetyCheck,
|
|
183
238
|
});
|
|
184
|
-
return formatRedisCommandToolResult(result);
|
|
239
|
+
return labeledText(scopedConfig, formatRedisCommandToolResult(result).content[0].text);
|
|
185
240
|
}
|
|
186
241
|
catch (e) {
|
|
187
242
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -189,23 +244,25 @@ export function createDbxMcpServer(backend, options = {}) {
|
|
|
189
244
|
}
|
|
190
245
|
});
|
|
191
246
|
server.tool("dbx_get_schema_context", "Get compact table and column context for writing SQL", {
|
|
247
|
+
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
192
248
|
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
193
249
|
database: z.string().optional().describe("Database name"),
|
|
194
250
|
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
|
|
195
251
|
tables: z.array(z.string()).optional().describe("Specific table names to include"),
|
|
196
252
|
max_tables: z.number().int().min(1).max(20).default(8).describe("Maximum number of tables to include"),
|
|
197
|
-
}, async ({ connection_name, database, schema, tables, max_tables }) => {
|
|
198
|
-
const { config, error } = await resolveConnection(backend, scope, connection_name);
|
|
253
|
+
}, async ({ connection_id, connection_name, database, schema, tables, max_tables }) => {
|
|
254
|
+
const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
|
|
199
255
|
if (error)
|
|
200
256
|
return error;
|
|
201
|
-
const
|
|
257
|
+
const resolvedConfig = config;
|
|
258
|
+
const context = await buildSchemaContext(backend, withDatabase(resolvedConfig, database ?? scope.database), {
|
|
202
259
|
schema,
|
|
203
260
|
tables,
|
|
204
261
|
maxTables: max_tables,
|
|
205
262
|
});
|
|
206
263
|
if (context.tables.length === 0)
|
|
207
264
|
return text("No matching tables found.");
|
|
208
|
-
return
|
|
265
|
+
return labeledText(resolvedConfig, formatSchemaContext(context));
|
|
209
266
|
});
|
|
210
267
|
if (!scoped) {
|
|
211
268
|
server.tool("dbx_add_connection", "Add a new database connection to DBX", {
|
|
@@ -250,7 +307,35 @@ export function createDbxMcpServer(backend, options = {}) {
|
|
|
250
307
|
});
|
|
251
308
|
server.tool("dbx_remove_connection", "Remove a database connection from DBX", {
|
|
252
309
|
connection_name: z.string().describe("Name of the connection to remove"),
|
|
253
|
-
|
|
310
|
+
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to remove by id instead of name)"),
|
|
311
|
+
}, async ({ connection_name, connection_id }) => {
|
|
312
|
+
if (connection_id?.trim()) {
|
|
313
|
+
if (backend.removeConnectionById) {
|
|
314
|
+
const removed = await backend.removeConnectionById(connection_id.trim());
|
|
315
|
+
if (!removed)
|
|
316
|
+
return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
|
|
317
|
+
await notifyReload();
|
|
318
|
+
return text(`Connection with id "${connection_id}" removed.`);
|
|
319
|
+
}
|
|
320
|
+
// Fallback: resolve by id then remove by name
|
|
321
|
+
const connections = await backend.loadConnections();
|
|
322
|
+
const config = connections.find((c) => c.id === connection_id.trim());
|
|
323
|
+
if (!config)
|
|
324
|
+
return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
|
|
325
|
+
const removed = await backend.removeConnection(config.name);
|
|
326
|
+
if (!removed)
|
|
327
|
+
return toolError("CONNECTION_NOT_FOUND", `Connection "${config.name}" could not be removed.`);
|
|
328
|
+
await notifyReload();
|
|
329
|
+
return text(`Connection "${config.name}" (id: ${config.id}) removed.`);
|
|
330
|
+
}
|
|
331
|
+
const allConnections = await backend.loadConnections();
|
|
332
|
+
const matching = allConnections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
|
|
333
|
+
if (matching.length === 0)
|
|
334
|
+
return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
|
|
335
|
+
if (matching.length > 1) {
|
|
336
|
+
const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
|
|
337
|
+
return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
|
|
338
|
+
}
|
|
254
339
|
const removed = await backend.removeConnection(connection_name);
|
|
255
340
|
if (!removed)
|
|
256
341
|
return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
|
|
@@ -261,24 +346,62 @@ export function createDbxMcpServer(backend, options = {}) {
|
|
|
261
346
|
// Desktop-only tools: open table and execute-and-show require the Tauri bridge
|
|
262
347
|
if (!isWebMode && !scoped) {
|
|
263
348
|
server.tool("dbx_open_table", "Open a table in DBX desktop app UI. Requires DBX to be running.", {
|
|
264
|
-
|
|
349
|
+
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
350
|
+
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
265
351
|
table: z.string().describe("Table name to open"),
|
|
266
352
|
database: z.string().optional().describe("Database name"),
|
|
267
353
|
schema: z.string().optional().describe("Schema name"),
|
|
268
|
-
}, async ({ connection_name, table, database, schema }) => {
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
354
|
+
}, async ({ connection_id, connection_name, table, database, schema }) => {
|
|
355
|
+
let config;
|
|
356
|
+
if (connection_id?.trim()) {
|
|
357
|
+
const connections = await backend.loadConnections();
|
|
358
|
+
config = connections.find((c) => c.id === connection_id.trim());
|
|
359
|
+
if (!config)
|
|
360
|
+
return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
|
|
361
|
+
}
|
|
362
|
+
else if (connection_name?.trim()) {
|
|
363
|
+
const connections = await backend.loadConnections();
|
|
364
|
+
const matching = connections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
|
|
365
|
+
if (matching.length === 0)
|
|
366
|
+
return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
|
|
367
|
+
if (matching.length > 1) {
|
|
368
|
+
const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
|
|
369
|
+
return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
|
|
370
|
+
}
|
|
371
|
+
config = matching[0];
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
return toolError("CONNECTION_NOT_FOUND", "Either connection_id or connection_name is required.");
|
|
375
|
+
}
|
|
376
|
+
return bridgeRequest("/open-table", { connection_id: config.id, connection_name: config.name, table, database, schema }, `Opened ${table} in DBX`);
|
|
273
377
|
});
|
|
274
378
|
server.tool("dbx_execute_and_show", "Execute a SQL query in DBX desktop app UI and show results there. Requires DBX to be running.", {
|
|
275
|
-
|
|
379
|
+
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
380
|
+
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
276
381
|
sql: z.string().describe("SQL query to execute"),
|
|
277
382
|
database: z.string().optional().describe("Database name"),
|
|
278
|
-
}, async ({ connection_name, sql, database }) => {
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
383
|
+
}, async ({ connection_id, connection_name, sql, database }) => {
|
|
384
|
+
let config;
|
|
385
|
+
if (connection_id?.trim()) {
|
|
386
|
+
const connections = await backend.loadConnections();
|
|
387
|
+
config = connections.find((c) => c.id === connection_id.trim());
|
|
388
|
+
if (!config)
|
|
389
|
+
return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
|
|
390
|
+
}
|
|
391
|
+
else if (connection_name?.trim()) {
|
|
392
|
+
const connections = await backend.loadConnections();
|
|
393
|
+
const matching = connections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
|
|
394
|
+
if (matching.length === 0)
|
|
395
|
+
return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
|
|
396
|
+
if (matching.length > 1) {
|
|
397
|
+
const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
|
|
398
|
+
return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
|
|
399
|
+
}
|
|
400
|
+
config = matching[0];
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
return toolError("CONNECTION_NOT_FOUND", "Either connection_id or connection_name is required.");
|
|
404
|
+
}
|
|
282
405
|
const safetyOptions = sqlSafetyFromEnv();
|
|
283
406
|
if (config?.db_type === "mongodb") {
|
|
284
407
|
const aggregate = parseMongoAggregateCommand(sql);
|
|
@@ -293,10 +416,22 @@ export function createDbxMcpServer(backend, options = {}) {
|
|
|
293
416
|
if (!safety.allowed)
|
|
294
417
|
return toolError("SQL_BLOCKED", safety.reason ?? "SQL blocked.");
|
|
295
418
|
}
|
|
419
|
+
if (config?.db_type === "mongodb") {
|
|
420
|
+
if (isProductionDatabase(config, database ?? scope.database ?? config.database) && isLikelyMongoMutation(sql)) {
|
|
421
|
+
return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot send writes against a production database to DBX.");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
const production = assessProductionSql(sql, config, database ?? scope.database ?? config.database);
|
|
426
|
+
if (production.active && production.isMutation) {
|
|
427
|
+
return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot send writes against a production database to DBX.");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
296
430
|
// MongoDB shell commands bypass the SQL safety evaluator; pass MCP
|
|
297
431
|
// safety flags to the desktop executor for command-aware gating.
|
|
298
432
|
return bridgeRequest("/execute-query", {
|
|
299
|
-
|
|
433
|
+
connection_id: config.id,
|
|
434
|
+
connection_name: config.name,
|
|
300
435
|
sql,
|
|
301
436
|
database,
|
|
302
437
|
allow_writes: safetyOptions.allowWrites,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbx-app/mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.27",
|
|
4
4
|
"description": "MCP server for DBX — query databases from Claude Code, Cursor, and other AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
32
32
|
"zod": "^3.25.20",
|
|
33
|
-
"@dbx-app/node-core": "^0.4.
|
|
33
|
+
"@dbx-app/node-core": "^0.4.27"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@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.
|
|
9
|
+
"version": "0.4.27",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@dbx-app/mcp-server",
|
|
14
|
-
"version": "0.4.
|
|
14
|
+
"version": "0.4.27",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|