@dbx-app/mcp-server 0.4.2 → 0.4.3
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/LICENSE +661 -0
- package/README.md +25 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +158 -205
- package/package.json +16 -23
- package/server.json +20 -0
- package/dist/connections.d.ts +0 -25
- package/dist/connections.js +0 -130
- package/dist/database.d.ts +0 -21
- package/dist/database.js +0 -408
- package/dist/schema-context.d.ts +0 -25
- package/dist/schema-context.js +0 -43
- package/dist/sql-safety.d.ts +0 -10
- package/dist/sql-safety.js +0 -103
- package/dist/web-backend.d.ts +0 -9
- package/dist/web-backend.js +0 -114
package/dist/connections.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { homedir, platform } from "node:os";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import Database from "better-sqlite3";
|
|
5
|
-
export function canonicalizeConnection(config) {
|
|
6
|
-
if (config.db_type === "mysql" && config.driver_profile?.toLowerCase() === "tdengine") {
|
|
7
|
-
return {
|
|
8
|
-
...config,
|
|
9
|
-
db_type: "tdengine",
|
|
10
|
-
driver_profile: "tdengine",
|
|
11
|
-
port: config.port === 0 || config.port === 6030 ? 6041 : config.port,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
if (config.db_type === "tdengine") {
|
|
15
|
-
return {
|
|
16
|
-
...config,
|
|
17
|
-
driver_profile: "tdengine",
|
|
18
|
-
port: config.port || 6041,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
return config;
|
|
22
|
-
}
|
|
23
|
-
function appDataDir() {
|
|
24
|
-
const home = homedir();
|
|
25
|
-
switch (platform()) {
|
|
26
|
-
case "darwin":
|
|
27
|
-
return join(home, "Library", "Application Support", "com.dbx.app");
|
|
28
|
-
case "win32":
|
|
29
|
-
return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "com.dbx.app");
|
|
30
|
-
default:
|
|
31
|
-
return join(home, ".config", "com.dbx.app");
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function openDb(readonly = false) {
|
|
35
|
-
const dbPath = join(appDataDir(), "dbx.db");
|
|
36
|
-
return new Database(dbPath, { readonly });
|
|
37
|
-
}
|
|
38
|
-
function getSecret(db, connectionId, key) {
|
|
39
|
-
const row = db
|
|
40
|
-
.prepare("SELECT secret FROM connection_secrets WHERE connection_id = ? AND key = ?")
|
|
41
|
-
.get(connectionId, key);
|
|
42
|
-
return row?.secret ?? "";
|
|
43
|
-
}
|
|
44
|
-
export async function loadConnections() {
|
|
45
|
-
try {
|
|
46
|
-
const db = openDb(true);
|
|
47
|
-
const rows = db.prepare("SELECT id, config_json FROM connections").all();
|
|
48
|
-
const configs = [];
|
|
49
|
-
for (const row of rows) {
|
|
50
|
-
const config = canonicalizeConnection(JSON.parse(row.config_json));
|
|
51
|
-
config.id = row.id;
|
|
52
|
-
if (!config.password)
|
|
53
|
-
config.password = getSecret(db, row.id, "password");
|
|
54
|
-
if (!config.proxy_password)
|
|
55
|
-
config.proxy_password = getSecret(db, row.id, "proxy_password");
|
|
56
|
-
configs.push(config);
|
|
57
|
-
}
|
|
58
|
-
db.close();
|
|
59
|
-
return configs;
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
export async function findConnection(name) {
|
|
66
|
-
const connections = await loadConnections();
|
|
67
|
-
return connections.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
|
68
|
-
}
|
|
69
|
-
export async function addConnection(config) {
|
|
70
|
-
const id = randomUUID();
|
|
71
|
-
const db = openDb();
|
|
72
|
-
const normalized = canonicalizeConnection({ ...config, id });
|
|
73
|
-
const full = {
|
|
74
|
-
id,
|
|
75
|
-
name: normalized.name,
|
|
76
|
-
db_type: normalized.db_type,
|
|
77
|
-
driver_profile: normalized.driver_profile ?? normalized.db_type,
|
|
78
|
-
driver_label: null,
|
|
79
|
-
url_params: normalized.url_params ?? "",
|
|
80
|
-
host: normalized.host,
|
|
81
|
-
port: normalized.port,
|
|
82
|
-
username: normalized.username,
|
|
83
|
-
password: "",
|
|
84
|
-
database: normalized.database ?? null,
|
|
85
|
-
color: null,
|
|
86
|
-
ssh_enabled: normalized.ssh_enabled ?? false,
|
|
87
|
-
ssh_host: "",
|
|
88
|
-
ssh_port: 22,
|
|
89
|
-
ssh_user: "",
|
|
90
|
-
ssh_password: "",
|
|
91
|
-
ssh_key_path: "",
|
|
92
|
-
ssh_key_passphrase: "",
|
|
93
|
-
ssh_expose_lan: false,
|
|
94
|
-
proxy_enabled: normalized.proxy_enabled ?? false,
|
|
95
|
-
proxy_type: normalized.proxy_type ?? "socks5",
|
|
96
|
-
proxy_host: normalized.proxy_host ?? "",
|
|
97
|
-
proxy_port: normalized.proxy_port ?? 1080,
|
|
98
|
-
proxy_username: normalized.proxy_username ?? "",
|
|
99
|
-
proxy_password: "",
|
|
100
|
-
ssl: normalized.ssl ?? false,
|
|
101
|
-
sysdba: false,
|
|
102
|
-
connection_string: null,
|
|
103
|
-
};
|
|
104
|
-
const configJson = JSON.stringify(full);
|
|
105
|
-
const insert = db.transaction(() => {
|
|
106
|
-
db.prepare("INSERT INTO connections (id, config_json) VALUES (?, ?)").run(id, configJson);
|
|
107
|
-
if (normalized.password) {
|
|
108
|
-
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "password", normalized.password);
|
|
109
|
-
}
|
|
110
|
-
if (normalized.proxy_password) {
|
|
111
|
-
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "proxy_password", normalized.proxy_password);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
insert();
|
|
115
|
-
db.close();
|
|
116
|
-
return normalized;
|
|
117
|
-
}
|
|
118
|
-
export async function removeConnection(name) {
|
|
119
|
-
const connection = await findConnection(name);
|
|
120
|
-
if (!connection)
|
|
121
|
-
return false;
|
|
122
|
-
const db = openDb();
|
|
123
|
-
const remove = db.transaction(() => {
|
|
124
|
-
db.prepare("DELETE FROM connections WHERE id = ?").run(connection.id);
|
|
125
|
-
db.prepare("DELETE FROM connection_secrets WHERE connection_id = ?").run(connection.id);
|
|
126
|
-
});
|
|
127
|
-
remove();
|
|
128
|
-
db.close();
|
|
129
|
-
return true;
|
|
130
|
-
}
|
package/dist/database.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import { createServer, connect as netConnect } from "node:net";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { homedir, platform } from "node:os";
|
|
5
|
-
import Database from "better-sqlite3";
|
|
6
|
-
import { sqlSafetyFromEnv } from "./sql-safety.js";
|
|
7
|
-
const MAX_ROWS = 100;
|
|
8
|
-
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
9
|
-
const QUERY_TIMEOUT_MS = 30_000;
|
|
10
|
-
const pools = new Map();
|
|
11
|
-
const proxyTunnels = new Map();
|
|
12
|
-
function poolKey(config) {
|
|
13
|
-
return `${config.id}:${config.database || ""}`;
|
|
14
|
-
}
|
|
15
|
-
function evictPool(key, entry) {
|
|
16
|
-
pools.delete(key);
|
|
17
|
-
if (entry.type === "pg") {
|
|
18
|
-
entry.pool.end().catch(() => { });
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
entry.pool.end().catch(() => { });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function resetIdleTimer(key, entry) {
|
|
25
|
-
clearTimeout(entry.timer);
|
|
26
|
-
entry.timer = setTimeout(() => evictPool(key, entry), IDLE_TIMEOUT_MS);
|
|
27
|
-
}
|
|
28
|
-
async function getPgPool(config) {
|
|
29
|
-
const key = poolKey(config);
|
|
30
|
-
const existing = pools.get(key);
|
|
31
|
-
if (existing?.type === "pg") {
|
|
32
|
-
resetIdleTimer(key, existing);
|
|
33
|
-
return existing.pool;
|
|
34
|
-
}
|
|
35
|
-
const pg = await import("pg");
|
|
36
|
-
const endpoint = await connectionEndpoint(config);
|
|
37
|
-
const pool = new pg.default.Pool({
|
|
38
|
-
connectionString: buildConnectionUrl(config, endpoint),
|
|
39
|
-
max: 3,
|
|
40
|
-
idleTimeoutMillis: 30_000,
|
|
41
|
-
connectionTimeoutMillis: 10_000,
|
|
42
|
-
});
|
|
43
|
-
pool.on("error", () => { });
|
|
44
|
-
const entry = { type: "pg", pool, timer: setTimeout(() => { }, 0) };
|
|
45
|
-
pools.set(key, entry);
|
|
46
|
-
resetIdleTimer(key, entry);
|
|
47
|
-
return pool;
|
|
48
|
-
}
|
|
49
|
-
async function getMysqlPool(config) {
|
|
50
|
-
const key = poolKey(config);
|
|
51
|
-
const existing = pools.get(key);
|
|
52
|
-
if (existing?.type === "mysql") {
|
|
53
|
-
resetIdleTimer(key, existing);
|
|
54
|
-
return existing.pool;
|
|
55
|
-
}
|
|
56
|
-
const mysql = await import("mysql2/promise");
|
|
57
|
-
const endpoint = await connectionEndpoint(config);
|
|
58
|
-
const pool = mysql.default.createPool({
|
|
59
|
-
uri: buildConnectionUrl(config, endpoint),
|
|
60
|
-
connectionLimit: 3,
|
|
61
|
-
idleTimeout: 30_000,
|
|
62
|
-
connectTimeout: 10_000,
|
|
63
|
-
});
|
|
64
|
-
const entry = { type: "mysql", pool, timer: setTimeout(() => { }, 0) };
|
|
65
|
-
pools.set(key, entry);
|
|
66
|
-
resetIdleTimer(key, entry);
|
|
67
|
-
return pool;
|
|
68
|
-
}
|
|
69
|
-
async function connectionEndpoint(config) {
|
|
70
|
-
if (!config.proxy_enabled || !config.proxy_host)
|
|
71
|
-
return { host: config.host, port: config.port };
|
|
72
|
-
const existing = proxyTunnels.get(config.id);
|
|
73
|
-
if (existing)
|
|
74
|
-
return { host: "127.0.0.1", port: existing.port };
|
|
75
|
-
const server = createServer((inbound) => {
|
|
76
|
-
connectViaProxy(config)
|
|
77
|
-
.then((outbound) => {
|
|
78
|
-
inbound.pipe(outbound);
|
|
79
|
-
outbound.pipe(inbound);
|
|
80
|
-
})
|
|
81
|
-
.catch(() => inbound.destroy());
|
|
82
|
-
});
|
|
83
|
-
const port = await new Promise((resolve, reject) => {
|
|
84
|
-
server.once("error", reject);
|
|
85
|
-
server.listen(0, "127.0.0.1", () => {
|
|
86
|
-
const address = server.address();
|
|
87
|
-
if (address && typeof address === "object")
|
|
88
|
-
resolve(address.port);
|
|
89
|
-
else
|
|
90
|
-
reject(new Error("Failed to bind proxy tunnel"));
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
proxyTunnels.set(config.id, { server, port });
|
|
94
|
-
return { host: "127.0.0.1", port };
|
|
95
|
-
}
|
|
96
|
-
function buildConnectionUrl(config, endpoint) {
|
|
97
|
-
const db = config.database || "";
|
|
98
|
-
const params = config.url_params || "";
|
|
99
|
-
const suffix = params ? `?${params}` : "";
|
|
100
|
-
if (isMysqlType(config.db_type)) {
|
|
101
|
-
return `mysql://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${endpoint.host}:${endpoint.port}/${db}${suffix}`;
|
|
102
|
-
}
|
|
103
|
-
return `postgres://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${endpoint.host}:${endpoint.port}/${db}${suffix}`;
|
|
104
|
-
}
|
|
105
|
-
function connectViaProxy(config) {
|
|
106
|
-
return new Promise((resolve, reject) => {
|
|
107
|
-
const socket = netConnect(config.proxy_port || 1080, config.proxy_host || "127.0.0.1");
|
|
108
|
-
socket.once("error", reject);
|
|
109
|
-
socket.once("connect", () => {
|
|
110
|
-
if ((config.proxy_type || "socks5") === "http") {
|
|
111
|
-
httpConnect(socket, config, resolve, reject);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
socks5Connect(socket, config, resolve, reject);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
function httpConnect(socket, config, resolve, reject) {
|
|
120
|
-
const target = `${config.host}:${config.port}`;
|
|
121
|
-
const lines = [`CONNECT ${target} HTTP/1.1`, `Host: ${target}`];
|
|
122
|
-
if (config.proxy_username || config.proxy_password) {
|
|
123
|
-
const token = Buffer.from(`${config.proxy_username || ""}:${config.proxy_password || ""}`).toString("base64");
|
|
124
|
-
lines.push(`Proxy-Authorization: Basic ${token}`);
|
|
125
|
-
}
|
|
126
|
-
socket.write(`${lines.join("\r\n")}\r\n\r\n`);
|
|
127
|
-
let buffer = Buffer.alloc(0);
|
|
128
|
-
socket.on("data", function onData(chunk) {
|
|
129
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
130
|
-
const end = buffer.indexOf("\r\n\r\n");
|
|
131
|
-
if (end < 0)
|
|
132
|
-
return;
|
|
133
|
-
socket.off("data", onData);
|
|
134
|
-
const head = buffer.subarray(0, end).toString("utf8");
|
|
135
|
-
if (!/^HTTP\/1\.[01] 200\b/.test(head)) {
|
|
136
|
-
reject(new Error(`HTTP proxy CONNECT failed: ${head.split("\r\n")[0] || "invalid response"}`));
|
|
137
|
-
socket.destroy();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const rest = buffer.subarray(end + 4);
|
|
141
|
-
if (rest.length)
|
|
142
|
-
socket.unshift(rest);
|
|
143
|
-
resolve(socket);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
function socks5Connect(socket, config, resolve, reject) {
|
|
147
|
-
const wantsAuth = !!(config.proxy_username || config.proxy_password);
|
|
148
|
-
socket.write(Buffer.from(wantsAuth ? [0x05, 0x02, 0x00, 0x02] : [0x05, 0x01, 0x00]));
|
|
149
|
-
socket.once("data", (method) => {
|
|
150
|
-
if (method.length < 2 || method[0] !== 0x05) {
|
|
151
|
-
reject(new Error("Invalid SOCKS greeting"));
|
|
152
|
-
socket.destroy();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (method[1] === 0x02) {
|
|
156
|
-
const user = Buffer.from(config.proxy_username || "");
|
|
157
|
-
const pass = Buffer.from(config.proxy_password || "");
|
|
158
|
-
socket.write(Buffer.concat([Buffer.from([0x01, user.length]), user, Buffer.from([pass.length]), pass]));
|
|
159
|
-
socket.once("data", (auth) => {
|
|
160
|
-
if (auth.length < 2 || auth[1] !== 0x00) {
|
|
161
|
-
reject(new Error("SOCKS proxy authentication failed"));
|
|
162
|
-
socket.destroy();
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
sendSocksConnect(socket, config, resolve, reject);
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
else if (method[1] === 0x00) {
|
|
169
|
-
sendSocksConnect(socket, config, resolve, reject);
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
reject(new Error("SOCKS proxy rejected authentication methods"));
|
|
173
|
-
socket.destroy();
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
function sendSocksConnect(socket, config, resolve, reject) {
|
|
178
|
-
const host = Buffer.from(config.host);
|
|
179
|
-
socket.write(Buffer.concat([Buffer.from([0x05, 0x01, 0x00, 0x03, host.length]), host, portBytes(config.port)]));
|
|
180
|
-
socket.once("data", (res) => {
|
|
181
|
-
if (res.length < 4 || res[0] !== 0x05 || res[1] !== 0x00) {
|
|
182
|
-
reject(new Error(`SOCKS proxy connect failed with code ${res[1] ?? "unknown"}`));
|
|
183
|
-
socket.destroy();
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
resolve(socket);
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
function portBytes(port) {
|
|
190
|
-
const buf = Buffer.alloc(2);
|
|
191
|
-
buf.writeUInt16BE(port);
|
|
192
|
-
return buf;
|
|
193
|
-
}
|
|
194
|
-
function isMysqlType(dbType) {
|
|
195
|
-
return dbType === "mysql" || dbType === "doris" || dbType === "starrocks";
|
|
196
|
-
}
|
|
197
|
-
function isDirectType(dbType) {
|
|
198
|
-
switch (dbType) {
|
|
199
|
-
case "postgres":
|
|
200
|
-
case "redshift":
|
|
201
|
-
case "mysql":
|
|
202
|
-
case "doris":
|
|
203
|
-
case "starrocks":
|
|
204
|
-
case "sqlite":
|
|
205
|
-
return true;
|
|
206
|
-
default:
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
function bridgeAppDataDir() {
|
|
211
|
-
const home = homedir();
|
|
212
|
-
switch (platform()) {
|
|
213
|
-
case "darwin":
|
|
214
|
-
return join(home, "Library", "Application Support", "com.dbx.app");
|
|
215
|
-
case "win32":
|
|
216
|
-
return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "com.dbx.app");
|
|
217
|
-
default:
|
|
218
|
-
return join(home, ".config", "com.dbx.app");
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
async function bridgeDataRequest(path, body) {
|
|
222
|
-
let bridgeUrl;
|
|
223
|
-
try {
|
|
224
|
-
const portFile = join(bridgeAppDataDir(), "mcp-bridge-port");
|
|
225
|
-
const port = (await readFile(portFile, "utf-8")).trim();
|
|
226
|
-
bridgeUrl = `http://127.0.0.1:${port}`;
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
throw new Error("DBX desktop app is not running. This database type requires DBX to be running for query execution.");
|
|
230
|
-
}
|
|
231
|
-
const res = await fetch(`${bridgeUrl}${path}`, {
|
|
232
|
-
method: "POST",
|
|
233
|
-
headers: { "Content-Type": "application/json" },
|
|
234
|
-
body: JSON.stringify(body),
|
|
235
|
-
});
|
|
236
|
-
if (!res.ok) {
|
|
237
|
-
const errBody = await res.text().catch(() => "");
|
|
238
|
-
let errorMsg;
|
|
239
|
-
try {
|
|
240
|
-
const parsed = JSON.parse(errBody);
|
|
241
|
-
errorMsg = parsed.error || errBody;
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
errorMsg = errBody;
|
|
245
|
-
}
|
|
246
|
-
throw new Error(errorMsg || `Bridge request failed: ${res.status}`);
|
|
247
|
-
}
|
|
248
|
-
return res.json();
|
|
249
|
-
}
|
|
250
|
-
function convertBridgeQueryResult(result) {
|
|
251
|
-
const rows = result.rows.slice(0, MAX_ROWS).map((row) => {
|
|
252
|
-
const obj = {};
|
|
253
|
-
result.columns.forEach((col, i) => { obj[col] = row[i]; });
|
|
254
|
-
return obj;
|
|
255
|
-
});
|
|
256
|
-
return { columns: result.columns, rows, row_count: rows.length };
|
|
257
|
-
}
|
|
258
|
-
function withTimeout(promise, ms) {
|
|
259
|
-
return new Promise((resolve, reject) => {
|
|
260
|
-
const timer = setTimeout(() => reject(new Error(`Query timed out after ${ms}ms`)), ms);
|
|
261
|
-
promise.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
async function queryWithRetry(config, fn) {
|
|
265
|
-
try {
|
|
266
|
-
return await withTimeout(fn(), QUERY_TIMEOUT_MS);
|
|
267
|
-
}
|
|
268
|
-
catch (e) {
|
|
269
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
270
|
-
const retriable = /terminating connection|Connection lost|ECONNRESET|EPIPE|connection refused/i.test(msg);
|
|
271
|
-
if (retriable) {
|
|
272
|
-
const key = poolKey(config);
|
|
273
|
-
const entry = pools.get(key);
|
|
274
|
-
if (entry)
|
|
275
|
-
evictPool(key, entry);
|
|
276
|
-
return withTimeout(fn(), QUERY_TIMEOUT_MS);
|
|
277
|
-
}
|
|
278
|
-
throw e;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
async function pgQuery(config, sql, params) {
|
|
282
|
-
return queryWithRetry(config, async () => {
|
|
283
|
-
const pool = await getPgPool(config);
|
|
284
|
-
const result = await pool.query(sql, params);
|
|
285
|
-
const rows = (result.rows || []).slice(0, MAX_ROWS);
|
|
286
|
-
return { columns: result.fields?.map((f) => f.name) ?? [], rows, row_count: rows.length };
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
async function mysqlQuery(config, sql, params) {
|
|
290
|
-
return queryWithRetry(config, async () => {
|
|
291
|
-
const pool = await getMysqlPool(config);
|
|
292
|
-
const [results, fields] = await pool.query(sql, params);
|
|
293
|
-
const rows = (Array.isArray(results) ? results : []).slice(0, MAX_ROWS);
|
|
294
|
-
return { columns: fields?.map((f) => f.name) ?? [], rows, row_count: rows.length };
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
async function query(config, sql, params) {
|
|
298
|
-
if (config.db_type === "sqlite")
|
|
299
|
-
return sqliteQuery(config, sql);
|
|
300
|
-
if (isMysqlType(config.db_type))
|
|
301
|
-
return mysqlQuery(config, sql, params);
|
|
302
|
-
return pgQuery(config, sql, params);
|
|
303
|
-
}
|
|
304
|
-
function sqlitePath(config) {
|
|
305
|
-
return expandTilde(config.host || config.database || "");
|
|
306
|
-
}
|
|
307
|
-
function expandTilde(path) {
|
|
308
|
-
if (path === "~")
|
|
309
|
-
return homedir();
|
|
310
|
-
if (path.startsWith("~/"))
|
|
311
|
-
return join(homedir(), path.slice(2));
|
|
312
|
-
return path;
|
|
313
|
-
}
|
|
314
|
-
function quoteSqliteIdentifier(identifier) {
|
|
315
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
316
|
-
}
|
|
317
|
-
function sqliteQuery(config, sql) {
|
|
318
|
-
const db = new Database(sqlitePath(config), { readonly: !sqlSafetyFromEnv().allowWrites });
|
|
319
|
-
try {
|
|
320
|
-
const stmt = db.prepare(sql);
|
|
321
|
-
if (stmt.reader) {
|
|
322
|
-
const rows = stmt.all().slice(0, MAX_ROWS);
|
|
323
|
-
return { columns: stmt.columns().map((column) => column.name), rows, row_count: rows.length };
|
|
324
|
-
}
|
|
325
|
-
const result = stmt.run();
|
|
326
|
-
return { columns: [], rows: [], row_count: result.changes };
|
|
327
|
-
}
|
|
328
|
-
finally {
|
|
329
|
-
db.close();
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
export async function executeQuery(config, sql) {
|
|
333
|
-
if (isDirectType(config.db_type)) {
|
|
334
|
-
return query(config, sql);
|
|
335
|
-
}
|
|
336
|
-
const result = await bridgeDataRequest("/data/execute-query", {
|
|
337
|
-
connection_name: config.name,
|
|
338
|
-
database: config.database || "",
|
|
339
|
-
sql,
|
|
340
|
-
});
|
|
341
|
-
return convertBridgeQueryResult(result);
|
|
342
|
-
}
|
|
343
|
-
export async function listTables(config, schema) {
|
|
344
|
-
if (config.db_type === "sqlite") {
|
|
345
|
-
const result = await query(config, `SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
346
|
-
return result.rows.map((r) => ({ name: String(r.name || ""), type: String(r.type || "table") }));
|
|
347
|
-
}
|
|
348
|
-
if (!isDirectType(config.db_type)) {
|
|
349
|
-
const tables = await bridgeDataRequest("/data/list-tables", {
|
|
350
|
-
connection_name: config.name,
|
|
351
|
-
database: config.database || "",
|
|
352
|
-
schema: schema || "",
|
|
353
|
-
});
|
|
354
|
-
return tables.map((t) => ({ name: t.name, type: t.table_type || "TABLE" }));
|
|
355
|
-
}
|
|
356
|
-
let result;
|
|
357
|
-
if (isMysqlType(config.db_type)) {
|
|
358
|
-
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`);
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
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"]);
|
|
362
|
-
}
|
|
363
|
-
return result.rows.map((r) => ({ name: String(r.name || r.NAME), type: String(r.type || r.TYPE || "TABLE") }));
|
|
364
|
-
}
|
|
365
|
-
export async function describeTable(config, table, schema) {
|
|
366
|
-
if (config.db_type === "sqlite") {
|
|
367
|
-
const result = await query(config, `PRAGMA table_info(${quoteSqliteIdentifier(table)})`);
|
|
368
|
-
return result.rows.map((r) => ({
|
|
369
|
-
name: String(r.name || ""),
|
|
370
|
-
data_type: String(r.type || ""),
|
|
371
|
-
is_nullable: Number(r.notnull || 0) === 0,
|
|
372
|
-
column_default: r.dflt_value != null ? String(r.dflt_value) : null,
|
|
373
|
-
is_primary_key: Number(r.pk || 0) > 0,
|
|
374
|
-
comment: null,
|
|
375
|
-
}));
|
|
376
|
-
}
|
|
377
|
-
if (!isDirectType(config.db_type)) {
|
|
378
|
-
const columns = await bridgeDataRequest("/data/describe-table", {
|
|
379
|
-
connection_name: config.name,
|
|
380
|
-
database: config.database || "",
|
|
381
|
-
schema: schema || "",
|
|
382
|
-
table,
|
|
383
|
-
});
|
|
384
|
-
return columns.map((c) => ({
|
|
385
|
-
name: c.name,
|
|
386
|
-
data_type: c.data_type,
|
|
387
|
-
is_nullable: c.is_nullable,
|
|
388
|
-
column_default: c.column_default,
|
|
389
|
-
is_primary_key: c.is_primary_key,
|
|
390
|
-
comment: c.comment,
|
|
391
|
-
}));
|
|
392
|
-
}
|
|
393
|
-
let result;
|
|
394
|
-
if (isMysqlType(config.db_type)) {
|
|
395
|
-
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]);
|
|
396
|
-
}
|
|
397
|
-
else {
|
|
398
|
-
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]);
|
|
399
|
-
}
|
|
400
|
-
return result.rows.map((r) => ({
|
|
401
|
-
name: String(r.name || ""),
|
|
402
|
-
data_type: String(r.data_type || ""),
|
|
403
|
-
is_nullable: Boolean(r.is_nullable),
|
|
404
|
-
column_default: r.column_default != null ? String(r.column_default) : null,
|
|
405
|
-
is_primary_key: Boolean(r.is_primary_key),
|
|
406
|
-
comment: r.comment != null ? String(r.comment) : null,
|
|
407
|
-
}));
|
|
408
|
-
}
|
package/dist/schema-context.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { ConnectionConfig } from "./connections.js";
|
|
2
|
-
import type { ColumnInfo, TableInfo } from "./database.js";
|
|
3
|
-
export interface SchemaContextBackend {
|
|
4
|
-
listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]>;
|
|
5
|
-
describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]>;
|
|
6
|
-
}
|
|
7
|
-
export interface SchemaContextOptions {
|
|
8
|
-
schema?: string;
|
|
9
|
-
tables?: string[];
|
|
10
|
-
maxTables?: number;
|
|
11
|
-
}
|
|
12
|
-
export interface SchemaContextTable {
|
|
13
|
-
name: string;
|
|
14
|
-
type: string;
|
|
15
|
-
columns: ColumnInfo[];
|
|
16
|
-
}
|
|
17
|
-
export interface SchemaContext {
|
|
18
|
-
connection: string;
|
|
19
|
-
database: string;
|
|
20
|
-
schema: string;
|
|
21
|
-
truncated: boolean;
|
|
22
|
-
tables: SchemaContextTable[];
|
|
23
|
-
}
|
|
24
|
-
export declare function buildSchemaContext(backend: SchemaContextBackend, config: ConnectionConfig, options?: SchemaContextOptions): Promise<SchemaContext>;
|
|
25
|
-
export declare function formatSchemaContext(context: SchemaContext): string;
|
package/dist/schema-context.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
const DEFAULT_MAX_TABLES = 8;
|
|
2
|
-
export async function buildSchemaContext(backend, config, options = {}) {
|
|
3
|
-
const maxTables = Math.max(1, Math.min(options.maxTables ?? DEFAULT_MAX_TABLES, 20));
|
|
4
|
-
const availableTables = await backend.listTables(config, options.schema);
|
|
5
|
-
const requested = new Set((options.tables ?? []).map((table) => table.toLowerCase()));
|
|
6
|
-
const selected = requested.size
|
|
7
|
-
? availableTables.filter((table) => requested.has(table.name.toLowerCase()))
|
|
8
|
-
: availableTables.slice(0, maxTables);
|
|
9
|
-
const limited = selected.slice(0, maxTables);
|
|
10
|
-
const tables = await Promise.all(limited.map(async (table) => ({
|
|
11
|
-
name: table.name,
|
|
12
|
-
type: table.type,
|
|
13
|
-
columns: await backend.describeTable(config, table.name, options.schema),
|
|
14
|
-
})));
|
|
15
|
-
return {
|
|
16
|
-
connection: config.name,
|
|
17
|
-
database: config.database || "",
|
|
18
|
-
schema: options.schema || "",
|
|
19
|
-
truncated: selected.length > limited.length || (!requested.size && availableTables.length > limited.length),
|
|
20
|
-
tables,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
export function formatSchemaContext(context) {
|
|
24
|
-
const header = [
|
|
25
|
-
`Connection: ${context.connection}`,
|
|
26
|
-
context.database ? `Database: ${context.database}` : "",
|
|
27
|
-
context.schema ? `Schema: ${context.schema}` : "",
|
|
28
|
-
].filter(Boolean);
|
|
29
|
-
const sections = context.tables.map((table) => {
|
|
30
|
-
const lines = table.columns.map((column) => {
|
|
31
|
-
const parts = [
|
|
32
|
-
column.name,
|
|
33
|
-
column.data_type,
|
|
34
|
-
column.is_nullable ? "NULL" : "NOT NULL",
|
|
35
|
-
column.is_primary_key ? "PK" : "",
|
|
36
|
-
].filter(Boolean);
|
|
37
|
-
return `- ${parts.join(" ")}${column.comment ? ` -- ${column.comment}` : ""}`;
|
|
38
|
-
});
|
|
39
|
-
return [`## ${table.name}`, `Type: ${table.type}`, ...lines].join("\n");
|
|
40
|
-
});
|
|
41
|
-
const suffix = context.truncated ? "\n\nNote: table list was truncated; request specific table names for more context." : "";
|
|
42
|
-
return `${header.join("\n")}\n\n${sections.join("\n\n")}${suffix}`;
|
|
43
|
-
}
|
package/dist/sql-safety.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export interface SqlSafetyOptions {
|
|
2
|
-
allowWrites?: boolean;
|
|
3
|
-
allowDangerous?: boolean;
|
|
4
|
-
}
|
|
5
|
-
export interface SqlSafetyDecision {
|
|
6
|
-
allowed: boolean;
|
|
7
|
-
reason?: string;
|
|
8
|
-
}
|
|
9
|
-
export declare function evaluateSqlSafety(sql: string, options?: SqlSafetyOptions): SqlSafetyDecision;
|
|
10
|
-
export declare function sqlSafetyFromEnv(env?: NodeJS.ProcessEnv): SqlSafetyOptions;
|