import assert from "node:assert/strict"; import { mkdtemp, rm, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "vitest"; import type { Backend, ConnectionConfig } from "@dbx-app/node-core"; import { createDbxMcpServer, DBX_MCP_PACKAGE_VERSION } from "../src/index.js"; const connection: ConnectionConfig = { id: "1", name: "local", db_type: "postgres", host: "127.0.0.1", port: 5432, username: "app", password: "", database: "demo", ssh_enabled: false, ssl: false, }; const backend: Backend = { loadConnections: async () => [connection], findConnection: async (name) => (name === "local" ? connection : undefined), addConnection: async () => connection, removeConnection: async () => true, listTables: async () => [{ name: "users", type: "BASE TABLE" }], describeTable: async () => [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, comment: null }], executeQuery: async () => ({ columns: ["total"], rows: [{ total: 1 }], row_count: 1 }), }; async function withScopedEnv(env: Record, fn: () => T | Promise): Promise { const oldValues = new Map(); for (const key of Object.keys(env)) { oldValues.set(key, process.env[key]); process.env[key] = env[key]; } try { return await fn(); } finally { for (const [key, value] of oldValues) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } } } test("creates an MCP server without starting stdio transport", () => { const server = createDbxMcpServer(backend, { isWebMode: true }); assert.equal(typeof server.connect, "function"); }); test("MCP server metadata version matches package metadata", () => { const server = createDbxMcpServer(backend, { isWebMode: true }); assert.equal((server as any).server._serverInfo.version, DBX_MCP_PACKAGE_VERSION); }); test("README runtime requirements match package engines", async () => { const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf-8")) as { engines: { node: string }; }; const readme = await readFile(new URL("../README.md", import.meta.url), "utf-8"); const minimumNodeVersion = packageJson.engines.node.replace(">=", ""); assert.match(readme, new RegExp(`Node\\.js ${minimumNodeVersion.replace(/\./g, "\\.")} or newer`)); assert.match(readme, new RegExp(`Node\\.js ${minimumNodeVersion.replace(/\./g, "\\.")} 或更高版本`)); }); test("execute query scopes the connection to the requested database", async () => { let usedDatabase = ""; const scopedBackend: Backend = { ...backend, executeQuery: async (config) => { usedDatabase = config.database || ""; return { columns: ["total"], rows: [{ total: 1 }], row_count: 1 }; }, }; const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); await (server as any)._registeredTools.dbx_execute_query.handler({ connection_name: "local", database: "stores_demo", sql: "SELECT FIRST 1 tabname FROM systables", }); assert.equal(usedDatabase, "stores_demo"); }); test("execute query runs safe multi-statement SQL one statement at a time", async () => { const executed: string[] = []; const scopedBackend: Backend = { ...backend, executeQuery: async (_config, sql) => { executed.push(sql); return { columns: ["value"], rows: [{ value: executed.length }], row_count: 1 }; }, }; const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_execute_query.handler({ connection_name: "local", sql: "select 1; select 2;", }); assert.deepEqual(executed, ["select 1", "select 2"]); assert.match(result.content[0].text, /Statement 1/); assert.match(result.content[0].text, /Statement 2/); }); test("execute query reports the blocked statement number for unsafe multi-statement SQL", async () => { const server = createDbxMcpServer(backend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_execute_query.handler({ connection_name: "local", sql: "select 1; delete from users;", }); assert.equal(result.isError, true); assert.match(result.content[0].text, /SQL_BLOCKED:/); assert.match(result.content[0].text, /Statement 2/); assert.match(result.content[0].text, /WHERE/); }); test("scoped MCP lists only the active connection", async () => { const other: ConnectionConfig = { ...connection, id: "2", name: "other", database: "other_db" }; const scopedBackend: Backend = { ...backend, loadConnections: async () => [connection, other], }; const result = await withScopedEnv({ DBX_MCP_SCOPE_CONNECTION_ID: "1" }, () => { const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); return (server as any)._registeredTools.dbx_list_connections.handler({}); }); assert.match(result.content[0].text, /local/); assert.doesNotMatch(result.content[0].text, /other/); }); test("scoped MCP rejects out-of-scope connection tool calls", async () => { const result = await withScopedEnv({ DBX_MCP_SCOPE_CONNECTION_ID: "1" }, () => { const server = createDbxMcpServer(backend, { isWebMode: true }); return (server as any)._registeredTools.dbx_list_tables.handler({ connection_name: "other" }); }); assert.equal(result.isError, true); assert.match(result.content[0].text, /CONNECTION_OUT_OF_SCOPE:/); }); test("scoped MCP defaults connection-taking tools to the active connection and database", async () => { let usedDatabase = ""; const scopedBackend: Backend = { ...backend, listTables: async (config) => { usedDatabase = config.database || ""; return [{ name: "users", type: "BASE TABLE" }]; }, }; const result = await withScopedEnv({ DBX_MCP_SCOPE_CONNECTION_ID: "1", DBX_MCP_SCOPE_DATABASE: "scoped_db" }, () => { const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); return (server as any)._registeredTools.dbx_list_tables.handler({}); }); assert.match(result.content[0].text, /users/); assert.equal(usedDatabase, "scoped_db"); }); test("scoped MCP does not register mutation or desktop bridge tools", async () => { await withScopedEnv({ DBX_MCP_SCOPE_CONNECTION_ID: "1" }, () => { const server = createDbxMcpServer(backend, { isWebMode: false }); const tools = (server as any)._registeredTools; assert.equal(tools.dbx_add_connection, undefined); assert.equal(tools.dbx_remove_connection, undefined); assert.equal(tools.dbx_open_table, undefined); assert.equal(tools.dbx_execute_and_show, undefined); }); }); test("scoped MCP with writes disabled blocks write SQL", async () => { const result = await withScopedEnv({ DBX_MCP_SCOPE_CONNECTION_ID: "1", DBX_MCP_ALLOW_WRITES: "0" }, () => { const server = createDbxMcpServer(backend, { isWebMode: true }); return (server as any)._registeredTools.dbx_execute_query.handler({ sql: "update users set name = 'x' where id = 1", }); }); assert.equal(result.isError, true); assert.match(result.content[0].text, /SQL_BLOCKED:/); }); test("redis execute query points callers to the redis command tool", async () => { const redisConnection: ConnectionConfig = { ...connection, db_type: "redis", database: "0" }; const scopedBackend: Backend = { ...backend, findConnection: async () => redisConnection, }; const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_execute_query.handler({ connection_name: "local", sql: "GET session:1", }); assert.equal(result.isError, true); assert.match(result.content[0].text, /REDIS_COMMAND_REQUIRED:/); assert.match(result.content[0].text, /dbx_execute_redis_command/); }); test("redis command tool executes redis commands on the selected database", async () => { const redisConnection: ConnectionConfig = { ...connection, db_type: "redis", database: "2" }; let usedDb = -1; let usedCommand = ""; const scopedBackend: Backend = { ...backend, findConnection: async () => redisConnection, executeRedisCommand: async (_config, db, command) => { usedDb = db; usedCommand = command; return { command: "GET", safety: "allowed", value: "value-1" }; }, }; const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_execute_redis_command.handler({ connection_name: "local", command: "GET session:1", }); assert.equal(result.isError, undefined); assert.equal(usedDb, 2); assert.equal(usedCommand, "GET session:1"); assert.match(result.content[0].text, /Command: GET/); assert.match(result.content[0].text, /value-1/); }); test("redis command tool blocks write commands in read-only MCP sessions", async () => { let executed = false; const redisConnection: ConnectionConfig = { ...connection, db_type: "redis" }; const scopedBackend: Backend = { ...backend, findConnection: async () => redisConnection, executeRedisCommand: async () => { executed = true; return { command: "SET", safety: "confirm", value: "OK" }; }, }; const result = await withScopedEnv({ DBX_MCP_ALLOW_WRITES: "0" }, () => { const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); return (server as any)._registeredTools.dbx_execute_redis_command.handler({ connection_name: "local", command: "SET session:1 value", }); }); assert.equal(executed, false); assert.equal(result.isError, true); assert.match(result.content[0].text, /REDIS_COMMAND_BLOCKED:/); }); test("redis command tool allows dangerous redis commands only when explicitly enabled", async () => { const redisConnection: ConnectionConfig = { ...connection, db_type: "redis" }; let skipSafetyCheck = false; const scopedBackend: Backend = { ...backend, findConnection: async () => redisConnection, executeRedisCommand: async (_config, _db, _command, options) => { skipSafetyCheck = options?.skipSafetyCheck ?? false; return { command: "KEYS", safety: "blocked", value: ["session:1"] }; }, }; const blocked = await withScopedEnv({ DBX_MCP_ALLOW_DANGEROUS_SQL: "0" }, () => { const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); return (server as any)._registeredTools.dbx_execute_redis_command.handler({ connection_name: "local", command: "KEYS *", }); }); const allowed = await withScopedEnv({ DBX_MCP_ALLOW_DANGEROUS_SQL: "1" }, () => { const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); return (server as any)._registeredTools.dbx_execute_redis_command.handler({ connection_name: "local", command: "KEYS *", }); }); assert.equal(blocked.isError, true); assert.match(blocked.content[0].text, /REDIS_COMMAND_BLOCKED:/); assert.equal(allowed.isError, undefined); assert.equal(skipSafetyCheck, true); assert.match(allowed.content[0].text, /session:1/); }); test("mongodb list tables returns collections from the selected database", async () => { let usedDatabase = ""; const mongoConnection: ConnectionConfig = { ...connection, db_type: "mongodb", database: "admin" }; const scopedBackend: Backend = { ...backend, findConnection: async () => mongoConnection, listTables: async (config) => { usedDatabase = config.database || ""; return [{ name: "projects", type: "COLLECTION" }]; }, }; const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_list_tables.handler({ connection_name: "local", database: "pystrument", }); assert.equal(usedDatabase, "pystrument"); assert.match(result.content[0].text, /projects/); assert.match(result.content[0].text, /COLLECTION/); }); test("mongodb describe table returns inferred document fields", async () => { const mongoConnection: ConnectionConfig = { ...connection, db_type: "mongodb" }; const scopedBackend: Backend = { ...backend, findConnection: async () => mongoConnection, describeTable: async () => [ { name: "_id", data_type: "object", is_nullable: false, column_default: null, is_primary_key: true, comment: null, }, { name: "name", data_type: "string", is_nullable: false, column_default: null, is_primary_key: false, comment: null, }, ], }; const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_describe_table.handler({ connection_name: "local", database: "pystrument", table: "projects", }); assert.match(result.content[0].text, /_id \(PK\)/); assert.match(result.content[0].text, /name/); }); test("mongodb execute query formats shell-style find results", async () => { const mongoConnection: ConnectionConfig = { ...connection, db_type: "mongodb" }; const scopedBackend: Backend = { ...backend, findConnection: async () => mongoConnection, executeQuery: async () => ({ columns: ["_id", "meta", "missing"], rows: [{ _id: "1", meta: { name: "demo" }, missing: null }], row_count: 1, }), }; const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_execute_query.handler({ connection_name: "local", database: "pystrument", sql: "db.projects.find({}).limit(1)", }); assert.match(result.content[0].text, /"name":"demo"/); assert.match(result.content[0].text, /NULL/); assert.match(result.content[0].text, /1 row\(s\)/); }); test("connection lookup failures include a stable MCP error code", async () => { const server = createDbxMcpServer(backend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_list_tables.handler({ connection_name: "missing", }); assert.equal(result.isError, true); assert.match(result.content[0].text, /CONNECTION_NOT_FOUND:/); assert.match(result.content[0].text, /missing/); }); test("add connection accepts H2 file paths without a port", async () => { let added: Omit | undefined; const scopedBackend: Backend = { ...backend, findConnection: async () => undefined, addConnection: async (config) => { added = config; return { id: "h2-file", ...config }; }, }; const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_add_connection.handler({ name: "h2-local", db_type: "h2", host: "/data/app.mv.db", username: "sa", password: "", }); assert.equal(result.isError, undefined); assert.equal(added?.db_type, "h2"); assert.equal(added?.host, "/data/app.mv.db"); assert.equal(added?.port, 0); }); test("SQL safety failures include a stable MCP error code", async () => { const server = createDbxMcpServer(backend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_execute_query.handler({ connection_name: "local", sql: "drop table users", }); assert.equal(result.isError, true); assert.match(result.content[0].text, /SQL_BLOCKED:/); assert.match(result.content[0].text, /Dangerous SQL/); }); test("query exceptions include a stable MCP error code", async () => { const scopedBackend: Backend = { ...backend, executeQuery: async () => { throw new Error("database timeout"); }, }; const server = createDbxMcpServer(scopedBackend, { isWebMode: true }); const result = await (server as any)._registeredTools.dbx_execute_query.handler({ connection_name: "local", sql: "select 1", }); assert.equal(result.isError, true); assert.match(result.content[0].text, /QUERY_ERROR: database timeout/); }); test("desktop bridge failures include a stable MCP error code", async () => { const oldHome = process.env.HOME; const dir = await mkdtemp(join(tmpdir(), "dbx-mcp-home-")); process.env.HOME = dir; try { const server = createDbxMcpServer(backend, { isWebMode: false }); const result = await (server as any)._registeredTools.dbx_open_table.handler({ connection_name: "local", table: "users", }); assert.equal(result.isError, true); assert.match(result.content[0].text, /DBX_NOT_RUNNING:/); assert.match(result.content[0].text, /DBX is not running/); } finally { if (oldHome === undefined) delete process.env.HOME; else process.env.HOME = oldHome; await rm(dir, { recursive: true, force: true }); } }); test("mongodb execute-and-show blocks aggregate write stages before desktop bridge", async () => { const oldAllowWrites = process.env.DBX_MCP_ALLOW_WRITES; const oldAllowDangerous = process.env.DBX_MCP_ALLOW_DANGEROUS_SQL; delete process.env.DBX_MCP_ALLOW_WRITES; delete process.env.DBX_MCP_ALLOW_DANGEROUS_SQL; const mongoConnection: ConnectionConfig = { ...connection, db_type: "mongodb" }; const scopedBackend: Backend = { ...backend, findConnection: async () => mongoConnection, }; const server = createDbxMcpServer(scopedBackend, { isWebMode: false }); try { const result = await (server as any)._registeredTools.dbx_execute_and_show.handler({ connection_name: "local", database: "pystrument", sql: 'db.projects.aggregate([{"$out":"projects_dump"}])', }); assert.match(result.content[0].text, /SQL_BLOCKED:/); assert.match(result.content[0].text, /DBX_MCP_ALLOW_DANGEROUS_SQL=1/); } finally { if (oldAllowWrites === undefined) delete process.env.DBX_MCP_ALLOW_WRITES; else process.env.DBX_MCP_ALLOW_WRITES = oldAllowWrites; if (oldAllowDangerous === undefined) delete process.env.DBX_MCP_ALLOW_DANGEROUS_SQL; else process.env.DBX_MCP_ALLOW_DANGEROUS_SQL = oldAllowDangerous; } });