From af363739536e591e895de3100f9fed9b526ba43f Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 5 May 2026 04:24:10 +0800 Subject: [PATCH] feat: implement web backend for API interactions and connection management --- mcp/src/index.ts | 101 +++++++++++++++++++++++------------- mcp/src/web-backend.ts | 113 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 34 deletions(-) create mode 100644 mcp/src/web-backend.ts diff --git a/mcp/src/index.ts b/mcp/src/index.ts index 734ae6da9f..09f268872a 100644 --- a/mcp/src/index.ts +++ b/mcp/src/index.ts @@ -5,8 +5,38 @@ import { z } from "zod"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { homedir, platform } from "node:os"; -import { loadConnections, findConnection } from "./connections.js"; -import { listTables, describeTable, executeQuery } from "./database.js"; +import { loadConnections as desktopLoadConnections, findConnection as desktopFindConnection } from "./connections.js"; +import { + listTables as desktopListTables, + describeTable as desktopDescribeTable, + executeQuery as desktopExecuteQuery, +} from "./database.js"; +import type { ConnectionConfig } from "./connections.js"; +import type { TableInfo, ColumnInfo, QueryResult } from "./database.js"; + +const isWebMode = !!process.env.DBX_WEB_URL; + +interface Backend { + loadConnections(): Promise; + findConnection(name: string): Promise; + listTables(config: ConnectionConfig, schema?: string): Promise; + describeTable(config: ConnectionConfig, table: string, schema?: string): Promise; + executeQuery(config: ConnectionConfig, sql: string): Promise; +} + +let backend: Backend; +if (isWebMode) { + const web = await import("./web-backend.js"); + backend = web; +} else { + backend = { + loadConnections: desktopLoadConnections, + findConnection: desktopFindConnection, + listTables: desktopListTables, + describeTable: desktopDescribeTable, + executeQuery: desktopExecuteQuery, + }; +} function text(s: string) { return { content: [{ type: "text" as const, text: s }] }; @@ -30,7 +60,7 @@ server.tool( "List all database connections configured in DBX", {}, async () => { - const connections = await loadConnections(); + const connections = await backend.loadConnections(); if (connections.length === 0) return text("No connections configured in DBX."); const rows = connections.map((c) => [c.name, c.db_type, c.host, String(c.port), c.database || ""]); return text(mdTable(["Name", "Type", "Host", "Port", "Database"], rows)); @@ -45,9 +75,9 @@ server.tool( schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"), }, async ({ connection_name, schema }) => { - const config = await findConnection(connection_name); + const config = await backend.findConnection(connection_name); if (!config) return text(`Connection "${connection_name}" not found`); - const tables = await listTables(config, schema); + const tables = await backend.listTables(config, schema); if (tables.length === 0) return text("No tables found."); const rows = tables.map((t) => [t.name, t.type]); return text(mdTable(["Table", "Type"], rows)); @@ -63,9 +93,9 @@ server.tool( schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"), }, async ({ connection_name, table, schema }) => { - const config = await findConnection(connection_name); + const config = await backend.findConnection(connection_name); if (!config) return text(`Connection "${connection_name}" not found`); - const columns = await describeTable(config, table, schema); + const columns = await backend.describeTable(config, table, schema); if (columns.length === 0) return text("No columns found."); const rows = columns.map((c) => [ c.is_primary_key ? `${c.name} (PK)` : c.name, @@ -86,10 +116,10 @@ server.tool( sql: z.string().describe("SQL query to execute"), }, async ({ connection_name, sql }) => { - const config = await findConnection(connection_name); + const config = await backend.findConnection(connection_name); if (!config) return text(`Connection "${connection_name}" not found`); try { - const result = await executeQuery(config, sql); + const result = await backend.executeQuery(config, sql); if (result.columns.length === 0) return text(`Query executed. ${result.row_count} row(s) affected.`); const rows = result.rows.map((r) => result.columns.map((c) => formatCell(r[c]))); return text(`${mdTable(result.columns, rows)}\n\n${result.row_count} row(s)`); @@ -124,32 +154,35 @@ async function getBridgeUrl(): Promise { return `http://127.0.0.1:${port}`; } -server.tool( - "dbx_open_table", - "Open a table in DBX desktop app UI. Requires DBX to be running.", - { - connection_name: z.string().describe("Name of the DBX connection"), - table: z.string().describe("Table name to open"), - database: z.string().optional().describe("Database name"), - schema: z.string().optional().describe("Schema name"), - }, - async ({ connection_name, table, database, schema }) => { - return bridgeRequest("/open-table", { connection_name, table, database, schema }, `Opened ${table} in DBX`); - }, -); +// Desktop-only tools: open table and execute-and-show require the Tauri bridge +if (!isWebMode) { + server.tool( + "dbx_open_table", + "Open a table in DBX desktop app UI. Requires DBX to be running.", + { + connection_name: z.string().describe("Name of the DBX connection"), + table: z.string().describe("Table name to open"), + database: z.string().optional().describe("Database name"), + schema: z.string().optional().describe("Schema name"), + }, + async ({ connection_name, table, database, schema }) => { + return bridgeRequest("/open-table", { connection_name, table, database, schema }, `Opened ${table} in DBX`); + }, + ); -server.tool( - "dbx_execute_and_show", - "Execute a SQL query in DBX desktop app UI and show results there. Requires DBX to be running.", - { - connection_name: z.string().describe("Name of the DBX connection"), - sql: z.string().describe("SQL query to execute"), - database: z.string().optional().describe("Database name"), - }, - async ({ connection_name, sql, database }) => { - return bridgeRequest("/execute-query", { connection_name, sql, database }, "Query sent to DBX"); - }, -); + server.tool( + "dbx_execute_and_show", + "Execute a SQL query in DBX desktop app UI and show results there. Requires DBX to be running.", + { + connection_name: z.string().describe("Name of the DBX connection"), + sql: z.string().describe("SQL query to execute"), + database: z.string().optional().describe("Database name"), + }, + async ({ connection_name, sql, database }) => { + return bridgeRequest("/execute-query", { connection_name, sql, database }, "Query sent to DBX"); + }, + ); +} async function bridgeRequest(path: string, body: Record, successMsg: string) { try { diff --git a/mcp/src/web-backend.ts b/mcp/src/web-backend.ts new file mode 100644 index 0000000000..f295386ceb --- /dev/null +++ b/mcp/src/web-backend.ts @@ -0,0 +1,113 @@ +import type { ConnectionConfig } from "./connections.js"; +import type { TableInfo, ColumnInfo, QueryResult } from "./database.js"; + +const baseUrl = process.env.DBX_WEB_URL!.replace(/\/+$/, ""); +const password = process.env.DBX_WEB_PASSWORD || ""; + +let sessionCookie: string | null = null; + +async function ensureAuth(): Promise { + if (sessionCookie) return; + if (!password) return; // no password set, assume no auth required + + const res = await fetch(`${baseUrl}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password }), + redirect: "manual", + }); + + if (!res.ok) { + throw new Error(`Authentication failed: ${res.status} ${res.statusText}`); + } + + const setCookie = res.headers.get("set-cookie"); + if (setCookie) { + const match = setCookie.match(/dbx_session=([^;]+)/); + if (match) { + sessionCookie = match[1]; + } + } +} + +function headers(extra?: Record): Record { + const h: Record = { "Content-Type": "application/json", ...extra }; + if (sessionCookie) { + h["Cookie"] = `dbx_session=${sessionCookie}`; + } + return h; +} + +async function apiFetch(path: string, init?: RequestInit): Promise { + await ensureAuth(); + const res = await fetch(`${baseUrl}${path}`, { + ...init, + headers: headers(init?.headers as Record | undefined), + }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`API request ${path} failed: ${res.status} ${res.statusText} ${body}`); + } + return res; +} + +export async function loadConnections(): Promise { + const res = await apiFetch("/api/connection/list"); + return res.json(); +} + +export async function findConnection(name: string): Promise { + const connections = await loadConnections(); + return connections.find((c) => c.name.toLowerCase() === name.toLowerCase()); +} + +async function ensureConnected(config: ConnectionConfig): Promise { + await apiFetch("/api/connection/connect", { + method: "POST", + body: JSON.stringify({ config }), + }); +} + +export async function listTables(config: ConnectionConfig, schema?: string): Promise { + await ensureConnected(config); + const params = new URLSearchParams({ + connection_id: config.id, + database: config.database || "", + schema: schema || "", + }); + const res = await apiFetch(`/api/schema/tables?${params}`); + return res.json(); +} + +export async function describeTable(config: ConnectionConfig, table: string, schema?: string): Promise { + await ensureConnected(config); + const params = new URLSearchParams({ + connection_id: config.id, + database: config.database || "", + schema: schema || "", + table, + }); + const res = await apiFetch(`/api/schema/columns?${params}`); + return res.json(); +} + +export async function executeQuery(config: ConnectionConfig, sql: string): Promise { + await ensureConnected(config); + const res = await apiFetch("/api/query/execute", { + method: "POST", + body: JSON.stringify({ + connectionId: config.id, + database: config.database || "", + sql, + }), + }); + const data = (await res.json()) as { columns: string[]; rows: unknown[][] }; + const rows = data.rows.map((row: unknown[]) => { + const obj: Record = {}; + data.columns.forEach((col: string, i: number) => { + obj[col] = row[i]; + }); + return obj; + }); + return { columns: data.columns, rows, row_count: rows.length }; +}