@dbx-app/mcp-server 0.1.0
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/README.md +103 -0
- package/dist/connections.d.ts +16 -0
- package/dist/connections.js +60 -0
- package/dist/database.d.ts +21 -0
- package/dist/database.js +145 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# DBX MCP Server
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Zero config** — Automatically reads your DBX connections (including passwords from system keyring)
|
|
8
|
+
- **5 tools** — List connections, list tables, describe table, execute SQL, open table in DBX UI
|
|
9
|
+
- **Connection pooling** — Reuses database connections across queries
|
|
10
|
+
- **PostgreSQL & MySQL** — Supports PostgreSQL, MySQL, and compatible databases (Doris, StarRocks, etc.)
|
|
11
|
+
- **DBX UI integration** — Open tables directly in the DBX desktop app from your AI agent
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### 1. Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @dbx-app/mcp-server
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or run directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @dbx-app/mcp-server
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. Configure Claude Code
|
|
28
|
+
|
|
29
|
+
Add to your project's `.mcp.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"dbx": {
|
|
35
|
+
"command": "dbx-mcp-server"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or for development (from source):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"dbx": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["tsx", "mcp/src/index.ts"],
|
|
49
|
+
"cwd": "/path/to/dbx"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Use
|
|
56
|
+
|
|
57
|
+
In Claude Code, just ask:
|
|
58
|
+
|
|
59
|
+
- "List my database connections"
|
|
60
|
+
- "Show the tables in my local-pg connection"
|
|
61
|
+
- "Describe the users table"
|
|
62
|
+
- "Query the average salary from employees"
|
|
63
|
+
- "Open the orders table in DBX"
|
|
64
|
+
|
|
65
|
+
## Tools
|
|
66
|
+
|
|
67
|
+
| Tool | Description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `dbx_list_connections` | List all database connections configured in DBX |
|
|
70
|
+
| `dbx_list_tables` | List tables and views for a connection |
|
|
71
|
+
| `dbx_describe_table` | Get column definitions for a table |
|
|
72
|
+
| `dbx_execute_query` | Execute a SQL query (max 100 rows) |
|
|
73
|
+
| `dbx_open_table` | Open a table in DBX desktop app UI |
|
|
74
|
+
|
|
75
|
+
## How It Works
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
AI Agent → MCP Server → Database
|
|
79
|
+
↓
|
|
80
|
+
DBX connections.json
|
|
81
|
+
+ system keyring (passwords)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The MCP server reads your database connections from DBX's config directory:
|
|
85
|
+
|
|
86
|
+
- **macOS**: `~/Library/Application Support/com.dbx.app/connections.json`
|
|
87
|
+
- **Linux**: `~/.config/com.dbx.app/connections.json`
|
|
88
|
+
- **Windows**: `%APPDATA%\com.dbx.app\connections.json`
|
|
89
|
+
|
|
90
|
+
Passwords are retrieved from the system keyring (macOS Keychain / Linux Secret Service / Windows Credential Manager).
|
|
91
|
+
|
|
92
|
+
## DBX UI Integration
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
## Requirements
|
|
97
|
+
|
|
98
|
+
- [DBX](https://github.com/t8y2/dbx) installed with at least one connection configured
|
|
99
|
+
- Node.js 18+
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ConnectionConfig {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
db_type: string;
|
|
5
|
+
driver_profile?: string;
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
username: string;
|
|
9
|
+
password: string;
|
|
10
|
+
database?: string;
|
|
11
|
+
url_params?: string;
|
|
12
|
+
ssh_enabled: boolean;
|
|
13
|
+
ssl: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function loadConnections(): Promise<ConnectionConfig[]>;
|
|
16
|
+
export declare function findConnection(name: string): Promise<ConnectionConfig | undefined>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
const KEYRING_SERVICE = "dev.dbx.connections";
|
|
5
|
+
function appDataDir() {
|
|
6
|
+
const home = homedir();
|
|
7
|
+
switch (platform()) {
|
|
8
|
+
case "darwin":
|
|
9
|
+
return join(home, "Library", "Application Support", "com.dbx.app");
|
|
10
|
+
case "win32":
|
|
11
|
+
return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "com.dbx.app");
|
|
12
|
+
default:
|
|
13
|
+
return join(home, ".config", "com.dbx.app");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function getKeytarSecret(connectionId, key) {
|
|
17
|
+
try {
|
|
18
|
+
const keytar = await import("keytar");
|
|
19
|
+
return await keytar.default.getPassword(KEYRING_SERVICE, `connection:${connectionId}:${key}`);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function getFileSecret(connectionId, key) {
|
|
26
|
+
try {
|
|
27
|
+
const secretsPath = join(appDataDir(), "secrets.json");
|
|
28
|
+
const data = JSON.parse(await readFile(secretsPath, "utf-8"));
|
|
29
|
+
return data[`connection:${connectionId}:${key}`] ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function getSecret(connectionId, key) {
|
|
36
|
+
const fromKeyring = await getKeytarSecret(connectionId, key);
|
|
37
|
+
if (fromKeyring)
|
|
38
|
+
return fromKeyring;
|
|
39
|
+
const fromFile = await getFileSecret(connectionId, key);
|
|
40
|
+
return fromFile ?? "";
|
|
41
|
+
}
|
|
42
|
+
export async function loadConnections() {
|
|
43
|
+
const configPath = join(appDataDir(), "connections.json");
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readFile(configPath, "utf-8");
|
|
46
|
+
const configs = JSON.parse(raw);
|
|
47
|
+
for (const config of configs) {
|
|
48
|
+
if (!config.password)
|
|
49
|
+
config.password = await getSecret(config.id, "password");
|
|
50
|
+
}
|
|
51
|
+
return configs;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function findConnection(name) {
|
|
58
|
+
const connections = await loadConnections();
|
|
59
|
+
return connections.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
|
60
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ConnectionConfig } from "./connections.js";
|
|
2
|
+
export interface TableInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ColumnInfo {
|
|
7
|
+
name: string;
|
|
8
|
+
data_type: string;
|
|
9
|
+
is_nullable: boolean;
|
|
10
|
+
column_default: string | null;
|
|
11
|
+
is_primary_key: boolean;
|
|
12
|
+
comment: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface QueryResult {
|
|
15
|
+
columns: string[];
|
|
16
|
+
rows: Record<string, unknown>[];
|
|
17
|
+
row_count: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function executeQuery(config: ConnectionConfig, sql: string): Promise<QueryResult>;
|
|
20
|
+
export declare function listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]>;
|
|
21
|
+
export declare function describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]>;
|
package/dist/database.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const MAX_ROWS = 100;
|
|
2
|
+
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
3
|
+
const QUERY_TIMEOUT_MS = 30_000;
|
|
4
|
+
const pools = new Map();
|
|
5
|
+
function poolKey(config) {
|
|
6
|
+
return `${config.id}:${config.database || ""}`;
|
|
7
|
+
}
|
|
8
|
+
function evictPool(key, entry) {
|
|
9
|
+
pools.delete(key);
|
|
10
|
+
if (entry.type === "pg") {
|
|
11
|
+
entry.pool.end().catch(() => { });
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
entry.pool.end().catch(() => { });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function resetIdleTimer(key, entry) {
|
|
18
|
+
clearTimeout(entry.timer);
|
|
19
|
+
entry.timer = setTimeout(() => evictPool(key, entry), IDLE_TIMEOUT_MS);
|
|
20
|
+
}
|
|
21
|
+
async function getPgPool(config) {
|
|
22
|
+
const key = poolKey(config);
|
|
23
|
+
const existing = pools.get(key);
|
|
24
|
+
if (existing?.type === "pg") {
|
|
25
|
+
resetIdleTimer(key, existing);
|
|
26
|
+
return existing.pool;
|
|
27
|
+
}
|
|
28
|
+
const pg = await import("pg");
|
|
29
|
+
const pool = new pg.default.Pool({
|
|
30
|
+
connectionString: buildConnectionUrl(config),
|
|
31
|
+
max: 3,
|
|
32
|
+
idleTimeoutMillis: 30_000,
|
|
33
|
+
connectionTimeoutMillis: 10_000,
|
|
34
|
+
});
|
|
35
|
+
pool.on("error", () => { });
|
|
36
|
+
const entry = { type: "pg", pool, timer: setTimeout(() => { }, 0) };
|
|
37
|
+
pools.set(key, entry);
|
|
38
|
+
resetIdleTimer(key, entry);
|
|
39
|
+
return pool;
|
|
40
|
+
}
|
|
41
|
+
async function getMysqlPool(config) {
|
|
42
|
+
const key = poolKey(config);
|
|
43
|
+
const existing = pools.get(key);
|
|
44
|
+
if (existing?.type === "mysql") {
|
|
45
|
+
resetIdleTimer(key, existing);
|
|
46
|
+
return existing.pool;
|
|
47
|
+
}
|
|
48
|
+
const mysql = await import("mysql2/promise");
|
|
49
|
+
const pool = mysql.default.createPool({
|
|
50
|
+
uri: buildConnectionUrl(config),
|
|
51
|
+
connectionLimit: 3,
|
|
52
|
+
idleTimeout: 30_000,
|
|
53
|
+
connectTimeout: 10_000,
|
|
54
|
+
});
|
|
55
|
+
const entry = { type: "mysql", pool, timer: setTimeout(() => { }, 0) };
|
|
56
|
+
pools.set(key, entry);
|
|
57
|
+
resetIdleTimer(key, entry);
|
|
58
|
+
return pool;
|
|
59
|
+
}
|
|
60
|
+
function buildConnectionUrl(config) {
|
|
61
|
+
const db = config.database || "";
|
|
62
|
+
const params = config.url_params || "";
|
|
63
|
+
const suffix = params ? `?${params}` : "";
|
|
64
|
+
if (isMysqlType(config.db_type)) {
|
|
65
|
+
return `mysql://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${config.host}:${config.port}/${db}${suffix}`;
|
|
66
|
+
}
|
|
67
|
+
return `postgres://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${config.host}:${config.port}/${db}${suffix}`;
|
|
68
|
+
}
|
|
69
|
+
function isMysqlType(dbType) {
|
|
70
|
+
return dbType === "mysql" || dbType === "doris" || dbType === "starrocks";
|
|
71
|
+
}
|
|
72
|
+
function withTimeout(promise, ms) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const timer = setTimeout(() => reject(new Error(`Query timed out after ${ms}ms`)), ms);
|
|
75
|
+
promise.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function queryWithRetry(config, fn) {
|
|
79
|
+
try {
|
|
80
|
+
return await withTimeout(fn(), QUERY_TIMEOUT_MS);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
84
|
+
const retriable = /terminating connection|Connection lost|ECONNRESET|EPIPE|connection refused/i.test(msg);
|
|
85
|
+
if (retriable) {
|
|
86
|
+
const key = poolKey(config);
|
|
87
|
+
const entry = pools.get(key);
|
|
88
|
+
if (entry)
|
|
89
|
+
evictPool(key, entry);
|
|
90
|
+
return withTimeout(fn(), QUERY_TIMEOUT_MS);
|
|
91
|
+
}
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function pgQuery(config, sql, params) {
|
|
96
|
+
return queryWithRetry(config, async () => {
|
|
97
|
+
const pool = await getPgPool(config);
|
|
98
|
+
const result = await pool.query(sql, params);
|
|
99
|
+
const rows = (result.rows || []).slice(0, MAX_ROWS);
|
|
100
|
+
return { columns: result.fields?.map((f) => f.name) ?? [], rows, row_count: rows.length };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function mysqlQuery(config, sql, params) {
|
|
104
|
+
return queryWithRetry(config, async () => {
|
|
105
|
+
const pool = await getMysqlPool(config);
|
|
106
|
+
const [results, fields] = await pool.query(sql, params);
|
|
107
|
+
const rows = (Array.isArray(results) ? results : []).slice(0, MAX_ROWS);
|
|
108
|
+
return { columns: fields?.map((f) => f.name) ?? [], rows, row_count: rows.length };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function query(config, sql, params) {
|
|
112
|
+
if (isMysqlType(config.db_type))
|
|
113
|
+
return mysqlQuery(config, sql, params);
|
|
114
|
+
return pgQuery(config, sql, params);
|
|
115
|
+
}
|
|
116
|
+
export async function executeQuery(config, sql) {
|
|
117
|
+
return query(config, sql);
|
|
118
|
+
}
|
|
119
|
+
export async function listTables(config, schema) {
|
|
120
|
+
let result;
|
|
121
|
+
if (isMysqlType(config.db_type)) {
|
|
122
|
+
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`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
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"]);
|
|
126
|
+
}
|
|
127
|
+
return result.rows.map((r) => ({ name: String(r.name || r.NAME), type: String(r.type || r.TYPE || "TABLE") }));
|
|
128
|
+
}
|
|
129
|
+
export async function describeTable(config, table, schema) {
|
|
130
|
+
let result;
|
|
131
|
+
if (isMysqlType(config.db_type)) {
|
|
132
|
+
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]);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
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]);
|
|
136
|
+
}
|
|
137
|
+
return result.rows.map((r) => ({
|
|
138
|
+
name: String(r.name || ""),
|
|
139
|
+
data_type: String(r.data_type || ""),
|
|
140
|
+
is_nullable: Boolean(r.is_nullable),
|
|
141
|
+
column_default: r.column_default != null ? String(r.column_default) : null,
|
|
142
|
+
is_primary_key: Boolean(r.is_primary_key),
|
|
143
|
+
comment: r.comment != null ? String(r.comment) : null,
|
|
144
|
+
}));
|
|
145
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { loadConnections, findConnection } from "./connections.js";
|
|
6
|
+
import { listTables, describeTable, executeQuery } from "./database.js";
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "dbx",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
});
|
|
11
|
+
server.tool("dbx_list_connections", "List all database connections configured in DBX", {}, async () => {
|
|
12
|
+
const connections = await loadConnections();
|
|
13
|
+
const list = connections.map((c) => ({
|
|
14
|
+
name: c.name,
|
|
15
|
+
type: c.db_type,
|
|
16
|
+
host: c.host,
|
|
17
|
+
port: c.port,
|
|
18
|
+
database: c.database || "",
|
|
19
|
+
}));
|
|
20
|
+
return { content: [{ type: "text", text: JSON.stringify(list, null, 2) }] };
|
|
21
|
+
});
|
|
22
|
+
server.tool("dbx_list_tables", "List tables and views for a database connection", {
|
|
23
|
+
connection_name: z.string().describe("Name of the DBX connection"),
|
|
24
|
+
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
|
|
25
|
+
}, async ({ connection_name, schema }) => {
|
|
26
|
+
const config = await findConnection(connection_name);
|
|
27
|
+
if (!config)
|
|
28
|
+
return { content: [{ type: "text", text: `Connection "${connection_name}" not found` }] };
|
|
29
|
+
const tables = await listTables(config, schema);
|
|
30
|
+
return { content: [{ type: "text", text: JSON.stringify(tables, null, 2) }] };
|
|
31
|
+
});
|
|
32
|
+
server.tool("dbx_describe_table", "Get column definitions for a table", {
|
|
33
|
+
connection_name: z.string().describe("Name of the DBX connection"),
|
|
34
|
+
table: z.string().describe("Table name"),
|
|
35
|
+
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
|
|
36
|
+
}, async ({ connection_name, table, schema }) => {
|
|
37
|
+
const config = await findConnection(connection_name);
|
|
38
|
+
if (!config)
|
|
39
|
+
return { content: [{ type: "text", text: `Connection "${connection_name}" not found` }] };
|
|
40
|
+
const columns = await describeTable(config, table, schema);
|
|
41
|
+
return { content: [{ type: "text", text: JSON.stringify(columns, null, 2) }] };
|
|
42
|
+
});
|
|
43
|
+
server.tool("dbx_execute_query", "Execute a SQL query on a database connection (max 100 rows returned)", {
|
|
44
|
+
connection_name: z.string().describe("Name of the DBX connection"),
|
|
45
|
+
sql: z.string().describe("SQL query to execute"),
|
|
46
|
+
}, async ({ connection_name, sql }) => {
|
|
47
|
+
const config = await findConnection(connection_name);
|
|
48
|
+
if (!config)
|
|
49
|
+
return { content: [{ type: "text", text: `Connection "${connection_name}" not found` }] };
|
|
50
|
+
try {
|
|
51
|
+
const result = await executeQuery(config, sql);
|
|
52
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
56
|
+
return { content: [{ type: "text", text: `Query error: ${msg}` }] };
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
import { readFile } from "node:fs/promises";
|
|
60
|
+
import { join } from "node:path";
|
|
61
|
+
import { homedir, platform } from "node:os";
|
|
62
|
+
function appDataDir() {
|
|
63
|
+
const home = homedir();
|
|
64
|
+
switch (platform()) {
|
|
65
|
+
case "darwin":
|
|
66
|
+
return join(home, "Library", "Application Support", "com.dbx.app");
|
|
67
|
+
case "win32":
|
|
68
|
+
return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "com.dbx.app");
|
|
69
|
+
default:
|
|
70
|
+
return join(home, ".config", "com.dbx.app");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function getBridgeUrl() {
|
|
74
|
+
const portFile = join(appDataDir(), "mcp-bridge-port");
|
|
75
|
+
const port = (await readFile(portFile, "utf-8")).trim();
|
|
76
|
+
return `http://127.0.0.1:${port}`;
|
|
77
|
+
}
|
|
78
|
+
server.tool("dbx_open_table", "Open a table in DBX desktop app UI. Requires DBX to be running.", {
|
|
79
|
+
connection_name: z.string().describe("Name of the DBX connection"),
|
|
80
|
+
table: z.string().describe("Table name to open"),
|
|
81
|
+
database: z.string().optional().describe("Database name"),
|
|
82
|
+
schema: z.string().optional().describe("Schema name"),
|
|
83
|
+
}, async ({ connection_name, table, database, schema }) => {
|
|
84
|
+
try {
|
|
85
|
+
const bridgeUrl = await getBridgeUrl();
|
|
86
|
+
const res = await fetch(`${bridgeUrl}/open-table`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ connection_name, table, database, schema }),
|
|
90
|
+
});
|
|
91
|
+
if (res.ok) {
|
|
92
|
+
return { content: [{ type: "text", text: `Opened ${table} in DBX` }] };
|
|
93
|
+
}
|
|
94
|
+
const text = await res.text();
|
|
95
|
+
return { content: [{ type: "text", text: `Failed: ${text}` }] };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { content: [{ type: "text", text: "DBX is not running. Please start DBX first." }] };
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
async function main() {
|
|
102
|
+
const transport = new StdioServerTransport();
|
|
103
|
+
await server.connect(transport);
|
|
104
|
+
}
|
|
105
|
+
main().catch((e) => {
|
|
106
|
+
console.error("MCP Server failed to start:", e);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dbx-app/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for DBX — query databases from Claude Code, Cursor, and other AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dbx-mcp-server": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "tsx src/index.ts",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"database",
|
|
20
|
+
"dbx",
|
|
21
|
+
"claude",
|
|
22
|
+
"postgresql",
|
|
23
|
+
"mysql",
|
|
24
|
+
"ai-agent"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/t8y2/dbx",
|
|
30
|
+
"directory": "mcp"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/t8y2/dbx/tree/main/mcp",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
35
|
+
"keytar": "^7.9.0",
|
|
36
|
+
"mysql2": "^3.14.1",
|
|
37
|
+
"pg": "^8.16.0",
|
|
38
|
+
"zod": "^3.25.20"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.15.21",
|
|
42
|
+
"@types/pg": "^8.15.4",
|
|
43
|
+
"tsx": "^4.19.4",
|
|
44
|
+
"typescript": "^5.8.3"
|
|
45
|
+
}
|
|
46
|
+
}
|