feat(mcp): add bridge data endpoints for all database types

Add /data/list-tables, /data/describe-table, /data/execute-query
bridge endpoints so the MCP server can delegate queries for non-PG/MySQL
database types (Oracle, H2, Trino, Hive, Neo4j, Cassandra, etc.)
through the DBX desktop app. Direct PG/MySQL connections remain
unchanged. Bump MCP server version to 0.3.0.
This commit is contained in:
t8y2
2026-05-13 18:45:02 +08:00
parent a4ad5cd08b
commit 1937ee9fd3
5 changed files with 248 additions and 7 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@dbx-app/mcp-server",
"version": "0.2.2",
"version": "0.3.0",
"mcpName": "io.github.t8y2/dbx",
"description": "MCP server for DBX \u2014 query databases from Claude Code, Cursor, and other AI agents",
"type": "module",
+2 -2
View File
@@ -6,12 +6,12 @@
"url": "https://github.com/t8y2/dbx",
"source": "github"
},
"version": "0.2.1",
"version": "0.3.0",
"packages": [
{
"registryType": "npm",
"identifier": "@dbx-app/mcp-server",
"version": "0.2.1",
"version": "0.3.0",
"transport": {
"type": "stdio"
}
+121 -1
View File
@@ -1,5 +1,8 @@
import type { ConnectionConfig } from "./connections.js";
import { createServer, connect as netConnect, type Server, type Socket } from "node:net";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { homedir, platform } from "node:os";
export interface TableInfo {
name: string;
@@ -225,6 +228,91 @@ function isMysqlType(dbType: string): boolean {
return dbType === "mysql" || dbType === "doris" || dbType === "starrocks";
}
function isDirectType(dbType: string): boolean {
switch (dbType) {
case "postgres":
case "redshift":
case "mysql":
case "doris":
case "starrocks":
return true;
default:
return false;
}
}
interface BridgeQueryResult {
columns: string[];
rows: unknown[][];
affected_rows: number;
execution_time_ms: number;
truncated: boolean;
}
interface BridgeTableInfo {
name: string;
table_type: string;
comment: string | null;
}
interface BridgeColumnInfo {
name: string;
data_type: string;
is_nullable: boolean;
column_default: string | null;
is_primary_key: boolean;
comment: string | null;
}
function bridgeAppDataDir(): string {
const home = homedir();
switch (platform()) {
case "darwin":
return join(home, "Library", "Application Support", "com.dbx.app");
case "win32":
return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "com.dbx.app");
default:
return join(home, ".config", "com.dbx.app");
}
}
async function bridgeDataRequest<T>(path: string, body: Record<string, unknown>): Promise<T> {
let bridgeUrl: string;
try {
const portFile = join(bridgeAppDataDir(), "mcp-bridge-port");
const port = (await readFile(portFile, "utf-8")).trim();
bridgeUrl = `http://127.0.0.1:${port}`;
} catch {
throw new Error("DBX desktop app is not running. This database type requires DBX to be running for query execution.");
}
const res = await fetch(`${bridgeUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const errBody = await res.text().catch(() => "");
let errorMsg: string;
try {
const parsed = JSON.parse(errBody);
errorMsg = parsed.error || errBody;
} catch {
errorMsg = errBody;
}
throw new Error(errorMsg || `Bridge request failed: ${res.status}`);
}
return res.json() as Promise<T>;
}
function convertBridgeQueryResult(result: BridgeQueryResult): QueryResult {
const rows = result.rows.slice(0, MAX_ROWS).map((row) => {
const obj: Record<string, unknown> = {};
result.columns.forEach((col, i) => { obj[col] = row[i]; });
return obj;
});
return { columns: result.columns, rows, row_count: rows.length };
}
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`Query timed out after ${ms}ms`)), ms);
@@ -272,10 +360,26 @@ async function query(config: ConnectionConfig, sql: string, params?: unknown[]):
}
export async function executeQuery(config: ConnectionConfig, sql: string): Promise<QueryResult> {
return query(config, sql);
if (isDirectType(config.db_type)) {
return query(config, sql);
}
const result = await bridgeDataRequest<BridgeQueryResult>("/data/execute-query", {
connection_name: config.name,
database: config.database || "",
sql,
});
return convertBridgeQueryResult(result);
}
export async function listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]> {
if (!isDirectType(config.db_type)) {
const tables = await bridgeDataRequest<BridgeTableInfo[]>("/data/list-tables", {
connection_name: config.name,
database: config.database || "",
schema: schema || "",
});
return tables.map((t) => ({ name: t.name, type: t.table_type || "TABLE" }));
}
let result: QueryResult;
if (isMysqlType(config.db_type)) {
result = await query(config, `SELECT TABLE_NAME AS name, TABLE_TYPE AS type FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME`);
@@ -290,6 +394,22 @@ export async function listTables(config: ConnectionConfig, schema?: string): Pro
}
export async function describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]> {
if (!isDirectType(config.db_type)) {
const columns = await bridgeDataRequest<BridgeColumnInfo[]>("/data/describe-table", {
connection_name: config.name,
database: config.database || "",
schema: schema || "",
table,
});
return columns.map((c) => ({
name: c.name,
data_type: c.data_type,
is_nullable: c.is_nullable,
column_default: c.column_default,
is_primary_key: c.is_primary_key,
comment: c.comment,
}));
}
let result: QueryResult;
if (isMysqlType(config.db_type)) {
result = await query(
+2 -2
View File
@@ -61,7 +61,7 @@ function mdTable(headers: string[], rows: string[][]): string {
const server = new McpServer({
name: "dbx",
version: "0.2.1",
version: "0.3.0",
});
server.tool(
@@ -144,7 +144,7 @@ server.tool(
"Add a new database connection to DBX",
{
name: z.string().describe("Connection name"),
db_type: z.string().describe("Database type: postgres, mysql, sqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch"),
db_type: z.string().describe("Database type: postgres, mysql, sqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, doris, starrocks, redshift, dameng, kingbase, vastbase, goldendb, gaussdb, h2, snowflake, trino, hive, db2, informix, neo4j, cassandra, bigquery, kylin, sundb, jdbc"),
host: z.string().describe("Database host"),
port: z.number().describe("Database port"),
username: z.string().default("").describe("Username"),