feat: add MCP Server for AI agent database access
TypeScript MCP Server that reads DBX connection configs and exposes 5 tools: list connections, list tables, describe table, execute query, and open table in DBX UI. Published as @dbx-app/mcp-server on npm.
This commit is contained in:
parent
aa59c26fa2
commit
09f32d7316
|
|
@ -0,0 +1,186 @@
|
|||
# DBX MCP Server
|
||||
|
||||
MCP server for [DBX](https://github.com/t8y2/dbx) — lets AI agents (Claude Code, Cursor, etc.) query your databases using connections already configured in DBX.
|
||||
|
||||
[中文](#中文说明) | English
|
||||
|
||||
## Features
|
||||
|
||||
- **Zero config** — Automatically reads your DBX connections (including passwords from system keyring)
|
||||
- **5 tools** — List connections, list tables, describe table, execute SQL, open table in DBX UI
|
||||
- **Connection pooling** — Reuses database connections across queries
|
||||
- **PostgreSQL & MySQL** — Supports PostgreSQL, MySQL, and compatible databases (Doris, StarRocks, etc.)
|
||||
- **DBX UI integration** — Open tables directly in the DBX desktop app from your AI agent
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install
|
||||
|
||||
```bash
|
||||
npm install -g @dbx-app/mcp-server
|
||||
```
|
||||
|
||||
Or run directly:
|
||||
|
||||
```bash
|
||||
npx @dbx-app/mcp-server
|
||||
```
|
||||
|
||||
### 2. Configure Claude Code
|
||||
|
||||
Add to your project's `.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"dbx": {
|
||||
"command": "dbx-mcp-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or for development (from source):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"dbx": {
|
||||
"command": "npx",
|
||||
"args": ["tsx", "mcp/src/index.ts"],
|
||||
"cwd": "/path/to/dbx"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use
|
||||
|
||||
In Claude Code, just ask:
|
||||
|
||||
- "List my database connections"
|
||||
- "Show the tables in my local-pg connection"
|
||||
- "Describe the users table"
|
||||
- "Query the average salary from employees"
|
||||
- "Open the orders table in DBX"
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `dbx_list_connections` | List all database connections configured in DBX |
|
||||
| `dbx_list_tables` | List tables and views for a connection |
|
||||
| `dbx_describe_table` | Get column definitions for a table |
|
||||
| `dbx_execute_query` | Execute a SQL query (max 100 rows) |
|
||||
| `dbx_open_table` | Open a table in DBX desktop app UI |
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
AI Agent → MCP Server → Database
|
||||
↓
|
||||
DBX connections.json
|
||||
+ system keyring (passwords)
|
||||
```
|
||||
|
||||
The MCP server reads your database connections from DBX's config directory:
|
||||
|
||||
- **macOS**: `~/Library/Application Support/com.dbx.app/connections.json`
|
||||
- **Linux**: `~/.config/com.dbx.app/connections.json`
|
||||
- **Windows**: `%APPDATA%\com.dbx.app\connections.json`
|
||||
|
||||
Passwords are retrieved from the system keyring (macOS Keychain / Linux Secret Service / Windows Credential Manager).
|
||||
|
||||
## DBX UI Integration
|
||||
|
||||
The `dbx_open_table` tool communicates with the running DBX app to open tables directly in the UI. This requires DBX to be running. If DBX is not running, the tool will return an error message.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [DBX](https://github.com/t8y2/dbx) installed with at least one connection configured
|
||||
- Node.js 18+
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
---
|
||||
|
||||
## 中文说明
|
||||
|
||||
[DBX](https://github.com/t8y2/dbx) 的 MCP Server,让 AI 编程助手(Claude Code、Cursor 等)直接使用 DBX 中已配置的数据库连接查询数据。
|
||||
|
||||
### 特性
|
||||
|
||||
- **零配置** — 自动读取 DBX 的连接配置(包括系统钥匙串中的密码)
|
||||
- **5 个工具** — 列出连接、列出表、查看表结构、执行 SQL、在 DBX 中打开表
|
||||
- **连接池** — 跨查询复用数据库连接
|
||||
- **PostgreSQL 和 MySQL** — 支持 PostgreSQL、MySQL 及兼容数据库(Doris、StarRocks 等)
|
||||
- **DBX UI 联动** — 从 AI 助手直接在 DBX 桌面端打开表
|
||||
|
||||
### 快速开始
|
||||
|
||||
#### 1. 安装
|
||||
|
||||
```bash
|
||||
npm install -g @dbx-app/mcp-server
|
||||
```
|
||||
|
||||
或直接运行:
|
||||
|
||||
```bash
|
||||
npx @dbx-app/mcp-server
|
||||
```
|
||||
|
||||
#### 2. 配置 Claude Code
|
||||
|
||||
在项目的 `.mcp.json` 中添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"dbx": {
|
||||
"command": "dbx-mcp-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 使用
|
||||
|
||||
在 Claude Code 中直接说:
|
||||
|
||||
- "列出我的数据库连接"
|
||||
- "查看 local-pg 上有哪些表"
|
||||
- "查看 users 表的结构"
|
||||
- "查询最近 7 天的订单数量"
|
||||
- "打开 orders 表"
|
||||
|
||||
### 工具列表
|
||||
|
||||
| 工具 | 说明 |
|
||||
|---|---|
|
||||
| `dbx_list_connections` | 列出 DBX 中所有已配置的数据库连接 |
|
||||
| `dbx_list_tables` | 列出指定连接的表和视图 |
|
||||
| `dbx_describe_table` | 获取表的列定义 |
|
||||
| `dbx_execute_query` | 执行 SQL 查询(最多返回 100 行) |
|
||||
| `dbx_open_table` | 在 DBX 桌面端打开指定表 |
|
||||
|
||||
### 工作原理
|
||||
|
||||
MCP Server 从 DBX 的配置目录读取连接信息:
|
||||
|
||||
- **macOS**: `~/Library/Application Support/com.dbx.app/connections.json`
|
||||
- **Linux**: `~/.config/com.dbx.app/connections.json`
|
||||
- **Windows**: `%APPDATA%\com.dbx.app\connections.json`
|
||||
|
||||
密码从系统钥匙串中获取(macOS Keychain / Linux Secret Service / Windows 凭据管理器)。
|
||||
|
||||
### DBX UI 联动
|
||||
|
||||
`dbx_open_table` 工具通过本地 HTTP 接口与运行中的 DBX 应用通信,直接在 UI 中打开表。需要 DBX 正在运行。
|
||||
|
||||
### 系统要求
|
||||
|
||||
- 已安装 [DBX](https://github.com/t8y2/dbx) 并配置了至少一个数据库连接
|
||||
- Node.js 18+
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "@dbx-app/mcp-server",
|
||||
"version": "0.1.1",
|
||||
"description": "MCP server for DBX — query databases from Claude Code, Cursor, and other AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"dbx-mcp-server": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "tsx src/index.ts",
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "tsc"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"database",
|
||||
"dbx",
|
||||
"claude",
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"ai-agent"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/t8y2/dbx",
|
||||
"directory": "mcp"
|
||||
},
|
||||
"homepage": "https://github.com/t8y2/dbx/tree/main/mcp",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"keytar": "^7.9.0",
|
||||
"mysql2": "^3.14.1",
|
||||
"pg": "^8.16.0",
|
||||
"zod": "^3.25.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/pg": "^8.15.4",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { homedir, platform } from "node:os";
|
||||
|
||||
export interface ConnectionConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
db_type: string;
|
||||
driver_profile?: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database?: string;
|
||||
url_params?: string;
|
||||
ssh_enabled: boolean;
|
||||
ssl: boolean;
|
||||
}
|
||||
|
||||
const KEYRING_SERVICE = "dev.dbx.connections";
|
||||
|
||||
function appDataDir(): 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 getKeytarSecret(connectionId: string, key: string): Promise<string | null> {
|
||||
try {
|
||||
const keytar = await import("keytar");
|
||||
return await keytar.default.getPassword(KEYRING_SERVICE, `connection:${connectionId}:${key}`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getFileSecret(connectionId: string, key: string): Promise<string | null> {
|
||||
try {
|
||||
const secretsPath = join(appDataDir(), "secrets.json");
|
||||
const data = JSON.parse(await readFile(secretsPath, "utf-8"));
|
||||
return data[`connection:${connectionId}:${key}`] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getSecret(connectionId: string, key: string): Promise<string> {
|
||||
const fromKeyring = await getKeytarSecret(connectionId, key);
|
||||
if (fromKeyring) return fromKeyring;
|
||||
const fromFile = await getFileSecret(connectionId, key);
|
||||
return fromFile ?? "";
|
||||
}
|
||||
|
||||
export async function loadConnections(): Promise<ConnectionConfig[]> {
|
||||
const configPath = join(appDataDir(), "connections.json");
|
||||
try {
|
||||
const raw = await readFile(configPath, "utf-8");
|
||||
const configs: ConnectionConfig[] = JSON.parse(raw);
|
||||
for (const config of configs) {
|
||||
if (!config.password) config.password = await getSecret(config.id, "password");
|
||||
}
|
||||
return configs;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function findConnection(name: string): Promise<ConnectionConfig | undefined> {
|
||||
const connections = await loadConnections();
|
||||
return connections.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
import type { ConnectionConfig } from "./connections.js";
|
||||
|
||||
export interface TableInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ColumnInfo {
|
||||
name: string;
|
||||
data_type: string;
|
||||
is_nullable: boolean;
|
||||
column_default: string | null;
|
||||
is_primary_key: boolean;
|
||||
comment: string | null;
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
columns: string[];
|
||||
rows: Record<string, unknown>[];
|
||||
row_count: number;
|
||||
}
|
||||
|
||||
const MAX_ROWS = 100;
|
||||
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const QUERY_TIMEOUT_MS = 30_000;
|
||||
|
||||
interface PoolEntry {
|
||||
type: "pg" | "mysql";
|
||||
pool: unknown;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
const pools = new Map<string, PoolEntry>();
|
||||
|
||||
function poolKey(config: ConnectionConfig): string {
|
||||
return `${config.id}:${config.database || ""}`;
|
||||
}
|
||||
|
||||
function evictPool(key: string, entry: PoolEntry) {
|
||||
pools.delete(key);
|
||||
if (entry.type === "pg") {
|
||||
(entry.pool as import("pg").Pool).end().catch(() => {});
|
||||
} else {
|
||||
(entry.pool as import("mysql2/promise").Pool).end().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function resetIdleTimer(key: string, entry: PoolEntry) {
|
||||
clearTimeout(entry.timer);
|
||||
entry.timer = setTimeout(() => evictPool(key, entry), IDLE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
async function getPgPool(config: ConnectionConfig): Promise<import("pg").Pool> {
|
||||
const key = poolKey(config);
|
||||
const existing = pools.get(key);
|
||||
if (existing?.type === "pg") {
|
||||
resetIdleTimer(key, existing);
|
||||
return existing.pool as import("pg").Pool;
|
||||
}
|
||||
|
||||
const pg = await import("pg");
|
||||
const pool = new pg.default.Pool({
|
||||
connectionString: buildConnectionUrl(config),
|
||||
max: 3,
|
||||
idleTimeoutMillis: 30_000,
|
||||
connectionTimeoutMillis: 10_000,
|
||||
});
|
||||
pool.on("error", () => {});
|
||||
const entry: PoolEntry = { type: "pg", pool, timer: setTimeout(() => {}, 0) };
|
||||
pools.set(key, entry);
|
||||
resetIdleTimer(key, entry);
|
||||
return pool;
|
||||
}
|
||||
|
||||
async function getMysqlPool(config: ConnectionConfig): Promise<import("mysql2/promise").Pool> {
|
||||
const key = poolKey(config);
|
||||
const existing = pools.get(key);
|
||||
if (existing?.type === "mysql") {
|
||||
resetIdleTimer(key, existing);
|
||||
return existing.pool as import("mysql2/promise").Pool;
|
||||
}
|
||||
|
||||
const mysql = await import("mysql2/promise");
|
||||
const pool = mysql.default.createPool({
|
||||
uri: buildConnectionUrl(config),
|
||||
connectionLimit: 3,
|
||||
idleTimeout: 30_000,
|
||||
connectTimeout: 10_000,
|
||||
});
|
||||
const entry: PoolEntry = { type: "mysql", pool, timer: setTimeout(() => {}, 0) };
|
||||
pools.set(key, entry);
|
||||
resetIdleTimer(key, entry);
|
||||
return pool;
|
||||
}
|
||||
|
||||
function buildConnectionUrl(config: ConnectionConfig): string {
|
||||
const db = config.database || "";
|
||||
const params = config.url_params || "";
|
||||
const suffix = params ? `?${params}` : "";
|
||||
if (isMysqlType(config.db_type)) {
|
||||
return `mysql://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${config.host}:${config.port}/${db}${suffix}`;
|
||||
}
|
||||
return `postgres://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${config.host}:${config.port}/${db}${suffix}`;
|
||||
}
|
||||
|
||||
function isMysqlType(dbType: string): boolean {
|
||||
return dbType === "mysql" || dbType === "doris" || dbType === "starrocks";
|
||||
}
|
||||
|
||||
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);
|
||||
promise.then(resolve, reject).finally(() => clearTimeout(timer));
|
||||
});
|
||||
}
|
||||
|
||||
async function queryWithRetry(config: ConnectionConfig, fn: () => Promise<QueryResult>): Promise<QueryResult> {
|
||||
try {
|
||||
return await withTimeout(fn(), QUERY_TIMEOUT_MS);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
const retriable = /terminating connection|Connection lost|ECONNRESET|EPIPE|connection refused/i.test(msg);
|
||||
if (retriable) {
|
||||
const key = poolKey(config);
|
||||
const entry = pools.get(key);
|
||||
if (entry) evictPool(key, entry);
|
||||
return withTimeout(fn(), QUERY_TIMEOUT_MS);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function pgQuery(config: ConnectionConfig, sql: string, params?: unknown[]): Promise<QueryResult> {
|
||||
return queryWithRetry(config, async () => {
|
||||
const pool = await getPgPool(config);
|
||||
const result = await pool.query(sql, params);
|
||||
const rows = (result.rows || []).slice(0, MAX_ROWS);
|
||||
return { columns: result.fields?.map((f) => f.name) ?? [], rows, row_count: rows.length };
|
||||
});
|
||||
}
|
||||
|
||||
async function mysqlQuery(config: ConnectionConfig, sql: string, params?: unknown[]): Promise<QueryResult> {
|
||||
return queryWithRetry(config, async () => {
|
||||
const pool = await getMysqlPool(config);
|
||||
const [results, fields] = await pool.query(sql, params);
|
||||
const rows = (Array.isArray(results) ? results : []).slice(0, MAX_ROWS) as Record<string, unknown>[];
|
||||
return { columns: (fields as Array<{ name: string }>)?.map((f) => f.name) ?? [], rows, row_count: rows.length };
|
||||
});
|
||||
}
|
||||
|
||||
async function query(config: ConnectionConfig, sql: string, params?: unknown[]): Promise<QueryResult> {
|
||||
if (isMysqlType(config.db_type)) return mysqlQuery(config, sql, params);
|
||||
return pgQuery(config, sql, params);
|
||||
}
|
||||
|
||||
export async function executeQuery(config: ConnectionConfig, sql: string): Promise<QueryResult> {
|
||||
return query(config, sql);
|
||||
}
|
||||
|
||||
export async function listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]> {
|
||||
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`);
|
||||
} else {
|
||||
result = await query(
|
||||
config,
|
||||
`SELECT table_name AS name, table_type AS type FROM information_schema.tables WHERE table_schema = $1 ORDER BY table_name`,
|
||||
[schema || "public"],
|
||||
);
|
||||
}
|
||||
return result.rows.map((r) => ({ name: String(r.name || r.NAME), type: String(r.type || r.TYPE || "TABLE") }));
|
||||
}
|
||||
|
||||
export async function describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]> {
|
||||
let result: QueryResult;
|
||||
if (isMysqlType(config.db_type)) {
|
||||
result = await query(
|
||||
config,
|
||||
`SELECT c.COLUMN_NAME AS name, c.DATA_TYPE AS data_type, c.IS_NULLABLE = 'YES' AS is_nullable, c.COLUMN_DEFAULT AS column_default, c.COLUMN_KEY = 'PRI' AS is_primary_key, c.COLUMN_COMMENT AS comment FROM information_schema.COLUMNS c WHERE c.TABLE_SCHEMA = DATABASE() AND c.TABLE_NAME = ? ORDER BY c.ORDINAL_POSITION`,
|
||||
[table],
|
||||
);
|
||||
} else {
|
||||
result = await query(
|
||||
config,
|
||||
`SELECT c.column_name AS name, c.data_type, c.is_nullable = 'YES' AS is_nullable, c.column_default, CASE WHEN tc.constraint_type = 'PRIMARY KEY' THEN true ELSE false END AS is_primary_key, col_description(cls.oid, c.ordinal_position) AS comment FROM information_schema.columns c LEFT JOIN information_schema.key_column_usage kcu ON kcu.table_schema = c.table_schema AND kcu.table_name = c.table_name AND kcu.column_name = c.column_name LEFT JOIN information_schema.table_constraints tc ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema AND tc.constraint_type = 'PRIMARY KEY' LEFT JOIN pg_class cls ON cls.relname = c.table_name AND cls.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema) WHERE c.table_schema = $1 AND c.table_name = $2 ORDER BY c.ordinal_position`,
|
||||
[schema || "public", table],
|
||||
);
|
||||
}
|
||||
return result.rows.map((r) => ({
|
||||
name: String(r.name || ""),
|
||||
data_type: String(r.data_type || ""),
|
||||
is_nullable: Boolean(r.is_nullable),
|
||||
column_default: r.column_default != null ? String(r.column_default) : null,
|
||||
is_primary_key: Boolean(r.is_primary_key),
|
||||
comment: r.comment != null ? String(r.comment) : null,
|
||||
}));
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env node
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { loadConnections, findConnection } from "./connections.js";
|
||||
import { listTables, describeTable, executeQuery } from "./database.js";
|
||||
|
||||
const server = new McpServer({
|
||||
name: "dbx",
|
||||
version: "0.1.0",
|
||||
});
|
||||
|
||||
server.tool(
|
||||
"dbx_list_connections",
|
||||
"List all database connections configured in DBX",
|
||||
{},
|
||||
async () => {
|
||||
const connections = await loadConnections();
|
||||
const list = connections.map((c) => ({
|
||||
name: c.name,
|
||||
type: c.db_type,
|
||||
host: c.host,
|
||||
port: c.port,
|
||||
database: c.database || "",
|
||||
}));
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(list, null, 2) }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"dbx_list_tables",
|
||||
"List tables and views for a database connection",
|
||||
{
|
||||
connection_name: z.string().describe("Name of the DBX connection"),
|
||||
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
|
||||
},
|
||||
async ({ connection_name, schema }) => {
|
||||
const config = await findConnection(connection_name);
|
||||
if (!config) return { content: [{ type: "text" as const, text: `Connection "${connection_name}" not found` }] };
|
||||
const tables = await listTables(config, schema);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(tables, null, 2) }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"dbx_describe_table",
|
||||
"Get column definitions for a table",
|
||||
{
|
||||
connection_name: z.string().describe("Name of the DBX connection"),
|
||||
table: z.string().describe("Table name"),
|
||||
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
|
||||
},
|
||||
async ({ connection_name, table, schema }) => {
|
||||
const config = await findConnection(connection_name);
|
||||
if (!config) return { content: [{ type: "text" as const, text: `Connection "${connection_name}" not found` }] };
|
||||
const columns = await describeTable(config, table, schema);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(columns, null, 2) }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"dbx_execute_query",
|
||||
"Execute a SQL query on a database connection (max 100 rows returned)",
|
||||
{
|
||||
connection_name: z.string().describe("Name of the DBX connection"),
|
||||
sql: z.string().describe("SQL query to execute"),
|
||||
},
|
||||
async ({ connection_name, sql }) => {
|
||||
const config = await findConnection(connection_name);
|
||||
if (!config) return { content: [{ type: "text" as const, text: `Connection "${connection_name}" not found` }] };
|
||||
try {
|
||||
const result = await executeQuery(config, sql);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return { content: [{ type: "text" as const, text: `Query error: ${msg}` }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { homedir, platform } from "node:os";
|
||||
|
||||
function appDataDir(): 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 getBridgeUrl(): Promise<string> {
|
||||
const portFile = join(appDataDir(), "mcp-bridge-port");
|
||||
const port = (await readFile(portFile, "utf-8")).trim();
|
||||
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 }) => {
|
||||
try {
|
||||
const bridgeUrl = await getBridgeUrl();
|
||||
const res = await fetch(`${bridgeUrl}/open-table`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ connection_name, table, database, schema }),
|
||||
});
|
||||
if (res.ok) {
|
||||
return { content: [{ type: "text" as const, text: `Opened ${table} in DBX` }] };
|
||||
}
|
||||
const text = await res.text();
|
||||
return { content: [{ type: "text" as const, text: `Failed: ${text}` }] };
|
||||
} catch {
|
||||
return { content: [{ type: "text" as const, text: "DBX is not running. Please start DBX first." }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("MCP Server failed to start:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Loading…
Reference in New Issue