@dbx-app/mcp-server 0.4.24 → 0.4.26

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 CHANGED
@@ -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 config = await backend.findConnection(requestedName);
74
- return config ? { config } : { error: toolError("CONNECTION_NOT_FOUND", `Connection "${requestedName}" not found.`) };
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 tables = await backend.listTables(withDatabase(config, database ?? scope.database), schema);
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 text(mdTable(["Table", "Type"], rows));
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 columns = await backend.describeTable(withDatabase(config, database ?? scope.database), table, schema);
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 text(mdTable(["Column", "Type", "Nullable", "Default", "Comment"], rows));
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;
@@ -151,8 +195,8 @@ export function createDbxMcpServer(backend, options = {}) {
151
195
  results.push(await backend.executeQuery(withDatabase(scopedConfig, database ?? scope.database), statement));
152
196
  }
153
197
  if (results.length === 1)
154
- return formatQueryToolResult(results[0]);
155
- return text(results.map((result, index) => formatQueryToolResult(result, `Statement ${index + 1}`).content[0].text).join("\n\n"));
198
+ return labeledText(scopedConfig, formatQueryToolResult(results[0]).content[0].text);
199
+ return labeledText(scopedConfig, results.map((result, index) => formatQueryToolResult(result, `Statement ${index + 1}`).content[0].text).join("\n\n"));
156
200
  }
157
201
  catch (e) {
158
202
  const msg = e instanceof Error ? e.message : String(e);
@@ -160,11 +204,12 @@ export function createDbxMcpServer(backend, options = {}) {
160
204
  }
161
205
  });
162
206
  server.tool("dbx_execute_redis_command", "Execute a Redis command on a Redis connection", {
207
+ connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
163
208
  connection_name: z.string().optional().describe("Name of the DBX Redis connection"),
164
209
  db: z.number().int().min(0).optional().describe("Redis logical database number (default: scoped/default database or 0)"),
165
210
  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);
211
+ }, async ({ connection_id, connection_name, db, command }) => {
212
+ const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
168
213
  if (error)
169
214
  return error;
170
215
  const scopedConfig = config;
@@ -181,7 +226,7 @@ export function createDbxMcpServer(backend, options = {}) {
181
226
  const result = await backend.executeRedisCommand(scopedConfig, defaultRedisDb(scopedConfig, scope, db), command, {
182
227
  skipSafetyCheck: safety.skipSafetyCheck,
183
228
  });
184
- return formatRedisCommandToolResult(result);
229
+ return labeledText(scopedConfig, formatRedisCommandToolResult(result).content[0].text);
185
230
  }
186
231
  catch (e) {
187
232
  const msg = e instanceof Error ? e.message : String(e);
@@ -189,23 +234,25 @@ export function createDbxMcpServer(backend, options = {}) {
189
234
  }
190
235
  });
191
236
  server.tool("dbx_get_schema_context", "Get compact table and column context for writing SQL", {
237
+ connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
192
238
  connection_name: z.string().optional().describe("Name of the DBX connection"),
193
239
  database: z.string().optional().describe("Database name"),
194
240
  schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
195
241
  tables: z.array(z.string()).optional().describe("Specific table names to include"),
196
242
  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);
243
+ }, async ({ connection_id, connection_name, database, schema, tables, max_tables }) => {
244
+ const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
199
245
  if (error)
200
246
  return error;
201
- const context = await buildSchemaContext(backend, withDatabase(config, database ?? scope.database), {
247
+ const resolvedConfig = config;
248
+ const context = await buildSchemaContext(backend, withDatabase(resolvedConfig, database ?? scope.database), {
202
249
  schema,
203
250
  tables,
204
251
  maxTables: max_tables,
205
252
  });
206
253
  if (context.tables.length === 0)
207
254
  return text("No matching tables found.");
208
- return text(formatSchemaContext(context));
255
+ return labeledText(resolvedConfig, formatSchemaContext(context));
209
256
  });
210
257
  if (!scoped) {
211
258
  server.tool("dbx_add_connection", "Add a new database connection to DBX", {
@@ -250,7 +297,35 @@ export function createDbxMcpServer(backend, options = {}) {
250
297
  });
251
298
  server.tool("dbx_remove_connection", "Remove a database connection from DBX", {
252
299
  connection_name: z.string().describe("Name of the connection to remove"),
253
- }, async ({ connection_name }) => {
300
+ connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to remove by id instead of name)"),
301
+ }, async ({ connection_name, connection_id }) => {
302
+ if (connection_id?.trim()) {
303
+ if (backend.removeConnectionById) {
304
+ const removed = await backend.removeConnectionById(connection_id.trim());
305
+ if (!removed)
306
+ return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
307
+ await notifyReload();
308
+ return text(`Connection with id "${connection_id}" removed.`);
309
+ }
310
+ // Fallback: resolve by id then remove by name
311
+ const connections = await backend.loadConnections();
312
+ const config = connections.find((c) => c.id === connection_id.trim());
313
+ if (!config)
314
+ return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
315
+ const removed = await backend.removeConnection(config.name);
316
+ if (!removed)
317
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${config.name}" could not be removed.`);
318
+ await notifyReload();
319
+ return text(`Connection "${config.name}" (id: ${config.id}) removed.`);
320
+ }
321
+ const allConnections = await backend.loadConnections();
322
+ const matching = allConnections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
323
+ if (matching.length === 0)
324
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
325
+ if (matching.length > 1) {
326
+ const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
327
+ return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
328
+ }
254
329
  const removed = await backend.removeConnection(connection_name);
255
330
  if (!removed)
256
331
  return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
@@ -261,24 +336,62 @@ export function createDbxMcpServer(backend, options = {}) {
261
336
  // Desktop-only tools: open table and execute-and-show require the Tauri bridge
262
337
  if (!isWebMode && !scoped) {
263
338
  server.tool("dbx_open_table", "Open a table in DBX desktop app UI. Requires DBX to be running.", {
264
- connection_name: z.string().describe("Name of the DBX connection"),
339
+ connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
340
+ connection_name: z.string().optional().describe("Name of the DBX connection"),
265
341
  table: z.string().describe("Table name to open"),
266
342
  database: z.string().optional().describe("Database name"),
267
343
  schema: z.string().optional().describe("Schema name"),
268
- }, async ({ connection_name, table, database, schema }) => {
269
- const config = await backend.findConnection(connection_name);
270
- if (!config)
271
- return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
272
- return bridgeRequest("/open-table", { connection_name, table, database, schema }, `Opened ${table} in DBX`);
344
+ }, async ({ connection_id, connection_name, table, database, schema }) => {
345
+ let config;
346
+ if (connection_id?.trim()) {
347
+ const connections = await backend.loadConnections();
348
+ config = connections.find((c) => c.id === connection_id.trim());
349
+ if (!config)
350
+ return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
351
+ }
352
+ else if (connection_name?.trim()) {
353
+ const connections = await backend.loadConnections();
354
+ const matching = connections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
355
+ if (matching.length === 0)
356
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
357
+ if (matching.length > 1) {
358
+ const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
359
+ return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
360
+ }
361
+ config = matching[0];
362
+ }
363
+ else {
364
+ return toolError("CONNECTION_NOT_FOUND", "Either connection_id or connection_name is required.");
365
+ }
366
+ return bridgeRequest("/open-table", { connection_id: config.id, connection_name: config.name, table, database, schema }, `Opened ${table} in DBX`);
273
367
  });
274
368
  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
- connection_name: z.string().describe("Name of the DBX connection"),
369
+ connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
370
+ connection_name: z.string().optional().describe("Name of the DBX connection"),
276
371
  sql: z.string().describe("SQL query to execute"),
277
372
  database: z.string().optional().describe("Database name"),
278
- }, async ({ connection_name, sql, database }) => {
279
- const config = await backend.findConnection(connection_name);
280
- if (!config)
281
- return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
373
+ }, async ({ connection_id, connection_name, sql, database }) => {
374
+ let config;
375
+ if (connection_id?.trim()) {
376
+ const connections = await backend.loadConnections();
377
+ config = connections.find((c) => c.id === connection_id.trim());
378
+ if (!config)
379
+ return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
380
+ }
381
+ else if (connection_name?.trim()) {
382
+ const connections = await backend.loadConnections();
383
+ const matching = connections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
384
+ if (matching.length === 0)
385
+ return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
386
+ if (matching.length > 1) {
387
+ const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
388
+ return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
389
+ }
390
+ config = matching[0];
391
+ }
392
+ else {
393
+ return toolError("CONNECTION_NOT_FOUND", "Either connection_id or connection_name is required.");
394
+ }
282
395
  const safetyOptions = sqlSafetyFromEnv();
283
396
  if (config?.db_type === "mongodb") {
284
397
  const aggregate = parseMongoAggregateCommand(sql);
@@ -296,7 +409,8 @@ export function createDbxMcpServer(backend, options = {}) {
296
409
  // MongoDB shell commands bypass the SQL safety evaluator; pass MCP
297
410
  // safety flags to the desktop executor for command-aware gating.
298
411
  return bridgeRequest("/execute-query", {
299
- connection_name,
412
+ connection_id: config.id,
413
+ connection_name: config.name,
300
414
  sql,
301
415
  database,
302
416
  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.24",
3
+ "version": "0.4.26",
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.24"
33
+ "@dbx-app/node-core": "^0.4.26"
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.24",
9
+ "version": "0.4.26",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@dbx-app/mcp-server",
14
- "version": "0.4.24",
14
+ "version": "0.4.26",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }