@bytebase/dbhub 0.11.9 → 0.12.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/dist/index.js CHANGED
@@ -149,38 +149,82 @@ var SafeURL = class {
149
149
  };
150
150
 
151
151
  // src/utils/dsn-obfuscate.ts
152
+ function parseConnectionInfoFromDSN(dsn) {
153
+ if (!dsn) {
154
+ return null;
155
+ }
156
+ try {
157
+ const type = getDatabaseTypeFromDSN(dsn);
158
+ if (typeof type === "undefined") {
159
+ return null;
160
+ }
161
+ if (type === "sqlite") {
162
+ const prefix = "sqlite:///";
163
+ if (dsn.length > prefix.length) {
164
+ const rawPath = dsn.substring(prefix.length);
165
+ const firstChar = rawPath[0];
166
+ const isWindowsDrive = rawPath.length > 1 && rawPath[1] === ":";
167
+ const isSpecialPath = firstChar === ":" || firstChar === "." || firstChar === "~" || isWindowsDrive;
168
+ return {
169
+ type,
170
+ database: isSpecialPath ? rawPath : "/" + rawPath
171
+ };
172
+ }
173
+ return { type };
174
+ }
175
+ const url = new SafeURL(dsn);
176
+ const info = { type };
177
+ if (url.hostname) {
178
+ info.host = url.hostname;
179
+ }
180
+ if (url.port) {
181
+ info.port = parseInt(url.port, 10);
182
+ }
183
+ if (url.pathname && url.pathname.length > 1) {
184
+ info.database = url.pathname.substring(1);
185
+ }
186
+ if (url.username) {
187
+ info.user = url.username;
188
+ }
189
+ return info;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
152
194
  function obfuscateDSNPassword(dsn) {
153
195
  if (!dsn) {
154
196
  return dsn;
155
197
  }
156
198
  try {
157
- const protocolMatch = dsn.match(/^([^:]+):/);
158
- if (!protocolMatch) {
199
+ const type = getDatabaseTypeFromDSN(dsn);
200
+ if (type === "sqlite") {
159
201
  return dsn;
160
202
  }
161
- const protocol = protocolMatch[1];
162
- if (protocol === "sqlite") {
203
+ const url = new SafeURL(dsn);
204
+ if (!url.password) {
163
205
  return dsn;
164
206
  }
165
- const protocolPart = dsn.split("://")[1];
166
- if (!protocolPart) {
167
- return dsn;
207
+ const obfuscatedPassword = "*".repeat(Math.min(url.password.length, 8));
208
+ const protocol = dsn.split(":")[0];
209
+ let result;
210
+ if (url.username) {
211
+ result = `${protocol}://${url.username}:${obfuscatedPassword}@${url.hostname}`;
212
+ } else {
213
+ result = `${protocol}://${obfuscatedPassword}@${url.hostname}`;
168
214
  }
169
- const lastAtIndex = protocolPart.lastIndexOf("@");
170
- if (lastAtIndex === -1) {
171
- return dsn;
215
+ if (url.port) {
216
+ result += `:${url.port}`;
172
217
  }
173
- const credentialsPart = protocolPart.substring(0, lastAtIndex);
174
- const hostPart = protocolPart.substring(lastAtIndex + 1);
175
- const colonIndex = credentialsPart.indexOf(":");
176
- if (colonIndex === -1) {
177
- return dsn;
218
+ result += url.pathname;
219
+ if (url.searchParams.size > 0) {
220
+ const params = [];
221
+ url.forEachSearchParam((value, key) => {
222
+ params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
223
+ });
224
+ result += `?${params.join("&")}`;
178
225
  }
179
- const username = credentialsPart.substring(0, colonIndex);
180
- const password = credentialsPart.substring(colonIndex + 1);
181
- const obfuscatedPassword = "*".repeat(Math.min(password.length, 8));
182
- return `${protocol}://${username}:${obfuscatedPassword}@${hostPart}`;
183
- } catch (error) {
226
+ return result;
227
+ } catch {
184
228
  return dsn;
185
229
  }
186
230
  }
@@ -189,7 +233,10 @@ function getDatabaseTypeFromDSN(dsn) {
189
233
  return void 0;
190
234
  }
191
235
  const protocol = dsn.split(":")[0];
192
- const protocolToType = {
236
+ return protocolToConnectorType(protocol);
237
+ }
238
+ function protocolToConnectorType(protocol) {
239
+ const mapping = {
193
240
  "postgres": "postgres",
194
241
  "postgresql": "postgres",
195
242
  "mysql": "mysql",
@@ -197,7 +244,17 @@ function getDatabaseTypeFromDSN(dsn) {
197
244
  "sqlserver": "sqlserver",
198
245
  "sqlite": "sqlite"
199
246
  };
200
- return protocolToType[protocol];
247
+ return mapping[protocol];
248
+ }
249
+ function getDefaultPortForType(type) {
250
+ const ports = {
251
+ "postgres": 5432,
252
+ "mysql": 3306,
253
+ "mariadb": 3306,
254
+ "sqlserver": 1433,
255
+ "sqlite": void 0
256
+ };
257
+ return ports[type];
201
258
  }
202
259
 
203
260
  // src/utils/sql-row-limiter.ts
@@ -360,6 +417,9 @@ var PostgresConnector = class _PostgresConnector {
360
417
  async connect(dsn, initScript, config) {
361
418
  try {
362
419
  const poolConfig = await this.dsnParser.parse(dsn, config);
420
+ if (config?.readonly) {
421
+ poolConfig.options = (poolConfig.options || "") + " -c default_transaction_read_only=on";
422
+ }
363
423
  this.pool = new Pool(poolConfig);
364
424
  const client = await this.pool.connect();
365
425
  console.error("Successfully connected to PostgreSQL database");
@@ -1009,6 +1069,38 @@ ConnectorRegistry.register(sqlServerConnector);
1009
1069
 
1010
1070
  // src/connectors/sqlite/index.ts
1011
1071
  import Database from "better-sqlite3";
1072
+
1073
+ // src/utils/identifier-quoter.ts
1074
+ function quoteIdentifier(identifier, dbType) {
1075
+ if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
1076
+ throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
1077
+ }
1078
+ if (!identifier) {
1079
+ throw new Error("Identifier cannot be empty");
1080
+ }
1081
+ switch (dbType) {
1082
+ case "postgres":
1083
+ case "sqlite":
1084
+ return `"${identifier.replace(/"/g, '""')}"`;
1085
+ case "mysql":
1086
+ case "mariadb":
1087
+ return `\`${identifier.replace(/`/g, "``")}\``;
1088
+ case "sqlserver":
1089
+ return `[${identifier.replace(/]/g, "]]")}]`;
1090
+ default:
1091
+ return `"${identifier.replace(/"/g, '""')}"`;
1092
+ }
1093
+ }
1094
+ function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
1095
+ const quotedTable = quoteIdentifier(tableName, dbType);
1096
+ if (schemaName) {
1097
+ const quotedSchema = quoteIdentifier(schemaName, dbType);
1098
+ return `${quotedSchema}.${quotedTable}`;
1099
+ }
1100
+ return quotedTable;
1101
+ }
1102
+
1103
+ // src/connectors/sqlite/index.ts
1012
1104
  var SQLiteDSNParser = class {
1013
1105
  async parse(dsn, config) {
1014
1106
  if (!this.isValidDSN(dsn)) {
@@ -1073,7 +1165,11 @@ var SQLiteConnector = class _SQLiteConnector {
1073
1165
  const parsedConfig = await this.dsnParser.parse(dsn, config);
1074
1166
  this.dbPath = parsedConfig.dbPath;
1075
1167
  try {
1076
- this.db = new Database(this.dbPath);
1168
+ const dbOptions = {};
1169
+ if (config?.readonly && this.dbPath !== ":memory:") {
1170
+ dbOptions.readonly = true;
1171
+ }
1172
+ this.db = new Database(this.dbPath, dbOptions);
1077
1173
  console.error("Successfully connected to SQLite database");
1078
1174
  if (initScript) {
1079
1175
  this.db.exec(initScript);
@@ -1158,16 +1254,18 @@ var SQLiteConnector = class _SQLiteConnector {
1158
1254
  AND tbl_name = ?
1159
1255
  `
1160
1256
  ).all(tableName);
1161
- const indexListRows = this.db.prepare(`PRAGMA index_list(${tableName})`).all();
1257
+ const quotedTableName = quoteIdentifier(tableName, "sqlite");
1258
+ const indexListRows = this.db.prepare(`PRAGMA index_list(${quotedTableName})`).all();
1162
1259
  const indexUniqueMap = /* @__PURE__ */ new Map();
1163
1260
  for (const indexListRow of indexListRows) {
1164
1261
  indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
1165
1262
  }
1166
- const tableInfo = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
1263
+ const tableInfo = this.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
1167
1264
  const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
1168
1265
  const results = [];
1169
1266
  for (const indexInfo of indexInfoRows) {
1170
- const indexDetailRows = this.db.prepare(`PRAGMA index_info(${indexInfo.index_name})`).all();
1267
+ const quotedIndexName = quoteIdentifier(indexInfo.index_name, "sqlite");
1268
+ const indexDetailRows = this.db.prepare(`PRAGMA index_info(${quotedIndexName})`).all();
1171
1269
  const columnNames = indexDetailRows.map((row) => row.name);
1172
1270
  results.push({
1173
1271
  index_name: indexInfo.index_name,
@@ -1194,7 +1292,8 @@ var SQLiteConnector = class _SQLiteConnector {
1194
1292
  throw new Error("Not connected to SQLite database");
1195
1293
  }
1196
1294
  try {
1197
- const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
1295
+ const quotedTableName = quoteIdentifier(tableName, "sqlite");
1296
+ const rows = this.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
1198
1297
  const columns = rows.map((row) => ({
1199
1298
  column_name: row.name,
1200
1299
  data_type: row.type,
@@ -2629,6 +2728,17 @@ async function resolveSourceConfigs() {
2629
2728
  if (!isDemoMode()) {
2630
2729
  const tomlConfig = loadTomlConfig();
2631
2730
  if (tomlConfig) {
2731
+ const idData = resolveId();
2732
+ if (idData) {
2733
+ throw new Error(
2734
+ "The --id flag cannot be used with TOML configuration. TOML config defines source IDs directly. Either remove the --id flag or use command-line DSN configuration instead."
2735
+ );
2736
+ }
2737
+ if (isReadOnlyMode()) {
2738
+ throw new Error(
2739
+ "The --readonly flag cannot be used with TOML configuration. TOML config defines readonly mode per-source using 'readonly = true'. Either remove the --readonly flag or use command-line DSN configuration instead."
2740
+ );
2741
+ }
2632
2742
  return tomlConfig;
2633
2743
  }
2634
2744
  }
@@ -2657,11 +2767,28 @@ async function resolveSourceConfigs() {
2657
2767
  } else {
2658
2768
  throw new Error(`Unsupported database type in DSN: ${protocol}`);
2659
2769
  }
2770
+ const idData = resolveId();
2771
+ const sourceId = idData?.id || "default";
2660
2772
  const source = {
2661
- id: "default",
2773
+ id: sourceId,
2662
2774
  type: dbType,
2663
2775
  dsn: dsnResult.dsn
2664
2776
  };
2777
+ const connectionInfo = parseConnectionInfoFromDSN(dsnResult.dsn);
2778
+ if (connectionInfo) {
2779
+ if (connectionInfo.host) {
2780
+ source.host = connectionInfo.host;
2781
+ }
2782
+ if (connectionInfo.port !== void 0) {
2783
+ source.port = connectionInfo.port;
2784
+ }
2785
+ if (connectionInfo.database) {
2786
+ source.database = connectionInfo.database;
2787
+ }
2788
+ if (connectionInfo.user) {
2789
+ source.user = connectionInfo.user;
2790
+ }
2791
+ }
2665
2792
  const sshResult = resolveSSHConfig();
2666
2793
  if (sshResult) {
2667
2794
  source.ssh_host = sshResult.config.host;
@@ -2832,6 +2959,26 @@ function processSourceConfigs(sources, configPath) {
2832
2959
  if (processed.dsn && processed.dsn.startsWith("sqlite:///~")) {
2833
2960
  processed.dsn = `sqlite:///${expandHomeDir(processed.dsn.substring(11))}`;
2834
2961
  }
2962
+ if (processed.dsn) {
2963
+ const connectionInfo = parseConnectionInfoFromDSN(processed.dsn);
2964
+ if (connectionInfo) {
2965
+ if (!processed.type && connectionInfo.type) {
2966
+ processed.type = connectionInfo.type;
2967
+ }
2968
+ if (!processed.host && connectionInfo.host) {
2969
+ processed.host = connectionInfo.host;
2970
+ }
2971
+ if (processed.port === void 0 && connectionInfo.port !== void 0) {
2972
+ processed.port = connectionInfo.port;
2973
+ }
2974
+ if (!processed.database && connectionInfo.database) {
2975
+ processed.database = connectionInfo.database;
2976
+ }
2977
+ if (!processed.user && connectionInfo.user) {
2978
+ processed.user = connectionInfo.user;
2979
+ }
2980
+ }
2981
+ }
2835
2982
  return processed;
2836
2983
  });
2837
2984
  }
@@ -2863,7 +3010,7 @@ function buildDSNFromSource(source) {
2863
3010
  `Source '${source.id}': missing required connection parameters. Required: type, host, user, password, database`
2864
3011
  );
2865
3012
  }
2866
- const port = source.port || (source.type === "postgres" ? 5432 : source.type === "mysql" || source.type === "mariadb" ? 3306 : source.type === "sqlserver" ? 1433 : void 0);
3013
+ const port = source.port || getDefaultPortForType(source.type);
2867
3014
  if (!port) {
2868
3015
  throw new Error(`Source '${source.id}': unable to determine port`);
2869
3016
  }
@@ -2963,6 +3110,9 @@ var ConnectorManager = class {
2963
3110
  if (connector.id === "sqlserver" && source.request_timeout !== void 0) {
2964
3111
  config.requestTimeoutSeconds = source.request_timeout;
2965
3112
  }
3113
+ if (source.readonly !== void 0) {
3114
+ config.readonly = source.readonly;
3115
+ }
2966
3116
  await connector.connect(actualDSN, source.init_script, config);
2967
3117
  this.connectors.set(sourceId, connector);
2968
3118
  this.sourceIds.push(sourceId);
@@ -3114,16 +3264,11 @@ var ConnectorManager = class {
3114
3264
  * Get default port for a database based on DSN protocol
3115
3265
  */
3116
3266
  getDefaultPort(dsn) {
3117
- if (dsn.startsWith("postgres://") || dsn.startsWith("postgresql://")) {
3118
- return 5432;
3119
- } else if (dsn.startsWith("mysql://")) {
3120
- return 3306;
3121
- } else if (dsn.startsWith("mariadb://")) {
3122
- return 3306;
3123
- } else if (dsn.startsWith("sqlserver://")) {
3124
- return 1433;
3267
+ const type = getDatabaseTypeFromDSN(dsn);
3268
+ if (!type) {
3269
+ return 0;
3125
3270
  }
3126
- return 0;
3271
+ return getDefaultPortForType(type) ?? 0;
3127
3272
  }
3128
3273
  };
3129
3274
 
@@ -3511,10 +3656,66 @@ var allowedKeywords = {
3511
3656
  sqlserver: ["select", "with", "explain", "showplan"]
3512
3657
  };
3513
3658
 
3659
+ // src/requests/store.ts
3660
+ var RequestStore = class {
3661
+ constructor() {
3662
+ this.store = /* @__PURE__ */ new Map();
3663
+ this.maxPerSource = 100;
3664
+ }
3665
+ /**
3666
+ * Add a request to the store
3667
+ * Evicts oldest entry if at capacity
3668
+ */
3669
+ add(request) {
3670
+ const requests = this.store.get(request.sourceId) ?? [];
3671
+ requests.push(request);
3672
+ if (requests.length > this.maxPerSource) {
3673
+ requests.shift();
3674
+ }
3675
+ this.store.set(request.sourceId, requests);
3676
+ }
3677
+ /**
3678
+ * Get requests, optionally filtered by source
3679
+ * Returns newest first
3680
+ */
3681
+ getAll(sourceId) {
3682
+ let requests;
3683
+ if (sourceId) {
3684
+ requests = [...this.store.get(sourceId) ?? []];
3685
+ } else {
3686
+ requests = Array.from(this.store.values()).flat();
3687
+ }
3688
+ return requests.sort(
3689
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
3690
+ );
3691
+ }
3692
+ /**
3693
+ * Get total count of requests across all sources
3694
+ */
3695
+ getTotal() {
3696
+ return Array.from(this.store.values()).reduce((sum, arr) => sum + arr.length, 0);
3697
+ }
3698
+ /**
3699
+ * Clear all requests (useful for testing)
3700
+ */
3701
+ clear() {
3702
+ this.store.clear();
3703
+ }
3704
+ };
3705
+
3706
+ // src/requests/index.ts
3707
+ var requestStore = new RequestStore();
3708
+
3514
3709
  // src/tools/execute-sql.ts
3710
+ function getClientIdentifier(extra) {
3711
+ const userAgent = extra?.requestInfo?.headers?.["user-agent"];
3712
+ if (userAgent) {
3713
+ return userAgent;
3714
+ }
3715
+ return "stdio";
3716
+ }
3515
3717
  var executeSqlSchema = {
3516
- sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)"),
3517
- source_id: z.string().optional().describe("Database source ID to execute the query against (optional, defaults to first configured source)")
3718
+ sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
3518
3719
  };
3519
3720
  function splitSQLStatements(sql2) {
3520
3721
  return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
@@ -3540,45 +3741,506 @@ function areAllStatementsReadOnly(sql2, connectorType) {
3540
3741
  const statements = splitSQLStatements(sql2);
3541
3742
  return statements.every((statement) => isReadOnlySQL(statement, connectorType));
3542
3743
  }
3543
- async function executeSqlToolHandler({ sql: sql2, source_id }, _extra) {
3744
+ function createExecuteSqlToolHandler(sourceId) {
3745
+ return async (args, extra) => {
3746
+ const { sql: sql2 } = args;
3747
+ const startTime = Date.now();
3748
+ const effectiveSourceId = sourceId || "default";
3749
+ let success = true;
3750
+ let errorMessage;
3751
+ let result;
3752
+ try {
3753
+ const connector = ConnectorManager.getCurrentConnector(sourceId);
3754
+ const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
3755
+ const isReadonly = executeOptions.readonly === true;
3756
+ if (isReadonly && !areAllStatementsReadOnly(sql2, connector.id)) {
3757
+ errorMessage = `Read-only mode is enabled for source '${effectiveSourceId}'. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
3758
+ success = false;
3759
+ return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
3760
+ }
3761
+ result = await connector.executeSQL(sql2, executeOptions);
3762
+ const responseData = {
3763
+ rows: result.rows,
3764
+ count: result.rows.length,
3765
+ source_id: effectiveSourceId
3766
+ };
3767
+ return createToolSuccessResponse(responseData);
3768
+ } catch (error) {
3769
+ success = false;
3770
+ errorMessage = error.message;
3771
+ return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
3772
+ } finally {
3773
+ requestStore.add({
3774
+ id: crypto.randomUUID(),
3775
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3776
+ sourceId: effectiveSourceId,
3777
+ toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
3778
+ sql: sql2,
3779
+ durationMs: Date.now() - startTime,
3780
+ client: getClientIdentifier(extra),
3781
+ success,
3782
+ error: errorMessage
3783
+ });
3784
+ }
3785
+ };
3786
+ }
3787
+
3788
+ // src/tools/search-objects.ts
3789
+ import { z as z2 } from "zod";
3790
+ var searchDatabaseObjectsSchema = {
3791
+ object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Type of database object to search for"),
3792
+ pattern: z2.string().optional().default("%").describe("Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."),
3793
+ schema: z2.string().optional().describe("Filter results to a specific schema/database"),
3794
+ detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Level of detail to return: names (minimal), summary (with metadata), full (complete structure)"),
3795
+ limit: z2.number().int().positive().max(1e3).default(100).describe("Maximum number of results to return (default: 100, max: 1000)")
3796
+ };
3797
+ function likePatternToRegex(pattern) {
3798
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
3799
+ return new RegExp(`^${escaped}$`, "i");
3800
+ }
3801
+ async function getTableRowCount(connector, tableName, schemaName) {
3544
3802
  try {
3545
- const connector = ConnectorManager.getCurrentConnector(source_id);
3546
- const executeOptions = ConnectorManager.getCurrentExecuteOptions(source_id);
3547
- if (isReadOnlyMode() && !areAllStatementsReadOnly(sql2, connector.id)) {
3803
+ const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
3804
+ const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
3805
+ const result = await connector.executeSQL(countQuery, { maxRows: 1 });
3806
+ if (result.rows && result.rows.length > 0) {
3807
+ return Number(result.rows[0].count || result.rows[0].COUNT || 0);
3808
+ }
3809
+ } catch (error) {
3810
+ return null;
3811
+ }
3812
+ return null;
3813
+ }
3814
+ async function searchSchemas(connector, pattern, detailLevel, limit) {
3815
+ const schemas = await connector.getSchemas();
3816
+ const regex = likePatternToRegex(pattern);
3817
+ const matched = schemas.filter((schema) => regex.test(schema)).slice(0, limit);
3818
+ if (detailLevel === "names") {
3819
+ return matched.map((name) => ({ name }));
3820
+ }
3821
+ const results = await Promise.all(
3822
+ matched.map(async (schemaName) => {
3823
+ try {
3824
+ const tables = await connector.getTables(schemaName);
3825
+ return {
3826
+ name: schemaName,
3827
+ table_count: tables.length
3828
+ };
3829
+ } catch (error) {
3830
+ return {
3831
+ name: schemaName,
3832
+ table_count: 0
3833
+ };
3834
+ }
3835
+ })
3836
+ );
3837
+ return results;
3838
+ }
3839
+ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit) {
3840
+ const regex = likePatternToRegex(pattern);
3841
+ const results = [];
3842
+ let schemasToSearch;
3843
+ if (schemaFilter) {
3844
+ schemasToSearch = [schemaFilter];
3845
+ } else {
3846
+ schemasToSearch = await connector.getSchemas();
3847
+ }
3848
+ for (const schemaName of schemasToSearch) {
3849
+ if (results.length >= limit) break;
3850
+ try {
3851
+ const tables = await connector.getTables(schemaName);
3852
+ const matched = tables.filter((table) => regex.test(table));
3853
+ for (const tableName of matched) {
3854
+ if (results.length >= limit) break;
3855
+ if (detailLevel === "names") {
3856
+ results.push({
3857
+ name: tableName,
3858
+ schema: schemaName
3859
+ });
3860
+ } else if (detailLevel === "summary") {
3861
+ try {
3862
+ const columns = await connector.getTableSchema(tableName, schemaName);
3863
+ const rowCount = await getTableRowCount(connector, tableName, schemaName);
3864
+ results.push({
3865
+ name: tableName,
3866
+ schema: schemaName,
3867
+ column_count: columns.length,
3868
+ row_count: rowCount
3869
+ });
3870
+ } catch (error) {
3871
+ results.push({
3872
+ name: tableName,
3873
+ schema: schemaName,
3874
+ column_count: null,
3875
+ row_count: null
3876
+ });
3877
+ }
3878
+ } else {
3879
+ try {
3880
+ const columns = await connector.getTableSchema(tableName, schemaName);
3881
+ const indexes = await connector.getTableIndexes(tableName, schemaName);
3882
+ const rowCount = await getTableRowCount(connector, tableName, schemaName);
3883
+ results.push({
3884
+ name: tableName,
3885
+ schema: schemaName,
3886
+ column_count: columns.length,
3887
+ row_count: rowCount,
3888
+ columns: columns.map((col) => ({
3889
+ name: col.column_name,
3890
+ type: col.data_type,
3891
+ nullable: col.is_nullable === "YES",
3892
+ default: col.column_default
3893
+ })),
3894
+ indexes: indexes.map((idx) => ({
3895
+ name: idx.index_name,
3896
+ columns: idx.column_names,
3897
+ unique: idx.is_unique,
3898
+ primary: idx.is_primary
3899
+ }))
3900
+ });
3901
+ } catch (error) {
3902
+ results.push({
3903
+ name: tableName,
3904
+ schema: schemaName,
3905
+ error: `Unable to fetch full details: ${error.message}`
3906
+ });
3907
+ }
3908
+ }
3909
+ }
3910
+ } catch (error) {
3911
+ continue;
3912
+ }
3913
+ }
3914
+ return results;
3915
+ }
3916
+ async function searchColumns(connector, pattern, schemaFilter, detailLevel, limit) {
3917
+ const regex = likePatternToRegex(pattern);
3918
+ const results = [];
3919
+ let schemasToSearch;
3920
+ if (schemaFilter) {
3921
+ schemasToSearch = [schemaFilter];
3922
+ } else {
3923
+ schemasToSearch = await connector.getSchemas();
3924
+ }
3925
+ for (const schemaName of schemasToSearch) {
3926
+ if (results.length >= limit) break;
3927
+ try {
3928
+ const tables = await connector.getTables(schemaName);
3929
+ for (const tableName of tables) {
3930
+ if (results.length >= limit) break;
3931
+ try {
3932
+ const columns = await connector.getTableSchema(tableName, schemaName);
3933
+ const matchedColumns = columns.filter((col) => regex.test(col.column_name));
3934
+ for (const column of matchedColumns) {
3935
+ if (results.length >= limit) break;
3936
+ if (detailLevel === "names") {
3937
+ results.push({
3938
+ name: column.column_name,
3939
+ table: tableName,
3940
+ schema: schemaName
3941
+ });
3942
+ } else {
3943
+ results.push({
3944
+ name: column.column_name,
3945
+ table: tableName,
3946
+ schema: schemaName,
3947
+ type: column.data_type,
3948
+ nullable: column.is_nullable === "YES",
3949
+ default: column.column_default
3950
+ });
3951
+ }
3952
+ }
3953
+ } catch (error) {
3954
+ continue;
3955
+ }
3956
+ }
3957
+ } catch (error) {
3958
+ continue;
3959
+ }
3960
+ }
3961
+ return results;
3962
+ }
3963
+ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit) {
3964
+ const regex = likePatternToRegex(pattern);
3965
+ const results = [];
3966
+ let schemasToSearch;
3967
+ if (schemaFilter) {
3968
+ schemasToSearch = [schemaFilter];
3969
+ } else {
3970
+ schemasToSearch = await connector.getSchemas();
3971
+ }
3972
+ for (const schemaName of schemasToSearch) {
3973
+ if (results.length >= limit) break;
3974
+ try {
3975
+ const procedures = await connector.getStoredProcedures(schemaName);
3976
+ const matched = procedures.filter((proc) => regex.test(proc));
3977
+ for (const procName of matched) {
3978
+ if (results.length >= limit) break;
3979
+ if (detailLevel === "names") {
3980
+ results.push({
3981
+ name: procName,
3982
+ schema: schemaName
3983
+ });
3984
+ } else {
3985
+ try {
3986
+ const details = await connector.getStoredProcedureDetail(procName, schemaName);
3987
+ results.push({
3988
+ name: procName,
3989
+ schema: schemaName,
3990
+ type: details.procedure_type,
3991
+ language: details.language,
3992
+ parameters: detailLevel === "full" ? details.parameter_list : void 0,
3993
+ return_type: details.return_type,
3994
+ definition: detailLevel === "full" ? details.definition : void 0
3995
+ });
3996
+ } catch (error) {
3997
+ results.push({
3998
+ name: procName,
3999
+ schema: schemaName,
4000
+ error: `Unable to fetch details: ${error.message}`
4001
+ });
4002
+ }
4003
+ }
4004
+ }
4005
+ } catch (error) {
4006
+ continue;
4007
+ }
4008
+ }
4009
+ return results;
4010
+ }
4011
+ async function searchIndexes(connector, pattern, schemaFilter, detailLevel, limit) {
4012
+ const regex = likePatternToRegex(pattern);
4013
+ const results = [];
4014
+ let schemasToSearch;
4015
+ if (schemaFilter) {
4016
+ schemasToSearch = [schemaFilter];
4017
+ } else {
4018
+ schemasToSearch = await connector.getSchemas();
4019
+ }
4020
+ for (const schemaName of schemasToSearch) {
4021
+ if (results.length >= limit) break;
4022
+ try {
4023
+ const tables = await connector.getTables(schemaName);
4024
+ for (const tableName of tables) {
4025
+ if (results.length >= limit) break;
4026
+ try {
4027
+ const indexes = await connector.getTableIndexes(tableName, schemaName);
4028
+ const matchedIndexes = indexes.filter((idx) => regex.test(idx.index_name));
4029
+ for (const index of matchedIndexes) {
4030
+ if (results.length >= limit) break;
4031
+ if (detailLevel === "names") {
4032
+ results.push({
4033
+ name: index.index_name,
4034
+ table: tableName,
4035
+ schema: schemaName
4036
+ });
4037
+ } else {
4038
+ results.push({
4039
+ name: index.index_name,
4040
+ table: tableName,
4041
+ schema: schemaName,
4042
+ columns: index.column_names,
4043
+ unique: index.is_unique,
4044
+ primary: index.is_primary
4045
+ });
4046
+ }
4047
+ }
4048
+ } catch (error) {
4049
+ continue;
4050
+ }
4051
+ }
4052
+ } catch (error) {
4053
+ continue;
4054
+ }
4055
+ }
4056
+ return results;
4057
+ }
4058
+ function createSearchDatabaseObjectsToolHandler(sourceId) {
4059
+ return async (args, _extra) => {
4060
+ const {
4061
+ object_type,
4062
+ pattern = "%",
4063
+ schema,
4064
+ detail_level = "names",
4065
+ limit = 100
4066
+ } = args;
4067
+ try {
4068
+ const connector = ConnectorManager.getCurrentConnector(sourceId);
4069
+ if (schema) {
4070
+ const schemas = await connector.getSchemas();
4071
+ if (!schemas.includes(schema)) {
4072
+ return createToolErrorResponse(
4073
+ `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`,
4074
+ "SCHEMA_NOT_FOUND"
4075
+ );
4076
+ }
4077
+ }
4078
+ let results = [];
4079
+ switch (object_type) {
4080
+ case "schema":
4081
+ results = await searchSchemas(connector, pattern, detail_level, limit);
4082
+ break;
4083
+ case "table":
4084
+ results = await searchTables(connector, pattern, schema, detail_level, limit);
4085
+ break;
4086
+ case "column":
4087
+ results = await searchColumns(connector, pattern, schema, detail_level, limit);
4088
+ break;
4089
+ case "procedure":
4090
+ results = await searchProcedures(connector, pattern, schema, detail_level, limit);
4091
+ break;
4092
+ case "index":
4093
+ results = await searchIndexes(connector, pattern, schema, detail_level, limit);
4094
+ break;
4095
+ default:
4096
+ return createToolErrorResponse(
4097
+ `Unsupported object_type: ${object_type}`,
4098
+ "INVALID_OBJECT_TYPE"
4099
+ );
4100
+ }
4101
+ return createToolSuccessResponse({
4102
+ object_type,
4103
+ pattern,
4104
+ schema,
4105
+ detail_level,
4106
+ count: results.length,
4107
+ results,
4108
+ truncated: results.length === limit
4109
+ });
4110
+ } catch (error) {
3548
4111
  return createToolErrorResponse(
3549
- `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`,
3550
- "READONLY_VIOLATION"
4112
+ `Error searching database objects: ${error.message}`,
4113
+ "SEARCH_ERROR"
3551
4114
  );
3552
4115
  }
3553
- const result = await connector.executeSQL(sql2, executeOptions);
3554
- const responseData = {
3555
- rows: result.rows,
3556
- count: result.rows.length,
3557
- source_id: source_id || "(default)"
3558
- // Include source_id in response for clarity
3559
- };
3560
- return createToolSuccessResponse(responseData);
3561
- } catch (error) {
3562
- return createToolErrorResponse(error.message, "EXECUTION_ERROR");
4116
+ };
4117
+ }
4118
+
4119
+ // src/utils/tool-metadata.ts
4120
+ import { z as z3 } from "zod";
4121
+
4122
+ // src/utils/normalize-id.ts
4123
+ function normalizeSourceId(id) {
4124
+ return id.replace(/[^a-zA-Z0-9]/g, "_");
4125
+ }
4126
+
4127
+ // src/utils/tool-metadata.ts
4128
+ function zodToParameters(schema) {
4129
+ const parameters = [];
4130
+ for (const [key, zodType] of Object.entries(schema)) {
4131
+ const description = zodType.description || "";
4132
+ const required = !(zodType instanceof z3.ZodOptional);
4133
+ let type = "string";
4134
+ if (zodType instanceof z3.ZodString) {
4135
+ type = "string";
4136
+ } else if (zodType instanceof z3.ZodNumber) {
4137
+ type = "number";
4138
+ } else if (zodType instanceof z3.ZodBoolean) {
4139
+ type = "boolean";
4140
+ } else if (zodType instanceof z3.ZodArray) {
4141
+ type = "array";
4142
+ } else if (zodType instanceof z3.ZodObject) {
4143
+ type = "object";
4144
+ }
4145
+ parameters.push({
4146
+ name: key,
4147
+ type,
4148
+ required,
4149
+ description
4150
+ });
3563
4151
  }
4152
+ return parameters;
4153
+ }
4154
+ function getToolMetadataForSource(sourceId) {
4155
+ const sourceIds = ConnectorManager.getAvailableSourceIds();
4156
+ const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
4157
+ const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
4158
+ const dbType = sourceConfig?.type || "database";
4159
+ const toolName = sourceId === "default" ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
4160
+ const isDefault = sourceIds[0] === sourceId;
4161
+ const title = isDefault ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
4162
+ const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
4163
+ const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
4164
+ const description = `Execute SQL queries on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}${readonlyNote}${maxRowsNote}`;
4165
+ const isReadonly = executeOptions.readonly === true;
4166
+ const annotations = {
4167
+ title,
4168
+ readOnlyHint: isReadonly,
4169
+ destructiveHint: !isReadonly,
4170
+ // Can be destructive if not readonly
4171
+ // In readonly mode, queries are more predictable (though still not strictly idempotent due to data changes)
4172
+ // In write mode, queries are definitely not idempotent
4173
+ idempotentHint: false,
4174
+ // In readonly mode, it's safer to operate on arbitrary tables (just reading)
4175
+ // In write mode, operating on arbitrary tables is more dangerous
4176
+ openWorldHint: isReadonly
4177
+ };
4178
+ return {
4179
+ name: toolName,
4180
+ description,
4181
+ schema: executeSqlSchema,
4182
+ annotations
4183
+ };
4184
+ }
4185
+ function getToolsForSource(sourceId) {
4186
+ const metadata = getToolMetadataForSource(sourceId);
4187
+ const parameters = zodToParameters(metadata.schema);
4188
+ return [
4189
+ {
4190
+ name: metadata.name,
4191
+ description: metadata.description,
4192
+ parameters
4193
+ }
4194
+ ];
3564
4195
  }
3565
4196
 
3566
4197
  // src/tools/index.ts
3567
- function registerTools(server, id) {
3568
- const toolName = id ? `execute_sql_${id}` : "execute_sql";
3569
- server.tool(
3570
- toolName,
3571
- "Execute a SQL query on the current database",
3572
- executeSqlSchema,
3573
- executeSqlToolHandler
3574
- );
4198
+ function registerTools(server) {
4199
+ const sourceIds = ConnectorManager.getAvailableSourceIds();
4200
+ if (sourceIds.length === 0) {
4201
+ throw new Error("No database sources configured");
4202
+ }
4203
+ for (const sourceId of sourceIds) {
4204
+ const isDefault = sourceIds[0] === sourceId;
4205
+ const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
4206
+ const dbType = sourceConfig?.type || "database";
4207
+ const executeSqlMetadata = getToolMetadataForSource(sourceId);
4208
+ server.registerTool(
4209
+ executeSqlMetadata.name,
4210
+ {
4211
+ description: executeSqlMetadata.description,
4212
+ inputSchema: executeSqlMetadata.schema,
4213
+ annotations: executeSqlMetadata.annotations
4214
+ },
4215
+ createExecuteSqlToolHandler(sourceId)
4216
+ );
4217
+ const searchToolName = sourceId === "default" ? "search_objects" : `search_objects_${sourceId}`;
4218
+ const searchToolTitle = isDefault ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
4219
+ const searchToolDescription = `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}. Supports SQL LIKE patterns (default: '%' for all), filtering, and token-efficient progressive disclosure.`;
4220
+ server.registerTool(
4221
+ searchToolName,
4222
+ {
4223
+ description: searchToolDescription,
4224
+ inputSchema: searchDatabaseObjectsSchema,
4225
+ annotations: {
4226
+ title: searchToolTitle,
4227
+ readOnlyHint: true,
4228
+ destructiveHint: false,
4229
+ idempotentHint: true,
4230
+ // Operation is read-only and idempotent
4231
+ openWorldHint: true
4232
+ }
4233
+ },
4234
+ createSearchDatabaseObjectsToolHandler(sourceId)
4235
+ );
4236
+ }
3575
4237
  }
3576
4238
 
3577
4239
  // src/prompts/sql-generator.ts
3578
- import { z as z2 } from "zod";
4240
+ import { z as z4 } from "zod";
3579
4241
  var sqlGeneratorSchema = {
3580
- description: z2.string().describe("Natural language description of the SQL query to generate"),
3581
- schema: z2.string().optional().describe("Optional database schema to use")
4242
+ description: z4.string().describe("Natural language description of the SQL query to generate"),
4243
+ schema: z4.string().optional().describe("Optional database schema to use")
3582
4244
  };
3583
4245
  async function sqlGeneratorPromptHandler({
3584
4246
  description,
@@ -3745,10 +4407,10 @@ LIMIT 10;`;
3745
4407
  }
3746
4408
 
3747
4409
  // src/prompts/db-explainer.ts
3748
- import { z as z3 } from "zod";
4410
+ import { z as z5 } from "zod";
3749
4411
  var dbExplainerSchema = {
3750
- schema: z3.string().optional().describe("Optional database schema to use"),
3751
- table: z3.string().optional().describe("Optional specific table to explain")
4412
+ schema: z5.string().optional().describe("Optional database schema to use"),
4413
+ table: z5.string().optional().describe("Optional specific table to explain")
3752
4414
  };
3753
4415
  async function dbExplainerPromptHandler({
3754
4416
  schema,
@@ -4036,6 +4698,7 @@ function transformSourceConfig(source, isDefault) {
4036
4698
  }
4037
4699
  dataSource.ssh_tunnel = sshTunnel;
4038
4700
  }
4701
+ dataSource.tools = getToolsForSource(source.id);
4039
4702
  return dataSource;
4040
4703
  }
4041
4704
  function listSources(req, res) {
@@ -4079,6 +4742,23 @@ function getSource(req, res) {
4079
4742
  }
4080
4743
  }
4081
4744
 
4745
+ // src/api/requests.ts
4746
+ function listRequests(req, res) {
4747
+ try {
4748
+ const sourceId = req.query.source_id;
4749
+ const requests = requestStore.getAll(sourceId);
4750
+ res.json({
4751
+ requests,
4752
+ total: requests.length
4753
+ });
4754
+ } catch (error) {
4755
+ console.error("Error listing requests:", error);
4756
+ res.status(500).json({
4757
+ error: error instanceof Error ? error.message : "Internal server error"
4758
+ });
4759
+ }
4760
+ }
4761
+
4082
4762
  // src/server.ts
4083
4763
  var __filename2 = fileURLToPath2(import.meta.url);
4084
4764
  var __dirname2 = path3.dirname(__filename2);
@@ -4101,12 +4781,10 @@ v${version}${modeText} - Universal Database MCP Server
4101
4781
  }
4102
4782
  async function main() {
4103
4783
  try {
4104
- const idData = resolveId();
4105
- const id = idData?.id;
4106
4784
  const sourceConfigsData = await resolveSourceConfigs();
4107
4785
  if (!sourceConfigsData) {
4108
4786
  const samples = ConnectorRegistry.getAllSampleDSNs();
4109
- const sampleFormats = Object.entries(samples).map(([id2, dsn]) => ` - ${id2}: ${dsn}`).join("\n");
4787
+ const sampleFormats = Object.entries(samples).map(([id, dsn]) => ` - ${id}: ${dsn}`).join("\n");
4110
4788
  console.error(`
4111
4789
  ERROR: Database connection configuration is required.
4112
4790
  Please provide configuration in one of these ways (in order of priority):
@@ -4135,16 +4813,13 @@ See documentation for more details on configuring database connections.
4135
4813
  version: SERVER_VERSION
4136
4814
  });
4137
4815
  registerResources(server);
4138
- registerTools(server, id);
4816
+ registerTools(server);
4139
4817
  registerPrompts(server);
4140
4818
  return server;
4141
4819
  };
4142
4820
  const connectorManager = new ConnectorManager();
4143
4821
  const sources = sourceConfigsData.sources;
4144
4822
  console.error(`Configuration source: ${sourceConfigsData.source}`);
4145
- if (idData) {
4146
- console.error(`ID: ${idData.id} (from ${idData.source})`);
4147
- }
4148
4823
  console.error(`Connecting to ${sources.length} database source(s)...`);
4149
4824
  for (const source of sources) {
4150
4825
  const dsn = source.dsn || buildDSNFromSource(source);
@@ -4201,13 +4876,20 @@ See documentation for more details on configuring database connections.
4201
4876
  });
4202
4877
  app.get("/api/sources", listSources);
4203
4878
  app.get("/api/sources/:sourceId", getSource);
4879
+ app.get("/api/requests", listRequests);
4880
+ app.get("/mcp", (req, res) => {
4881
+ res.status(405).json({
4882
+ error: "Method Not Allowed",
4883
+ message: "SSE streaming is not supported in stateless mode. Use POST requests with JSON responses."
4884
+ });
4885
+ });
4204
4886
  app.post("/mcp", async (req, res) => {
4205
4887
  try {
4206
4888
  const transport = new StreamableHTTPServerTransport({
4207
4889
  sessionIdGenerator: void 0,
4208
4890
  // Disable session management for stateless mode
4209
- enableJsonResponse: false
4210
- // Use SSE streaming
4891
+ enableJsonResponse: true
4892
+ // Use JSON responses (SSE not supported in stateless mode)
4211
4893
  });
4212
4894
  const server = createServer2();
4213
4895
  await server.connect(transport);
@@ -4219,9 +4901,11 @@ See documentation for more details on configuring database connections.
4219
4901
  }
4220
4902
  }
4221
4903
  });
4222
- app.get("*", (req, res) => {
4223
- res.sendFile(path3.join(frontendPath, "index.html"));
4224
- });
4904
+ if (process.env.NODE_ENV !== "development") {
4905
+ app.get("*", (req, res) => {
4906
+ res.sendFile(path3.join(frontendPath, "index.html"));
4907
+ });
4908
+ }
4225
4909
  app.listen(port, "0.0.0.0", () => {
4226
4910
  if (process.env.NODE_ENV === "development") {
4227
4911
  console.error("Development mode detected!");