@xcr1234/dbhub-fork 1.0.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/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/chunk-6O5I6UAW.js +2150 -0
- package/dist/chunk-BRXZ5ZQB.js +127 -0
- package/dist/chunk-C7WEAPX4.js +485 -0
- package/dist/chunk-FAIJPBT5.js +40 -0
- package/dist/chunk-JFWX35TB.js +34 -0
- package/dist/chunk-RTB262PR.js +60 -0
- package/dist/chunk-WVVMH6FJ.js +49 -0
- package/dist/demo/employee-sqlite/employee.sql +117 -0
- package/dist/demo/employee-sqlite/load_department.sql +10 -0
- package/dist/demo/employee-sqlite/load_dept_emp.sql +1103 -0
- package/dist/demo/employee-sqlite/load_dept_manager.sql +17 -0
- package/dist/demo/employee-sqlite/load_employee.sql +1000 -0
- package/dist/demo/employee-sqlite/load_salary1.sql +9488 -0
- package/dist/demo/employee-sqlite/load_title.sql +1470 -0
- package/dist/demo-loader-WKQAEFSX.js +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1584 -0
- package/dist/mariadb-QHRMVK47.js +468 -0
- package/dist/mysql-GOLPG2Q6.js +475 -0
- package/dist/oracle-3V2T7ZWF.js +27739 -0
- package/dist/postgres-CK6N5BXI.js +520 -0
- package/dist/public/assets/index-BiTHVJQj.js +144 -0
- package/dist/public/assets/index-CfPYb3wl.css +1 -0
- package/dist/public/assets/postgres-BpcazhJg.svg +22 -0
- package/dist/public/assets/sqlserver-ByfFYYpV.svg +11 -0
- package/dist/public/favicon.svg +57 -0
- package/dist/public/index.html +14 -0
- package/dist/public/logo-full-light.svg +58 -0
- package/dist/registry-KI3KJMRY.js +13 -0
- package/dist/sqlite-P6NXTMYE.js +2473 -0
- package/dist/sqlserver-5RM44GWI.js +493 -0
- package/package.json +99 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1584 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_TOOL_EXECUTE_SQL,
|
|
4
|
+
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
5
|
+
ConnectorManager,
|
|
6
|
+
getToolRegistry,
|
|
7
|
+
initializeToolRegistry,
|
|
8
|
+
isDemoMode,
|
|
9
|
+
loadTomlConfig,
|
|
10
|
+
mapArgumentsToArray,
|
|
11
|
+
resolvePort,
|
|
12
|
+
resolveSourceConfigs,
|
|
13
|
+
resolveTomlConfigPath,
|
|
14
|
+
resolveTransport
|
|
15
|
+
} from "./chunk-6O5I6UAW.js";
|
|
16
|
+
import {
|
|
17
|
+
loadConnectors
|
|
18
|
+
} from "./chunk-WVVMH6FJ.js";
|
|
19
|
+
import {
|
|
20
|
+
quoteQualifiedIdentifier
|
|
21
|
+
} from "./chunk-JFWX35TB.js";
|
|
22
|
+
import {
|
|
23
|
+
ConnectorRegistry,
|
|
24
|
+
getDatabaseTypeFromDSN,
|
|
25
|
+
getDefaultPortForType,
|
|
26
|
+
parseConnectionInfoFromDSN,
|
|
27
|
+
splitSQLStatements,
|
|
28
|
+
stripCommentsAndStrings
|
|
29
|
+
} from "./chunk-C7WEAPX4.js";
|
|
30
|
+
import "./chunk-FAIJPBT5.js";
|
|
31
|
+
|
|
32
|
+
// src/server.ts
|
|
33
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
34
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
35
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
36
|
+
import express from "express";
|
|
37
|
+
import path from "path";
|
|
38
|
+
import { readFileSync } from "fs";
|
|
39
|
+
import { fileURLToPath } from "url";
|
|
40
|
+
|
|
41
|
+
// src/tools/execute-sql.ts
|
|
42
|
+
import { z } from "zod";
|
|
43
|
+
|
|
44
|
+
// src/utils/response-formatter.ts
|
|
45
|
+
var MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
|
|
46
|
+
var MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
47
|
+
function bigIntReplacer(_key, value) {
|
|
48
|
+
if (typeof value === "bigint") {
|
|
49
|
+
if (value >= MIN_SAFE_BIGINT && value <= MAX_SAFE_BIGINT) {
|
|
50
|
+
return Number(value);
|
|
51
|
+
}
|
|
52
|
+
return value.toString();
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function formatSuccessResponse(data, meta = {}) {
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
data,
|
|
60
|
+
...Object.keys(meta).length > 0 ? { meta } : {}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function formatErrorResponse(error, code = "ERROR", details) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
error,
|
|
67
|
+
code,
|
|
68
|
+
...details ? { details } : {}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function createToolErrorResponse(error, code = "ERROR", details) {
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
|
|
77
|
+
mimeType: "application/json"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
isError: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function createToolSuccessResponse(data, meta = {}) {
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: "text",
|
|
88
|
+
text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
|
|
89
|
+
mimeType: "application/json"
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/utils/allowed-keywords.ts
|
|
96
|
+
var allowedKeywords = {
|
|
97
|
+
postgres: ["select", "with", "explain", "show"],
|
|
98
|
+
mysql: ["select", "with", "explain", "show", "describe", "desc"],
|
|
99
|
+
mariadb: ["select", "with", "explain", "show", "describe", "desc"],
|
|
100
|
+
sqlite: ["select", "with", "explain", "pragma"],
|
|
101
|
+
sqlserver: ["select", "with", "explain", "showplan"]
|
|
102
|
+
};
|
|
103
|
+
var mutatingKeywords = [
|
|
104
|
+
"insert",
|
|
105
|
+
"update",
|
|
106
|
+
"delete",
|
|
107
|
+
"drop",
|
|
108
|
+
"alter",
|
|
109
|
+
"create",
|
|
110
|
+
"truncate",
|
|
111
|
+
"merge",
|
|
112
|
+
"grant",
|
|
113
|
+
"revoke",
|
|
114
|
+
"rename"
|
|
115
|
+
];
|
|
116
|
+
var mutatingPattern = new RegExp(
|
|
117
|
+
`\\b(?:${mutatingKeywords.join("|")})\\b`,
|
|
118
|
+
"i"
|
|
119
|
+
);
|
|
120
|
+
var mutatingPatternWithReplace = new RegExp(
|
|
121
|
+
`\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`,
|
|
122
|
+
"i"
|
|
123
|
+
);
|
|
124
|
+
var mutatingPatterns = {
|
|
125
|
+
postgres: mutatingPattern,
|
|
126
|
+
mysql: mutatingPatternWithReplace,
|
|
127
|
+
mariadb: mutatingPatternWithReplace,
|
|
128
|
+
sqlite: mutatingPatternWithReplace,
|
|
129
|
+
sqlserver: mutatingPattern
|
|
130
|
+
};
|
|
131
|
+
var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
|
|
132
|
+
var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
|
|
133
|
+
function isReadOnlySQL(sql, connectorType) {
|
|
134
|
+
return checkReadOnly(
|
|
135
|
+
stripCommentsAndStrings(sql, connectorType).trim().toLowerCase(),
|
|
136
|
+
connectorType
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function checkReadOnly(cleanedSQL, connectorType) {
|
|
140
|
+
if (!cleanedSQL) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const firstWord = cleanedSQL.match(/\S+/)?.[0] ?? "";
|
|
144
|
+
const keywordList = allowedKeywords[connectorType] || [];
|
|
145
|
+
if (!keywordList.includes(firstWord)) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (firstWord === "with") {
|
|
149
|
+
const pattern = mutatingPatterns[connectorType] ?? mutatingPattern;
|
|
150
|
+
if (pattern.test(cleanedSQL)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (firstWord === "explain") {
|
|
158
|
+
const m = explainAnalyzePattern.exec(cleanedSQL);
|
|
159
|
+
if (m) {
|
|
160
|
+
const afterExplain = cleanedSQL.slice(m[0].length).trim();
|
|
161
|
+
if (afterExplain && !checkReadOnly(afterExplain, connectorType)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/requests/store.ts
|
|
170
|
+
var RequestStore = class {
|
|
171
|
+
constructor() {
|
|
172
|
+
this.store = /* @__PURE__ */ new Map();
|
|
173
|
+
this.maxPerSource = 100;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Add a request to the store
|
|
177
|
+
* Evicts oldest entry if at capacity
|
|
178
|
+
*/
|
|
179
|
+
add(request) {
|
|
180
|
+
const requests = this.store.get(request.sourceId) ?? [];
|
|
181
|
+
requests.push(request);
|
|
182
|
+
if (requests.length > this.maxPerSource) {
|
|
183
|
+
requests.shift();
|
|
184
|
+
}
|
|
185
|
+
this.store.set(request.sourceId, requests);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get requests, optionally filtered by source
|
|
189
|
+
* Returns newest first
|
|
190
|
+
*/
|
|
191
|
+
getAll(sourceId) {
|
|
192
|
+
let requests;
|
|
193
|
+
if (sourceId) {
|
|
194
|
+
requests = [...this.store.get(sourceId) ?? []];
|
|
195
|
+
} else {
|
|
196
|
+
requests = Array.from(this.store.values()).flat();
|
|
197
|
+
}
|
|
198
|
+
return requests.sort(
|
|
199
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get total count of requests across all sources
|
|
204
|
+
*/
|
|
205
|
+
getTotal() {
|
|
206
|
+
return Array.from(this.store.values()).reduce((sum, arr) => sum + arr.length, 0);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Clear all requests (useful for testing)
|
|
210
|
+
*/
|
|
211
|
+
clear() {
|
|
212
|
+
this.store.clear();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/requests/index.ts
|
|
217
|
+
var requestStore = new RequestStore();
|
|
218
|
+
|
|
219
|
+
// src/utils/client-identifier.ts
|
|
220
|
+
function getClientIdentifier(extra) {
|
|
221
|
+
const userAgent = extra?.requestInfo?.headers?.["user-agent"];
|
|
222
|
+
if (userAgent) {
|
|
223
|
+
return userAgent;
|
|
224
|
+
}
|
|
225
|
+
return "stdio";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/utils/tool-handler-helpers.ts
|
|
229
|
+
function getEffectiveSourceId(sourceId) {
|
|
230
|
+
return sourceId || "default";
|
|
231
|
+
}
|
|
232
|
+
function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
|
|
233
|
+
return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
|
|
234
|
+
}
|
|
235
|
+
function trackToolRequest(metadata, startTime, extra, success, error) {
|
|
236
|
+
requestStore.add({
|
|
237
|
+
id: crypto.randomUUID(),
|
|
238
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
239
|
+
sourceId: metadata.sourceId,
|
|
240
|
+
toolName: metadata.toolName,
|
|
241
|
+
sql: metadata.sql,
|
|
242
|
+
durationMs: Date.now() - startTime,
|
|
243
|
+
client: getClientIdentifier(extra),
|
|
244
|
+
success,
|
|
245
|
+
error
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/tools/execute-sql.ts
|
|
250
|
+
var executeSqlSchema = {
|
|
251
|
+
sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
|
|
252
|
+
};
|
|
253
|
+
function areAllStatementsReadOnly(sql, connectorType) {
|
|
254
|
+
const statements = splitSQLStatements(sql, connectorType);
|
|
255
|
+
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
256
|
+
}
|
|
257
|
+
function createExecuteSqlToolHandler(sourceId) {
|
|
258
|
+
return async (args, extra) => {
|
|
259
|
+
const { sql } = args;
|
|
260
|
+
const startTime = Date.now();
|
|
261
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
262
|
+
let success = true;
|
|
263
|
+
let errorMessage;
|
|
264
|
+
let result;
|
|
265
|
+
try {
|
|
266
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
267
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
268
|
+
const actualSourceId = connector.getId();
|
|
269
|
+
const registry = getToolRegistry();
|
|
270
|
+
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
|
|
271
|
+
const isReadonly = toolConfig?.readonly === true;
|
|
272
|
+
if (isReadonly && !areAllStatementsReadOnly(sql, connector.id)) {
|
|
273
|
+
errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
274
|
+
success = false;
|
|
275
|
+
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
276
|
+
}
|
|
277
|
+
const executeOptions = {
|
|
278
|
+
readonly: toolConfig?.readonly,
|
|
279
|
+
maxRows: toolConfig?.max_rows
|
|
280
|
+
};
|
|
281
|
+
result = await connector.executeSQL(sql, executeOptions);
|
|
282
|
+
const responseData = {
|
|
283
|
+
rows: result.rows,
|
|
284
|
+
count: result.rowCount,
|
|
285
|
+
source_id: effectiveSourceId
|
|
286
|
+
};
|
|
287
|
+
return createToolSuccessResponse(responseData);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
success = false;
|
|
290
|
+
errorMessage = error.message;
|
|
291
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
292
|
+
} finally {
|
|
293
|
+
trackToolRequest(
|
|
294
|
+
{
|
|
295
|
+
sourceId: effectiveSourceId,
|
|
296
|
+
toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
|
|
297
|
+
sql
|
|
298
|
+
},
|
|
299
|
+
startTime,
|
|
300
|
+
extra,
|
|
301
|
+
success,
|
|
302
|
+
errorMessage
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/tools/search-objects.ts
|
|
309
|
+
import { z as z2 } from "zod";
|
|
310
|
+
var searchDatabaseObjectsSchema = {
|
|
311
|
+
object_type: z2.enum(["schema", "table", "column", "procedure", "function", "index"]).describe("Object type to search"),
|
|
312
|
+
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
|
|
313
|
+
schema: z2.string().optional().describe("Filter to schema"),
|
|
314
|
+
table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
|
|
315
|
+
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
|
|
316
|
+
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results (default: 100, max: 1000)")
|
|
317
|
+
};
|
|
318
|
+
function likePatternToRegex(pattern) {
|
|
319
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
320
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
321
|
+
}
|
|
322
|
+
async function getTableRowCount(connector, tableName, schemaName) {
|
|
323
|
+
try {
|
|
324
|
+
if (connector.getTableRowCount) {
|
|
325
|
+
return await connector.getTableRowCount(tableName, schemaName);
|
|
326
|
+
}
|
|
327
|
+
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
328
|
+
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
329
|
+
const result = await connector.executeSQL(countQuery, { maxRows: 1 });
|
|
330
|
+
if (result.rows && result.rows.length > 0) {
|
|
331
|
+
return Number(result.rows[0].count || result.rows[0].COUNT || 0);
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
async function getTableComment(connector, tableName, schemaName) {
|
|
339
|
+
try {
|
|
340
|
+
if (connector.getTableComment) {
|
|
341
|
+
return await connector.getTableComment(tableName, schemaName);
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async function searchSchemas(connector, pattern, detailLevel, limit) {
|
|
349
|
+
const schemas = await connector.getSchemas();
|
|
350
|
+
const regex = likePatternToRegex(pattern);
|
|
351
|
+
const matched = schemas.filter((schema) => regex.test(schema)).slice(0, limit);
|
|
352
|
+
if (detailLevel === "names") {
|
|
353
|
+
return matched.map((name) => ({ name }));
|
|
354
|
+
}
|
|
355
|
+
const results = await Promise.all(
|
|
356
|
+
matched.map(async (schemaName) => {
|
|
357
|
+
try {
|
|
358
|
+
const tables = await connector.getTables(schemaName);
|
|
359
|
+
return {
|
|
360
|
+
name: schemaName,
|
|
361
|
+
table_count: tables.length
|
|
362
|
+
};
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return {
|
|
365
|
+
name: schemaName,
|
|
366
|
+
table_count: 0
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
})
|
|
370
|
+
);
|
|
371
|
+
return results;
|
|
372
|
+
}
|
|
373
|
+
async function searchTables(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
374
|
+
const regex = likePatternToRegex(pattern);
|
|
375
|
+
const results = [];
|
|
376
|
+
let schemasToSearch;
|
|
377
|
+
if (schemaFilter) {
|
|
378
|
+
schemasToSearch = [schemaFilter];
|
|
379
|
+
} else {
|
|
380
|
+
schemasToSearch = await connector.getSchemas();
|
|
381
|
+
}
|
|
382
|
+
for (const schemaName of schemasToSearch) {
|
|
383
|
+
if (results.length >= limit) break;
|
|
384
|
+
try {
|
|
385
|
+
const tables = await connector.getTables(schemaName);
|
|
386
|
+
const matched = tables.filter((table) => regex.test(table));
|
|
387
|
+
for (const tableName of matched) {
|
|
388
|
+
if (results.length >= limit) break;
|
|
389
|
+
if (detailLevel === "names") {
|
|
390
|
+
results.push({
|
|
391
|
+
name: tableName,
|
|
392
|
+
schema: schemaName
|
|
393
|
+
});
|
|
394
|
+
} else if (detailLevel === "summary") {
|
|
395
|
+
try {
|
|
396
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
397
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
398
|
+
const comment = await getTableComment(connector, tableName, schemaName);
|
|
399
|
+
results.push({
|
|
400
|
+
name: tableName,
|
|
401
|
+
schema: schemaName,
|
|
402
|
+
column_count: columns.length,
|
|
403
|
+
row_count: rowCount,
|
|
404
|
+
...comment ? { comment } : {}
|
|
405
|
+
});
|
|
406
|
+
} catch (error) {
|
|
407
|
+
results.push({
|
|
408
|
+
name: tableName,
|
|
409
|
+
schema: schemaName,
|
|
410
|
+
column_count: null,
|
|
411
|
+
row_count: null
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
try {
|
|
416
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
417
|
+
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
418
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
419
|
+
const comment = await getTableComment(connector, tableName, schemaName);
|
|
420
|
+
results.push({
|
|
421
|
+
name: tableName,
|
|
422
|
+
schema: schemaName,
|
|
423
|
+
column_count: columns.length,
|
|
424
|
+
row_count: rowCount,
|
|
425
|
+
...comment ? { comment } : {},
|
|
426
|
+
columns: columns.map((col) => ({
|
|
427
|
+
name: col.column_name,
|
|
428
|
+
type: col.data_type,
|
|
429
|
+
nullable: col.is_nullable === "YES",
|
|
430
|
+
default: col.column_default,
|
|
431
|
+
...col.description ? { description: col.description } : {}
|
|
432
|
+
})),
|
|
433
|
+
indexes: indexes.map((idx) => ({
|
|
434
|
+
name: idx.index_name,
|
|
435
|
+
columns: idx.column_names,
|
|
436
|
+
unique: idx.is_unique,
|
|
437
|
+
primary: idx.is_primary
|
|
438
|
+
}))
|
|
439
|
+
});
|
|
440
|
+
} catch (error) {
|
|
441
|
+
results.push({
|
|
442
|
+
name: tableName,
|
|
443
|
+
schema: schemaName,
|
|
444
|
+
error: `Unable to fetch full details: ${error.message}`
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return results;
|
|
454
|
+
}
|
|
455
|
+
async function searchColumns(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
456
|
+
const regex = likePatternToRegex(pattern);
|
|
457
|
+
const results = [];
|
|
458
|
+
let schemasToSearch;
|
|
459
|
+
if (schemaFilter) {
|
|
460
|
+
schemasToSearch = [schemaFilter];
|
|
461
|
+
} else {
|
|
462
|
+
schemasToSearch = await connector.getSchemas();
|
|
463
|
+
}
|
|
464
|
+
for (const schemaName of schemasToSearch) {
|
|
465
|
+
if (results.length >= limit) break;
|
|
466
|
+
try {
|
|
467
|
+
let tablesToSearch;
|
|
468
|
+
if (tableFilter) {
|
|
469
|
+
tablesToSearch = [tableFilter];
|
|
470
|
+
} else {
|
|
471
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
472
|
+
}
|
|
473
|
+
for (const tableName of tablesToSearch) {
|
|
474
|
+
if (results.length >= limit) break;
|
|
475
|
+
try {
|
|
476
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
477
|
+
const matchedColumns = columns.filter((col) => regex.test(col.column_name));
|
|
478
|
+
for (const column of matchedColumns) {
|
|
479
|
+
if (results.length >= limit) break;
|
|
480
|
+
if (detailLevel === "names") {
|
|
481
|
+
results.push({
|
|
482
|
+
name: column.column_name,
|
|
483
|
+
table: tableName,
|
|
484
|
+
schema: schemaName
|
|
485
|
+
});
|
|
486
|
+
} else {
|
|
487
|
+
results.push({
|
|
488
|
+
name: column.column_name,
|
|
489
|
+
table: tableName,
|
|
490
|
+
schema: schemaName,
|
|
491
|
+
type: column.data_type,
|
|
492
|
+
nullable: column.is_nullable === "YES",
|
|
493
|
+
default: column.column_default,
|
|
494
|
+
...column.description ? { description: column.description } : {}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} catch (error) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return results;
|
|
507
|
+
}
|
|
508
|
+
async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit, routineType) {
|
|
509
|
+
const regex = likePatternToRegex(pattern);
|
|
510
|
+
const results = [];
|
|
511
|
+
let schemasToSearch;
|
|
512
|
+
if (schemaFilter) {
|
|
513
|
+
schemasToSearch = [schemaFilter];
|
|
514
|
+
} else {
|
|
515
|
+
schemasToSearch = await connector.getSchemas();
|
|
516
|
+
}
|
|
517
|
+
for (const schemaName of schemasToSearch) {
|
|
518
|
+
if (results.length >= limit) break;
|
|
519
|
+
try {
|
|
520
|
+
const procedures = await connector.getStoredProcedures(schemaName, routineType);
|
|
521
|
+
const matched = procedures.filter((proc) => regex.test(proc));
|
|
522
|
+
for (const procName of matched) {
|
|
523
|
+
if (results.length >= limit) break;
|
|
524
|
+
if (detailLevel === "names") {
|
|
525
|
+
results.push({
|
|
526
|
+
name: procName,
|
|
527
|
+
schema: schemaName
|
|
528
|
+
});
|
|
529
|
+
} else {
|
|
530
|
+
try {
|
|
531
|
+
const details = await connector.getStoredProcedureDetail(procName, schemaName);
|
|
532
|
+
results.push({
|
|
533
|
+
name: procName,
|
|
534
|
+
schema: schemaName,
|
|
535
|
+
type: details.procedure_type,
|
|
536
|
+
language: details.language,
|
|
537
|
+
parameters: detailLevel === "full" ? details.parameter_list : void 0,
|
|
538
|
+
return_type: details.return_type,
|
|
539
|
+
definition: detailLevel === "full" ? details.definition : void 0
|
|
540
|
+
});
|
|
541
|
+
} catch (error) {
|
|
542
|
+
results.push({
|
|
543
|
+
name: procName,
|
|
544
|
+
schema: schemaName,
|
|
545
|
+
error: `Unable to fetch details: ${error.message}`
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return results;
|
|
555
|
+
}
|
|
556
|
+
async function searchIndexes(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
557
|
+
const regex = likePatternToRegex(pattern);
|
|
558
|
+
const results = [];
|
|
559
|
+
let schemasToSearch;
|
|
560
|
+
if (schemaFilter) {
|
|
561
|
+
schemasToSearch = [schemaFilter];
|
|
562
|
+
} else {
|
|
563
|
+
schemasToSearch = await connector.getSchemas();
|
|
564
|
+
}
|
|
565
|
+
for (const schemaName of schemasToSearch) {
|
|
566
|
+
if (results.length >= limit) break;
|
|
567
|
+
try {
|
|
568
|
+
let tablesToSearch;
|
|
569
|
+
if (tableFilter) {
|
|
570
|
+
tablesToSearch = [tableFilter];
|
|
571
|
+
} else {
|
|
572
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
573
|
+
}
|
|
574
|
+
for (const tableName of tablesToSearch) {
|
|
575
|
+
if (results.length >= limit) break;
|
|
576
|
+
try {
|
|
577
|
+
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
578
|
+
const matchedIndexes = indexes.filter((idx) => regex.test(idx.index_name));
|
|
579
|
+
for (const index of matchedIndexes) {
|
|
580
|
+
if (results.length >= limit) break;
|
|
581
|
+
if (detailLevel === "names") {
|
|
582
|
+
results.push({
|
|
583
|
+
name: index.index_name,
|
|
584
|
+
table: tableName,
|
|
585
|
+
schema: schemaName
|
|
586
|
+
});
|
|
587
|
+
} else {
|
|
588
|
+
results.push({
|
|
589
|
+
name: index.index_name,
|
|
590
|
+
table: tableName,
|
|
591
|
+
schema: schemaName,
|
|
592
|
+
columns: index.column_names,
|
|
593
|
+
unique: index.is_unique,
|
|
594
|
+
primary: index.is_primary
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} catch (error) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} catch (error) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return results;
|
|
607
|
+
}
|
|
608
|
+
function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
609
|
+
return async (args, extra) => {
|
|
610
|
+
const {
|
|
611
|
+
object_type,
|
|
612
|
+
pattern = "%",
|
|
613
|
+
schema,
|
|
614
|
+
table,
|
|
615
|
+
detail_level = "names",
|
|
616
|
+
limit = 100
|
|
617
|
+
} = args;
|
|
618
|
+
const startTime = Date.now();
|
|
619
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
620
|
+
let success = true;
|
|
621
|
+
let errorMessage;
|
|
622
|
+
try {
|
|
623
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
624
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
625
|
+
if (table) {
|
|
626
|
+
if (!schema) {
|
|
627
|
+
success = false;
|
|
628
|
+
errorMessage = "The 'table' parameter requires 'schema' to be specified";
|
|
629
|
+
return createToolErrorResponse(errorMessage, "SCHEMA_REQUIRED");
|
|
630
|
+
}
|
|
631
|
+
if (!["column", "index"].includes(object_type)) {
|
|
632
|
+
success = false;
|
|
633
|
+
errorMessage = `The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`;
|
|
634
|
+
return createToolErrorResponse(errorMessage, "INVALID_TABLE_FILTER");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (schema) {
|
|
638
|
+
const schemas = await connector.getSchemas();
|
|
639
|
+
if (!schemas.includes(schema)) {
|
|
640
|
+
success = false;
|
|
641
|
+
errorMessage = `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`;
|
|
642
|
+
return createToolErrorResponse(errorMessage, "SCHEMA_NOT_FOUND");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
let results = [];
|
|
646
|
+
switch (object_type) {
|
|
647
|
+
case "schema":
|
|
648
|
+
results = await searchSchemas(connector, pattern, detail_level, limit);
|
|
649
|
+
break;
|
|
650
|
+
case "table":
|
|
651
|
+
results = await searchTables(connector, pattern, schema, detail_level, limit);
|
|
652
|
+
break;
|
|
653
|
+
case "column":
|
|
654
|
+
results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
|
|
655
|
+
break;
|
|
656
|
+
case "procedure":
|
|
657
|
+
results = await searchProcedures(connector, pattern, schema, detail_level, limit, "procedure");
|
|
658
|
+
break;
|
|
659
|
+
case "function":
|
|
660
|
+
results = await searchProcedures(connector, pattern, schema, detail_level, limit, "function");
|
|
661
|
+
break;
|
|
662
|
+
case "index":
|
|
663
|
+
results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
|
|
664
|
+
break;
|
|
665
|
+
default:
|
|
666
|
+
success = false;
|
|
667
|
+
errorMessage = `Unsupported object_type: ${object_type}`;
|
|
668
|
+
return createToolErrorResponse(errorMessage, "INVALID_OBJECT_TYPE");
|
|
669
|
+
}
|
|
670
|
+
return createToolSuccessResponse({
|
|
671
|
+
object_type,
|
|
672
|
+
pattern,
|
|
673
|
+
schema,
|
|
674
|
+
table,
|
|
675
|
+
detail_level,
|
|
676
|
+
count: results.length,
|
|
677
|
+
results,
|
|
678
|
+
truncated: results.length === limit
|
|
679
|
+
});
|
|
680
|
+
} catch (error) {
|
|
681
|
+
success = false;
|
|
682
|
+
errorMessage = error.message;
|
|
683
|
+
return createToolErrorResponse(
|
|
684
|
+
`Error searching database objects: ${errorMessage}`,
|
|
685
|
+
"SEARCH_ERROR"
|
|
686
|
+
);
|
|
687
|
+
} finally {
|
|
688
|
+
trackToolRequest(
|
|
689
|
+
{
|
|
690
|
+
sourceId: effectiveSourceId,
|
|
691
|
+
toolName: effectiveSourceId === "default" ? "search_objects" : `search_objects_${effectiveSourceId}`,
|
|
692
|
+
sql: `search_objects(object_type=${object_type}, pattern=${pattern}, schema=${schema || "all"}, table=${table || "all"}, detail_level=${detail_level})`
|
|
693
|
+
},
|
|
694
|
+
startTime,
|
|
695
|
+
extra,
|
|
696
|
+
success,
|
|
697
|
+
errorMessage
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/utils/tool-metadata.ts
|
|
704
|
+
import { z as z3 } from "zod";
|
|
705
|
+
|
|
706
|
+
// src/utils/normalize-id.ts
|
|
707
|
+
function normalizeSourceId(id) {
|
|
708
|
+
return id.replace(/[^a-zA-Z0-9]/g, "_");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/utils/tool-metadata.ts
|
|
712
|
+
function buildSourceDescriptionPrefix(description) {
|
|
713
|
+
const trimmed = description?.trim() ?? "";
|
|
714
|
+
if (!trimmed) return "";
|
|
715
|
+
return /[.!?:]$/.test(trimmed) ? `${trimmed} ` : `${trimmed}. `;
|
|
716
|
+
}
|
|
717
|
+
function zodToParameters(schema) {
|
|
718
|
+
const parameters = [];
|
|
719
|
+
for (const [key, zodType] of Object.entries(schema)) {
|
|
720
|
+
const description = zodType.description || "";
|
|
721
|
+
const required = !(zodType instanceof z3.ZodOptional);
|
|
722
|
+
let type = "string";
|
|
723
|
+
if (zodType instanceof z3.ZodString) {
|
|
724
|
+
type = "string";
|
|
725
|
+
} else if (zodType instanceof z3.ZodNumber) {
|
|
726
|
+
type = "number";
|
|
727
|
+
} else if (zodType instanceof z3.ZodBoolean) {
|
|
728
|
+
type = "boolean";
|
|
729
|
+
} else if (zodType instanceof z3.ZodArray) {
|
|
730
|
+
type = "array";
|
|
731
|
+
} else if (zodType instanceof z3.ZodObject) {
|
|
732
|
+
type = "object";
|
|
733
|
+
}
|
|
734
|
+
parameters.push({
|
|
735
|
+
name: key,
|
|
736
|
+
type,
|
|
737
|
+
required,
|
|
738
|
+
description
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
return parameters;
|
|
742
|
+
}
|
|
743
|
+
function getExecuteSqlMetadata(sourceId) {
|
|
744
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
745
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
746
|
+
const dbType = sourceConfig.type;
|
|
747
|
+
const isSingleSource = sourceIds.length === 1;
|
|
748
|
+
const registry = getToolRegistry();
|
|
749
|
+
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
|
|
750
|
+
const executeOptions = {
|
|
751
|
+
readonly: toolConfig?.readonly,
|
|
752
|
+
maxRows: toolConfig?.max_rows
|
|
753
|
+
};
|
|
754
|
+
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
755
|
+
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
756
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
757
|
+
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
758
|
+
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
759
|
+
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
760
|
+
const isReadonly = executeOptions.readonly === true;
|
|
761
|
+
const annotations = {
|
|
762
|
+
title,
|
|
763
|
+
readOnlyHint: isReadonly,
|
|
764
|
+
destructiveHint: !isReadonly,
|
|
765
|
+
// Can be destructive if not readonly
|
|
766
|
+
// In readonly mode, queries are more predictable (though still not strictly idempotent due to data changes)
|
|
767
|
+
// In write mode, queries are definitely not idempotent
|
|
768
|
+
idempotentHint: false,
|
|
769
|
+
// Database operations are always against internal/closed systems, not open-world
|
|
770
|
+
openWorldHint: false
|
|
771
|
+
};
|
|
772
|
+
return {
|
|
773
|
+
name: toolName,
|
|
774
|
+
description,
|
|
775
|
+
schema: executeSqlSchema,
|
|
776
|
+
annotations
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function getSearchObjectsMetadata(sourceId) {
|
|
780
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
781
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
782
|
+
const dbType = sourceConfig.type;
|
|
783
|
+
const isSingleSource = sourceIds.length === 1;
|
|
784
|
+
const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
785
|
+
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
786
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
787
|
+
const description = isSingleSource ? `${userDescPrefix}Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the ${dbType} database` : `${userDescPrefix}Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the '${sourceId}' ${dbType} database`;
|
|
788
|
+
return {
|
|
789
|
+
name: toolName,
|
|
790
|
+
description,
|
|
791
|
+
title
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function customParamsToToolParams(params) {
|
|
795
|
+
if (!params || params.length === 0) {
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
return params.map((param) => ({
|
|
799
|
+
name: param.name,
|
|
800
|
+
type: param.type,
|
|
801
|
+
required: param.required !== false && param.default === void 0,
|
|
802
|
+
description: param.description
|
|
803
|
+
}));
|
|
804
|
+
}
|
|
805
|
+
function buildExecuteSqlTool(sourceId, toolConfig) {
|
|
806
|
+
const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
|
|
807
|
+
const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
|
|
808
|
+
const readonly = toolConfig && "readonly" in toolConfig ? toolConfig.readonly : void 0;
|
|
809
|
+
const max_rows = toolConfig && "max_rows" in toolConfig ? toolConfig.max_rows : void 0;
|
|
810
|
+
return {
|
|
811
|
+
name: executeSqlMetadata.name,
|
|
812
|
+
description: executeSqlMetadata.description,
|
|
813
|
+
parameters: executeSqlParameters,
|
|
814
|
+
readonly,
|
|
815
|
+
max_rows
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function buildSearchObjectsTool(sourceId) {
|
|
819
|
+
const searchMetadata = getSearchObjectsMetadata(sourceId);
|
|
820
|
+
return {
|
|
821
|
+
name: searchMetadata.name,
|
|
822
|
+
description: searchMetadata.description,
|
|
823
|
+
parameters: [
|
|
824
|
+
{
|
|
825
|
+
name: "object_type",
|
|
826
|
+
type: "string",
|
|
827
|
+
required: true,
|
|
828
|
+
description: "Object type to search"
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: "pattern",
|
|
832
|
+
type: "string",
|
|
833
|
+
required: false,
|
|
834
|
+
description: "LIKE pattern (% = any chars, _ = one char). Default: %"
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: "schema",
|
|
838
|
+
type: "string",
|
|
839
|
+
required: false,
|
|
840
|
+
description: "Filter to schema"
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: "table",
|
|
844
|
+
type: "string",
|
|
845
|
+
required: false,
|
|
846
|
+
description: "Filter to table (requires schema; column/index only)"
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
name: "detail_level",
|
|
850
|
+
type: "string",
|
|
851
|
+
required: false,
|
|
852
|
+
description: "Detail: names (minimal), summary (metadata), full (all)"
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
name: "limit",
|
|
856
|
+
type: "integer",
|
|
857
|
+
required: false,
|
|
858
|
+
description: "Max results (default: 100, max: 1000)"
|
|
859
|
+
}
|
|
860
|
+
],
|
|
861
|
+
readonly: true
|
|
862
|
+
// search_objects is always readonly
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function buildCustomTool(toolConfig) {
|
|
866
|
+
return {
|
|
867
|
+
name: toolConfig.name,
|
|
868
|
+
description: toolConfig.description,
|
|
869
|
+
parameters: customParamsToToolParams(toolConfig.parameters),
|
|
870
|
+
statement: toolConfig.statement,
|
|
871
|
+
readonly: toolConfig.readonly,
|
|
872
|
+
max_rows: toolConfig.max_rows
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function getToolsForSource(sourceId) {
|
|
876
|
+
const registry = getToolRegistry();
|
|
877
|
+
const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
|
|
878
|
+
return enabledToolConfigs.map((toolConfig) => {
|
|
879
|
+
if (toolConfig.name === "execute_sql") {
|
|
880
|
+
return buildExecuteSqlTool(sourceId, toolConfig);
|
|
881
|
+
} else if (toolConfig.name === "search_objects") {
|
|
882
|
+
return buildSearchObjectsTool(sourceId);
|
|
883
|
+
} else {
|
|
884
|
+
return buildCustomTool(toolConfig);
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/tools/custom-tool-handler.ts
|
|
890
|
+
import { z as z4 } from "zod";
|
|
891
|
+
function buildZodSchemaFromParameters(parameters) {
|
|
892
|
+
if (!parameters || parameters.length === 0) {
|
|
893
|
+
return {};
|
|
894
|
+
}
|
|
895
|
+
const schemaShape = {};
|
|
896
|
+
for (const param of parameters) {
|
|
897
|
+
let fieldSchema;
|
|
898
|
+
switch (param.type) {
|
|
899
|
+
case "string":
|
|
900
|
+
fieldSchema = z4.string().describe(param.description);
|
|
901
|
+
break;
|
|
902
|
+
case "integer":
|
|
903
|
+
fieldSchema = z4.number().int().describe(param.description);
|
|
904
|
+
break;
|
|
905
|
+
case "float":
|
|
906
|
+
fieldSchema = z4.number().describe(param.description);
|
|
907
|
+
break;
|
|
908
|
+
case "boolean":
|
|
909
|
+
fieldSchema = z4.boolean().describe(param.description);
|
|
910
|
+
break;
|
|
911
|
+
case "array":
|
|
912
|
+
fieldSchema = z4.array(z4.unknown()).describe(param.description);
|
|
913
|
+
break;
|
|
914
|
+
default:
|
|
915
|
+
throw new Error(`Unsupported parameter type: ${param.type}`);
|
|
916
|
+
}
|
|
917
|
+
if (param.allowed_values && param.allowed_values.length > 0) {
|
|
918
|
+
if (param.type === "string") {
|
|
919
|
+
fieldSchema = z4.enum(param.allowed_values).describe(param.description);
|
|
920
|
+
} else {
|
|
921
|
+
fieldSchema = fieldSchema.refine(
|
|
922
|
+
(val) => param.allowed_values.includes(val),
|
|
923
|
+
{
|
|
924
|
+
message: `Value must be one of: ${param.allowed_values.join(", ")}`
|
|
925
|
+
}
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (param.default !== void 0 || param.required === false) {
|
|
930
|
+
fieldSchema = fieldSchema.optional();
|
|
931
|
+
}
|
|
932
|
+
schemaShape[param.name] = fieldSchema;
|
|
933
|
+
}
|
|
934
|
+
return schemaShape;
|
|
935
|
+
}
|
|
936
|
+
function createCustomToolHandler(toolConfig) {
|
|
937
|
+
const zodSchemaShape = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
938
|
+
const zodSchema = z4.object(zodSchemaShape);
|
|
939
|
+
return async (args, extra) => {
|
|
940
|
+
const startTime = Date.now();
|
|
941
|
+
let success = true;
|
|
942
|
+
let errorMessage;
|
|
943
|
+
let paramValues = [];
|
|
944
|
+
try {
|
|
945
|
+
const validatedArgs = zodSchema.parse(args);
|
|
946
|
+
await ConnectorManager.ensureConnected(toolConfig.source);
|
|
947
|
+
const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
|
|
948
|
+
const executeOptions = {
|
|
949
|
+
readonly: toolConfig.readonly,
|
|
950
|
+
maxRows: toolConfig.max_rows
|
|
951
|
+
};
|
|
952
|
+
const isReadonly = executeOptions.readonly === true;
|
|
953
|
+
if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
|
|
954
|
+
errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
|
|
955
|
+
success = false;
|
|
956
|
+
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
957
|
+
}
|
|
958
|
+
paramValues = mapArgumentsToArray(
|
|
959
|
+
toolConfig.parameters,
|
|
960
|
+
validatedArgs
|
|
961
|
+
);
|
|
962
|
+
const result = await connector.executeSQL(
|
|
963
|
+
toolConfig.statement,
|
|
964
|
+
executeOptions,
|
|
965
|
+
paramValues
|
|
966
|
+
);
|
|
967
|
+
const responseData = {
|
|
968
|
+
rows: result.rows,
|
|
969
|
+
count: result.rowCount,
|
|
970
|
+
source_id: toolConfig.source
|
|
971
|
+
};
|
|
972
|
+
return createToolSuccessResponse(responseData);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
success = false;
|
|
975
|
+
errorMessage = error.message;
|
|
976
|
+
if (error instanceof z4.ZodError) {
|
|
977
|
+
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
978
|
+
errorMessage = `Parameter validation failed: ${issues}`;
|
|
979
|
+
} else {
|
|
980
|
+
errorMessage = `${errorMessage}
|
|
981
|
+
|
|
982
|
+
SQL: ${toolConfig.statement}
|
|
983
|
+
Parameters: ${JSON.stringify(paramValues)}`;
|
|
984
|
+
}
|
|
985
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
986
|
+
} finally {
|
|
987
|
+
trackToolRequest(
|
|
988
|
+
{
|
|
989
|
+
sourceId: toolConfig.source,
|
|
990
|
+
toolName: toolConfig.name,
|
|
991
|
+
sql: toolConfig.statement
|
|
992
|
+
},
|
|
993
|
+
startTime,
|
|
994
|
+
extra,
|
|
995
|
+
success,
|
|
996
|
+
errorMessage
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/tools/index.ts
|
|
1003
|
+
function registerTools(server) {
|
|
1004
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
1005
|
+
if (sourceIds.length === 0) {
|
|
1006
|
+
throw new Error("No database sources configured");
|
|
1007
|
+
}
|
|
1008
|
+
const registry = getToolRegistry();
|
|
1009
|
+
for (const sourceId of sourceIds) {
|
|
1010
|
+
const enabledTools = registry.getEnabledToolConfigs(sourceId);
|
|
1011
|
+
for (const toolConfig of enabledTools) {
|
|
1012
|
+
if (toolConfig.name === BUILTIN_TOOL_EXECUTE_SQL) {
|
|
1013
|
+
registerExecuteSqlTool(server, sourceId);
|
|
1014
|
+
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
1015
|
+
registerSearchObjectsTool(server, sourceId);
|
|
1016
|
+
} else {
|
|
1017
|
+
registerCustomTool(server, sourceId, toolConfig);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function registerExecuteSqlTool(server, sourceId) {
|
|
1023
|
+
const metadata = getExecuteSqlMetadata(sourceId);
|
|
1024
|
+
server.registerTool(
|
|
1025
|
+
metadata.name,
|
|
1026
|
+
{
|
|
1027
|
+
description: metadata.description,
|
|
1028
|
+
inputSchema: metadata.schema,
|
|
1029
|
+
annotations: metadata.annotations
|
|
1030
|
+
},
|
|
1031
|
+
createExecuteSqlToolHandler(sourceId)
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
function registerSearchObjectsTool(server, sourceId) {
|
|
1035
|
+
const metadata = getSearchObjectsMetadata(sourceId);
|
|
1036
|
+
server.registerTool(
|
|
1037
|
+
metadata.name,
|
|
1038
|
+
{
|
|
1039
|
+
description: metadata.description,
|
|
1040
|
+
inputSchema: searchDatabaseObjectsSchema,
|
|
1041
|
+
annotations: {
|
|
1042
|
+
title: metadata.title,
|
|
1043
|
+
readOnlyHint: true,
|
|
1044
|
+
destructiveHint: false,
|
|
1045
|
+
idempotentHint: true,
|
|
1046
|
+
openWorldHint: false
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
function registerCustomTool(server, sourceId, toolConfig) {
|
|
1053
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
1054
|
+
const dbType = sourceConfig.type;
|
|
1055
|
+
const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
|
|
1056
|
+
const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
1057
|
+
server.registerTool(
|
|
1058
|
+
toolConfig.name,
|
|
1059
|
+
{
|
|
1060
|
+
description: toolConfig.description,
|
|
1061
|
+
inputSchema: zodSchema,
|
|
1062
|
+
annotations: {
|
|
1063
|
+
title: `${toolConfig.name} (${dbType})`,
|
|
1064
|
+
readOnlyHint: isReadOnly,
|
|
1065
|
+
destructiveHint: !isReadOnly,
|
|
1066
|
+
idempotentHint: isReadOnly,
|
|
1067
|
+
openWorldHint: false
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
createCustomToolHandler(toolConfig)
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/api/sources.ts
|
|
1075
|
+
function transformSourceConfig(source) {
|
|
1076
|
+
if (!source.type && source.dsn) {
|
|
1077
|
+
const inferredType = getDatabaseTypeFromDSN(source.dsn);
|
|
1078
|
+
if (inferredType) {
|
|
1079
|
+
source.type = inferredType;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (!source.type) {
|
|
1083
|
+
throw new Error(`Source ${source.id} is missing required type field`);
|
|
1084
|
+
}
|
|
1085
|
+
const dataSource = {
|
|
1086
|
+
id: source.id,
|
|
1087
|
+
type: source.type
|
|
1088
|
+
};
|
|
1089
|
+
if (source.description) {
|
|
1090
|
+
dataSource.description = source.description;
|
|
1091
|
+
}
|
|
1092
|
+
if (source.host) {
|
|
1093
|
+
dataSource.host = source.host;
|
|
1094
|
+
}
|
|
1095
|
+
if (source.port !== void 0) {
|
|
1096
|
+
dataSource.port = source.port;
|
|
1097
|
+
}
|
|
1098
|
+
if (source.database) {
|
|
1099
|
+
dataSource.database = source.database;
|
|
1100
|
+
}
|
|
1101
|
+
if (source.user) {
|
|
1102
|
+
dataSource.user = source.user;
|
|
1103
|
+
}
|
|
1104
|
+
if (source.ssh_host) {
|
|
1105
|
+
const sshTunnel = {
|
|
1106
|
+
enabled: true,
|
|
1107
|
+
ssh_host: source.ssh_host
|
|
1108
|
+
};
|
|
1109
|
+
if (source.ssh_port !== void 0) {
|
|
1110
|
+
sshTunnel.ssh_port = source.ssh_port;
|
|
1111
|
+
}
|
|
1112
|
+
if (source.ssh_user) {
|
|
1113
|
+
sshTunnel.ssh_user = source.ssh_user;
|
|
1114
|
+
}
|
|
1115
|
+
dataSource.ssh_tunnel = sshTunnel;
|
|
1116
|
+
}
|
|
1117
|
+
dataSource.tools = getToolsForSource(source.id);
|
|
1118
|
+
return dataSource;
|
|
1119
|
+
}
|
|
1120
|
+
function listSources(req, res) {
|
|
1121
|
+
try {
|
|
1122
|
+
const sourceConfigs = ConnectorManager.getAllSourceConfigs();
|
|
1123
|
+
const sources = sourceConfigs.map((config) => {
|
|
1124
|
+
return transformSourceConfig(config);
|
|
1125
|
+
});
|
|
1126
|
+
res.json(sources);
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
console.error("Error listing sources:", error);
|
|
1129
|
+
const errorResponse = {
|
|
1130
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
1131
|
+
};
|
|
1132
|
+
res.status(500).json(errorResponse);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function getSource(req, res) {
|
|
1136
|
+
try {
|
|
1137
|
+
const sourceId = req.params.sourceId;
|
|
1138
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
1139
|
+
if (!sourceConfig) {
|
|
1140
|
+
const errorResponse = {
|
|
1141
|
+
error: "Source not found",
|
|
1142
|
+
source_id: sourceId
|
|
1143
|
+
};
|
|
1144
|
+
res.status(404).json(errorResponse);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const dataSource = transformSourceConfig(sourceConfig);
|
|
1148
|
+
res.json(dataSource);
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
console.error(`Error getting source ${req.params.sourceId}:`, error);
|
|
1151
|
+
const errorResponse = {
|
|
1152
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
1153
|
+
};
|
|
1154
|
+
res.status(500).json(errorResponse);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/api/requests.ts
|
|
1159
|
+
function listRequests(req, res) {
|
|
1160
|
+
try {
|
|
1161
|
+
const sourceId = req.query.source_id;
|
|
1162
|
+
const requests = requestStore.getAll(sourceId);
|
|
1163
|
+
res.json({
|
|
1164
|
+
requests,
|
|
1165
|
+
total: requests.length
|
|
1166
|
+
});
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
console.error("Error listing requests:", error);
|
|
1169
|
+
res.status(500).json({
|
|
1170
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/utils/startup-table.ts
|
|
1176
|
+
var BOX = {
|
|
1177
|
+
topLeft: "\u250C",
|
|
1178
|
+
topRight: "\u2510",
|
|
1179
|
+
bottomLeft: "\u2514",
|
|
1180
|
+
bottomRight: "\u2518",
|
|
1181
|
+
horizontal: "\u2500",
|
|
1182
|
+
vertical: "\u2502",
|
|
1183
|
+
leftT: "\u251C",
|
|
1184
|
+
rightT: "\u2524",
|
|
1185
|
+
bullet: "\u2022"
|
|
1186
|
+
};
|
|
1187
|
+
function parseHostAndDatabase(source) {
|
|
1188
|
+
if (source.dsn) {
|
|
1189
|
+
const parsed = parseConnectionInfoFromDSN(source.dsn);
|
|
1190
|
+
if (parsed) {
|
|
1191
|
+
if (parsed.type === "sqlite") {
|
|
1192
|
+
return { host: "", database: parsed.database || ":memory:" };
|
|
1193
|
+
}
|
|
1194
|
+
if (!parsed.host) {
|
|
1195
|
+
return { host: "", database: parsed.database || "" };
|
|
1196
|
+
}
|
|
1197
|
+
const port = parsed.port ?? getDefaultPortForType(parsed.type);
|
|
1198
|
+
const host2 = port ? `${parsed.host}:${port}` : parsed.host;
|
|
1199
|
+
return { host: host2, database: parsed.database || "" };
|
|
1200
|
+
}
|
|
1201
|
+
return { host: "unknown", database: "" };
|
|
1202
|
+
}
|
|
1203
|
+
const host = source.host ? source.port ? `${source.host}:${source.port}` : source.host : "";
|
|
1204
|
+
const database = source.database || "";
|
|
1205
|
+
return { host, database };
|
|
1206
|
+
}
|
|
1207
|
+
function horizontalLine(width, left, right) {
|
|
1208
|
+
return left + BOX.horizontal.repeat(width - 2) + right;
|
|
1209
|
+
}
|
|
1210
|
+
function fitString(str, width) {
|
|
1211
|
+
if (str.length > width) {
|
|
1212
|
+
return str.slice(0, width - 1) + "\u2026";
|
|
1213
|
+
}
|
|
1214
|
+
return str.padEnd(width);
|
|
1215
|
+
}
|
|
1216
|
+
function formatHostDatabase(host, database) {
|
|
1217
|
+
return host ? database ? `${host}/${database}` : host : database || "";
|
|
1218
|
+
}
|
|
1219
|
+
function generateStartupTable(sources) {
|
|
1220
|
+
if (sources.length === 0) {
|
|
1221
|
+
return "";
|
|
1222
|
+
}
|
|
1223
|
+
const idTypeWidth = Math.max(
|
|
1224
|
+
20,
|
|
1225
|
+
...sources.map((s) => `${s.id} (${s.type})`.length)
|
|
1226
|
+
);
|
|
1227
|
+
const hostDbWidth = Math.max(
|
|
1228
|
+
24,
|
|
1229
|
+
...sources.map((s) => formatHostDatabase(s.host, s.database).length)
|
|
1230
|
+
);
|
|
1231
|
+
const modeWidth = Math.max(
|
|
1232
|
+
10,
|
|
1233
|
+
...sources.map((s) => {
|
|
1234
|
+
const modes = [];
|
|
1235
|
+
if (s.isDemo) modes.push("DEMO");
|
|
1236
|
+
if (s.readonly) modes.push("READ-ONLY");
|
|
1237
|
+
return modes.join(" ").length;
|
|
1238
|
+
})
|
|
1239
|
+
);
|
|
1240
|
+
const totalWidth = 2 + idTypeWidth + 3 + hostDbWidth + 3 + modeWidth + 2;
|
|
1241
|
+
const lines = [];
|
|
1242
|
+
for (let i = 0; i < sources.length; i++) {
|
|
1243
|
+
const source = sources[i];
|
|
1244
|
+
const isFirst = i === 0;
|
|
1245
|
+
const isLast = i === sources.length - 1;
|
|
1246
|
+
if (isFirst) {
|
|
1247
|
+
lines.push(horizontalLine(totalWidth, BOX.topLeft, BOX.topRight));
|
|
1248
|
+
}
|
|
1249
|
+
const idType = fitString(`${source.id} (${source.type})`, idTypeWidth);
|
|
1250
|
+
const hostDb = fitString(
|
|
1251
|
+
formatHostDatabase(source.host, source.database),
|
|
1252
|
+
hostDbWidth
|
|
1253
|
+
);
|
|
1254
|
+
const modes = [];
|
|
1255
|
+
if (source.isDemo) modes.push("DEMO");
|
|
1256
|
+
if (source.readonly) modes.push("READ-ONLY");
|
|
1257
|
+
const modeStr = fitString(modes.join(" "), modeWidth);
|
|
1258
|
+
lines.push(
|
|
1259
|
+
`${BOX.vertical} ${idType} ${BOX.vertical} ${hostDb} ${BOX.vertical} ${modeStr} ${BOX.vertical}`
|
|
1260
|
+
);
|
|
1261
|
+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
|
|
1262
|
+
for (const tool of source.tools) {
|
|
1263
|
+
const toolLine = ` ${BOX.bullet} ${tool}`;
|
|
1264
|
+
lines.push(
|
|
1265
|
+
`${BOX.vertical} ${fitString(toolLine, totalWidth - 4)} ${BOX.vertical}`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
if (isLast) {
|
|
1269
|
+
lines.push(horizontalLine(totalWidth, BOX.bottomLeft, BOX.bottomRight));
|
|
1270
|
+
} else {
|
|
1271
|
+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return lines.join("\n");
|
|
1275
|
+
}
|
|
1276
|
+
function buildSourceDisplayInfo(sourceConfigs, getToolsForSource2, isDemo) {
|
|
1277
|
+
return sourceConfigs.map((source) => {
|
|
1278
|
+
const { host, database } = parseHostAndDatabase(source);
|
|
1279
|
+
return {
|
|
1280
|
+
id: source.id,
|
|
1281
|
+
type: source.type || "sqlite",
|
|
1282
|
+
host,
|
|
1283
|
+
database,
|
|
1284
|
+
readonly: source.readonly || false,
|
|
1285
|
+
isDemo,
|
|
1286
|
+
tools: getToolsForSource2(source.id)
|
|
1287
|
+
};
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/utils/config-watcher.ts
|
|
1292
|
+
import fs from "fs";
|
|
1293
|
+
var DEBOUNCE_MS = 500;
|
|
1294
|
+
function startConfigWatcher(options) {
|
|
1295
|
+
const { connectorManager, initialTools } = options;
|
|
1296
|
+
const configPath = resolveTomlConfigPath();
|
|
1297
|
+
if (!configPath) {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
let debounceTimer = null;
|
|
1301
|
+
let isReloading = false;
|
|
1302
|
+
let reloadPending = false;
|
|
1303
|
+
let lastGoodSources = connectorManager.getAllSourceConfigs();
|
|
1304
|
+
let lastGoodTools = initialTools;
|
|
1305
|
+
const scheduleReload = () => {
|
|
1306
|
+
if (debounceTimer) {
|
|
1307
|
+
clearTimeout(debounceTimer);
|
|
1308
|
+
}
|
|
1309
|
+
debounceTimer = setTimeout(reload, DEBOUNCE_MS);
|
|
1310
|
+
};
|
|
1311
|
+
const reload = async () => {
|
|
1312
|
+
if (isReloading) {
|
|
1313
|
+
reloadPending = true;
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
isReloading = true;
|
|
1317
|
+
reloadPending = false;
|
|
1318
|
+
try {
|
|
1319
|
+
console.error(`
|
|
1320
|
+
Detected change in ${configPath}, reloading configuration...`);
|
|
1321
|
+
const newConfig = loadTomlConfig();
|
|
1322
|
+
if (!newConfig) {
|
|
1323
|
+
console.error("Config reload: failed to load TOML config, keeping existing connections.");
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const oldSources = lastGoodSources;
|
|
1327
|
+
const oldTools = lastGoodTools;
|
|
1328
|
+
await connectorManager.disconnect();
|
|
1329
|
+
try {
|
|
1330
|
+
await connectorManager.connectWithSources(newConfig.sources);
|
|
1331
|
+
initializeToolRegistry({
|
|
1332
|
+
sources: newConfig.sources,
|
|
1333
|
+
tools: newConfig.tools
|
|
1334
|
+
});
|
|
1335
|
+
lastGoodSources = newConfig.sources;
|
|
1336
|
+
lastGoodTools = newConfig.tools;
|
|
1337
|
+
console.error("Configuration reloaded successfully.");
|
|
1338
|
+
} catch (connectError) {
|
|
1339
|
+
console.error("Failed to connect with new config, rolling back:", connectError);
|
|
1340
|
+
try {
|
|
1341
|
+
await connectorManager.disconnect();
|
|
1342
|
+
} catch {
|
|
1343
|
+
}
|
|
1344
|
+
try {
|
|
1345
|
+
await connectorManager.connectWithSources(oldSources);
|
|
1346
|
+
initializeToolRegistry({ sources: oldSources, tools: oldTools });
|
|
1347
|
+
console.error("Rolled back to previous configuration.");
|
|
1348
|
+
} catch (rollbackError) {
|
|
1349
|
+
console.error("Rollback also failed, server has no active connections:", rollbackError);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
console.error("Config reload failed, keeping existing connections:", error);
|
|
1354
|
+
} finally {
|
|
1355
|
+
isReloading = false;
|
|
1356
|
+
if (reloadPending) {
|
|
1357
|
+
reloadPending = false;
|
|
1358
|
+
scheduleReload();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
const watcher = fs.watch(configPath, (eventType) => {
|
|
1363
|
+
if (eventType === "change") {
|
|
1364
|
+
scheduleReload();
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
watcher.unref?.();
|
|
1368
|
+
watcher.on("error", (err) => {
|
|
1369
|
+
console.error("Config file watcher error:", err);
|
|
1370
|
+
});
|
|
1371
|
+
console.error(`Watching ${configPath} for changes (hot reload enabled)`);
|
|
1372
|
+
return () => {
|
|
1373
|
+
if (debounceTimer) {
|
|
1374
|
+
clearTimeout(debounceTimer);
|
|
1375
|
+
}
|
|
1376
|
+
watcher.close();
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/server.ts
|
|
1381
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1382
|
+
var __dirname = path.dirname(__filename);
|
|
1383
|
+
var packageJsonPath = path.join(__dirname, "..", "package.json");
|
|
1384
|
+
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
1385
|
+
var SERVER_NAME = "DBHub MCP Server";
|
|
1386
|
+
var SERVER_VERSION = packageJson.version;
|
|
1387
|
+
function generateBanner(version, modes = []) {
|
|
1388
|
+
const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
|
|
1389
|
+
return `
|
|
1390
|
+
_____ ____ _ _ _
|
|
1391
|
+
| __ \\| _ \\| | | | | |
|
|
1392
|
+
| | | | |_) | |_| |_ _| |__
|
|
1393
|
+
| | | | _ <| _ | | | | '_ \\
|
|
1394
|
+
| |__| | |_) | | | | |_| | |_) |
|
|
1395
|
+
|_____/|____/|_| |_|\\__,_|_.__/
|
|
1396
|
+
|
|
1397
|
+
v${version}${modeText} - Minimal Database MCP Server
|
|
1398
|
+
`;
|
|
1399
|
+
}
|
|
1400
|
+
async function main() {
|
|
1401
|
+
try {
|
|
1402
|
+
const sourceConfigsData = await resolveSourceConfigs();
|
|
1403
|
+
if (!sourceConfigsData) {
|
|
1404
|
+
const samples = ConnectorRegistry.getAllSampleDSNs();
|
|
1405
|
+
const sampleFormats = Object.entries(samples).map(([id, dsn]) => ` - ${id}: ${dsn}`).join("\n");
|
|
1406
|
+
console.error(`
|
|
1407
|
+
ERROR: Database connection configuration is required.
|
|
1408
|
+
Please provide configuration in one of these ways (in order of priority):
|
|
1409
|
+
|
|
1410
|
+
1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
|
|
1411
|
+
2. TOML config file: --config=path/to/dbhub.toml or ./dbhub.toml
|
|
1412
|
+
3. Command line argument: --dsn="your-connection-string"
|
|
1413
|
+
4. Environment variable: export DSN="your-connection-string"
|
|
1414
|
+
5. .env file: DSN=your-connection-string
|
|
1415
|
+
|
|
1416
|
+
Example DSN formats:
|
|
1417
|
+
${sampleFormats}
|
|
1418
|
+
|
|
1419
|
+
Example TOML config (dbhub.toml):
|
|
1420
|
+
[[sources]]
|
|
1421
|
+
id = "my_db"
|
|
1422
|
+
dsn = "postgres://user:pass@localhost:5432/dbname"
|
|
1423
|
+
|
|
1424
|
+
See documentation for more details on configuring database connections.
|
|
1425
|
+
`);
|
|
1426
|
+
process.exit(1);
|
|
1427
|
+
}
|
|
1428
|
+
const connectorManager = new ConnectorManager();
|
|
1429
|
+
const sources = sourceConfigsData.sources;
|
|
1430
|
+
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1431
|
+
await connectorManager.connectWithSources(sources);
|
|
1432
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-KI3KJMRY.js");
|
|
1433
|
+
initializeToolRegistry2({
|
|
1434
|
+
sources: sourceConfigsData.sources,
|
|
1435
|
+
tools: sourceConfigsData.tools
|
|
1436
|
+
});
|
|
1437
|
+
console.error("Tool registry initialized");
|
|
1438
|
+
const stopConfigWatcher = startConfigWatcher({
|
|
1439
|
+
connectorManager,
|
|
1440
|
+
initialTools: sourceConfigsData.tools
|
|
1441
|
+
});
|
|
1442
|
+
const createServer = () => {
|
|
1443
|
+
const server = new McpServer({
|
|
1444
|
+
name: SERVER_NAME,
|
|
1445
|
+
version: SERVER_VERSION
|
|
1446
|
+
});
|
|
1447
|
+
registerTools(server);
|
|
1448
|
+
return server;
|
|
1449
|
+
};
|
|
1450
|
+
const transportData = resolveTransport();
|
|
1451
|
+
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
1452
|
+
const activeModes = [];
|
|
1453
|
+
const modeDescriptions = [];
|
|
1454
|
+
const isDemo = isDemoMode();
|
|
1455
|
+
if (isDemo) {
|
|
1456
|
+
activeModes.push("DEMO");
|
|
1457
|
+
modeDescriptions.push("using sample employee database");
|
|
1458
|
+
}
|
|
1459
|
+
if (activeModes.length > 0) {
|
|
1460
|
+
console.error(`Running in ${activeModes.join(" and ")} mode - ${modeDescriptions.join(", ")}`);
|
|
1461
|
+
}
|
|
1462
|
+
console.error(generateBanner(SERVER_VERSION, activeModes));
|
|
1463
|
+
const sourceDisplayInfos = buildSourceDisplayInfo(
|
|
1464
|
+
sources,
|
|
1465
|
+
(sourceId) => getToolsForSource(sourceId).map((t) => t.readonly ? `\u{1F512} ${t.name}` : t.name),
|
|
1466
|
+
isDemo
|
|
1467
|
+
);
|
|
1468
|
+
console.error(generateStartupTable(sourceDisplayInfos));
|
|
1469
|
+
process.on("exit", () => {
|
|
1470
|
+
stopConfigWatcher?.();
|
|
1471
|
+
});
|
|
1472
|
+
if (transportData.type === "http") {
|
|
1473
|
+
const app = express();
|
|
1474
|
+
app.use(express.json());
|
|
1475
|
+
app.use((req, res, next) => {
|
|
1476
|
+
const origin = req.headers.origin;
|
|
1477
|
+
if (origin) {
|
|
1478
|
+
const host = (req.headers.host ?? "").split(":")[0].toLowerCase();
|
|
1479
|
+
try {
|
|
1480
|
+
const originHost = new URL(origin).hostname.toLowerCase();
|
|
1481
|
+
if (originHost !== host) {
|
|
1482
|
+
return res.status(403).json({
|
|
1483
|
+
error: "Forbidden",
|
|
1484
|
+
message: "Origin does not match Host header (DNS rebinding protection)"
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
} catch {
|
|
1488
|
+
return res.status(400).json({ error: "Bad Request", message: "Malformed Origin header" });
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
1492
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1493
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
|
1494
|
+
res.header("Access-Control-Allow-Credentials", "true");
|
|
1495
|
+
if (req.method === "OPTIONS") {
|
|
1496
|
+
return res.sendStatus(200);
|
|
1497
|
+
}
|
|
1498
|
+
next();
|
|
1499
|
+
});
|
|
1500
|
+
const frontendPath = path.join(__dirname, "public");
|
|
1501
|
+
app.use(express.static(frontendPath));
|
|
1502
|
+
app.get("/healthz", (req, res) => {
|
|
1503
|
+
res.status(200).send("OK");
|
|
1504
|
+
});
|
|
1505
|
+
app.get("/api/sources", listSources);
|
|
1506
|
+
app.get("/api/sources/:sourceId", getSource);
|
|
1507
|
+
app.get("/api/requests", listRequests);
|
|
1508
|
+
app.get("/mcp", (req, res) => {
|
|
1509
|
+
res.status(405).json({
|
|
1510
|
+
error: "Method Not Allowed",
|
|
1511
|
+
message: "SSE streaming is not supported in stateless mode. Use POST requests with JSON responses."
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
app.post("/mcp", async (req, res) => {
|
|
1515
|
+
try {
|
|
1516
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1517
|
+
sessionIdGenerator: void 0,
|
|
1518
|
+
// Disable session management for stateless mode
|
|
1519
|
+
enableJsonResponse: true
|
|
1520
|
+
// Use JSON responses (SSE not supported in stateless mode)
|
|
1521
|
+
});
|
|
1522
|
+
const server = createServer();
|
|
1523
|
+
await server.connect(transport);
|
|
1524
|
+
await transport.handleRequest(req, res, req.body);
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
console.error("Error handling request:", error);
|
|
1527
|
+
if (!res.headersSent) {
|
|
1528
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
if (process.env.NODE_ENV !== "development") {
|
|
1533
|
+
app.get("*", (req, res) => {
|
|
1534
|
+
res.sendFile(path.join(frontendPath, "index.html"));
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
app.listen(port, "0.0.0.0", () => {
|
|
1538
|
+
if (process.env.NODE_ENV === "development") {
|
|
1539
|
+
console.error("Development mode detected!");
|
|
1540
|
+
console.error(" Workbench dev server (with HMR): http://localhost:5173");
|
|
1541
|
+
console.error(" Backend API: http://localhost:8080");
|
|
1542
|
+
console.error("");
|
|
1543
|
+
} else {
|
|
1544
|
+
console.error(`Workbench at http://localhost:${port}/`);
|
|
1545
|
+
}
|
|
1546
|
+
console.error(`MCP server endpoint at http://localhost:${port}/mcp`);
|
|
1547
|
+
});
|
|
1548
|
+
} else {
|
|
1549
|
+
const server = createServer();
|
|
1550
|
+
const transport = new StdioServerTransport();
|
|
1551
|
+
await server.connect(transport);
|
|
1552
|
+
console.error("MCP server running on stdio");
|
|
1553
|
+
let isShuttingDown = false;
|
|
1554
|
+
const shutdown = async () => {
|
|
1555
|
+
if (isShuttingDown) return;
|
|
1556
|
+
isShuttingDown = true;
|
|
1557
|
+
console.error("Shutting down...");
|
|
1558
|
+
await transport.close();
|
|
1559
|
+
await connectorManager.disconnect();
|
|
1560
|
+
process.exit(0);
|
|
1561
|
+
};
|
|
1562
|
+
process.on("SIGINT", shutdown);
|
|
1563
|
+
process.on("SIGTERM", shutdown);
|
|
1564
|
+
process.stdin.on("end", shutdown);
|
|
1565
|
+
}
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
console.error("Fatal error:", err);
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/index.ts
|
|
1573
|
+
var connectorModules = [
|
|
1574
|
+
{ load: () => import("./postgres-CK6N5BXI.js"), name: "PostgreSQL", driver: "pg" },
|
|
1575
|
+
{ load: () => import("./sqlserver-5RM44GWI.js"), name: "SQL Server", driver: "mssql" },
|
|
1576
|
+
{ load: () => import("./sqlite-P6NXTMYE.js"), name: "SQLite", driver: "better-sqlite3" },
|
|
1577
|
+
{ load: () => import("./mysql-GOLPG2Q6.js"), name: "MySQL", driver: "mysql2" },
|
|
1578
|
+
{ load: () => import("./mariadb-QHRMVK47.js"), name: "MariaDB", driver: "mariadb" },
|
|
1579
|
+
{ load: () => import("./oracle-3V2T7ZWF.js"), name: "Oracle", driver: "oracle" }
|
|
1580
|
+
];
|
|
1581
|
+
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1582
|
+
console.error("Fatal error:", error);
|
|
1583
|
+
process.exit(1);
|
|
1584
|
+
});
|