capybara-db-mcp 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 +230 -0
- package/dist/chunk-H73NN6K4.js +2175 -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-PSMTLZ2T.js +46 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3615 -0
- package/dist/public/favicon.svg +57 -0
- package/dist/public/logo-full-light.svg +58 -0
- package/dist/registry-54CGLMGK.js +10 -0
- package/package.json +89 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3615 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_TOOL_EXECUTE_SQL,
|
|
4
|
+
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
5
|
+
ConnectorManager,
|
|
6
|
+
ConnectorRegistry,
|
|
7
|
+
SafeURL,
|
|
8
|
+
getDatabaseTypeFromDSN,
|
|
9
|
+
getDefaultPortForType,
|
|
10
|
+
getToolRegistry,
|
|
11
|
+
isDemoMode,
|
|
12
|
+
mapArgumentsToArray,
|
|
13
|
+
obfuscateDSNPassword,
|
|
14
|
+
parseConnectionInfoFromDSN,
|
|
15
|
+
resolveOutputFormat,
|
|
16
|
+
resolvePort,
|
|
17
|
+
resolveSourceConfigs,
|
|
18
|
+
resolveTransport,
|
|
19
|
+
stripCommentsAndStrings
|
|
20
|
+
} from "./chunk-H73NN6K4.js";
|
|
21
|
+
|
|
22
|
+
// src/connectors/postgres/index.ts
|
|
23
|
+
import pg from "pg";
|
|
24
|
+
|
|
25
|
+
// src/utils/sql-row-limiter.ts
|
|
26
|
+
var SQLRowLimiter = class {
|
|
27
|
+
/**
|
|
28
|
+
* Check if a SQL statement is a SELECT query that can benefit from row limiting
|
|
29
|
+
* Only handles SELECT queries
|
|
30
|
+
*/
|
|
31
|
+
static isSelectQuery(sql2) {
|
|
32
|
+
const trimmed = sql2.trim().toLowerCase();
|
|
33
|
+
return trimmed.startsWith("select");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if a SQL statement already has a LIMIT clause.
|
|
37
|
+
* Strips comments and string literals first to avoid false positives.
|
|
38
|
+
*/
|
|
39
|
+
static hasLimitClause(sql2) {
|
|
40
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
41
|
+
const limitRegex = /\blimit\s+(?:\d+|\$\d+|\?|@p\d+)/i;
|
|
42
|
+
return limitRegex.test(cleanedSQL);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if a SQL statement already has a TOP clause (SQL Server).
|
|
46
|
+
* Strips comments and string literals first to avoid false positives.
|
|
47
|
+
*/
|
|
48
|
+
static hasTopClause(sql2) {
|
|
49
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
50
|
+
const topRegex = /\bselect\s+top\s+\d+/i;
|
|
51
|
+
return topRegex.test(cleanedSQL);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract existing LIMIT value from SQL if present.
|
|
55
|
+
* Strips comments and string literals first to avoid false positives.
|
|
56
|
+
*/
|
|
57
|
+
static extractLimitValue(sql2) {
|
|
58
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
59
|
+
const limitMatch = cleanedSQL.match(/\blimit\s+(\d+)/i);
|
|
60
|
+
if (limitMatch) {
|
|
61
|
+
return parseInt(limitMatch[1], 10);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Extract existing TOP value from SQL if present (SQL Server).
|
|
67
|
+
* Strips comments and string literals first to avoid false positives.
|
|
68
|
+
*/
|
|
69
|
+
static extractTopValue(sql2) {
|
|
70
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
71
|
+
const topMatch = cleanedSQL.match(/\bselect\s+top\s+(\d+)/i);
|
|
72
|
+
if (topMatch) {
|
|
73
|
+
return parseInt(topMatch[1], 10);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Add or modify LIMIT clause in a SQL statement
|
|
79
|
+
*/
|
|
80
|
+
static applyLimitToQuery(sql2, maxRows) {
|
|
81
|
+
const existingLimit = this.extractLimitValue(sql2);
|
|
82
|
+
if (existingLimit !== null) {
|
|
83
|
+
const effectiveLimit = Math.min(existingLimit, maxRows);
|
|
84
|
+
return sql2.replace(/\blimit\s+\d+/i, `LIMIT ${effectiveLimit}`);
|
|
85
|
+
} else {
|
|
86
|
+
const trimmed = sql2.trim();
|
|
87
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
88
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
89
|
+
return `${sqlWithoutSemicolon} LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Add or modify TOP clause in a SQL statement (SQL Server)
|
|
94
|
+
*/
|
|
95
|
+
static applyTopToQuery(sql2, maxRows) {
|
|
96
|
+
const existingTop = this.extractTopValue(sql2);
|
|
97
|
+
if (existingTop !== null) {
|
|
98
|
+
const effectiveTop = Math.min(existingTop, maxRows);
|
|
99
|
+
return sql2.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
|
|
100
|
+
} else {
|
|
101
|
+
return sql2.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if a LIMIT clause uses a parameter placeholder (not a literal number).
|
|
106
|
+
* Strips comments and string literals first to avoid false positives.
|
|
107
|
+
*/
|
|
108
|
+
static hasParameterizedLimit(sql2) {
|
|
109
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
110
|
+
const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
|
|
111
|
+
return parameterizedLimitRegex.test(cleanedSQL);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Apply maxRows limit to a SELECT query only
|
|
115
|
+
*
|
|
116
|
+
* This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
|
|
117
|
+
* the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
|
|
118
|
+
*
|
|
119
|
+
* For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
|
|
120
|
+
* to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
|
|
121
|
+
*/
|
|
122
|
+
static applyMaxRows(sql2, maxRows) {
|
|
123
|
+
if (!maxRows || !this.isSelectQuery(sql2)) {
|
|
124
|
+
return sql2;
|
|
125
|
+
}
|
|
126
|
+
if (this.hasParameterizedLimit(sql2)) {
|
|
127
|
+
const trimmed = sql2.trim();
|
|
128
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
129
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
130
|
+
return `SELECT * FROM (${sqlWithoutSemicolon}) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
131
|
+
}
|
|
132
|
+
return this.applyLimitToQuery(sql2, maxRows);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Apply maxRows limit to a SELECT query using SQL Server TOP syntax
|
|
136
|
+
*/
|
|
137
|
+
static applyMaxRowsForSQLServer(sql2, maxRows) {
|
|
138
|
+
if (!maxRows || !this.isSelectQuery(sql2)) {
|
|
139
|
+
return sql2;
|
|
140
|
+
}
|
|
141
|
+
return this.applyTopToQuery(sql2, maxRows);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/connectors/postgres/index.ts
|
|
146
|
+
var { Pool } = pg;
|
|
147
|
+
var PostgresDSNParser = class {
|
|
148
|
+
async parse(dsn, config) {
|
|
149
|
+
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
150
|
+
const queryTimeoutSeconds = config?.queryTimeoutSeconds;
|
|
151
|
+
if (!this.isValidDSN(dsn)) {
|
|
152
|
+
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
153
|
+
const expectedFormat = this.getSampleDSN();
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Invalid PostgreSQL DSN format.
|
|
156
|
+
Provided: ${obfuscatedDSN}
|
|
157
|
+
Expected: ${expectedFormat}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const url = new SafeURL(dsn);
|
|
162
|
+
const poolConfig = {
|
|
163
|
+
host: url.hostname,
|
|
164
|
+
port: url.port ? parseInt(url.port) : 5432,
|
|
165
|
+
database: url.pathname ? url.pathname.substring(1) : "",
|
|
166
|
+
// Remove leading '/' if exists
|
|
167
|
+
user: url.username,
|
|
168
|
+
password: url.password
|
|
169
|
+
};
|
|
170
|
+
url.forEachSearchParam((value, key) => {
|
|
171
|
+
if (key === "sslmode") {
|
|
172
|
+
if (value === "disable") {
|
|
173
|
+
poolConfig.ssl = false;
|
|
174
|
+
} else if (value === "require") {
|
|
175
|
+
poolConfig.ssl = { rejectUnauthorized: false };
|
|
176
|
+
} else {
|
|
177
|
+
poolConfig.ssl = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (connectionTimeoutSeconds !== void 0) {
|
|
182
|
+
poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
|
|
183
|
+
}
|
|
184
|
+
if (queryTimeoutSeconds !== void 0) {
|
|
185
|
+
poolConfig.query_timeout = queryTimeoutSeconds * 1e3;
|
|
186
|
+
}
|
|
187
|
+
return poolConfig;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
getSampleDSN() {
|
|
195
|
+
return "postgres://postgres:password@localhost:5432/postgres?sslmode=require";
|
|
196
|
+
}
|
|
197
|
+
isValidDSN(dsn) {
|
|
198
|
+
try {
|
|
199
|
+
return dsn.startsWith("postgres://") || dsn.startsWith("postgresql://");
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
var PostgresConnector = class _PostgresConnector {
|
|
206
|
+
constructor() {
|
|
207
|
+
this.id = "postgres";
|
|
208
|
+
this.name = "PostgreSQL";
|
|
209
|
+
this.dsnParser = new PostgresDSNParser();
|
|
210
|
+
this.pool = null;
|
|
211
|
+
// Source ID is set by ConnectorManager after cloning
|
|
212
|
+
this.sourceId = "default";
|
|
213
|
+
}
|
|
214
|
+
getId() {
|
|
215
|
+
return this.sourceId;
|
|
216
|
+
}
|
|
217
|
+
clone() {
|
|
218
|
+
return new _PostgresConnector();
|
|
219
|
+
}
|
|
220
|
+
async connect(dsn, initScript, config) {
|
|
221
|
+
try {
|
|
222
|
+
const poolConfig = await this.dsnParser.parse(dsn, config);
|
|
223
|
+
if (config?.readonly) {
|
|
224
|
+
poolConfig.options = (poolConfig.options || "") + " -c default_transaction_read_only=on";
|
|
225
|
+
}
|
|
226
|
+
if (config?.schema) {
|
|
227
|
+
poolConfig.options = (poolConfig.options || "") + ` -c search_path=${config.schema},public`;
|
|
228
|
+
}
|
|
229
|
+
this.pool = new Pool(poolConfig);
|
|
230
|
+
const client = await this.pool.connect();
|
|
231
|
+
client.release();
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error("Failed to connect to PostgreSQL database:", err);
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async disconnect() {
|
|
238
|
+
if (this.pool) {
|
|
239
|
+
await this.pool.end();
|
|
240
|
+
this.pool = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async getSchemas() {
|
|
244
|
+
if (!this.pool) {
|
|
245
|
+
throw new Error("Not connected to database");
|
|
246
|
+
}
|
|
247
|
+
const client = await this.pool.connect();
|
|
248
|
+
try {
|
|
249
|
+
const result = await client.query(`
|
|
250
|
+
SELECT schema_name
|
|
251
|
+
FROM information_schema.schemata
|
|
252
|
+
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
253
|
+
ORDER BY schema_name
|
|
254
|
+
`);
|
|
255
|
+
return result.rows.map((row) => row.schema_name);
|
|
256
|
+
} finally {
|
|
257
|
+
client.release();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async getTables(schema) {
|
|
261
|
+
if (!this.pool) {
|
|
262
|
+
throw new Error("Not connected to database");
|
|
263
|
+
}
|
|
264
|
+
const client = await this.pool.connect();
|
|
265
|
+
try {
|
|
266
|
+
const schemaToUse = schema || "public";
|
|
267
|
+
const result = await client.query(
|
|
268
|
+
`
|
|
269
|
+
SELECT table_name
|
|
270
|
+
FROM information_schema.tables
|
|
271
|
+
WHERE table_schema = $1
|
|
272
|
+
ORDER BY table_name
|
|
273
|
+
`,
|
|
274
|
+
[schemaToUse]
|
|
275
|
+
);
|
|
276
|
+
return result.rows.map((row) => row.table_name);
|
|
277
|
+
} finally {
|
|
278
|
+
client.release();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async tableExists(tableName, schema) {
|
|
282
|
+
if (!this.pool) {
|
|
283
|
+
throw new Error("Not connected to database");
|
|
284
|
+
}
|
|
285
|
+
const client = await this.pool.connect();
|
|
286
|
+
try {
|
|
287
|
+
const schemaToUse = schema || "public";
|
|
288
|
+
const result = await client.query(
|
|
289
|
+
`
|
|
290
|
+
SELECT EXISTS (
|
|
291
|
+
SELECT FROM information_schema.tables
|
|
292
|
+
WHERE table_schema = $1
|
|
293
|
+
AND table_name = $2
|
|
294
|
+
)
|
|
295
|
+
`,
|
|
296
|
+
[schemaToUse, tableName]
|
|
297
|
+
);
|
|
298
|
+
return result.rows[0].exists;
|
|
299
|
+
} finally {
|
|
300
|
+
client.release();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async getTableIndexes(tableName, schema) {
|
|
304
|
+
if (!this.pool) {
|
|
305
|
+
throw new Error("Not connected to database");
|
|
306
|
+
}
|
|
307
|
+
const client = await this.pool.connect();
|
|
308
|
+
try {
|
|
309
|
+
const schemaToUse = schema || "public";
|
|
310
|
+
const result = await client.query(
|
|
311
|
+
`
|
|
312
|
+
SELECT
|
|
313
|
+
i.relname as index_name,
|
|
314
|
+
array_agg(a.attname) as column_names,
|
|
315
|
+
ix.indisunique as is_unique,
|
|
316
|
+
ix.indisprimary as is_primary
|
|
317
|
+
FROM
|
|
318
|
+
pg_class t,
|
|
319
|
+
pg_class i,
|
|
320
|
+
pg_index ix,
|
|
321
|
+
pg_attribute a,
|
|
322
|
+
pg_namespace ns
|
|
323
|
+
WHERE
|
|
324
|
+
t.oid = ix.indrelid
|
|
325
|
+
AND i.oid = ix.indexrelid
|
|
326
|
+
AND a.attrelid = t.oid
|
|
327
|
+
AND a.attnum = ANY(ix.indkey)
|
|
328
|
+
AND t.relkind = 'r'
|
|
329
|
+
AND t.relname = $1
|
|
330
|
+
AND ns.oid = t.relnamespace
|
|
331
|
+
AND ns.nspname = $2
|
|
332
|
+
GROUP BY
|
|
333
|
+
i.relname,
|
|
334
|
+
ix.indisunique,
|
|
335
|
+
ix.indisprimary
|
|
336
|
+
ORDER BY
|
|
337
|
+
i.relname
|
|
338
|
+
`,
|
|
339
|
+
[tableName, schemaToUse]
|
|
340
|
+
);
|
|
341
|
+
return result.rows.map((row) => ({
|
|
342
|
+
index_name: row.index_name,
|
|
343
|
+
column_names: row.column_names,
|
|
344
|
+
is_unique: row.is_unique,
|
|
345
|
+
is_primary: row.is_primary
|
|
346
|
+
}));
|
|
347
|
+
} finally {
|
|
348
|
+
client.release();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async getTableSchema(tableName, schema) {
|
|
352
|
+
if (!this.pool) {
|
|
353
|
+
throw new Error("Not connected to database");
|
|
354
|
+
}
|
|
355
|
+
const client = await this.pool.connect();
|
|
356
|
+
try {
|
|
357
|
+
const schemaToUse = schema || "public";
|
|
358
|
+
const result = await client.query(
|
|
359
|
+
`
|
|
360
|
+
SELECT
|
|
361
|
+
column_name,
|
|
362
|
+
data_type,
|
|
363
|
+
is_nullable,
|
|
364
|
+
column_default
|
|
365
|
+
FROM information_schema.columns
|
|
366
|
+
WHERE table_schema = $1
|
|
367
|
+
AND table_name = $2
|
|
368
|
+
ORDER BY ordinal_position
|
|
369
|
+
`,
|
|
370
|
+
[schemaToUse, tableName]
|
|
371
|
+
);
|
|
372
|
+
return result.rows;
|
|
373
|
+
} finally {
|
|
374
|
+
client.release();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async getStoredProcedures(schema) {
|
|
378
|
+
if (!this.pool) {
|
|
379
|
+
throw new Error("Not connected to database");
|
|
380
|
+
}
|
|
381
|
+
const client = await this.pool.connect();
|
|
382
|
+
try {
|
|
383
|
+
const schemaToUse = schema || "public";
|
|
384
|
+
const result = await client.query(
|
|
385
|
+
`
|
|
386
|
+
SELECT
|
|
387
|
+
routine_name
|
|
388
|
+
FROM information_schema.routines
|
|
389
|
+
WHERE routine_schema = $1
|
|
390
|
+
ORDER BY routine_name
|
|
391
|
+
`,
|
|
392
|
+
[schemaToUse]
|
|
393
|
+
);
|
|
394
|
+
return result.rows.map((row) => row.routine_name);
|
|
395
|
+
} finally {
|
|
396
|
+
client.release();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async getStoredProcedureDetail(procedureName, schema) {
|
|
400
|
+
if (!this.pool) {
|
|
401
|
+
throw new Error("Not connected to database");
|
|
402
|
+
}
|
|
403
|
+
const client = await this.pool.connect();
|
|
404
|
+
try {
|
|
405
|
+
const schemaToUse = schema || "public";
|
|
406
|
+
const result = await client.query(
|
|
407
|
+
`
|
|
408
|
+
SELECT
|
|
409
|
+
routine_name as procedure_name,
|
|
410
|
+
routine_type,
|
|
411
|
+
CASE WHEN routine_type = 'PROCEDURE' THEN 'procedure' ELSE 'function' END as procedure_type,
|
|
412
|
+
external_language as language,
|
|
413
|
+
data_type as return_type,
|
|
414
|
+
routine_definition as definition,
|
|
415
|
+
(
|
|
416
|
+
SELECT string_agg(
|
|
417
|
+
parameter_name || ' ' ||
|
|
418
|
+
parameter_mode || ' ' ||
|
|
419
|
+
data_type,
|
|
420
|
+
', '
|
|
421
|
+
)
|
|
422
|
+
FROM information_schema.parameters
|
|
423
|
+
WHERE specific_schema = $1
|
|
424
|
+
AND specific_name = $2
|
|
425
|
+
AND parameter_name IS NOT NULL
|
|
426
|
+
) as parameter_list
|
|
427
|
+
FROM information_schema.routines
|
|
428
|
+
WHERE routine_schema = $1
|
|
429
|
+
AND routine_name = $2
|
|
430
|
+
`,
|
|
431
|
+
[schemaToUse, procedureName]
|
|
432
|
+
);
|
|
433
|
+
if (result.rows.length === 0) {
|
|
434
|
+
throw new Error(`Stored procedure '${procedureName}' not found in schema '${schemaToUse}'`);
|
|
435
|
+
}
|
|
436
|
+
const procedure = result.rows[0];
|
|
437
|
+
let definition = procedure.definition;
|
|
438
|
+
try {
|
|
439
|
+
const oidResult = await client.query(
|
|
440
|
+
`
|
|
441
|
+
SELECT p.oid, p.prosrc
|
|
442
|
+
FROM pg_proc p
|
|
443
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
444
|
+
WHERE p.proname = $1
|
|
445
|
+
AND n.nspname = $2
|
|
446
|
+
`,
|
|
447
|
+
[procedureName, schemaToUse]
|
|
448
|
+
);
|
|
449
|
+
if (oidResult.rows.length > 0) {
|
|
450
|
+
if (!definition) {
|
|
451
|
+
const oid = oidResult.rows[0].oid;
|
|
452
|
+
const defResult = await client.query(`SELECT pg_get_functiondef($1)`, [oid]);
|
|
453
|
+
if (defResult.rows.length > 0) {
|
|
454
|
+
definition = defResult.rows[0].pg_get_functiondef;
|
|
455
|
+
} else {
|
|
456
|
+
definition = oidResult.rows[0].prosrc;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.error(`Error getting procedure definition: ${err}`);
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
procedure_name: procedure.procedure_name,
|
|
465
|
+
procedure_type: procedure.procedure_type,
|
|
466
|
+
language: procedure.language || "sql",
|
|
467
|
+
parameter_list: procedure.parameter_list || "",
|
|
468
|
+
return_type: procedure.return_type !== "void" ? procedure.return_type : void 0,
|
|
469
|
+
definition: definition || void 0
|
|
470
|
+
};
|
|
471
|
+
} finally {
|
|
472
|
+
client.release();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async executeSQL(sql2, options, parameters) {
|
|
476
|
+
if (!this.pool) {
|
|
477
|
+
throw new Error("Not connected to database");
|
|
478
|
+
}
|
|
479
|
+
const client = await this.pool.connect();
|
|
480
|
+
try {
|
|
481
|
+
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
482
|
+
if (statements.length === 1) {
|
|
483
|
+
const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
|
|
484
|
+
let result;
|
|
485
|
+
if (parameters && parameters.length > 0) {
|
|
486
|
+
try {
|
|
487
|
+
result = await client.query(processedStatement, parameters);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
|
|
490
|
+
console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
|
|
491
|
+
console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
result = await client.query(processedStatement);
|
|
496
|
+
}
|
|
497
|
+
return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length };
|
|
498
|
+
} else {
|
|
499
|
+
if (parameters && parameters.length > 0) {
|
|
500
|
+
throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
|
|
501
|
+
}
|
|
502
|
+
let allRows = [];
|
|
503
|
+
let totalRowCount = 0;
|
|
504
|
+
await client.query("BEGIN");
|
|
505
|
+
try {
|
|
506
|
+
for (let statement of statements) {
|
|
507
|
+
const processedStatement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
|
|
508
|
+
const result = await client.query(processedStatement);
|
|
509
|
+
if (result.rows && result.rows.length > 0) {
|
|
510
|
+
allRows.push(...result.rows);
|
|
511
|
+
}
|
|
512
|
+
if (result.rowCount) {
|
|
513
|
+
totalRowCount += result.rowCount;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
await client.query("COMMIT");
|
|
517
|
+
} catch (error) {
|
|
518
|
+
await client.query("ROLLBACK");
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
521
|
+
return { rows: allRows, rowCount: totalRowCount };
|
|
522
|
+
}
|
|
523
|
+
} finally {
|
|
524
|
+
client.release();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
var postgresConnector = new PostgresConnector();
|
|
529
|
+
ConnectorRegistry.register(postgresConnector);
|
|
530
|
+
|
|
531
|
+
// src/connectors/sqlserver/index.ts
|
|
532
|
+
import sql from "mssql";
|
|
533
|
+
import { DefaultAzureCredential } from "@azure/identity";
|
|
534
|
+
var SQLServerDSNParser = class {
|
|
535
|
+
async parse(dsn, config) {
|
|
536
|
+
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
537
|
+
const queryTimeoutSeconds = config?.queryTimeoutSeconds;
|
|
538
|
+
if (!this.isValidDSN(dsn)) {
|
|
539
|
+
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
540
|
+
const expectedFormat = this.getSampleDSN();
|
|
541
|
+
throw new Error(
|
|
542
|
+
`Invalid SQL Server DSN format.
|
|
543
|
+
Provided: ${obfuscatedDSN}
|
|
544
|
+
Expected: ${expectedFormat}`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
const url = new SafeURL(dsn);
|
|
549
|
+
const options = {};
|
|
550
|
+
url.forEachSearchParam((value, key) => {
|
|
551
|
+
if (key === "authentication") {
|
|
552
|
+
options.authentication = value;
|
|
553
|
+
} else if (key === "sslmode") {
|
|
554
|
+
options.sslmode = value;
|
|
555
|
+
} else if (key === "instanceName") {
|
|
556
|
+
options.instanceName = value;
|
|
557
|
+
} else if (key === "domain") {
|
|
558
|
+
options.domain = value;
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
if (options.authentication === "ntlm" && !options.domain) {
|
|
562
|
+
throw new Error("NTLM authentication requires 'domain' parameter");
|
|
563
|
+
}
|
|
564
|
+
if (options.domain && options.authentication !== "ntlm") {
|
|
565
|
+
throw new Error("Parameter 'domain' requires 'authentication=ntlm'");
|
|
566
|
+
}
|
|
567
|
+
if (options.sslmode) {
|
|
568
|
+
if (options.sslmode === "disable") {
|
|
569
|
+
options.encrypt = false;
|
|
570
|
+
options.trustServerCertificate = false;
|
|
571
|
+
} else if (options.sslmode === "require") {
|
|
572
|
+
options.encrypt = true;
|
|
573
|
+
options.trustServerCertificate = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const config2 = {
|
|
577
|
+
server: url.hostname,
|
|
578
|
+
port: url.port ? parseInt(url.port) : 1433,
|
|
579
|
+
// Default SQL Server port
|
|
580
|
+
database: url.pathname ? url.pathname.substring(1) : "",
|
|
581
|
+
// Remove leading slash
|
|
582
|
+
options: {
|
|
583
|
+
encrypt: options.encrypt ?? false,
|
|
584
|
+
// Default to unencrypted for development
|
|
585
|
+
trustServerCertificate: options.trustServerCertificate ?? false,
|
|
586
|
+
...connectionTimeoutSeconds !== void 0 && {
|
|
587
|
+
connectTimeout: connectionTimeoutSeconds * 1e3
|
|
588
|
+
},
|
|
589
|
+
...queryTimeoutSeconds !== void 0 && {
|
|
590
|
+
requestTimeout: queryTimeoutSeconds * 1e3
|
|
591
|
+
},
|
|
592
|
+
instanceName: options.instanceName
|
|
593
|
+
// Add named instance support
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
switch (options.authentication) {
|
|
597
|
+
case "azure-active-directory-access-token": {
|
|
598
|
+
try {
|
|
599
|
+
const credential = new DefaultAzureCredential();
|
|
600
|
+
const token = await credential.getToken("https://database.windows.net/");
|
|
601
|
+
config2.authentication = {
|
|
602
|
+
type: "azure-active-directory-access-token",
|
|
603
|
+
options: {
|
|
604
|
+
token: token.token
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
} catch (error) {
|
|
608
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
609
|
+
throw new Error(`Failed to get Azure AD token: ${errorMessage}`);
|
|
610
|
+
}
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
case "ntlm":
|
|
614
|
+
config2.authentication = {
|
|
615
|
+
type: "ntlm",
|
|
616
|
+
options: {
|
|
617
|
+
domain: options.domain,
|
|
618
|
+
userName: url.username,
|
|
619
|
+
password: url.password
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
break;
|
|
623
|
+
default:
|
|
624
|
+
config2.user = url.username;
|
|
625
|
+
config2.password = url.password;
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
return config2;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
throw new Error(
|
|
631
|
+
`Failed to parse SQL Server DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
getSampleDSN() {
|
|
636
|
+
return "sqlserver://username:password@localhost:1433/database?sslmode=disable&instanceName=INSTANCE1";
|
|
637
|
+
}
|
|
638
|
+
isValidDSN(dsn) {
|
|
639
|
+
try {
|
|
640
|
+
return dsn.startsWith("sqlserver://");
|
|
641
|
+
} catch (error) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
var SQLServerConnector = class _SQLServerConnector {
|
|
647
|
+
constructor() {
|
|
648
|
+
this.id = "sqlserver";
|
|
649
|
+
this.name = "SQL Server";
|
|
650
|
+
this.dsnParser = new SQLServerDSNParser();
|
|
651
|
+
// Source ID is set by ConnectorManager after cloning
|
|
652
|
+
this.sourceId = "default";
|
|
653
|
+
}
|
|
654
|
+
getId() {
|
|
655
|
+
return this.sourceId;
|
|
656
|
+
}
|
|
657
|
+
clone() {
|
|
658
|
+
return new _SQLServerConnector();
|
|
659
|
+
}
|
|
660
|
+
async connect(dsn, initScript, config) {
|
|
661
|
+
try {
|
|
662
|
+
this.config = await this.dsnParser.parse(dsn, config);
|
|
663
|
+
if (!this.config.options) {
|
|
664
|
+
this.config.options = {};
|
|
665
|
+
}
|
|
666
|
+
this.connection = await new sql.ConnectionPool(this.config).connect();
|
|
667
|
+
} catch (error) {
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async disconnect() {
|
|
672
|
+
if (this.connection) {
|
|
673
|
+
await this.connection.close();
|
|
674
|
+
this.connection = void 0;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async getSchemas() {
|
|
678
|
+
if (!this.connection) {
|
|
679
|
+
throw new Error("Not connected to SQL Server database");
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
const result = await this.connection.request().query(`
|
|
683
|
+
SELECT SCHEMA_NAME
|
|
684
|
+
FROM INFORMATION_SCHEMA.SCHEMATA
|
|
685
|
+
ORDER BY SCHEMA_NAME
|
|
686
|
+
`);
|
|
687
|
+
return result.recordset.map((row) => row.SCHEMA_NAME);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
throw new Error(`Failed to get schemas: ${error.message}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async getTables(schema) {
|
|
693
|
+
if (!this.connection) {
|
|
694
|
+
throw new Error("Not connected to SQL Server database");
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
const schemaToUse = schema || "dbo";
|
|
698
|
+
const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
|
|
699
|
+
const query = `
|
|
700
|
+
SELECT TABLE_NAME
|
|
701
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
702
|
+
WHERE TABLE_SCHEMA = @schema
|
|
703
|
+
ORDER BY TABLE_NAME
|
|
704
|
+
`;
|
|
705
|
+
const result = await request.query(query);
|
|
706
|
+
return result.recordset.map((row) => row.TABLE_NAME);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
throw new Error(`Failed to get tables: ${error.message}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async tableExists(tableName, schema) {
|
|
712
|
+
if (!this.connection) {
|
|
713
|
+
throw new Error("Not connected to SQL Server database");
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
const schemaToUse = schema || "dbo";
|
|
717
|
+
const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
|
|
718
|
+
const query = `
|
|
719
|
+
SELECT COUNT(*) as count
|
|
720
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
721
|
+
WHERE TABLE_NAME = @tableName
|
|
722
|
+
AND TABLE_SCHEMA = @schema
|
|
723
|
+
`;
|
|
724
|
+
const result = await request.query(query);
|
|
725
|
+
return result.recordset[0].count > 0;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
throw new Error(`Failed to check if table exists: ${error.message}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async getTableIndexes(tableName, schema) {
|
|
731
|
+
if (!this.connection) {
|
|
732
|
+
throw new Error("Not connected to SQL Server database");
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
const schemaToUse = schema || "dbo";
|
|
736
|
+
const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
|
|
737
|
+
const query = `
|
|
738
|
+
SELECT i.name AS index_name,
|
|
739
|
+
i.is_unique,
|
|
740
|
+
i.is_primary_key,
|
|
741
|
+
c.name AS column_name,
|
|
742
|
+
ic.key_ordinal
|
|
743
|
+
FROM sys.indexes i
|
|
744
|
+
INNER JOIN
|
|
745
|
+
sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
746
|
+
INNER JOIN
|
|
747
|
+
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
748
|
+
INNER JOIN
|
|
749
|
+
sys.tables t ON i.object_id = t.object_id
|
|
750
|
+
INNER JOIN
|
|
751
|
+
sys.schemas s ON t.schema_id = s.schema_id
|
|
752
|
+
WHERE t.name = @tableName
|
|
753
|
+
AND s.name = @schema
|
|
754
|
+
ORDER BY i.name,
|
|
755
|
+
ic.key_ordinal
|
|
756
|
+
`;
|
|
757
|
+
const result = await request.query(query);
|
|
758
|
+
const indexMap = /* @__PURE__ */ new Map();
|
|
759
|
+
for (const row of result.recordset) {
|
|
760
|
+
const indexName = row.index_name;
|
|
761
|
+
const columnName = row.column_name;
|
|
762
|
+
const isUnique = !!row.is_unique;
|
|
763
|
+
const isPrimary = !!row.is_primary_key;
|
|
764
|
+
if (!indexMap.has(indexName)) {
|
|
765
|
+
indexMap.set(indexName, {
|
|
766
|
+
columns: [],
|
|
767
|
+
is_unique: isUnique,
|
|
768
|
+
is_primary: isPrimary
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const indexInfo = indexMap.get(indexName);
|
|
772
|
+
indexInfo.columns.push(columnName);
|
|
773
|
+
}
|
|
774
|
+
const indexes = [];
|
|
775
|
+
indexMap.forEach((info, name) => {
|
|
776
|
+
indexes.push({
|
|
777
|
+
index_name: name,
|
|
778
|
+
column_names: info.columns,
|
|
779
|
+
is_unique: info.is_unique,
|
|
780
|
+
is_primary: info.is_primary
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
return indexes;
|
|
784
|
+
} catch (error) {
|
|
785
|
+
throw new Error(`Failed to get indexes for table ${tableName}: ${error.message}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
async getTableSchema(tableName, schema) {
|
|
789
|
+
if (!this.connection) {
|
|
790
|
+
throw new Error("Not connected to SQL Server database");
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
const schemaToUse = schema || "dbo";
|
|
794
|
+
const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
|
|
795
|
+
const query = `
|
|
796
|
+
SELECT COLUMN_NAME as column_name,
|
|
797
|
+
DATA_TYPE as data_type,
|
|
798
|
+
IS_NULLABLE as is_nullable,
|
|
799
|
+
COLUMN_DEFAULT as column_default
|
|
800
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
801
|
+
WHERE TABLE_NAME = @tableName
|
|
802
|
+
AND TABLE_SCHEMA = @schema
|
|
803
|
+
ORDER BY ORDINAL_POSITION
|
|
804
|
+
`;
|
|
805
|
+
const result = await request.query(query);
|
|
806
|
+
return result.recordset;
|
|
807
|
+
} catch (error) {
|
|
808
|
+
throw new Error(`Failed to get schema for table ${tableName}: ${error.message}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async getStoredProcedures(schema) {
|
|
812
|
+
if (!this.connection) {
|
|
813
|
+
throw new Error("Not connected to SQL Server database");
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const schemaToUse = schema || "dbo";
|
|
817
|
+
const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
|
|
818
|
+
const query = `
|
|
819
|
+
SELECT ROUTINE_NAME
|
|
820
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
821
|
+
WHERE ROUTINE_SCHEMA = @schema
|
|
822
|
+
AND (ROUTINE_TYPE = 'PROCEDURE' OR ROUTINE_TYPE = 'FUNCTION')
|
|
823
|
+
ORDER BY ROUTINE_NAME
|
|
824
|
+
`;
|
|
825
|
+
const result = await request.query(query);
|
|
826
|
+
return result.recordset.map((row) => row.ROUTINE_NAME);
|
|
827
|
+
} catch (error) {
|
|
828
|
+
throw new Error(`Failed to get stored procedures: ${error.message}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async getStoredProcedureDetail(procedureName, schema) {
|
|
832
|
+
if (!this.connection) {
|
|
833
|
+
throw new Error("Not connected to SQL Server database");
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
const schemaToUse = schema || "dbo";
|
|
837
|
+
const request = this.connection.request().input("procedureName", sql.VarChar, procedureName).input("schema", sql.VarChar, schemaToUse);
|
|
838
|
+
const routineQuery = `
|
|
839
|
+
SELECT ROUTINE_NAME as procedure_name,
|
|
840
|
+
ROUTINE_TYPE,
|
|
841
|
+
DATA_TYPE as return_data_type
|
|
842
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
843
|
+
WHERE ROUTINE_NAME = @procedureName
|
|
844
|
+
AND ROUTINE_SCHEMA = @schema
|
|
845
|
+
`;
|
|
846
|
+
const routineResult = await request.query(routineQuery);
|
|
847
|
+
if (routineResult.recordset.length === 0) {
|
|
848
|
+
throw new Error(`Stored procedure '${procedureName}' not found in schema '${schemaToUse}'`);
|
|
849
|
+
}
|
|
850
|
+
const routine = routineResult.recordset[0];
|
|
851
|
+
const parameterQuery = `
|
|
852
|
+
SELECT PARAMETER_NAME,
|
|
853
|
+
PARAMETER_MODE,
|
|
854
|
+
DATA_TYPE,
|
|
855
|
+
CHARACTER_MAXIMUM_LENGTH,
|
|
856
|
+
ORDINAL_POSITION
|
|
857
|
+
FROM INFORMATION_SCHEMA.PARAMETERS
|
|
858
|
+
WHERE SPECIFIC_NAME = @procedureName
|
|
859
|
+
AND SPECIFIC_SCHEMA = @schema
|
|
860
|
+
ORDER BY ORDINAL_POSITION
|
|
861
|
+
`;
|
|
862
|
+
const parameterResult = await request.query(parameterQuery);
|
|
863
|
+
let parameterList = "";
|
|
864
|
+
if (parameterResult.recordset.length > 0) {
|
|
865
|
+
parameterList = parameterResult.recordset.map(
|
|
866
|
+
(param) => {
|
|
867
|
+
const lengthStr = param.CHARACTER_MAXIMUM_LENGTH > 0 ? `(${param.CHARACTER_MAXIMUM_LENGTH})` : "";
|
|
868
|
+
return `${param.PARAMETER_NAME} ${param.PARAMETER_MODE} ${param.DATA_TYPE}${lengthStr}`;
|
|
869
|
+
}
|
|
870
|
+
).join(", ");
|
|
871
|
+
}
|
|
872
|
+
const definitionQuery = `
|
|
873
|
+
SELECT definition
|
|
874
|
+
FROM sys.sql_modules sm
|
|
875
|
+
JOIN sys.objects o ON sm.object_id = o.object_id
|
|
876
|
+
JOIN sys.schemas s ON o.schema_id = s.schema_id
|
|
877
|
+
WHERE o.name = @procedureName
|
|
878
|
+
AND s.name = @schema
|
|
879
|
+
`;
|
|
880
|
+
const definitionResult = await request.query(definitionQuery);
|
|
881
|
+
let definition = void 0;
|
|
882
|
+
if (definitionResult.recordset.length > 0) {
|
|
883
|
+
definition = definitionResult.recordset[0].definition;
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
procedure_name: routine.procedure_name,
|
|
887
|
+
procedure_type: routine.ROUTINE_TYPE === "PROCEDURE" ? "procedure" : "function",
|
|
888
|
+
language: "sql",
|
|
889
|
+
// SQL Server procedures are typically in T-SQL
|
|
890
|
+
parameter_list: parameterList,
|
|
891
|
+
return_type: routine.ROUTINE_TYPE === "FUNCTION" ? routine.return_data_type : void 0,
|
|
892
|
+
definition
|
|
893
|
+
};
|
|
894
|
+
} catch (error) {
|
|
895
|
+
throw new Error(`Failed to get stored procedure details: ${error.message}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async executeSQL(sqlQuery, options, parameters) {
|
|
899
|
+
if (!this.connection) {
|
|
900
|
+
throw new Error("Not connected to SQL Server database");
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
let processedSQL = sqlQuery;
|
|
904
|
+
if (options.maxRows) {
|
|
905
|
+
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
|
|
906
|
+
}
|
|
907
|
+
const request = this.connection.request();
|
|
908
|
+
if (parameters && parameters.length > 0) {
|
|
909
|
+
parameters.forEach((param, index) => {
|
|
910
|
+
const paramName = `p${index + 1}`;
|
|
911
|
+
if (typeof param === "string") {
|
|
912
|
+
request.input(paramName, sql.VarChar, param);
|
|
913
|
+
} else if (typeof param === "number") {
|
|
914
|
+
if (Number.isInteger(param)) {
|
|
915
|
+
request.input(paramName, sql.Int, param);
|
|
916
|
+
} else {
|
|
917
|
+
request.input(paramName, sql.Float, param);
|
|
918
|
+
}
|
|
919
|
+
} else if (typeof param === "boolean") {
|
|
920
|
+
request.input(paramName, sql.Bit, param);
|
|
921
|
+
} else if (param === null || param === void 0) {
|
|
922
|
+
request.input(paramName, sql.VarChar, param);
|
|
923
|
+
} else if (Array.isArray(param)) {
|
|
924
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
925
|
+
} else {
|
|
926
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
let result;
|
|
931
|
+
try {
|
|
932
|
+
result = await request.query(processedSQL);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
if (parameters && parameters.length > 0) {
|
|
935
|
+
console.error(`[SQL Server executeSQL] ERROR: ${error.message}`);
|
|
936
|
+
console.error(`[SQL Server executeSQL] SQL: ${processedSQL}`);
|
|
937
|
+
console.error(`[SQL Server executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
938
|
+
}
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
rows: result.recordset || [],
|
|
943
|
+
rowCount: result.rowsAffected[0] || 0
|
|
944
|
+
};
|
|
945
|
+
} catch (error) {
|
|
946
|
+
throw new Error(`Failed to execute query: ${error.message}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
var sqlServerConnector = new SQLServerConnector();
|
|
951
|
+
ConnectorRegistry.register(sqlServerConnector);
|
|
952
|
+
|
|
953
|
+
// src/connectors/sqlite/index.ts
|
|
954
|
+
import Database from "better-sqlite3";
|
|
955
|
+
|
|
956
|
+
// src/utils/identifier-quoter.ts
|
|
957
|
+
function quoteIdentifier(identifier, dbType) {
|
|
958
|
+
if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
|
|
959
|
+
throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
|
|
960
|
+
}
|
|
961
|
+
if (!identifier) {
|
|
962
|
+
throw new Error("Identifier cannot be empty");
|
|
963
|
+
}
|
|
964
|
+
switch (dbType) {
|
|
965
|
+
case "postgres":
|
|
966
|
+
case "sqlite":
|
|
967
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
968
|
+
case "mysql":
|
|
969
|
+
case "mariadb":
|
|
970
|
+
return `\`${identifier.replace(/`/g, "``")}\``;
|
|
971
|
+
case "sqlserver":
|
|
972
|
+
return `[${identifier.replace(/]/g, "]]")}]`;
|
|
973
|
+
default:
|
|
974
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
|
|
978
|
+
const quotedTable = quoteIdentifier(tableName, dbType);
|
|
979
|
+
if (schemaName) {
|
|
980
|
+
const quotedSchema = quoteIdentifier(schemaName, dbType);
|
|
981
|
+
return `${quotedSchema}.${quotedTable}`;
|
|
982
|
+
}
|
|
983
|
+
return quotedTable;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/connectors/sqlite/index.ts
|
|
987
|
+
var SQLiteDSNParser = class {
|
|
988
|
+
async parse(dsn, config) {
|
|
989
|
+
if (!this.isValidDSN(dsn)) {
|
|
990
|
+
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
991
|
+
const expectedFormat = this.getSampleDSN();
|
|
992
|
+
throw new Error(
|
|
993
|
+
`Invalid SQLite DSN format.
|
|
994
|
+
Provided: ${obfuscatedDSN}
|
|
995
|
+
Expected: ${expectedFormat}`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
try {
|
|
999
|
+
const url = new SafeURL(dsn);
|
|
1000
|
+
let dbPath;
|
|
1001
|
+
if (url.hostname === "" && url.pathname === "/:memory:") {
|
|
1002
|
+
dbPath = ":memory:";
|
|
1003
|
+
} else {
|
|
1004
|
+
if (url.pathname.startsWith("//")) {
|
|
1005
|
+
dbPath = url.pathname.substring(2);
|
|
1006
|
+
} else if (url.pathname.match(/^\/[A-Za-z]:\//)) {
|
|
1007
|
+
dbPath = url.pathname.substring(1);
|
|
1008
|
+
} else {
|
|
1009
|
+
dbPath = url.pathname;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return { dbPath };
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
throw new Error(
|
|
1015
|
+
`Failed to parse SQLite DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
getSampleDSN() {
|
|
1020
|
+
return "sqlite:///path/to/database.db";
|
|
1021
|
+
}
|
|
1022
|
+
isValidDSN(dsn) {
|
|
1023
|
+
try {
|
|
1024
|
+
return dsn.startsWith("sqlite://");
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
var SQLiteConnector = class _SQLiteConnector {
|
|
1031
|
+
constructor() {
|
|
1032
|
+
this.id = "sqlite";
|
|
1033
|
+
this.name = "SQLite";
|
|
1034
|
+
this.dsnParser = new SQLiteDSNParser();
|
|
1035
|
+
this.db = null;
|
|
1036
|
+
this.dbPath = ":memory:";
|
|
1037
|
+
// Default to in-memory database
|
|
1038
|
+
// Source ID is set by ConnectorManager after cloning
|
|
1039
|
+
this.sourceId = "default";
|
|
1040
|
+
}
|
|
1041
|
+
getId() {
|
|
1042
|
+
return this.sourceId;
|
|
1043
|
+
}
|
|
1044
|
+
clone() {
|
|
1045
|
+
return new _SQLiteConnector();
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Connect to SQLite database
|
|
1049
|
+
* Note: SQLite does not support connection timeouts as it's a local file-based database.
|
|
1050
|
+
* The config parameter is accepted for interface compliance but ignored.
|
|
1051
|
+
*/
|
|
1052
|
+
async connect(dsn, initScript, config) {
|
|
1053
|
+
const parsedConfig = await this.dsnParser.parse(dsn, config);
|
|
1054
|
+
this.dbPath = parsedConfig.dbPath;
|
|
1055
|
+
try {
|
|
1056
|
+
const dbOptions = {};
|
|
1057
|
+
if (config?.readonly && this.dbPath !== ":memory:") {
|
|
1058
|
+
dbOptions.readonly = true;
|
|
1059
|
+
}
|
|
1060
|
+
this.db = new Database(this.dbPath, dbOptions);
|
|
1061
|
+
if (initScript) {
|
|
1062
|
+
this.db.exec(initScript);
|
|
1063
|
+
}
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
console.error("Failed to connect to SQLite database:", error);
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
async disconnect() {
|
|
1070
|
+
if (this.db) {
|
|
1071
|
+
try {
|
|
1072
|
+
if (!this.db.inTransaction) {
|
|
1073
|
+
this.db.close();
|
|
1074
|
+
} else {
|
|
1075
|
+
try {
|
|
1076
|
+
this.db.exec("ROLLBACK");
|
|
1077
|
+
} catch (rollbackError) {
|
|
1078
|
+
}
|
|
1079
|
+
this.db.close();
|
|
1080
|
+
}
|
|
1081
|
+
this.db = null;
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
console.error("Error during SQLite disconnect:", error);
|
|
1084
|
+
this.db = null;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return Promise.resolve();
|
|
1088
|
+
}
|
|
1089
|
+
async getSchemas() {
|
|
1090
|
+
if (!this.db) {
|
|
1091
|
+
throw new Error("Not connected to SQLite database");
|
|
1092
|
+
}
|
|
1093
|
+
return ["main"];
|
|
1094
|
+
}
|
|
1095
|
+
async getTables(schema) {
|
|
1096
|
+
if (!this.db) {
|
|
1097
|
+
throw new Error("Not connected to SQLite database");
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
const rows = this.db.prepare(
|
|
1101
|
+
`
|
|
1102
|
+
SELECT name FROM sqlite_master
|
|
1103
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
1104
|
+
ORDER BY name
|
|
1105
|
+
`
|
|
1106
|
+
).all();
|
|
1107
|
+
return rows.map((row) => row.name);
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
throw error;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
async tableExists(tableName, schema) {
|
|
1113
|
+
if (!this.db) {
|
|
1114
|
+
throw new Error("Not connected to SQLite database");
|
|
1115
|
+
}
|
|
1116
|
+
try {
|
|
1117
|
+
const row = this.db.prepare(
|
|
1118
|
+
`
|
|
1119
|
+
SELECT name FROM sqlite_master
|
|
1120
|
+
WHERE type='table' AND name = ?
|
|
1121
|
+
`
|
|
1122
|
+
).get(tableName);
|
|
1123
|
+
return !!row;
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
throw error;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
async getTableIndexes(tableName, schema) {
|
|
1129
|
+
if (!this.db) {
|
|
1130
|
+
throw new Error("Not connected to SQLite database");
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
const indexInfoRows = this.db.prepare(
|
|
1134
|
+
`
|
|
1135
|
+
SELECT
|
|
1136
|
+
name as index_name,
|
|
1137
|
+
0 as is_unique
|
|
1138
|
+
FROM sqlite_master
|
|
1139
|
+
WHERE type = 'index'
|
|
1140
|
+
AND tbl_name = ?
|
|
1141
|
+
`
|
|
1142
|
+
).all(tableName);
|
|
1143
|
+
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
1144
|
+
const indexListRows = this.db.prepare(`PRAGMA index_list(${quotedTableName})`).all();
|
|
1145
|
+
const indexUniqueMap = /* @__PURE__ */ new Map();
|
|
1146
|
+
for (const indexListRow of indexListRows) {
|
|
1147
|
+
indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
|
|
1148
|
+
}
|
|
1149
|
+
const tableInfo = this.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
|
|
1150
|
+
const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
|
|
1151
|
+
const results = [];
|
|
1152
|
+
for (const indexInfo of indexInfoRows) {
|
|
1153
|
+
const quotedIndexName = quoteIdentifier(indexInfo.index_name, "sqlite");
|
|
1154
|
+
const indexDetailRows = this.db.prepare(`PRAGMA index_info(${quotedIndexName})`).all();
|
|
1155
|
+
const columnNames = indexDetailRows.map((row) => row.name);
|
|
1156
|
+
results.push({
|
|
1157
|
+
index_name: indexInfo.index_name,
|
|
1158
|
+
column_names: columnNames,
|
|
1159
|
+
is_unique: indexUniqueMap.get(indexInfo.index_name) || false,
|
|
1160
|
+
is_primary: false
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
if (pkColumns.length > 0) {
|
|
1164
|
+
results.push({
|
|
1165
|
+
index_name: "PRIMARY",
|
|
1166
|
+
column_names: pkColumns,
|
|
1167
|
+
is_unique: true,
|
|
1168
|
+
is_primary: true
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
return results;
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
throw error;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async getTableSchema(tableName, schema) {
|
|
1177
|
+
if (!this.db) {
|
|
1178
|
+
throw new Error("Not connected to SQLite database");
|
|
1179
|
+
}
|
|
1180
|
+
try {
|
|
1181
|
+
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
1182
|
+
const rows = this.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
|
|
1183
|
+
const columns = rows.map((row) => ({
|
|
1184
|
+
column_name: row.name,
|
|
1185
|
+
data_type: row.type,
|
|
1186
|
+
// In SQLite, primary key columns are automatically NOT NULL even if notnull=0
|
|
1187
|
+
is_nullable: row.notnull === 1 || row.pk > 0 ? "NO" : "YES",
|
|
1188
|
+
column_default: row.dflt_value
|
|
1189
|
+
}));
|
|
1190
|
+
return columns;
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
throw error;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
async getStoredProcedures(schema) {
|
|
1196
|
+
if (!this.db) {
|
|
1197
|
+
throw new Error("Not connected to SQLite database");
|
|
1198
|
+
}
|
|
1199
|
+
return [];
|
|
1200
|
+
}
|
|
1201
|
+
async getStoredProcedureDetail(procedureName, schema) {
|
|
1202
|
+
if (!this.db) {
|
|
1203
|
+
throw new Error("Not connected to SQLite database");
|
|
1204
|
+
}
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
"SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
async executeSQL(sql2, options, parameters) {
|
|
1210
|
+
if (!this.db) {
|
|
1211
|
+
throw new Error("Not connected to SQLite database");
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
1215
|
+
if (statements.length === 1) {
|
|
1216
|
+
let processedStatement = statements[0];
|
|
1217
|
+
const trimmedStatement = statements[0].toLowerCase().trim();
|
|
1218
|
+
const isReadStatement = trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"));
|
|
1219
|
+
if (options.maxRows) {
|
|
1220
|
+
processedStatement = SQLRowLimiter.applyMaxRows(processedStatement, options.maxRows);
|
|
1221
|
+
}
|
|
1222
|
+
if (isReadStatement) {
|
|
1223
|
+
if (parameters && parameters.length > 0) {
|
|
1224
|
+
try {
|
|
1225
|
+
const rows = this.db.prepare(processedStatement).all(...parameters);
|
|
1226
|
+
return { rows, rowCount: rows.length };
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
1229
|
+
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
1230
|
+
console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
1231
|
+
throw error;
|
|
1232
|
+
}
|
|
1233
|
+
} else {
|
|
1234
|
+
const rows = this.db.prepare(processedStatement).all();
|
|
1235
|
+
return { rows, rowCount: rows.length };
|
|
1236
|
+
}
|
|
1237
|
+
} else {
|
|
1238
|
+
let result;
|
|
1239
|
+
if (parameters && parameters.length > 0) {
|
|
1240
|
+
try {
|
|
1241
|
+
result = this.db.prepare(processedStatement).run(...parameters);
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
1244
|
+
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
1245
|
+
console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
result = this.db.prepare(processedStatement).run();
|
|
1250
|
+
}
|
|
1251
|
+
return { rows: [], rowCount: result.changes };
|
|
1252
|
+
}
|
|
1253
|
+
} else {
|
|
1254
|
+
if (parameters && parameters.length > 0) {
|
|
1255
|
+
throw new Error("Parameters are not supported for multi-statement queries in SQLite");
|
|
1256
|
+
}
|
|
1257
|
+
const readStatements = [];
|
|
1258
|
+
const writeStatements = [];
|
|
1259
|
+
for (const statement of statements) {
|
|
1260
|
+
const trimmedStatement = statement.toLowerCase().trim();
|
|
1261
|
+
if (trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"))) {
|
|
1262
|
+
readStatements.push(statement);
|
|
1263
|
+
} else {
|
|
1264
|
+
writeStatements.push(statement);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
let totalChanges = 0;
|
|
1268
|
+
for (const statement of writeStatements) {
|
|
1269
|
+
const result = this.db.prepare(statement).run();
|
|
1270
|
+
totalChanges += result.changes;
|
|
1271
|
+
}
|
|
1272
|
+
let allRows = [];
|
|
1273
|
+
for (let statement of readStatements) {
|
|
1274
|
+
statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
|
|
1275
|
+
const result = this.db.prepare(statement).all();
|
|
1276
|
+
allRows.push(...result);
|
|
1277
|
+
}
|
|
1278
|
+
return { rows: allRows, rowCount: totalChanges + allRows.length };
|
|
1279
|
+
}
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
throw error;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
var sqliteConnector = new SQLiteConnector();
|
|
1286
|
+
ConnectorRegistry.register(sqliteConnector);
|
|
1287
|
+
|
|
1288
|
+
// src/connectors/mysql/index.ts
|
|
1289
|
+
import mysql from "mysql2/promise";
|
|
1290
|
+
|
|
1291
|
+
// src/utils/multi-statement-result-parser.ts
|
|
1292
|
+
function isMetadataObject(element) {
|
|
1293
|
+
if (!element || typeof element !== "object" || Array.isArray(element)) {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
return "affectedRows" in element || "insertId" in element || "fieldCount" in element || "warningStatus" in element;
|
|
1297
|
+
}
|
|
1298
|
+
function isMultiStatementResult(results) {
|
|
1299
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
const firstElement = results[0];
|
|
1303
|
+
return isMetadataObject(firstElement) || Array.isArray(firstElement);
|
|
1304
|
+
}
|
|
1305
|
+
function extractRowsFromMultiStatement(results) {
|
|
1306
|
+
if (!Array.isArray(results)) {
|
|
1307
|
+
return [];
|
|
1308
|
+
}
|
|
1309
|
+
const allRows = [];
|
|
1310
|
+
for (const result of results) {
|
|
1311
|
+
if (Array.isArray(result)) {
|
|
1312
|
+
allRows.push(...result);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return allRows;
|
|
1316
|
+
}
|
|
1317
|
+
function extractAffectedRows(results) {
|
|
1318
|
+
if (isMetadataObject(results)) {
|
|
1319
|
+
return results.affectedRows || 0;
|
|
1320
|
+
}
|
|
1321
|
+
if (!Array.isArray(results)) {
|
|
1322
|
+
return 0;
|
|
1323
|
+
}
|
|
1324
|
+
if (isMultiStatementResult(results)) {
|
|
1325
|
+
let totalAffected = 0;
|
|
1326
|
+
for (const result of results) {
|
|
1327
|
+
if (isMetadataObject(result)) {
|
|
1328
|
+
totalAffected += result.affectedRows || 0;
|
|
1329
|
+
} else if (Array.isArray(result)) {
|
|
1330
|
+
totalAffected += result.length;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return totalAffected;
|
|
1334
|
+
}
|
|
1335
|
+
return results.length;
|
|
1336
|
+
}
|
|
1337
|
+
function parseQueryResults(results) {
|
|
1338
|
+
if (!Array.isArray(results)) {
|
|
1339
|
+
return [];
|
|
1340
|
+
}
|
|
1341
|
+
if (isMultiStatementResult(results)) {
|
|
1342
|
+
return extractRowsFromMultiStatement(results);
|
|
1343
|
+
}
|
|
1344
|
+
return results;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/connectors/mysql/index.ts
|
|
1348
|
+
var MySQLDSNParser = class {
|
|
1349
|
+
async parse(dsn, config) {
|
|
1350
|
+
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
1351
|
+
if (!this.isValidDSN(dsn)) {
|
|
1352
|
+
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
1353
|
+
const expectedFormat = this.getSampleDSN();
|
|
1354
|
+
throw new Error(
|
|
1355
|
+
`Invalid MySQL DSN format.
|
|
1356
|
+
Provided: ${obfuscatedDSN}
|
|
1357
|
+
Expected: ${expectedFormat}`
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
try {
|
|
1361
|
+
const url = new SafeURL(dsn);
|
|
1362
|
+
const config2 = {
|
|
1363
|
+
host: url.hostname,
|
|
1364
|
+
port: url.port ? parseInt(url.port) : 3306,
|
|
1365
|
+
database: url.pathname ? url.pathname.substring(1) : "",
|
|
1366
|
+
// Remove leading '/' if exists
|
|
1367
|
+
user: url.username,
|
|
1368
|
+
password: url.password,
|
|
1369
|
+
multipleStatements: true
|
|
1370
|
+
// Enable native multi-statement support
|
|
1371
|
+
};
|
|
1372
|
+
url.forEachSearchParam((value, key) => {
|
|
1373
|
+
if (key === "sslmode") {
|
|
1374
|
+
if (value === "disable") {
|
|
1375
|
+
config2.ssl = void 0;
|
|
1376
|
+
} else if (value === "require") {
|
|
1377
|
+
config2.ssl = { rejectUnauthorized: false };
|
|
1378
|
+
} else {
|
|
1379
|
+
config2.ssl = {};
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
if (connectionTimeoutSeconds !== void 0) {
|
|
1384
|
+
config2.connectTimeout = connectionTimeoutSeconds * 1e3;
|
|
1385
|
+
}
|
|
1386
|
+
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
1387
|
+
config2.authPlugins = {
|
|
1388
|
+
mysql_clear_password: () => () => {
|
|
1389
|
+
return Buffer.from(url.password + "\0");
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
if (config2.ssl === void 0) {
|
|
1393
|
+
config2.ssl = { rejectUnauthorized: false };
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return config2;
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
`Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
getSampleDSN() {
|
|
1404
|
+
return "mysql://root:password@localhost:3306/mysql?sslmode=require";
|
|
1405
|
+
}
|
|
1406
|
+
isValidDSN(dsn) {
|
|
1407
|
+
try {
|
|
1408
|
+
return dsn.startsWith("mysql://");
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
return false;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
var MySQLConnector = class _MySQLConnector {
|
|
1415
|
+
constructor() {
|
|
1416
|
+
this.id = "mysql";
|
|
1417
|
+
this.name = "MySQL";
|
|
1418
|
+
this.dsnParser = new MySQLDSNParser();
|
|
1419
|
+
this.pool = null;
|
|
1420
|
+
// Source ID is set by ConnectorManager after cloning
|
|
1421
|
+
this.sourceId = "default";
|
|
1422
|
+
}
|
|
1423
|
+
getId() {
|
|
1424
|
+
return this.sourceId;
|
|
1425
|
+
}
|
|
1426
|
+
clone() {
|
|
1427
|
+
return new _MySQLConnector();
|
|
1428
|
+
}
|
|
1429
|
+
async connect(dsn, initScript, config) {
|
|
1430
|
+
try {
|
|
1431
|
+
const connectionOptions = await this.dsnParser.parse(dsn, config);
|
|
1432
|
+
this.pool = mysql.createPool(connectionOptions);
|
|
1433
|
+
if (config?.queryTimeoutSeconds !== void 0) {
|
|
1434
|
+
this.queryTimeoutMs = config.queryTimeoutSeconds * 1e3;
|
|
1435
|
+
}
|
|
1436
|
+
const [rows] = await this.pool.query("SELECT 1");
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
console.error("Failed to connect to MySQL database:", err);
|
|
1439
|
+
throw err;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async disconnect() {
|
|
1443
|
+
if (this.pool) {
|
|
1444
|
+
await this.pool.end();
|
|
1445
|
+
this.pool = null;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
async getSchemas() {
|
|
1449
|
+
if (!this.pool) {
|
|
1450
|
+
throw new Error("Not connected to database");
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
const [rows] = await this.pool.query(`
|
|
1454
|
+
SELECT SCHEMA_NAME
|
|
1455
|
+
FROM INFORMATION_SCHEMA.SCHEMATA
|
|
1456
|
+
ORDER BY SCHEMA_NAME
|
|
1457
|
+
`);
|
|
1458
|
+
return rows.map((row) => row.SCHEMA_NAME);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
console.error("Error getting schemas:", error);
|
|
1461
|
+
throw error;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async getTables(schema) {
|
|
1465
|
+
if (!this.pool) {
|
|
1466
|
+
throw new Error("Not connected to database");
|
|
1467
|
+
}
|
|
1468
|
+
try {
|
|
1469
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1470
|
+
const queryParams = schema ? [schema] : [];
|
|
1471
|
+
const [rows] = await this.pool.query(
|
|
1472
|
+
`
|
|
1473
|
+
SELECT TABLE_NAME
|
|
1474
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
1475
|
+
${schemaClause}
|
|
1476
|
+
ORDER BY TABLE_NAME
|
|
1477
|
+
`,
|
|
1478
|
+
queryParams
|
|
1479
|
+
);
|
|
1480
|
+
return rows.map((row) => row.TABLE_NAME);
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
console.error("Error getting tables:", error);
|
|
1483
|
+
throw error;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
async tableExists(tableName, schema) {
|
|
1487
|
+
if (!this.pool) {
|
|
1488
|
+
throw new Error("Not connected to database");
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1492
|
+
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1493
|
+
const [rows] = await this.pool.query(
|
|
1494
|
+
`
|
|
1495
|
+
SELECT COUNT(*) AS COUNT
|
|
1496
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
1497
|
+
${schemaClause}
|
|
1498
|
+
AND TABLE_NAME = ?
|
|
1499
|
+
`,
|
|
1500
|
+
queryParams
|
|
1501
|
+
);
|
|
1502
|
+
return rows[0].COUNT > 0;
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
console.error("Error checking if table exists:", error);
|
|
1505
|
+
throw error;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async getTableIndexes(tableName, schema) {
|
|
1509
|
+
if (!this.pool) {
|
|
1510
|
+
throw new Error("Not connected to database");
|
|
1511
|
+
}
|
|
1512
|
+
try {
|
|
1513
|
+
const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
|
|
1514
|
+
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1515
|
+
const [indexRows] = await this.pool.query(
|
|
1516
|
+
`
|
|
1517
|
+
SELECT
|
|
1518
|
+
INDEX_NAME,
|
|
1519
|
+
COLUMN_NAME,
|
|
1520
|
+
NON_UNIQUE,
|
|
1521
|
+
SEQ_IN_INDEX
|
|
1522
|
+
FROM
|
|
1523
|
+
INFORMATION_SCHEMA.STATISTICS
|
|
1524
|
+
WHERE
|
|
1525
|
+
${schemaClause}
|
|
1526
|
+
AND TABLE_NAME = ?
|
|
1527
|
+
ORDER BY
|
|
1528
|
+
INDEX_NAME,
|
|
1529
|
+
SEQ_IN_INDEX
|
|
1530
|
+
`,
|
|
1531
|
+
queryParams
|
|
1532
|
+
);
|
|
1533
|
+
const indexMap = /* @__PURE__ */ new Map();
|
|
1534
|
+
for (const row of indexRows) {
|
|
1535
|
+
const indexName = row.INDEX_NAME;
|
|
1536
|
+
const columnName = row.COLUMN_NAME;
|
|
1537
|
+
const isUnique = row.NON_UNIQUE === 0;
|
|
1538
|
+
const isPrimary = indexName === "PRIMARY";
|
|
1539
|
+
if (!indexMap.has(indexName)) {
|
|
1540
|
+
indexMap.set(indexName, {
|
|
1541
|
+
columns: [],
|
|
1542
|
+
is_unique: isUnique,
|
|
1543
|
+
is_primary: isPrimary
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
const indexInfo = indexMap.get(indexName);
|
|
1547
|
+
indexInfo.columns.push(columnName);
|
|
1548
|
+
}
|
|
1549
|
+
const results = [];
|
|
1550
|
+
indexMap.forEach((indexInfo, indexName) => {
|
|
1551
|
+
results.push({
|
|
1552
|
+
index_name: indexName,
|
|
1553
|
+
column_names: indexInfo.columns,
|
|
1554
|
+
is_unique: indexInfo.is_unique,
|
|
1555
|
+
is_primary: indexInfo.is_primary
|
|
1556
|
+
});
|
|
1557
|
+
});
|
|
1558
|
+
return results;
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
console.error("Error getting table indexes:", error);
|
|
1561
|
+
throw error;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
async getTableSchema(tableName, schema) {
|
|
1565
|
+
if (!this.pool) {
|
|
1566
|
+
throw new Error("Not connected to database");
|
|
1567
|
+
}
|
|
1568
|
+
try {
|
|
1569
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1570
|
+
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1571
|
+
const [rows] = await this.pool.query(
|
|
1572
|
+
`
|
|
1573
|
+
SELECT
|
|
1574
|
+
COLUMN_NAME as column_name,
|
|
1575
|
+
DATA_TYPE as data_type,
|
|
1576
|
+
IS_NULLABLE as is_nullable,
|
|
1577
|
+
COLUMN_DEFAULT as column_default
|
|
1578
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
1579
|
+
${schemaClause}
|
|
1580
|
+
AND TABLE_NAME = ?
|
|
1581
|
+
ORDER BY ORDINAL_POSITION
|
|
1582
|
+
`,
|
|
1583
|
+
queryParams
|
|
1584
|
+
);
|
|
1585
|
+
return rows;
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
console.error("Error getting table schema:", error);
|
|
1588
|
+
throw error;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
async getStoredProcedures(schema) {
|
|
1592
|
+
if (!this.pool) {
|
|
1593
|
+
throw new Error("Not connected to database");
|
|
1594
|
+
}
|
|
1595
|
+
try {
|
|
1596
|
+
const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
|
|
1597
|
+
const queryParams = schema ? [schema] : [];
|
|
1598
|
+
const [rows] = await this.pool.query(
|
|
1599
|
+
`
|
|
1600
|
+
SELECT ROUTINE_NAME
|
|
1601
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
1602
|
+
${schemaClause}
|
|
1603
|
+
ORDER BY ROUTINE_NAME
|
|
1604
|
+
`,
|
|
1605
|
+
queryParams
|
|
1606
|
+
);
|
|
1607
|
+
return rows.map((row) => row.ROUTINE_NAME);
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
console.error("Error getting stored procedures:", error);
|
|
1610
|
+
throw error;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
async getStoredProcedureDetail(procedureName, schema) {
|
|
1614
|
+
if (!this.pool) {
|
|
1615
|
+
throw new Error("Not connected to database");
|
|
1616
|
+
}
|
|
1617
|
+
try {
|
|
1618
|
+
const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
|
|
1619
|
+
const queryParams = schema ? [schema, procedureName] : [procedureName];
|
|
1620
|
+
const [rows] = await this.pool.query(
|
|
1621
|
+
`
|
|
1622
|
+
SELECT
|
|
1623
|
+
r.ROUTINE_NAME AS procedure_name,
|
|
1624
|
+
CASE
|
|
1625
|
+
WHEN r.ROUTINE_TYPE = 'PROCEDURE' THEN 'procedure'
|
|
1626
|
+
ELSE 'function'
|
|
1627
|
+
END AS procedure_type,
|
|
1628
|
+
LOWER(r.ROUTINE_TYPE) AS routine_type,
|
|
1629
|
+
r.ROUTINE_DEFINITION,
|
|
1630
|
+
r.DTD_IDENTIFIER AS return_type,
|
|
1631
|
+
(
|
|
1632
|
+
SELECT GROUP_CONCAT(
|
|
1633
|
+
CONCAT(p.PARAMETER_NAME, ' ', p.PARAMETER_MODE, ' ', p.DATA_TYPE)
|
|
1634
|
+
ORDER BY p.ORDINAL_POSITION
|
|
1635
|
+
SEPARATOR ', '
|
|
1636
|
+
)
|
|
1637
|
+
FROM INFORMATION_SCHEMA.PARAMETERS p
|
|
1638
|
+
WHERE p.SPECIFIC_SCHEMA = r.ROUTINE_SCHEMA
|
|
1639
|
+
AND p.SPECIFIC_NAME = r.ROUTINE_NAME
|
|
1640
|
+
AND p.PARAMETER_NAME IS NOT NULL
|
|
1641
|
+
) AS parameter_list
|
|
1642
|
+
FROM INFORMATION_SCHEMA.ROUTINES r
|
|
1643
|
+
${schemaClause}
|
|
1644
|
+
AND r.ROUTINE_NAME = ?
|
|
1645
|
+
`,
|
|
1646
|
+
queryParams
|
|
1647
|
+
);
|
|
1648
|
+
if (rows.length === 0) {
|
|
1649
|
+
const schemaName = schema || "current schema";
|
|
1650
|
+
throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
|
|
1651
|
+
}
|
|
1652
|
+
const procedure = rows[0];
|
|
1653
|
+
let definition = procedure.ROUTINE_DEFINITION;
|
|
1654
|
+
try {
|
|
1655
|
+
const schemaValue = schema || await this.getCurrentSchema();
|
|
1656
|
+
if (procedure.procedure_type === "procedure") {
|
|
1657
|
+
try {
|
|
1658
|
+
const [defRows] = await this.pool.query(`
|
|
1659
|
+
SHOW CREATE PROCEDURE ${schemaValue}.${procedureName}
|
|
1660
|
+
`);
|
|
1661
|
+
if (defRows && defRows.length > 0) {
|
|
1662
|
+
definition = defRows[0]["Create Procedure"];
|
|
1663
|
+
}
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
console.error(`Error getting procedure definition with SHOW CREATE: ${err}`);
|
|
1666
|
+
}
|
|
1667
|
+
} else {
|
|
1668
|
+
try {
|
|
1669
|
+
const [defRows] = await this.pool.query(`
|
|
1670
|
+
SHOW CREATE FUNCTION ${schemaValue}.${procedureName}
|
|
1671
|
+
`);
|
|
1672
|
+
if (defRows && defRows.length > 0) {
|
|
1673
|
+
definition = defRows[0]["Create Function"];
|
|
1674
|
+
}
|
|
1675
|
+
} catch (innerErr) {
|
|
1676
|
+
console.error(`Error getting function definition with SHOW CREATE: ${innerErr}`);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (!definition) {
|
|
1680
|
+
const [bodyRows] = await this.pool.query(
|
|
1681
|
+
`
|
|
1682
|
+
SELECT ROUTINE_DEFINITION, ROUTINE_BODY
|
|
1683
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
1684
|
+
WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
|
|
1685
|
+
`,
|
|
1686
|
+
[schemaValue, procedureName]
|
|
1687
|
+
);
|
|
1688
|
+
if (bodyRows && bodyRows.length > 0) {
|
|
1689
|
+
if (bodyRows[0].ROUTINE_DEFINITION) {
|
|
1690
|
+
definition = bodyRows[0].ROUTINE_DEFINITION;
|
|
1691
|
+
} else if (bodyRows[0].ROUTINE_BODY) {
|
|
1692
|
+
definition = bodyRows[0].ROUTINE_BODY;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
console.error(`Error getting procedure/function details: ${error}`);
|
|
1698
|
+
}
|
|
1699
|
+
return {
|
|
1700
|
+
procedure_name: procedure.procedure_name,
|
|
1701
|
+
procedure_type: procedure.procedure_type,
|
|
1702
|
+
language: "sql",
|
|
1703
|
+
// MySQL procedures are generally in SQL
|
|
1704
|
+
parameter_list: procedure.parameter_list || "",
|
|
1705
|
+
return_type: procedure.routine_type === "function" ? procedure.return_type : void 0,
|
|
1706
|
+
definition: definition || void 0
|
|
1707
|
+
};
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
console.error("Error getting stored procedure detail:", error);
|
|
1710
|
+
throw error;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
// Helper method to get current schema (database) name
|
|
1714
|
+
async getCurrentSchema() {
|
|
1715
|
+
const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
|
|
1716
|
+
return rows[0].DB;
|
|
1717
|
+
}
|
|
1718
|
+
async executeSQL(sql2, options, parameters) {
|
|
1719
|
+
if (!this.pool) {
|
|
1720
|
+
throw new Error("Not connected to database");
|
|
1721
|
+
}
|
|
1722
|
+
const conn = await this.pool.getConnection();
|
|
1723
|
+
try {
|
|
1724
|
+
let processedSQL = sql2;
|
|
1725
|
+
if (options.maxRows) {
|
|
1726
|
+
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
1727
|
+
const processedStatements = statements.map(
|
|
1728
|
+
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
1729
|
+
);
|
|
1730
|
+
processedSQL = processedStatements.join("; ");
|
|
1731
|
+
if (sql2.trim().endsWith(";")) {
|
|
1732
|
+
processedSQL += ";";
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
let results;
|
|
1736
|
+
if (parameters && parameters.length > 0) {
|
|
1737
|
+
try {
|
|
1738
|
+
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs }, parameters);
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
console.error(`[MySQL executeSQL] ERROR: ${error.message}`);
|
|
1741
|
+
console.error(`[MySQL executeSQL] SQL: ${processedSQL}`);
|
|
1742
|
+
console.error(`[MySQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
1743
|
+
throw error;
|
|
1744
|
+
}
|
|
1745
|
+
} else {
|
|
1746
|
+
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs });
|
|
1747
|
+
}
|
|
1748
|
+
const [firstResult] = results;
|
|
1749
|
+
const rows = parseQueryResults(firstResult);
|
|
1750
|
+
const rowCount = extractAffectedRows(firstResult);
|
|
1751
|
+
return { rows, rowCount };
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
console.error("Error executing query:", error);
|
|
1754
|
+
throw error;
|
|
1755
|
+
} finally {
|
|
1756
|
+
conn.release();
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
var mysqlConnector = new MySQLConnector();
|
|
1761
|
+
ConnectorRegistry.register(mysqlConnector);
|
|
1762
|
+
|
|
1763
|
+
// src/connectors/mariadb/index.ts
|
|
1764
|
+
import * as mariadb from "mariadb";
|
|
1765
|
+
var MariadbDSNParser = class {
|
|
1766
|
+
async parse(dsn, config) {
|
|
1767
|
+
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
1768
|
+
const queryTimeoutSeconds = config?.queryTimeoutSeconds;
|
|
1769
|
+
if (!this.isValidDSN(dsn)) {
|
|
1770
|
+
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
1771
|
+
const expectedFormat = this.getSampleDSN();
|
|
1772
|
+
throw new Error(
|
|
1773
|
+
`Invalid MariaDB DSN format.
|
|
1774
|
+
Provided: ${obfuscatedDSN}
|
|
1775
|
+
Expected: ${expectedFormat}`
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
try {
|
|
1779
|
+
const url = new SafeURL(dsn);
|
|
1780
|
+
const connectionConfig = {
|
|
1781
|
+
host: url.hostname,
|
|
1782
|
+
port: url.port ? parseInt(url.port) : 3306,
|
|
1783
|
+
database: url.pathname ? url.pathname.substring(1) : "",
|
|
1784
|
+
// Remove leading '/' if exists
|
|
1785
|
+
user: url.username,
|
|
1786
|
+
password: url.password,
|
|
1787
|
+
multipleStatements: true,
|
|
1788
|
+
// Enable native multi-statement support
|
|
1789
|
+
...connectionTimeoutSeconds !== void 0 && {
|
|
1790
|
+
connectTimeout: connectionTimeoutSeconds * 1e3
|
|
1791
|
+
},
|
|
1792
|
+
...queryTimeoutSeconds !== void 0 && {
|
|
1793
|
+
queryTimeout: queryTimeoutSeconds * 1e3
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
url.forEachSearchParam((value, key) => {
|
|
1797
|
+
if (key === "sslmode") {
|
|
1798
|
+
if (value === "disable") {
|
|
1799
|
+
connectionConfig.ssl = void 0;
|
|
1800
|
+
} else if (value === "require") {
|
|
1801
|
+
connectionConfig.ssl = { rejectUnauthorized: false };
|
|
1802
|
+
} else {
|
|
1803
|
+
connectionConfig.ssl = {};
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
1808
|
+
if (connectionConfig.ssl === void 0) {
|
|
1809
|
+
connectionConfig.ssl = { rejectUnauthorized: false };
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return connectionConfig;
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
throw new Error(
|
|
1815
|
+
`Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
getSampleDSN() {
|
|
1820
|
+
return "mariadb://root:password@localhost:3306/db?sslmode=require";
|
|
1821
|
+
}
|
|
1822
|
+
isValidDSN(dsn) {
|
|
1823
|
+
try {
|
|
1824
|
+
return dsn.startsWith("mariadb://");
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
return false;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
var MariaDBConnector = class _MariaDBConnector {
|
|
1831
|
+
constructor() {
|
|
1832
|
+
this.id = "mariadb";
|
|
1833
|
+
this.name = "MariaDB";
|
|
1834
|
+
this.dsnParser = new MariadbDSNParser();
|
|
1835
|
+
this.pool = null;
|
|
1836
|
+
// Source ID is set by ConnectorManager after cloning
|
|
1837
|
+
this.sourceId = "default";
|
|
1838
|
+
}
|
|
1839
|
+
getId() {
|
|
1840
|
+
return this.sourceId;
|
|
1841
|
+
}
|
|
1842
|
+
clone() {
|
|
1843
|
+
return new _MariaDBConnector();
|
|
1844
|
+
}
|
|
1845
|
+
async connect(dsn, initScript, config) {
|
|
1846
|
+
try {
|
|
1847
|
+
const connectionConfig = await this.dsnParser.parse(dsn, config);
|
|
1848
|
+
this.pool = mariadb.createPool(connectionConfig);
|
|
1849
|
+
await this.pool.query("SELECT 1");
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
console.error("Failed to connect to MariaDB database:", err);
|
|
1852
|
+
throw err;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
async disconnect() {
|
|
1856
|
+
if (this.pool) {
|
|
1857
|
+
await this.pool.end();
|
|
1858
|
+
this.pool = null;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
async getSchemas() {
|
|
1862
|
+
if (!this.pool) {
|
|
1863
|
+
throw new Error("Not connected to database");
|
|
1864
|
+
}
|
|
1865
|
+
try {
|
|
1866
|
+
const rows = await this.pool.query(`
|
|
1867
|
+
SELECT SCHEMA_NAME
|
|
1868
|
+
FROM INFORMATION_SCHEMA.SCHEMATA
|
|
1869
|
+
ORDER BY SCHEMA_NAME
|
|
1870
|
+
`);
|
|
1871
|
+
return rows.map((row) => row.SCHEMA_NAME);
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
console.error("Error getting schemas:", error);
|
|
1874
|
+
throw error;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
async getTables(schema) {
|
|
1878
|
+
if (!this.pool) {
|
|
1879
|
+
throw new Error("Not connected to database");
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1883
|
+
const queryParams = schema ? [schema] : [];
|
|
1884
|
+
const rows = await this.pool.query(
|
|
1885
|
+
`
|
|
1886
|
+
SELECT TABLE_NAME
|
|
1887
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
1888
|
+
${schemaClause}
|
|
1889
|
+
ORDER BY TABLE_NAME
|
|
1890
|
+
`,
|
|
1891
|
+
queryParams
|
|
1892
|
+
);
|
|
1893
|
+
return rows.map((row) => row.TABLE_NAME);
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
console.error("Error getting tables:", error);
|
|
1896
|
+
throw error;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
async tableExists(tableName, schema) {
|
|
1900
|
+
if (!this.pool) {
|
|
1901
|
+
throw new Error("Not connected to database");
|
|
1902
|
+
}
|
|
1903
|
+
try {
|
|
1904
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1905
|
+
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1906
|
+
const rows = await this.pool.query(
|
|
1907
|
+
`
|
|
1908
|
+
SELECT COUNT(*) AS COUNT
|
|
1909
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
1910
|
+
${schemaClause}
|
|
1911
|
+
AND TABLE_NAME = ?
|
|
1912
|
+
`,
|
|
1913
|
+
queryParams
|
|
1914
|
+
);
|
|
1915
|
+
return rows[0].COUNT > 0;
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
console.error("Error checking if table exists:", error);
|
|
1918
|
+
throw error;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async getTableIndexes(tableName, schema) {
|
|
1922
|
+
if (!this.pool) {
|
|
1923
|
+
throw new Error("Not connected to database");
|
|
1924
|
+
}
|
|
1925
|
+
try {
|
|
1926
|
+
const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
|
|
1927
|
+
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1928
|
+
const indexRows = await this.pool.query(
|
|
1929
|
+
`
|
|
1930
|
+
SELECT
|
|
1931
|
+
INDEX_NAME,
|
|
1932
|
+
COLUMN_NAME,
|
|
1933
|
+
NON_UNIQUE,
|
|
1934
|
+
SEQ_IN_INDEX
|
|
1935
|
+
FROM
|
|
1936
|
+
INFORMATION_SCHEMA.STATISTICS
|
|
1937
|
+
WHERE
|
|
1938
|
+
${schemaClause}
|
|
1939
|
+
AND TABLE_NAME = ?
|
|
1940
|
+
ORDER BY
|
|
1941
|
+
INDEX_NAME,
|
|
1942
|
+
SEQ_IN_INDEX
|
|
1943
|
+
`,
|
|
1944
|
+
queryParams
|
|
1945
|
+
);
|
|
1946
|
+
const indexMap = /* @__PURE__ */ new Map();
|
|
1947
|
+
for (const row of indexRows) {
|
|
1948
|
+
const indexName = row.INDEX_NAME;
|
|
1949
|
+
const columnName = row.COLUMN_NAME;
|
|
1950
|
+
const isUnique = row.NON_UNIQUE === 0;
|
|
1951
|
+
const isPrimary = indexName === "PRIMARY";
|
|
1952
|
+
if (!indexMap.has(indexName)) {
|
|
1953
|
+
indexMap.set(indexName, {
|
|
1954
|
+
columns: [],
|
|
1955
|
+
is_unique: isUnique,
|
|
1956
|
+
is_primary: isPrimary
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
const indexInfo = indexMap.get(indexName);
|
|
1960
|
+
indexInfo.columns.push(columnName);
|
|
1961
|
+
}
|
|
1962
|
+
const results = [];
|
|
1963
|
+
indexMap.forEach((indexInfo, indexName) => {
|
|
1964
|
+
results.push({
|
|
1965
|
+
index_name: indexName,
|
|
1966
|
+
column_names: indexInfo.columns,
|
|
1967
|
+
is_unique: indexInfo.is_unique,
|
|
1968
|
+
is_primary: indexInfo.is_primary
|
|
1969
|
+
});
|
|
1970
|
+
});
|
|
1971
|
+
return results;
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
console.error("Error getting table indexes:", error);
|
|
1974
|
+
throw error;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
async getTableSchema(tableName, schema) {
|
|
1978
|
+
if (!this.pool) {
|
|
1979
|
+
throw new Error("Not connected to database");
|
|
1980
|
+
}
|
|
1981
|
+
try {
|
|
1982
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1983
|
+
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1984
|
+
const rows = await this.pool.query(
|
|
1985
|
+
`
|
|
1986
|
+
SELECT
|
|
1987
|
+
COLUMN_NAME as column_name,
|
|
1988
|
+
DATA_TYPE as data_type,
|
|
1989
|
+
IS_NULLABLE as is_nullable,
|
|
1990
|
+
COLUMN_DEFAULT as column_default
|
|
1991
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
1992
|
+
${schemaClause}
|
|
1993
|
+
AND TABLE_NAME = ?
|
|
1994
|
+
ORDER BY ORDINAL_POSITION
|
|
1995
|
+
`,
|
|
1996
|
+
queryParams
|
|
1997
|
+
);
|
|
1998
|
+
return rows;
|
|
1999
|
+
} catch (error) {
|
|
2000
|
+
console.error("Error getting table schema:", error);
|
|
2001
|
+
throw error;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
async getStoredProcedures(schema) {
|
|
2005
|
+
if (!this.pool) {
|
|
2006
|
+
throw new Error("Not connected to database");
|
|
2007
|
+
}
|
|
2008
|
+
try {
|
|
2009
|
+
const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
|
|
2010
|
+
const queryParams = schema ? [schema] : [];
|
|
2011
|
+
const rows = await this.pool.query(
|
|
2012
|
+
`
|
|
2013
|
+
SELECT ROUTINE_NAME
|
|
2014
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
2015
|
+
${schemaClause}
|
|
2016
|
+
ORDER BY ROUTINE_NAME
|
|
2017
|
+
`,
|
|
2018
|
+
queryParams
|
|
2019
|
+
);
|
|
2020
|
+
return rows.map((row) => row.ROUTINE_NAME);
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
console.error("Error getting stored procedures:", error);
|
|
2023
|
+
throw error;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
async getStoredProcedureDetail(procedureName, schema) {
|
|
2027
|
+
if (!this.pool) {
|
|
2028
|
+
throw new Error("Not connected to database");
|
|
2029
|
+
}
|
|
2030
|
+
try {
|
|
2031
|
+
const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
|
|
2032
|
+
const queryParams = schema ? [schema, procedureName] : [procedureName];
|
|
2033
|
+
const rows = await this.pool.query(
|
|
2034
|
+
`
|
|
2035
|
+
SELECT
|
|
2036
|
+
r.ROUTINE_NAME AS procedure_name,
|
|
2037
|
+
CASE
|
|
2038
|
+
WHEN r.ROUTINE_TYPE = 'PROCEDURE' THEN 'procedure'
|
|
2039
|
+
ELSE 'function'
|
|
2040
|
+
END AS procedure_type,
|
|
2041
|
+
LOWER(r.ROUTINE_TYPE) AS routine_type,
|
|
2042
|
+
r.ROUTINE_DEFINITION,
|
|
2043
|
+
r.DTD_IDENTIFIER AS return_type,
|
|
2044
|
+
(
|
|
2045
|
+
SELECT GROUP_CONCAT(
|
|
2046
|
+
CONCAT(p.PARAMETER_NAME, ' ', p.PARAMETER_MODE, ' ', p.DATA_TYPE)
|
|
2047
|
+
ORDER BY p.ORDINAL_POSITION
|
|
2048
|
+
SEPARATOR ', '
|
|
2049
|
+
)
|
|
2050
|
+
FROM INFORMATION_SCHEMA.PARAMETERS p
|
|
2051
|
+
WHERE p.SPECIFIC_SCHEMA = r.ROUTINE_SCHEMA
|
|
2052
|
+
AND p.SPECIFIC_NAME = r.ROUTINE_NAME
|
|
2053
|
+
AND p.PARAMETER_NAME IS NOT NULL
|
|
2054
|
+
) AS parameter_list
|
|
2055
|
+
FROM INFORMATION_SCHEMA.ROUTINES r
|
|
2056
|
+
${schemaClause}
|
|
2057
|
+
AND r.ROUTINE_NAME = ?
|
|
2058
|
+
`,
|
|
2059
|
+
queryParams
|
|
2060
|
+
);
|
|
2061
|
+
if (rows.length === 0) {
|
|
2062
|
+
const schemaName = schema || "current schema";
|
|
2063
|
+
throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
|
|
2064
|
+
}
|
|
2065
|
+
const procedure = rows[0];
|
|
2066
|
+
let definition = procedure.ROUTINE_DEFINITION;
|
|
2067
|
+
try {
|
|
2068
|
+
const schemaValue = schema || await this.getCurrentSchema();
|
|
2069
|
+
if (procedure.procedure_type === "procedure") {
|
|
2070
|
+
try {
|
|
2071
|
+
const defRows = await this.pool.query(`
|
|
2072
|
+
SHOW CREATE PROCEDURE ${schemaValue}.${procedureName}
|
|
2073
|
+
`);
|
|
2074
|
+
if (defRows && defRows.length > 0) {
|
|
2075
|
+
definition = defRows[0]["Create Procedure"];
|
|
2076
|
+
}
|
|
2077
|
+
} catch (err) {
|
|
2078
|
+
console.error(`Error getting procedure definition with SHOW CREATE: ${err}`);
|
|
2079
|
+
}
|
|
2080
|
+
} else {
|
|
2081
|
+
try {
|
|
2082
|
+
const defRows = await this.pool.query(`
|
|
2083
|
+
SHOW CREATE FUNCTION ${schemaValue}.${procedureName}
|
|
2084
|
+
`);
|
|
2085
|
+
if (defRows && defRows.length > 0) {
|
|
2086
|
+
definition = defRows[0]["Create Function"];
|
|
2087
|
+
}
|
|
2088
|
+
} catch (innerErr) {
|
|
2089
|
+
console.error(`Error getting function definition with SHOW CREATE: ${innerErr}`);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
if (!definition) {
|
|
2093
|
+
const bodyRows = await this.pool.query(
|
|
2094
|
+
`
|
|
2095
|
+
SELECT ROUTINE_DEFINITION, ROUTINE_BODY
|
|
2096
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
2097
|
+
WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
|
|
2098
|
+
`,
|
|
2099
|
+
[schemaValue, procedureName]
|
|
2100
|
+
);
|
|
2101
|
+
if (bodyRows && bodyRows.length > 0) {
|
|
2102
|
+
if (bodyRows[0].ROUTINE_DEFINITION) {
|
|
2103
|
+
definition = bodyRows[0].ROUTINE_DEFINITION;
|
|
2104
|
+
} else if (bodyRows[0].ROUTINE_BODY) {
|
|
2105
|
+
definition = bodyRows[0].ROUTINE_BODY;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
console.error(`Error getting procedure/function details: ${error}`);
|
|
2111
|
+
}
|
|
2112
|
+
return {
|
|
2113
|
+
procedure_name: procedure.procedure_name,
|
|
2114
|
+
procedure_type: procedure.procedure_type,
|
|
2115
|
+
language: "sql",
|
|
2116
|
+
// MariaDB procedures are generally in SQL
|
|
2117
|
+
parameter_list: procedure.parameter_list || "",
|
|
2118
|
+
return_type: procedure.routine_type === "function" ? procedure.return_type : void 0,
|
|
2119
|
+
definition: definition || void 0
|
|
2120
|
+
};
|
|
2121
|
+
} catch (error) {
|
|
2122
|
+
console.error("Error getting stored procedure detail:", error);
|
|
2123
|
+
throw error;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
// Helper method to get current schema (database) name
|
|
2127
|
+
async getCurrentSchema() {
|
|
2128
|
+
const rows = await this.pool.query("SELECT DATABASE() AS DB");
|
|
2129
|
+
return rows[0].DB;
|
|
2130
|
+
}
|
|
2131
|
+
async executeSQL(sql2, options, parameters) {
|
|
2132
|
+
if (!this.pool) {
|
|
2133
|
+
throw new Error("Not connected to database");
|
|
2134
|
+
}
|
|
2135
|
+
const conn = await this.pool.getConnection();
|
|
2136
|
+
try {
|
|
2137
|
+
let processedSQL = sql2;
|
|
2138
|
+
if (options.maxRows) {
|
|
2139
|
+
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
2140
|
+
const processedStatements = statements.map(
|
|
2141
|
+
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
2142
|
+
);
|
|
2143
|
+
processedSQL = processedStatements.join("; ");
|
|
2144
|
+
if (sql2.trim().endsWith(";")) {
|
|
2145
|
+
processedSQL += ";";
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
let results;
|
|
2149
|
+
if (parameters && parameters.length > 0) {
|
|
2150
|
+
try {
|
|
2151
|
+
results = await conn.query(processedSQL, parameters);
|
|
2152
|
+
} catch (error) {
|
|
2153
|
+
console.error(`[MariaDB executeSQL] ERROR: ${error.message}`);
|
|
2154
|
+
console.error(`[MariaDB executeSQL] SQL: ${processedSQL}`);
|
|
2155
|
+
console.error(`[MariaDB executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
2156
|
+
throw error;
|
|
2157
|
+
}
|
|
2158
|
+
} else {
|
|
2159
|
+
results = await conn.query(processedSQL);
|
|
2160
|
+
}
|
|
2161
|
+
const rows = parseQueryResults(results);
|
|
2162
|
+
const rowCount = extractAffectedRows(results);
|
|
2163
|
+
return { rows, rowCount };
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
console.error("Error executing query:", error);
|
|
2166
|
+
throw error;
|
|
2167
|
+
} finally {
|
|
2168
|
+
conn.release();
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
var mariadbConnector = new MariaDBConnector();
|
|
2173
|
+
ConnectorRegistry.register(mariadbConnector);
|
|
2174
|
+
|
|
2175
|
+
// src/server.ts
|
|
2176
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2177
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2178
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2179
|
+
import express from "express";
|
|
2180
|
+
import path2 from "path";
|
|
2181
|
+
import { readFileSync } from "fs";
|
|
2182
|
+
import { fileURLToPath } from "url";
|
|
2183
|
+
|
|
2184
|
+
// src/config/output-format.ts
|
|
2185
|
+
var outputFormat = "csv";
|
|
2186
|
+
function setOutputFormat(format) {
|
|
2187
|
+
outputFormat = format;
|
|
2188
|
+
}
|
|
2189
|
+
function getOutputFormat() {
|
|
2190
|
+
return outputFormat;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// src/tools/execute-sql.ts
|
|
2194
|
+
import { z } from "zod";
|
|
2195
|
+
|
|
2196
|
+
// src/utils/response-formatter.ts
|
|
2197
|
+
function bigIntReplacer(_key, value) {
|
|
2198
|
+
if (typeof value === "bigint") {
|
|
2199
|
+
return value.toString();
|
|
2200
|
+
}
|
|
2201
|
+
return value;
|
|
2202
|
+
}
|
|
2203
|
+
function formatSuccessResponse(data, meta = {}) {
|
|
2204
|
+
return {
|
|
2205
|
+
success: true,
|
|
2206
|
+
data,
|
|
2207
|
+
...Object.keys(meta).length > 0 ? { meta } : {}
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
function formatErrorResponse(error, code = "ERROR", details) {
|
|
2211
|
+
return {
|
|
2212
|
+
success: false,
|
|
2213
|
+
error,
|
|
2214
|
+
code,
|
|
2215
|
+
...details ? { details } : {}
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
function createToolErrorResponse(error, code = "ERROR", details) {
|
|
2219
|
+
return {
|
|
2220
|
+
content: [
|
|
2221
|
+
{
|
|
2222
|
+
type: "text",
|
|
2223
|
+
text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
|
|
2224
|
+
mimeType: "application/json"
|
|
2225
|
+
}
|
|
2226
|
+
],
|
|
2227
|
+
isError: true
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
function createPiiSafeToolResponse() {
|
|
2231
|
+
return {
|
|
2232
|
+
content: [
|
|
2233
|
+
{
|
|
2234
|
+
type: "text",
|
|
2235
|
+
text: JSON.stringify(formatSuccessResponse({}), bigIntReplacer, 2),
|
|
2236
|
+
mimeType: "application/json"
|
|
2237
|
+
}
|
|
2238
|
+
]
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
function createToolSuccessResponse(data, meta = {}) {
|
|
2242
|
+
return {
|
|
2243
|
+
content: [
|
|
2244
|
+
{
|
|
2245
|
+
type: "text",
|
|
2246
|
+
text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
|
|
2247
|
+
mimeType: "application/json"
|
|
2248
|
+
}
|
|
2249
|
+
]
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// src/utils/result-writer.ts
|
|
2254
|
+
import { exec } from "child_process";
|
|
2255
|
+
import fs from "fs";
|
|
2256
|
+
import path from "path";
|
|
2257
|
+
var OUTPUT_DIR = ".safe-sql-results";
|
|
2258
|
+
function ensureOutputDir() {
|
|
2259
|
+
const dir = path.join(process.cwd(), OUTPUT_DIR);
|
|
2260
|
+
if (!fs.existsSync(dir)) {
|
|
2261
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2262
|
+
}
|
|
2263
|
+
return dir;
|
|
2264
|
+
}
|
|
2265
|
+
function timestamp() {
|
|
2266
|
+
const d = /* @__PURE__ */ new Date();
|
|
2267
|
+
const y = d.getFullYear();
|
|
2268
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
2269
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
2270
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
2271
|
+
const min = String(d.getMinutes()).padStart(2, "0");
|
|
2272
|
+
const s = String(d.getSeconds()).padStart(2, "0");
|
|
2273
|
+
return `${y}${m}${day}_${h}${min}${s}`;
|
|
2274
|
+
}
|
|
2275
|
+
function escapeCsvValue(value) {
|
|
2276
|
+
if (value === null || value === void 0) {
|
|
2277
|
+
return "";
|
|
2278
|
+
}
|
|
2279
|
+
const str = typeof value === "object" ? JSON.stringify(value, bigIntReplacer) : String(value);
|
|
2280
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
2281
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
2282
|
+
}
|
|
2283
|
+
return str;
|
|
2284
|
+
}
|
|
2285
|
+
function toCsv(rows) {
|
|
2286
|
+
if (rows.length === 0) {
|
|
2287
|
+
return "";
|
|
2288
|
+
}
|
|
2289
|
+
const columns = Object.keys(rows[0]);
|
|
2290
|
+
const header = columns.map(escapeCsvValue).join(",");
|
|
2291
|
+
const lines = rows.map(
|
|
2292
|
+
(row) => columns.map((col) => escapeCsvValue(row[col])).join(",")
|
|
2293
|
+
);
|
|
2294
|
+
return [header, ...lines].join("\n");
|
|
2295
|
+
}
|
|
2296
|
+
function toMarkdownTable(rows) {
|
|
2297
|
+
if (rows.length === 0) {
|
|
2298
|
+
return "";
|
|
2299
|
+
}
|
|
2300
|
+
const columns = Object.keys(rows[0]);
|
|
2301
|
+
const header = "| " + columns.join(" | ") + " |";
|
|
2302
|
+
const separator = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
2303
|
+
const dataRows = rows.map(
|
|
2304
|
+
(row) => "| " + columns.map((col) => {
|
|
2305
|
+
const val = row[col];
|
|
2306
|
+
const str = val === null || val === void 0 ? "" : typeof val === "object" ? JSON.stringify(val, bigIntReplacer) : String(val);
|
|
2307
|
+
return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2308
|
+
}).join(" | ") + " |"
|
|
2309
|
+
);
|
|
2310
|
+
return [header, separator, ...dataRows].join("\n");
|
|
2311
|
+
}
|
|
2312
|
+
function writeResultFile(rows, toolName, format) {
|
|
2313
|
+
const dir = ensureOutputDir();
|
|
2314
|
+
const ext = format === "markdown" ? "md" : format;
|
|
2315
|
+
const sanitizedName = toolName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2316
|
+
const filePath = path.join(dir, `${timestamp()}_${sanitizedName}.${ext}`);
|
|
2317
|
+
let content;
|
|
2318
|
+
switch (format) {
|
|
2319
|
+
case "csv":
|
|
2320
|
+
content = toCsv(rows);
|
|
2321
|
+
break;
|
|
2322
|
+
case "json":
|
|
2323
|
+
content = JSON.stringify(rows, bigIntReplacer, 2);
|
|
2324
|
+
break;
|
|
2325
|
+
case "markdown":
|
|
2326
|
+
content = toMarkdownTable(rows);
|
|
2327
|
+
break;
|
|
2328
|
+
}
|
|
2329
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
2330
|
+
const resolvedPath = path.resolve(filePath);
|
|
2331
|
+
exec(`cursor "${resolvedPath}"`, { timeout: 5e3 });
|
|
2332
|
+
return resolvedPath;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// src/utils/allowed-keywords.ts
|
|
2336
|
+
var allowedKeywords = {
|
|
2337
|
+
postgres: ["select", "with", "explain", "analyze", "show"],
|
|
2338
|
+
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2339
|
+
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2340
|
+
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
2341
|
+
sqlserver: ["select", "with", "explain", "showplan"]
|
|
2342
|
+
};
|
|
2343
|
+
function isReadOnlySQL(sql2, connectorType) {
|
|
2344
|
+
const cleanedSQL = stripCommentsAndStrings(sql2).trim().toLowerCase();
|
|
2345
|
+
if (!cleanedSQL) {
|
|
2346
|
+
return true;
|
|
2347
|
+
}
|
|
2348
|
+
const firstWord = cleanedSQL.split(/\s+/)[0];
|
|
2349
|
+
const keywordList = allowedKeywords[connectorType] || [];
|
|
2350
|
+
return keywordList.includes(firstWord);
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// src/requests/store.ts
|
|
2354
|
+
var RequestStore = class {
|
|
2355
|
+
constructor() {
|
|
2356
|
+
this.store = /* @__PURE__ */ new Map();
|
|
2357
|
+
this.maxPerSource = 100;
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Add a request to the store
|
|
2361
|
+
* Evicts oldest entry if at capacity
|
|
2362
|
+
*/
|
|
2363
|
+
add(request) {
|
|
2364
|
+
const requests = this.store.get(request.sourceId) ?? [];
|
|
2365
|
+
requests.push(request);
|
|
2366
|
+
if (requests.length > this.maxPerSource) {
|
|
2367
|
+
requests.shift();
|
|
2368
|
+
}
|
|
2369
|
+
this.store.set(request.sourceId, requests);
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* Get requests, optionally filtered by source
|
|
2373
|
+
* Returns newest first
|
|
2374
|
+
*/
|
|
2375
|
+
getAll(sourceId) {
|
|
2376
|
+
let requests;
|
|
2377
|
+
if (sourceId) {
|
|
2378
|
+
requests = [...this.store.get(sourceId) ?? []];
|
|
2379
|
+
} else {
|
|
2380
|
+
requests = Array.from(this.store.values()).flat();
|
|
2381
|
+
}
|
|
2382
|
+
return requests.sort(
|
|
2383
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Get total count of requests across all sources
|
|
2388
|
+
*/
|
|
2389
|
+
getTotal() {
|
|
2390
|
+
return Array.from(this.store.values()).reduce((sum, arr) => sum + arr.length, 0);
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Clear all requests (useful for testing)
|
|
2394
|
+
*/
|
|
2395
|
+
clear() {
|
|
2396
|
+
this.store.clear();
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2400
|
+
// src/requests/index.ts
|
|
2401
|
+
var requestStore = new RequestStore();
|
|
2402
|
+
|
|
2403
|
+
// src/utils/client-identifier.ts
|
|
2404
|
+
function getClientIdentifier(extra) {
|
|
2405
|
+
const userAgent = extra?.requestInfo?.headers?.["user-agent"];
|
|
2406
|
+
if (userAgent) {
|
|
2407
|
+
return userAgent;
|
|
2408
|
+
}
|
|
2409
|
+
return "stdio";
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// src/utils/tool-handler-helpers.ts
|
|
2413
|
+
function getEffectiveSourceId(sourceId) {
|
|
2414
|
+
return sourceId || "default";
|
|
2415
|
+
}
|
|
2416
|
+
function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
|
|
2417
|
+
return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
|
|
2418
|
+
}
|
|
2419
|
+
function trackToolRequest(metadata, startTime, extra, success, error) {
|
|
2420
|
+
requestStore.add({
|
|
2421
|
+
id: crypto.randomUUID(),
|
|
2422
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2423
|
+
sourceId: metadata.sourceId,
|
|
2424
|
+
toolName: metadata.toolName,
|
|
2425
|
+
sql: metadata.sql,
|
|
2426
|
+
durationMs: Date.now() - startTime,
|
|
2427
|
+
client: getClientIdentifier(extra),
|
|
2428
|
+
success,
|
|
2429
|
+
error
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// src/tools/execute-sql.ts
|
|
2434
|
+
var executeSqlSchema = {
|
|
2435
|
+
sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
|
|
2436
|
+
};
|
|
2437
|
+
function splitSQLStatements(sql2) {
|
|
2438
|
+
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
2439
|
+
}
|
|
2440
|
+
function areAllStatementsReadOnly(sql2, connectorType) {
|
|
2441
|
+
const statements = splitSQLStatements(sql2);
|
|
2442
|
+
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
2443
|
+
}
|
|
2444
|
+
function createExecuteSqlToolHandler(sourceId) {
|
|
2445
|
+
return async (args, extra) => {
|
|
2446
|
+
const { sql: sql2 } = args;
|
|
2447
|
+
const startTime = Date.now();
|
|
2448
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
2449
|
+
let success = true;
|
|
2450
|
+
let errorMessage;
|
|
2451
|
+
let result;
|
|
2452
|
+
try {
|
|
2453
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
2454
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
2455
|
+
const actualSourceId = connector.getId();
|
|
2456
|
+
const registry = getToolRegistry();
|
|
2457
|
+
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
|
|
2458
|
+
const isReadonly = toolConfig?.readonly !== false;
|
|
2459
|
+
if (isReadonly && !areAllStatementsReadOnly(sql2, connector.id)) {
|
|
2460
|
+
errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
2461
|
+
success = false;
|
|
2462
|
+
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
2463
|
+
}
|
|
2464
|
+
const executeOptions = {
|
|
2465
|
+
readonly: toolConfig?.readonly,
|
|
2466
|
+
maxRows: toolConfig?.max_rows
|
|
2467
|
+
};
|
|
2468
|
+
result = await connector.executeSQL(sql2, executeOptions);
|
|
2469
|
+
writeResultFile(result.rows, "execute_sql", getOutputFormat());
|
|
2470
|
+
return createPiiSafeToolResponse();
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
success = false;
|
|
2473
|
+
errorMessage = error.message;
|
|
2474
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
2475
|
+
} finally {
|
|
2476
|
+
trackToolRequest(
|
|
2477
|
+
{
|
|
2478
|
+
sourceId: effectiveSourceId,
|
|
2479
|
+
toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
|
|
2480
|
+
sql: sql2
|
|
2481
|
+
},
|
|
2482
|
+
startTime,
|
|
2483
|
+
extra,
|
|
2484
|
+
success,
|
|
2485
|
+
errorMessage
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// src/tools/search-objects.ts
|
|
2492
|
+
import { z as z2 } from "zod";
|
|
2493
|
+
var searchDatabaseObjectsSchema = {
|
|
2494
|
+
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Object type to search"),
|
|
2495
|
+
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
|
|
2496
|
+
schema: z2.string().optional().describe("Filter to schema"),
|
|
2497
|
+
table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
|
|
2498
|
+
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
|
|
2499
|
+
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results (default: 100, max: 1000)")
|
|
2500
|
+
};
|
|
2501
|
+
function likePatternToRegex(pattern) {
|
|
2502
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
2503
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
2504
|
+
}
|
|
2505
|
+
async function getTableRowCount(connector, tableName, schemaName) {
|
|
2506
|
+
try {
|
|
2507
|
+
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
2508
|
+
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
2509
|
+
const result = await connector.executeSQL(countQuery, { maxRows: 1 });
|
|
2510
|
+
if (result.rows && result.rows.length > 0) {
|
|
2511
|
+
return Number(result.rows[0].count || result.rows[0].COUNT || 0);
|
|
2512
|
+
}
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
return null;
|
|
2517
|
+
}
|
|
2518
|
+
async function searchSchemas(connector, pattern, detailLevel, limit) {
|
|
2519
|
+
const schemas = await connector.getSchemas();
|
|
2520
|
+
const regex = likePatternToRegex(pattern);
|
|
2521
|
+
const matched = schemas.filter((schema) => regex.test(schema)).slice(0, limit);
|
|
2522
|
+
if (detailLevel === "names") {
|
|
2523
|
+
return matched.map((name) => ({ name }));
|
|
2524
|
+
}
|
|
2525
|
+
const results = await Promise.all(
|
|
2526
|
+
matched.map(async (schemaName) => {
|
|
2527
|
+
try {
|
|
2528
|
+
const tables = await connector.getTables(schemaName);
|
|
2529
|
+
return {
|
|
2530
|
+
name: schemaName,
|
|
2531
|
+
table_count: tables.length
|
|
2532
|
+
};
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
return {
|
|
2535
|
+
name: schemaName,
|
|
2536
|
+
table_count: 0
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
})
|
|
2540
|
+
);
|
|
2541
|
+
return results;
|
|
2542
|
+
}
|
|
2543
|
+
async function searchTables(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2544
|
+
const regex = likePatternToRegex(pattern);
|
|
2545
|
+
const results = [];
|
|
2546
|
+
let schemasToSearch;
|
|
2547
|
+
if (schemaFilter) {
|
|
2548
|
+
schemasToSearch = [schemaFilter];
|
|
2549
|
+
} else {
|
|
2550
|
+
schemasToSearch = await connector.getSchemas();
|
|
2551
|
+
}
|
|
2552
|
+
for (const schemaName of schemasToSearch) {
|
|
2553
|
+
if (results.length >= limit) break;
|
|
2554
|
+
try {
|
|
2555
|
+
const tables = await connector.getTables(schemaName);
|
|
2556
|
+
const matched = tables.filter((table) => regex.test(table));
|
|
2557
|
+
for (const tableName of matched) {
|
|
2558
|
+
if (results.length >= limit) break;
|
|
2559
|
+
if (detailLevel === "names") {
|
|
2560
|
+
results.push({
|
|
2561
|
+
name: tableName,
|
|
2562
|
+
schema: schemaName
|
|
2563
|
+
});
|
|
2564
|
+
} else if (detailLevel === "summary") {
|
|
2565
|
+
try {
|
|
2566
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
2567
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
2568
|
+
results.push({
|
|
2569
|
+
name: tableName,
|
|
2570
|
+
schema: schemaName,
|
|
2571
|
+
column_count: columns.length,
|
|
2572
|
+
row_count: rowCount
|
|
2573
|
+
});
|
|
2574
|
+
} catch (error) {
|
|
2575
|
+
results.push({
|
|
2576
|
+
name: tableName,
|
|
2577
|
+
schema: schemaName,
|
|
2578
|
+
column_count: null,
|
|
2579
|
+
row_count: null
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
} else {
|
|
2583
|
+
try {
|
|
2584
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
2585
|
+
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
2586
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
2587
|
+
results.push({
|
|
2588
|
+
name: tableName,
|
|
2589
|
+
schema: schemaName,
|
|
2590
|
+
column_count: columns.length,
|
|
2591
|
+
row_count: rowCount,
|
|
2592
|
+
columns: columns.map((col) => ({
|
|
2593
|
+
name: col.column_name,
|
|
2594
|
+
type: col.data_type,
|
|
2595
|
+
nullable: col.is_nullable === "YES",
|
|
2596
|
+
default: col.column_default
|
|
2597
|
+
})),
|
|
2598
|
+
indexes: indexes.map((idx) => ({
|
|
2599
|
+
name: idx.index_name,
|
|
2600
|
+
columns: idx.column_names,
|
|
2601
|
+
unique: idx.is_unique,
|
|
2602
|
+
primary: idx.is_primary
|
|
2603
|
+
}))
|
|
2604
|
+
});
|
|
2605
|
+
} catch (error) {
|
|
2606
|
+
results.push({
|
|
2607
|
+
name: tableName,
|
|
2608
|
+
schema: schemaName,
|
|
2609
|
+
error: `Unable to fetch full details: ${error.message}`
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
} catch (error) {
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
return results;
|
|
2619
|
+
}
|
|
2620
|
+
async function searchColumns(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
2621
|
+
const regex = likePatternToRegex(pattern);
|
|
2622
|
+
const results = [];
|
|
2623
|
+
let schemasToSearch;
|
|
2624
|
+
if (schemaFilter) {
|
|
2625
|
+
schemasToSearch = [schemaFilter];
|
|
2626
|
+
} else {
|
|
2627
|
+
schemasToSearch = await connector.getSchemas();
|
|
2628
|
+
}
|
|
2629
|
+
for (const schemaName of schemasToSearch) {
|
|
2630
|
+
if (results.length >= limit) break;
|
|
2631
|
+
try {
|
|
2632
|
+
let tablesToSearch;
|
|
2633
|
+
if (tableFilter) {
|
|
2634
|
+
tablesToSearch = [tableFilter];
|
|
2635
|
+
} else {
|
|
2636
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
2637
|
+
}
|
|
2638
|
+
for (const tableName of tablesToSearch) {
|
|
2639
|
+
if (results.length >= limit) break;
|
|
2640
|
+
try {
|
|
2641
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
2642
|
+
const matchedColumns = columns.filter((col) => regex.test(col.column_name));
|
|
2643
|
+
for (const column of matchedColumns) {
|
|
2644
|
+
if (results.length >= limit) break;
|
|
2645
|
+
if (detailLevel === "names") {
|
|
2646
|
+
results.push({
|
|
2647
|
+
name: column.column_name,
|
|
2648
|
+
table: tableName,
|
|
2649
|
+
schema: schemaName
|
|
2650
|
+
});
|
|
2651
|
+
} else {
|
|
2652
|
+
results.push({
|
|
2653
|
+
name: column.column_name,
|
|
2654
|
+
table: tableName,
|
|
2655
|
+
schema: schemaName,
|
|
2656
|
+
type: column.data_type,
|
|
2657
|
+
nullable: column.is_nullable === "YES",
|
|
2658
|
+
default: column.column_default
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
} catch (error) {
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
continue;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return results;
|
|
2671
|
+
}
|
|
2672
|
+
async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2673
|
+
const regex = likePatternToRegex(pattern);
|
|
2674
|
+
const results = [];
|
|
2675
|
+
let schemasToSearch;
|
|
2676
|
+
if (schemaFilter) {
|
|
2677
|
+
schemasToSearch = [schemaFilter];
|
|
2678
|
+
} else {
|
|
2679
|
+
schemasToSearch = await connector.getSchemas();
|
|
2680
|
+
}
|
|
2681
|
+
for (const schemaName of schemasToSearch) {
|
|
2682
|
+
if (results.length >= limit) break;
|
|
2683
|
+
try {
|
|
2684
|
+
const procedures = await connector.getStoredProcedures(schemaName);
|
|
2685
|
+
const matched = procedures.filter((proc) => regex.test(proc));
|
|
2686
|
+
for (const procName of matched) {
|
|
2687
|
+
if (results.length >= limit) break;
|
|
2688
|
+
if (detailLevel === "names") {
|
|
2689
|
+
results.push({
|
|
2690
|
+
name: procName,
|
|
2691
|
+
schema: schemaName
|
|
2692
|
+
});
|
|
2693
|
+
} else {
|
|
2694
|
+
try {
|
|
2695
|
+
const details = await connector.getStoredProcedureDetail(procName, schemaName);
|
|
2696
|
+
results.push({
|
|
2697
|
+
name: procName,
|
|
2698
|
+
schema: schemaName,
|
|
2699
|
+
type: details.procedure_type,
|
|
2700
|
+
language: details.language,
|
|
2701
|
+
parameters: detailLevel === "full" ? details.parameter_list : void 0,
|
|
2702
|
+
return_type: details.return_type,
|
|
2703
|
+
definition: detailLevel === "full" ? details.definition : void 0
|
|
2704
|
+
});
|
|
2705
|
+
} catch (error) {
|
|
2706
|
+
results.push({
|
|
2707
|
+
name: procName,
|
|
2708
|
+
schema: schemaName,
|
|
2709
|
+
error: `Unable to fetch details: ${error.message}`
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
} catch (error) {
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return results;
|
|
2719
|
+
}
|
|
2720
|
+
async function searchIndexes(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
2721
|
+
const regex = likePatternToRegex(pattern);
|
|
2722
|
+
const results = [];
|
|
2723
|
+
let schemasToSearch;
|
|
2724
|
+
if (schemaFilter) {
|
|
2725
|
+
schemasToSearch = [schemaFilter];
|
|
2726
|
+
} else {
|
|
2727
|
+
schemasToSearch = await connector.getSchemas();
|
|
2728
|
+
}
|
|
2729
|
+
for (const schemaName of schemasToSearch) {
|
|
2730
|
+
if (results.length >= limit) break;
|
|
2731
|
+
try {
|
|
2732
|
+
let tablesToSearch;
|
|
2733
|
+
if (tableFilter) {
|
|
2734
|
+
tablesToSearch = [tableFilter];
|
|
2735
|
+
} else {
|
|
2736
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
2737
|
+
}
|
|
2738
|
+
for (const tableName of tablesToSearch) {
|
|
2739
|
+
if (results.length >= limit) break;
|
|
2740
|
+
try {
|
|
2741
|
+
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
2742
|
+
const matchedIndexes = indexes.filter((idx) => regex.test(idx.index_name));
|
|
2743
|
+
for (const index of matchedIndexes) {
|
|
2744
|
+
if (results.length >= limit) break;
|
|
2745
|
+
if (detailLevel === "names") {
|
|
2746
|
+
results.push({
|
|
2747
|
+
name: index.index_name,
|
|
2748
|
+
table: tableName,
|
|
2749
|
+
schema: schemaName
|
|
2750
|
+
});
|
|
2751
|
+
} else {
|
|
2752
|
+
results.push({
|
|
2753
|
+
name: index.index_name,
|
|
2754
|
+
table: tableName,
|
|
2755
|
+
schema: schemaName,
|
|
2756
|
+
columns: index.column_names,
|
|
2757
|
+
unique: index.is_unique,
|
|
2758
|
+
primary: index.is_primary
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
} catch (error) {
|
|
2763
|
+
continue;
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
} catch (error) {
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
return results;
|
|
2771
|
+
}
|
|
2772
|
+
function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
2773
|
+
return async (args, extra) => {
|
|
2774
|
+
const {
|
|
2775
|
+
object_type,
|
|
2776
|
+
pattern = "%",
|
|
2777
|
+
schema,
|
|
2778
|
+
table,
|
|
2779
|
+
detail_level = "names",
|
|
2780
|
+
limit = 100
|
|
2781
|
+
} = args;
|
|
2782
|
+
const startTime = Date.now();
|
|
2783
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
2784
|
+
let success = true;
|
|
2785
|
+
let errorMessage;
|
|
2786
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2787
|
+
const effectiveSchema = schema ?? sourceConfig?.schema;
|
|
2788
|
+
try {
|
|
2789
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
2790
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
2791
|
+
if (table) {
|
|
2792
|
+
if (!effectiveSchema) {
|
|
2793
|
+
success = false;
|
|
2794
|
+
errorMessage = "The 'table' parameter requires 'schema' to be specified";
|
|
2795
|
+
return createToolErrorResponse(errorMessage, "SCHEMA_REQUIRED");
|
|
2796
|
+
}
|
|
2797
|
+
if (!["column", "index"].includes(object_type)) {
|
|
2798
|
+
success = false;
|
|
2799
|
+
errorMessage = `The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`;
|
|
2800
|
+
return createToolErrorResponse(errorMessage, "INVALID_TABLE_FILTER");
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
if (effectiveSchema) {
|
|
2804
|
+
const schemas = await connector.getSchemas();
|
|
2805
|
+
if (!schemas.includes(effectiveSchema)) {
|
|
2806
|
+
success = false;
|
|
2807
|
+
errorMessage = `Schema '${effectiveSchema}' does not exist. Available schemas: ${schemas.join(", ")}`;
|
|
2808
|
+
return createToolErrorResponse(errorMessage, "SCHEMA_NOT_FOUND");
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
let results = [];
|
|
2812
|
+
switch (object_type) {
|
|
2813
|
+
case "schema":
|
|
2814
|
+
if (effectiveSchema) {
|
|
2815
|
+
results = await searchSchemas(connector, effectiveSchema, detail_level, limit);
|
|
2816
|
+
} else {
|
|
2817
|
+
results = await searchSchemas(connector, pattern, detail_level, limit);
|
|
2818
|
+
}
|
|
2819
|
+
break;
|
|
2820
|
+
case "table":
|
|
2821
|
+
results = await searchTables(connector, pattern, effectiveSchema, detail_level, limit);
|
|
2822
|
+
break;
|
|
2823
|
+
case "column":
|
|
2824
|
+
results = await searchColumns(connector, pattern, effectiveSchema, table, detail_level, limit);
|
|
2825
|
+
break;
|
|
2826
|
+
case "procedure":
|
|
2827
|
+
results = await searchProcedures(connector, pattern, effectiveSchema, detail_level, limit);
|
|
2828
|
+
break;
|
|
2829
|
+
case "index":
|
|
2830
|
+
results = await searchIndexes(connector, pattern, effectiveSchema, table, detail_level, limit);
|
|
2831
|
+
break;
|
|
2832
|
+
default:
|
|
2833
|
+
success = false;
|
|
2834
|
+
errorMessage = `Unsupported object_type: ${object_type}`;
|
|
2835
|
+
return createToolErrorResponse(errorMessage, "INVALID_OBJECT_TYPE");
|
|
2836
|
+
}
|
|
2837
|
+
return createToolSuccessResponse({
|
|
2838
|
+
object_type,
|
|
2839
|
+
pattern,
|
|
2840
|
+
schema: effectiveSchema,
|
|
2841
|
+
table,
|
|
2842
|
+
detail_level,
|
|
2843
|
+
count: results.length,
|
|
2844
|
+
results,
|
|
2845
|
+
truncated: results.length === limit
|
|
2846
|
+
});
|
|
2847
|
+
} catch (error) {
|
|
2848
|
+
success = false;
|
|
2849
|
+
errorMessage = error.message;
|
|
2850
|
+
return createToolErrorResponse(
|
|
2851
|
+
`Error searching database objects: ${errorMessage}`,
|
|
2852
|
+
"SEARCH_ERROR"
|
|
2853
|
+
);
|
|
2854
|
+
} finally {
|
|
2855
|
+
trackToolRequest(
|
|
2856
|
+
{
|
|
2857
|
+
sourceId: effectiveSourceId,
|
|
2858
|
+
toolName: effectiveSourceId === "default" ? "search_objects" : `search_objects_${effectiveSourceId}`,
|
|
2859
|
+
sql: `search_objects(object_type=${object_type}, pattern=${pattern}, schema=${effectiveSchema || "all"}, table=${table || "all"}, detail_level=${detail_level})`
|
|
2860
|
+
},
|
|
2861
|
+
startTime,
|
|
2862
|
+
extra,
|
|
2863
|
+
success,
|
|
2864
|
+
errorMessage
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
// src/utils/tool-metadata.ts
|
|
2871
|
+
import { z as z3 } from "zod";
|
|
2872
|
+
|
|
2873
|
+
// src/utils/normalize-id.ts
|
|
2874
|
+
function normalizeSourceId(id) {
|
|
2875
|
+
return id.replace(/[^a-zA-Z0-9]/g, "_");
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// src/utils/tool-metadata.ts
|
|
2879
|
+
function zodToParameters(schema) {
|
|
2880
|
+
const parameters = [];
|
|
2881
|
+
for (const [key, zodType] of Object.entries(schema)) {
|
|
2882
|
+
const description = zodType.description || "";
|
|
2883
|
+
const required = !(zodType instanceof z3.ZodOptional);
|
|
2884
|
+
let type = "string";
|
|
2885
|
+
if (zodType instanceof z3.ZodString) {
|
|
2886
|
+
type = "string";
|
|
2887
|
+
} else if (zodType instanceof z3.ZodNumber) {
|
|
2888
|
+
type = "number";
|
|
2889
|
+
} else if (zodType instanceof z3.ZodBoolean) {
|
|
2890
|
+
type = "boolean";
|
|
2891
|
+
} else if (zodType instanceof z3.ZodArray) {
|
|
2892
|
+
type = "array";
|
|
2893
|
+
} else if (zodType instanceof z3.ZodObject) {
|
|
2894
|
+
type = "object";
|
|
2895
|
+
}
|
|
2896
|
+
parameters.push({
|
|
2897
|
+
name: key,
|
|
2898
|
+
type,
|
|
2899
|
+
required,
|
|
2900
|
+
description
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
return parameters;
|
|
2904
|
+
}
|
|
2905
|
+
function getExecuteSqlMetadata(sourceId) {
|
|
2906
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2907
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2908
|
+
const dbType = sourceConfig.type;
|
|
2909
|
+
const isSingleSource = sourceIds.length === 1;
|
|
2910
|
+
const registry = getToolRegistry();
|
|
2911
|
+
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
|
|
2912
|
+
const maxRows = toolConfig?.max_rows;
|
|
2913
|
+
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
2914
|
+
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
2915
|
+
const readonlyNote = " [READ-ONLY MODE]";
|
|
2916
|
+
const maxRowsNote = maxRows ? ` (limited to ${maxRows} rows)` : "";
|
|
2917
|
+
const description = isSingleSource ? `Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
2918
|
+
const annotations = {
|
|
2919
|
+
title,
|
|
2920
|
+
readOnlyHint: true,
|
|
2921
|
+
destructiveHint: false,
|
|
2922
|
+
// Read-only queries are more predictable (though still not strictly idempotent due to data changes)
|
|
2923
|
+
idempotentHint: false,
|
|
2924
|
+
// Database operations are always against internal/closed systems, not open-world
|
|
2925
|
+
openWorldHint: false
|
|
2926
|
+
};
|
|
2927
|
+
return {
|
|
2928
|
+
name: toolName,
|
|
2929
|
+
description,
|
|
2930
|
+
schema: executeSqlSchema,
|
|
2931
|
+
annotations
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
function getSearchObjectsMetadata(sourceId) {
|
|
2935
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2936
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2937
|
+
const dbType = sourceConfig.type;
|
|
2938
|
+
const isSingleSource = sourceIds.length === 1;
|
|
2939
|
+
const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
2940
|
+
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
2941
|
+
const description = isSingleSource ? `Search and list database objects (schemas, tables, columns, procedures, indexes) on the ${dbType} database` : `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database`;
|
|
2942
|
+
return {
|
|
2943
|
+
name: toolName,
|
|
2944
|
+
description,
|
|
2945
|
+
title
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
function customParamsToToolParams(params) {
|
|
2949
|
+
if (!params || params.length === 0) {
|
|
2950
|
+
return [];
|
|
2951
|
+
}
|
|
2952
|
+
return params.map((param) => ({
|
|
2953
|
+
name: param.name,
|
|
2954
|
+
type: param.type,
|
|
2955
|
+
required: param.required !== false && param.default === void 0,
|
|
2956
|
+
description: param.description
|
|
2957
|
+
}));
|
|
2958
|
+
}
|
|
2959
|
+
function buildExecuteSqlTool(sourceId, toolConfig) {
|
|
2960
|
+
const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
|
|
2961
|
+
const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
|
|
2962
|
+
const max_rows = toolConfig && "max_rows" in toolConfig ? toolConfig.max_rows : void 0;
|
|
2963
|
+
return {
|
|
2964
|
+
name: executeSqlMetadata.name,
|
|
2965
|
+
description: executeSqlMetadata.description,
|
|
2966
|
+
parameters: executeSqlParameters,
|
|
2967
|
+
readonly: true,
|
|
2968
|
+
max_rows
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
function buildSearchObjectsTool(sourceId) {
|
|
2972
|
+
const searchMetadata = getSearchObjectsMetadata(sourceId);
|
|
2973
|
+
return {
|
|
2974
|
+
name: searchMetadata.name,
|
|
2975
|
+
description: searchMetadata.description,
|
|
2976
|
+
parameters: [
|
|
2977
|
+
{
|
|
2978
|
+
name: "object_type",
|
|
2979
|
+
type: "string",
|
|
2980
|
+
required: true,
|
|
2981
|
+
description: "Object type to search"
|
|
2982
|
+
},
|
|
2983
|
+
{
|
|
2984
|
+
name: "pattern",
|
|
2985
|
+
type: "string",
|
|
2986
|
+
required: false,
|
|
2987
|
+
description: "LIKE pattern (% = any chars, _ = one char). Default: %"
|
|
2988
|
+
},
|
|
2989
|
+
{
|
|
2990
|
+
name: "schema",
|
|
2991
|
+
type: "string",
|
|
2992
|
+
required: false,
|
|
2993
|
+
description: "Filter to schema"
|
|
2994
|
+
},
|
|
2995
|
+
{
|
|
2996
|
+
name: "table",
|
|
2997
|
+
type: "string",
|
|
2998
|
+
required: false,
|
|
2999
|
+
description: "Filter to table (requires schema; column/index only)"
|
|
3000
|
+
},
|
|
3001
|
+
{
|
|
3002
|
+
name: "detail_level",
|
|
3003
|
+
type: "string",
|
|
3004
|
+
required: false,
|
|
3005
|
+
description: "Detail: names (minimal), summary (metadata), full (all)"
|
|
3006
|
+
},
|
|
3007
|
+
{
|
|
3008
|
+
name: "limit",
|
|
3009
|
+
type: "integer",
|
|
3010
|
+
required: false,
|
|
3011
|
+
description: "Max results (default: 100, max: 1000)"
|
|
3012
|
+
}
|
|
3013
|
+
],
|
|
3014
|
+
readonly: true
|
|
3015
|
+
// search_objects is always readonly
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
function buildCustomTool(toolConfig) {
|
|
3019
|
+
return {
|
|
3020
|
+
name: toolConfig.name,
|
|
3021
|
+
description: toolConfig.description,
|
|
3022
|
+
parameters: customParamsToToolParams(toolConfig.parameters),
|
|
3023
|
+
statement: toolConfig.statement,
|
|
3024
|
+
readonly: true,
|
|
3025
|
+
// This fork is unconditionally read-only
|
|
3026
|
+
max_rows: toolConfig.max_rows
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
function getToolsForSource(sourceId) {
|
|
3030
|
+
const registry = getToolRegistry();
|
|
3031
|
+
const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
|
|
3032
|
+
return enabledToolConfigs.map((toolConfig) => {
|
|
3033
|
+
if (toolConfig.name === "execute_sql") {
|
|
3034
|
+
return buildExecuteSqlTool(sourceId, toolConfig);
|
|
3035
|
+
} else if (toolConfig.name === "search_objects") {
|
|
3036
|
+
return buildSearchObjectsTool(sourceId);
|
|
3037
|
+
} else {
|
|
3038
|
+
return buildCustomTool(toolConfig);
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// src/tools/custom-tool-handler.ts
|
|
3044
|
+
import { z as z4 } from "zod";
|
|
3045
|
+
function buildZodSchemaFromParameters(parameters) {
|
|
3046
|
+
if (!parameters || parameters.length === 0) {
|
|
3047
|
+
return {};
|
|
3048
|
+
}
|
|
3049
|
+
const schemaShape = {};
|
|
3050
|
+
for (const param of parameters) {
|
|
3051
|
+
let fieldSchema;
|
|
3052
|
+
switch (param.type) {
|
|
3053
|
+
case "string":
|
|
3054
|
+
fieldSchema = z4.string().describe(param.description);
|
|
3055
|
+
break;
|
|
3056
|
+
case "integer":
|
|
3057
|
+
fieldSchema = z4.number().int().describe(param.description);
|
|
3058
|
+
break;
|
|
3059
|
+
case "float":
|
|
3060
|
+
fieldSchema = z4.number().describe(param.description);
|
|
3061
|
+
break;
|
|
3062
|
+
case "boolean":
|
|
3063
|
+
fieldSchema = z4.boolean().describe(param.description);
|
|
3064
|
+
break;
|
|
3065
|
+
case "array":
|
|
3066
|
+
fieldSchema = z4.array(z4.unknown()).describe(param.description);
|
|
3067
|
+
break;
|
|
3068
|
+
default:
|
|
3069
|
+
throw new Error(`Unsupported parameter type: ${param.type}`);
|
|
3070
|
+
}
|
|
3071
|
+
if (param.allowed_values && param.allowed_values.length > 0) {
|
|
3072
|
+
if (param.type === "string") {
|
|
3073
|
+
fieldSchema = z4.enum(param.allowed_values).describe(param.description);
|
|
3074
|
+
} else {
|
|
3075
|
+
fieldSchema = fieldSchema.refine(
|
|
3076
|
+
(val) => param.allowed_values.includes(val),
|
|
3077
|
+
{
|
|
3078
|
+
message: `Value must be one of: ${param.allowed_values.join(", ")}`
|
|
3079
|
+
}
|
|
3080
|
+
);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
if (param.default !== void 0 || param.required === false) {
|
|
3084
|
+
fieldSchema = fieldSchema.optional();
|
|
3085
|
+
}
|
|
3086
|
+
schemaShape[param.name] = fieldSchema;
|
|
3087
|
+
}
|
|
3088
|
+
return schemaShape;
|
|
3089
|
+
}
|
|
3090
|
+
function createCustomToolHandler(toolConfig) {
|
|
3091
|
+
const zodSchemaShape = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
3092
|
+
const zodSchema = z4.object(zodSchemaShape);
|
|
3093
|
+
return async (args, extra) => {
|
|
3094
|
+
const startTime = Date.now();
|
|
3095
|
+
let success = true;
|
|
3096
|
+
let errorMessage;
|
|
3097
|
+
let paramValues = [];
|
|
3098
|
+
try {
|
|
3099
|
+
const validatedArgs = zodSchema.parse(args);
|
|
3100
|
+
await ConnectorManager.ensureConnected(toolConfig.source);
|
|
3101
|
+
const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
|
|
3102
|
+
const executeOptions = {
|
|
3103
|
+
readonly: toolConfig.readonly,
|
|
3104
|
+
maxRows: toolConfig.max_rows
|
|
3105
|
+
};
|
|
3106
|
+
const isReadonly = executeOptions.readonly !== false;
|
|
3107
|
+
if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
|
|
3108
|
+
errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
|
|
3109
|
+
success = false;
|
|
3110
|
+
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
3111
|
+
}
|
|
3112
|
+
paramValues = mapArgumentsToArray(
|
|
3113
|
+
toolConfig.parameters,
|
|
3114
|
+
validatedArgs
|
|
3115
|
+
);
|
|
3116
|
+
const result = await connector.executeSQL(
|
|
3117
|
+
toolConfig.statement,
|
|
3118
|
+
executeOptions,
|
|
3119
|
+
paramValues
|
|
3120
|
+
);
|
|
3121
|
+
writeResultFile(result.rows, toolConfig.name, getOutputFormat());
|
|
3122
|
+
return createPiiSafeToolResponse();
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
success = false;
|
|
3125
|
+
errorMessage = error.message;
|
|
3126
|
+
if (error instanceof z4.ZodError) {
|
|
3127
|
+
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
3128
|
+
errorMessage = `Parameter validation failed: ${issues}`;
|
|
3129
|
+
} else {
|
|
3130
|
+
errorMessage = `${errorMessage}
|
|
3131
|
+
|
|
3132
|
+
SQL: ${toolConfig.statement}
|
|
3133
|
+
Parameters: ${JSON.stringify(paramValues)}`;
|
|
3134
|
+
}
|
|
3135
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
3136
|
+
} finally {
|
|
3137
|
+
trackToolRequest(
|
|
3138
|
+
{
|
|
3139
|
+
sourceId: toolConfig.source,
|
|
3140
|
+
toolName: toolConfig.name,
|
|
3141
|
+
sql: toolConfig.statement
|
|
3142
|
+
},
|
|
3143
|
+
startTime,
|
|
3144
|
+
extra,
|
|
3145
|
+
success,
|
|
3146
|
+
errorMessage
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// src/tools/index.ts
|
|
3153
|
+
function registerTools(server) {
|
|
3154
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
3155
|
+
if (sourceIds.length === 0) {
|
|
3156
|
+
throw new Error("No database sources configured");
|
|
3157
|
+
}
|
|
3158
|
+
const registry = getToolRegistry();
|
|
3159
|
+
for (const sourceId of sourceIds) {
|
|
3160
|
+
const enabledTools = registry.getEnabledToolConfigs(sourceId);
|
|
3161
|
+
for (const toolConfig of enabledTools) {
|
|
3162
|
+
if (toolConfig.name === BUILTIN_TOOL_EXECUTE_SQL) {
|
|
3163
|
+
registerExecuteSqlTool(server, sourceId);
|
|
3164
|
+
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
3165
|
+
registerSearchObjectsTool(server, sourceId);
|
|
3166
|
+
} else {
|
|
3167
|
+
registerCustomTool(server, sourceId, toolConfig);
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
function registerExecuteSqlTool(server, sourceId) {
|
|
3173
|
+
const metadata = getExecuteSqlMetadata(sourceId);
|
|
3174
|
+
server.registerTool(
|
|
3175
|
+
metadata.name,
|
|
3176
|
+
{
|
|
3177
|
+
description: metadata.description,
|
|
3178
|
+
inputSchema: metadata.schema,
|
|
3179
|
+
annotations: metadata.annotations
|
|
3180
|
+
},
|
|
3181
|
+
createExecuteSqlToolHandler(sourceId)
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
3184
|
+
function registerSearchObjectsTool(server, sourceId) {
|
|
3185
|
+
const metadata = getSearchObjectsMetadata(sourceId);
|
|
3186
|
+
server.registerTool(
|
|
3187
|
+
metadata.name,
|
|
3188
|
+
{
|
|
3189
|
+
description: metadata.description,
|
|
3190
|
+
inputSchema: searchDatabaseObjectsSchema,
|
|
3191
|
+
annotations: {
|
|
3192
|
+
title: metadata.title,
|
|
3193
|
+
readOnlyHint: true,
|
|
3194
|
+
destructiveHint: false,
|
|
3195
|
+
idempotentHint: true,
|
|
3196
|
+
openWorldHint: false
|
|
3197
|
+
}
|
|
3198
|
+
},
|
|
3199
|
+
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
function registerCustomTool(server, sourceId, toolConfig) {
|
|
3203
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
3204
|
+
const dbType = sourceConfig.type;
|
|
3205
|
+
const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
|
|
3206
|
+
const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
3207
|
+
server.registerTool(
|
|
3208
|
+
toolConfig.name,
|
|
3209
|
+
{
|
|
3210
|
+
description: toolConfig.description,
|
|
3211
|
+
inputSchema: zodSchema,
|
|
3212
|
+
annotations: {
|
|
3213
|
+
title: `${toolConfig.name} (${dbType})`,
|
|
3214
|
+
readOnlyHint: isReadOnly,
|
|
3215
|
+
destructiveHint: !isReadOnly,
|
|
3216
|
+
idempotentHint: isReadOnly,
|
|
3217
|
+
openWorldHint: false
|
|
3218
|
+
}
|
|
3219
|
+
},
|
|
3220
|
+
createCustomToolHandler(toolConfig)
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
// src/api/sources.ts
|
|
3225
|
+
function transformSourceConfig(source) {
|
|
3226
|
+
if (!source.type && source.dsn) {
|
|
3227
|
+
const inferredType = getDatabaseTypeFromDSN(source.dsn);
|
|
3228
|
+
if (inferredType) {
|
|
3229
|
+
source.type = inferredType;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
if (!source.type) {
|
|
3233
|
+
throw new Error(`Source ${source.id} is missing required type field`);
|
|
3234
|
+
}
|
|
3235
|
+
const dataSource = {
|
|
3236
|
+
id: source.id,
|
|
3237
|
+
type: source.type
|
|
3238
|
+
};
|
|
3239
|
+
if (source.description) {
|
|
3240
|
+
dataSource.description = source.description;
|
|
3241
|
+
}
|
|
3242
|
+
if (source.host) {
|
|
3243
|
+
dataSource.host = source.host;
|
|
3244
|
+
}
|
|
3245
|
+
if (source.port !== void 0) {
|
|
3246
|
+
dataSource.port = source.port;
|
|
3247
|
+
}
|
|
3248
|
+
if (source.database) {
|
|
3249
|
+
dataSource.database = source.database;
|
|
3250
|
+
}
|
|
3251
|
+
if (source.user) {
|
|
3252
|
+
dataSource.user = source.user;
|
|
3253
|
+
}
|
|
3254
|
+
if (source.ssh_host) {
|
|
3255
|
+
const sshTunnel = {
|
|
3256
|
+
enabled: true,
|
|
3257
|
+
ssh_host: source.ssh_host
|
|
3258
|
+
};
|
|
3259
|
+
if (source.ssh_port !== void 0) {
|
|
3260
|
+
sshTunnel.ssh_port = source.ssh_port;
|
|
3261
|
+
}
|
|
3262
|
+
if (source.ssh_user) {
|
|
3263
|
+
sshTunnel.ssh_user = source.ssh_user;
|
|
3264
|
+
}
|
|
3265
|
+
dataSource.ssh_tunnel = sshTunnel;
|
|
3266
|
+
}
|
|
3267
|
+
dataSource.tools = getToolsForSource(source.id);
|
|
3268
|
+
return dataSource;
|
|
3269
|
+
}
|
|
3270
|
+
function listSources(req, res) {
|
|
3271
|
+
try {
|
|
3272
|
+
const sourceConfigs = ConnectorManager.getAllSourceConfigs();
|
|
3273
|
+
const sources = sourceConfigs.map((config) => {
|
|
3274
|
+
return transformSourceConfig(config);
|
|
3275
|
+
});
|
|
3276
|
+
res.json(sources);
|
|
3277
|
+
} catch (error) {
|
|
3278
|
+
console.error("Error listing sources:", error);
|
|
3279
|
+
const errorResponse = {
|
|
3280
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
3281
|
+
};
|
|
3282
|
+
res.status(500).json(errorResponse);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
function getSource(req, res) {
|
|
3286
|
+
try {
|
|
3287
|
+
const sourceId = req.params.sourceId;
|
|
3288
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
3289
|
+
if (!sourceConfig) {
|
|
3290
|
+
const errorResponse = {
|
|
3291
|
+
error: "Source not found",
|
|
3292
|
+
source_id: sourceId
|
|
3293
|
+
};
|
|
3294
|
+
res.status(404).json(errorResponse);
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
const dataSource = transformSourceConfig(sourceConfig);
|
|
3298
|
+
res.json(dataSource);
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
console.error(`Error getting source ${req.params.sourceId}:`, error);
|
|
3301
|
+
const errorResponse = {
|
|
3302
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
3303
|
+
};
|
|
3304
|
+
res.status(500).json(errorResponse);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// src/api/requests.ts
|
|
3309
|
+
function listRequests(req, res) {
|
|
3310
|
+
try {
|
|
3311
|
+
const sourceId = req.query.source_id;
|
|
3312
|
+
const requests = requestStore.getAll(sourceId);
|
|
3313
|
+
res.json({
|
|
3314
|
+
requests,
|
|
3315
|
+
total: requests.length
|
|
3316
|
+
});
|
|
3317
|
+
} catch (error) {
|
|
3318
|
+
console.error("Error listing requests:", error);
|
|
3319
|
+
res.status(500).json({
|
|
3320
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
// src/utils/startup-table.ts
|
|
3326
|
+
var BOX = {
|
|
3327
|
+
topLeft: "\u250C",
|
|
3328
|
+
topRight: "\u2510",
|
|
3329
|
+
bottomLeft: "\u2514",
|
|
3330
|
+
bottomRight: "\u2518",
|
|
3331
|
+
horizontal: "\u2500",
|
|
3332
|
+
vertical: "\u2502",
|
|
3333
|
+
leftT: "\u251C",
|
|
3334
|
+
rightT: "\u2524",
|
|
3335
|
+
bullet: "\u2022"
|
|
3336
|
+
};
|
|
3337
|
+
function parseHostAndDatabase(source) {
|
|
3338
|
+
if (source.dsn) {
|
|
3339
|
+
const parsed = parseConnectionInfoFromDSN(source.dsn);
|
|
3340
|
+
if (parsed) {
|
|
3341
|
+
if (parsed.type === "sqlite") {
|
|
3342
|
+
return { host: "", database: parsed.database || ":memory:" };
|
|
3343
|
+
}
|
|
3344
|
+
if (!parsed.host) {
|
|
3345
|
+
return { host: "", database: parsed.database || "" };
|
|
3346
|
+
}
|
|
3347
|
+
const port = parsed.port ?? getDefaultPortForType(parsed.type);
|
|
3348
|
+
const host2 = port ? `${parsed.host}:${port}` : parsed.host;
|
|
3349
|
+
return { host: host2, database: parsed.database || "" };
|
|
3350
|
+
}
|
|
3351
|
+
return { host: "unknown", database: "" };
|
|
3352
|
+
}
|
|
3353
|
+
const host = source.host ? source.port ? `${source.host}:${source.port}` : source.host : "";
|
|
3354
|
+
const database = source.database || "";
|
|
3355
|
+
return { host, database };
|
|
3356
|
+
}
|
|
3357
|
+
function horizontalLine(width, left, right) {
|
|
3358
|
+
return left + BOX.horizontal.repeat(width - 2) + right;
|
|
3359
|
+
}
|
|
3360
|
+
function fitString(str, width) {
|
|
3361
|
+
if (str.length > width) {
|
|
3362
|
+
return str.slice(0, width - 1) + "\u2026";
|
|
3363
|
+
}
|
|
3364
|
+
return str.padEnd(width);
|
|
3365
|
+
}
|
|
3366
|
+
function formatHostDatabase(host, database) {
|
|
3367
|
+
return host ? database ? `${host}/${database}` : host : database || "";
|
|
3368
|
+
}
|
|
3369
|
+
function generateStartupTable(sources) {
|
|
3370
|
+
if (sources.length === 0) {
|
|
3371
|
+
return "";
|
|
3372
|
+
}
|
|
3373
|
+
const idTypeWidth = Math.max(
|
|
3374
|
+
20,
|
|
3375
|
+
...sources.map((s) => `${s.id} (${s.type})`.length)
|
|
3376
|
+
);
|
|
3377
|
+
const hostDbWidth = Math.max(
|
|
3378
|
+
24,
|
|
3379
|
+
...sources.map((s) => formatHostDatabase(s.host, s.database).length)
|
|
3380
|
+
);
|
|
3381
|
+
const modeWidth = Math.max(
|
|
3382
|
+
10,
|
|
3383
|
+
...sources.map((s) => {
|
|
3384
|
+
const modes = [];
|
|
3385
|
+
if (s.isDemo) modes.push("DEMO");
|
|
3386
|
+
if (s.readonly) modes.push("READ-ONLY");
|
|
3387
|
+
return modes.join(" ").length;
|
|
3388
|
+
})
|
|
3389
|
+
);
|
|
3390
|
+
const totalWidth = 2 + idTypeWidth + 3 + hostDbWidth + 3 + modeWidth + 2;
|
|
3391
|
+
const lines = [];
|
|
3392
|
+
for (let i = 0; i < sources.length; i++) {
|
|
3393
|
+
const source = sources[i];
|
|
3394
|
+
const isFirst = i === 0;
|
|
3395
|
+
const isLast = i === sources.length - 1;
|
|
3396
|
+
if (isFirst) {
|
|
3397
|
+
lines.push(horizontalLine(totalWidth, BOX.topLeft, BOX.topRight));
|
|
3398
|
+
}
|
|
3399
|
+
const idType = fitString(`${source.id} (${source.type})`, idTypeWidth);
|
|
3400
|
+
const hostDb = fitString(
|
|
3401
|
+
formatHostDatabase(source.host, source.database),
|
|
3402
|
+
hostDbWidth
|
|
3403
|
+
);
|
|
3404
|
+
const modes = [];
|
|
3405
|
+
if (source.isDemo) modes.push("DEMO");
|
|
3406
|
+
if (source.readonly) modes.push("READ-ONLY");
|
|
3407
|
+
const modeStr = fitString(modes.join(" "), modeWidth);
|
|
3408
|
+
lines.push(
|
|
3409
|
+
`${BOX.vertical} ${idType} ${BOX.vertical} ${hostDb} ${BOX.vertical} ${modeStr} ${BOX.vertical}`
|
|
3410
|
+
);
|
|
3411
|
+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
|
|
3412
|
+
for (const tool of source.tools) {
|
|
3413
|
+
const toolLine = ` ${BOX.bullet} ${tool}`;
|
|
3414
|
+
lines.push(
|
|
3415
|
+
`${BOX.vertical} ${fitString(toolLine, totalWidth - 4)} ${BOX.vertical}`
|
|
3416
|
+
);
|
|
3417
|
+
}
|
|
3418
|
+
if (isLast) {
|
|
3419
|
+
lines.push(horizontalLine(totalWidth, BOX.bottomLeft, BOX.bottomRight));
|
|
3420
|
+
} else {
|
|
3421
|
+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
return lines.join("\n");
|
|
3425
|
+
}
|
|
3426
|
+
function buildSourceDisplayInfo(sourceConfigs, getToolsForSource2, isDemo) {
|
|
3427
|
+
return sourceConfigs.map((source) => {
|
|
3428
|
+
const { host, database } = parseHostAndDatabase(source);
|
|
3429
|
+
return {
|
|
3430
|
+
id: source.id,
|
|
3431
|
+
type: source.type || "sqlite",
|
|
3432
|
+
host,
|
|
3433
|
+
database,
|
|
3434
|
+
readonly: source.readonly || false,
|
|
3435
|
+
isDemo,
|
|
3436
|
+
tools: getToolsForSource2(source.id)
|
|
3437
|
+
};
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// src/server.ts
|
|
3442
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3443
|
+
var __dirname = path2.dirname(__filename);
|
|
3444
|
+
var SERVER_VERSION = "0.0.0";
|
|
3445
|
+
try {
|
|
3446
|
+
const packageJsonPath = path2.join(__dirname, "..", "package.json");
|
|
3447
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
3448
|
+
if (typeof packageJson.version === "string") {
|
|
3449
|
+
SERVER_VERSION = packageJson.version;
|
|
3450
|
+
}
|
|
3451
|
+
} catch {
|
|
3452
|
+
}
|
|
3453
|
+
var SERVER_NAME = "DBHub MCP Server";
|
|
3454
|
+
function generateBanner(version, modes = []) {
|
|
3455
|
+
const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
|
|
3456
|
+
return `
|
|
3457
|
+
____ _ _____ _____ ____ ___ _
|
|
3458
|
+
/ ___| / \\ | ___| ____| / ___| / _ \\| |
|
|
3459
|
+
\\___ \\ / _ \\ | |_ | _| \\___ \\| | | | |
|
|
3460
|
+
___) / ___ \\| _| | |___ ___) | |_| | |___
|
|
3461
|
+
|____/_/ \\_\\_| |_____| |____/ \\__\\_\\_____|
|
|
3462
|
+
|
|
3463
|
+
v${version}${modeText} - Minimal Readonly Database MCP Server
|
|
3464
|
+
`;
|
|
3465
|
+
}
|
|
3466
|
+
async function main() {
|
|
3467
|
+
try {
|
|
3468
|
+
const sourceConfigsData = await resolveSourceConfigs();
|
|
3469
|
+
if (!sourceConfigsData) {
|
|
3470
|
+
const samples = ConnectorRegistry.getAllSampleDSNs();
|
|
3471
|
+
const sampleFormats = Object.entries(samples).map(([id, dsn]) => ` - ${id}: ${dsn}`).join("\n");
|
|
3472
|
+
console.error(`
|
|
3473
|
+
ERROR: Database connection configuration is required.
|
|
3474
|
+
Please provide configuration in one of these ways (in order of priority):
|
|
3475
|
+
|
|
3476
|
+
1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
|
|
3477
|
+
2. TOML config file: --config=path/to/dbhub.toml or ./dbhub.toml
|
|
3478
|
+
3. Command line argument: --dsn="your-connection-string"
|
|
3479
|
+
4. Environment variable: export DSN="your-connection-string"
|
|
3480
|
+
5. .env file: DSN=your-connection-string
|
|
3481
|
+
|
|
3482
|
+
Example DSN formats:
|
|
3483
|
+
${sampleFormats}
|
|
3484
|
+
|
|
3485
|
+
Example TOML config (dbhub.toml):
|
|
3486
|
+
[[sources]]
|
|
3487
|
+
id = "my_db"
|
|
3488
|
+
dsn = "postgres://user:pass@localhost:5432/dbname"
|
|
3489
|
+
|
|
3490
|
+
See documentation for more details on configuring database connections.
|
|
3491
|
+
`);
|
|
3492
|
+
process.exit(1);
|
|
3493
|
+
}
|
|
3494
|
+
const connectorManager = new ConnectorManager();
|
|
3495
|
+
const sources = sourceConfigsData.sources;
|
|
3496
|
+
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
3497
|
+
await connectorManager.connectWithSources(sources);
|
|
3498
|
+
const { initializeToolRegistry } = await import("./registry-54CGLMGK.js");
|
|
3499
|
+
initializeToolRegistry({
|
|
3500
|
+
sources: sourceConfigsData.sources,
|
|
3501
|
+
tools: sourceConfigsData.tools
|
|
3502
|
+
});
|
|
3503
|
+
console.error("Tool registry initialized");
|
|
3504
|
+
setOutputFormat(resolveOutputFormat());
|
|
3505
|
+
const createServer = () => {
|
|
3506
|
+
const server = new McpServer({
|
|
3507
|
+
name: SERVER_NAME,
|
|
3508
|
+
version: SERVER_VERSION
|
|
3509
|
+
});
|
|
3510
|
+
registerTools(server);
|
|
3511
|
+
return server;
|
|
3512
|
+
};
|
|
3513
|
+
const transportData = resolveTransport();
|
|
3514
|
+
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
3515
|
+
const activeModes = [];
|
|
3516
|
+
const modeDescriptions = [];
|
|
3517
|
+
const isDemo = isDemoMode();
|
|
3518
|
+
if (isDemo) {
|
|
3519
|
+
activeModes.push("DEMO");
|
|
3520
|
+
modeDescriptions.push("using sample employee database");
|
|
3521
|
+
}
|
|
3522
|
+
if (activeModes.length > 0) {
|
|
3523
|
+
console.error(`Running in ${activeModes.join(" and ")} mode - ${modeDescriptions.join(", ")}`);
|
|
3524
|
+
}
|
|
3525
|
+
console.error(generateBanner(SERVER_VERSION, activeModes));
|
|
3526
|
+
const sourceDisplayInfos = buildSourceDisplayInfo(
|
|
3527
|
+
sources,
|
|
3528
|
+
(sourceId) => getToolsForSource(sourceId).map((t) => t.readonly ? `\u{1F512} ${t.name}` : t.name),
|
|
3529
|
+
isDemo
|
|
3530
|
+
);
|
|
3531
|
+
console.error(generateStartupTable(sourceDisplayInfos));
|
|
3532
|
+
if (transportData.type === "http") {
|
|
3533
|
+
const app = express();
|
|
3534
|
+
app.use(express.json());
|
|
3535
|
+
app.use((req, res, next) => {
|
|
3536
|
+
const origin = req.headers.origin;
|
|
3537
|
+
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
3538
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
3539
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
|
3540
|
+
res.header("Access-Control-Allow-Credentials", "true");
|
|
3541
|
+
if (req.method === "OPTIONS") {
|
|
3542
|
+
return res.sendStatus(200);
|
|
3543
|
+
}
|
|
3544
|
+
next();
|
|
3545
|
+
});
|
|
3546
|
+
const frontendPath = path2.join(__dirname, "public");
|
|
3547
|
+
app.use(express.static(frontendPath));
|
|
3548
|
+
app.get("/healthz", (req, res) => {
|
|
3549
|
+
res.status(200).send("OK");
|
|
3550
|
+
});
|
|
3551
|
+
app.get("/api/sources", listSources);
|
|
3552
|
+
app.get("/api/sources/:sourceId", getSource);
|
|
3553
|
+
app.get("/api/requests", listRequests);
|
|
3554
|
+
app.get("/mcp", (req, res) => {
|
|
3555
|
+
res.status(405).json({
|
|
3556
|
+
error: "Method Not Allowed",
|
|
3557
|
+
message: "SSE streaming is not supported in stateless mode. Use POST requests with JSON responses."
|
|
3558
|
+
});
|
|
3559
|
+
});
|
|
3560
|
+
app.post("/mcp", async (req, res) => {
|
|
3561
|
+
try {
|
|
3562
|
+
const transport = new StreamableHTTPServerTransport({
|
|
3563
|
+
sessionIdGenerator: void 0,
|
|
3564
|
+
// Disable session management for stateless mode
|
|
3565
|
+
enableJsonResponse: true
|
|
3566
|
+
// Use JSON responses (SSE not supported in stateless mode)
|
|
3567
|
+
});
|
|
3568
|
+
const server = createServer();
|
|
3569
|
+
await server.connect(transport);
|
|
3570
|
+
await transport.handleRequest(req, res, req.body);
|
|
3571
|
+
} catch (error) {
|
|
3572
|
+
console.error("Error handling request:", error);
|
|
3573
|
+
if (!res.headersSent) {
|
|
3574
|
+
res.status(500).json({ error: "Internal server error" });
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
});
|
|
3578
|
+
if (process.env.NODE_ENV !== "development") {
|
|
3579
|
+
app.get("*", (req, res) => {
|
|
3580
|
+
res.sendFile(path2.join(frontendPath, "index.html"));
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
app.listen(port, "0.0.0.0", () => {
|
|
3584
|
+
if (process.env.NODE_ENV === "development") {
|
|
3585
|
+
console.error("Development mode detected!");
|
|
3586
|
+
console.error(" Workbench dev server (with HMR): http://localhost:5173");
|
|
3587
|
+
console.error(" Backend API: http://localhost:8080");
|
|
3588
|
+
console.error("");
|
|
3589
|
+
} else {
|
|
3590
|
+
console.error(`Workbench at http://localhost:${port}/`);
|
|
3591
|
+
}
|
|
3592
|
+
console.error(`MCP server endpoint at http://localhost:${port}/mcp`);
|
|
3593
|
+
});
|
|
3594
|
+
} else {
|
|
3595
|
+
const server = createServer();
|
|
3596
|
+
const transport = new StdioServerTransport();
|
|
3597
|
+
await server.connect(transport);
|
|
3598
|
+
console.error("MCP server running on stdio");
|
|
3599
|
+
process.on("SIGINT", async () => {
|
|
3600
|
+
console.error("Shutting down...");
|
|
3601
|
+
await transport.close();
|
|
3602
|
+
process.exit(0);
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
} catch (err) {
|
|
3606
|
+
console.error("Fatal error:", err);
|
|
3607
|
+
process.exit(1);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// src/index.ts
|
|
3612
|
+
main().catch((error) => {
|
|
3613
|
+
console.error("Fatal error:", error);
|
|
3614
|
+
process.exit(1);
|
|
3615
|
+
});
|