@bytebase/dbhub 0.3.1 → 0.3.2

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.
Files changed (3) hide show
  1. package/README.md +22 -8
  2. package/dist/index.js +267 -203
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -49,7 +49,7 @@ https://demo.dbhub.ai/sse connects a [sample employee database](https://github.c
49
49
 
50
50
  | Tool | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
51
51
  | --------------- | ----------------- | :--------: | :---: | :-----: | :--------: | ------ |
52
- | Execute Query | `run_query` | ✅ | ✅ | ✅ | ✅ | ✅ |
52
+ | Execute SQL | `execute_sql` | ✅ | ✅ | ✅ | ✅ | ✅ |
53
53
  | List Connectors | `list_connectors` | ✅ | ✅ | ✅ | ✅ | ✅ |
54
54
 
55
55
  ### Prompt Capabilities
@@ -151,6 +151,19 @@ npx @bytebase/dbhub --transport sse --port 8080 --demo
151
151
 
152
152
  ## Usage
153
153
 
154
+ ### Read-only Mode
155
+
156
+ You can run DBHub in read-only mode, which restricts SQL query execution to read-only operations:
157
+
158
+ ```bash
159
+ # Enable read-only mode
160
+ npx @bytebase/dbhub --readonly --dsn "postgres://user:password@localhost:5432/dbname"
161
+ ```
162
+
163
+ In read-only mode, only [readonly SQL operations](https://github.com/bytebase/dbhub/blob/main/src/utils/allowed-keywords.ts) are allowed.
164
+
165
+ This provides an additional layer of security when connecting to production databases.
166
+
154
167
  ### Configure your database connection
155
168
 
156
169
  You can use DBHub in demo mode with a sample employee database for testing:
@@ -186,13 +199,13 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
186
199
 
187
200
  DBHub supports the following database connection string formats:
188
201
 
189
- | Database | DSN Format | Example |
190
- | ---------- | -------------------------------------------------------- | ---------------------------------------------------------------- |
191
- | MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname` |
192
- | MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname` |
193
- | PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
194
- | SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname` |
195
- | SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite::memory:` |
202
+ | Database | DSN Format | Example |
203
+ | ---------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
204
+ | MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname` |
205
+ | MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname` |
206
+ | PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
207
+ | SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname` |
208
+ | SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite::memory:` |
196
209
 
197
210
  #### SQL Server
198
211
 
@@ -223,6 +236,7 @@ Extra query parameters:
223
236
  | dsn | Database connection string | Required if not in demo mode |
224
237
  | transport | Transport mode: `stdio` or `sse` | `stdio` |
225
238
  | port | HTTP server port (only applicable when using `--transport=sse`) | `8080` |
239
+ | readonly | Restrict SQL execution to read-only operations | `false` |
226
240
 
227
241
  The demo mode uses an in-memory SQLite database loaded with the [sample employee database](https://github.com/bytebase/dbhub/tree/main/resources/employee-sqlite) that includes tables for employees, departments, titles, salaries, department employees, and department managers. The sample database includes SQL scripts for table creation, data loading, and testing.
228
242
 
package/dist/index.js CHANGED
@@ -38,8 +38,8 @@ var _ConnectorRegistry = class _ConnectorRegistry {
38
38
  /**
39
39
  * Get sample DSN for a specific connector
40
40
  */
41
- static getSampleDSN(connectorId) {
42
- const connector = _ConnectorRegistry.getConnector(connectorId);
41
+ static getSampleDSN(connectorType) {
42
+ const connector = _ConnectorRegistry.getConnector(connectorType);
43
43
  if (!connector) return null;
44
44
  return connector.dsnParser.getSampleDSN();
45
45
  }
@@ -81,7 +81,9 @@ var PostgresDSNParser = class {
81
81
  });
82
82
  return config;
83
83
  } catch (error) {
84
- throw new Error(`Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`);
84
+ throw new Error(
85
+ `Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`
86
+ );
85
87
  }
86
88
  }
87
89
  getSampleDSN() {
@@ -145,12 +147,15 @@ var PostgresConnector = class {
145
147
  const client = await this.pool.connect();
146
148
  try {
147
149
  const schemaToUse = schema || "public";
148
- const result = await client.query(`
150
+ const result = await client.query(
151
+ `
149
152
  SELECT table_name
150
153
  FROM information_schema.tables
151
154
  WHERE table_schema = $1
152
155
  ORDER BY table_name
153
- `, [schemaToUse]);
156
+ `,
157
+ [schemaToUse]
158
+ );
154
159
  return result.rows.map((row) => row.table_name);
155
160
  } finally {
156
161
  client.release();
@@ -163,13 +168,16 @@ var PostgresConnector = class {
163
168
  const client = await this.pool.connect();
164
169
  try {
165
170
  const schemaToUse = schema || "public";
166
- const result = await client.query(`
171
+ const result = await client.query(
172
+ `
167
173
  SELECT EXISTS (
168
174
  SELECT FROM information_schema.tables
169
175
  WHERE table_schema = $1
170
176
  AND table_name = $2
171
177
  )
172
- `, [schemaToUse, tableName]);
178
+ `,
179
+ [schemaToUse, tableName]
180
+ );
173
181
  return result.rows[0].exists;
174
182
  } finally {
175
183
  client.release();
@@ -182,7 +190,8 @@ var PostgresConnector = class {
182
190
  const client = await this.pool.connect();
183
191
  try {
184
192
  const schemaToUse = schema || "public";
185
- const result = await client.query(`
193
+ const result = await client.query(
194
+ `
186
195
  SELECT
187
196
  i.relname as index_name,
188
197
  array_agg(a.attname) as column_names,
@@ -209,7 +218,9 @@ var PostgresConnector = class {
209
218
  ix.indisprimary
210
219
  ORDER BY
211
220
  i.relname
212
- `, [tableName, schemaToUse]);
221
+ `,
222
+ [tableName, schemaToUse]
223
+ );
213
224
  return result.rows.map((row) => ({
214
225
  index_name: row.index_name,
215
226
  column_names: row.column_names,
@@ -227,7 +238,8 @@ var PostgresConnector = class {
227
238
  const client = await this.pool.connect();
228
239
  try {
229
240
  const schemaToUse = schema || "public";
230
- const result = await client.query(`
241
+ const result = await client.query(
242
+ `
231
243
  SELECT
232
244
  column_name,
233
245
  data_type,
@@ -237,7 +249,9 @@ var PostgresConnector = class {
237
249
  WHERE table_schema = $1
238
250
  AND table_name = $2
239
251
  ORDER BY ordinal_position
240
- `, [schemaToUse, tableName]);
252
+ `,
253
+ [schemaToUse, tableName]
254
+ );
241
255
  return result.rows;
242
256
  } finally {
243
257
  client.release();
@@ -250,13 +264,16 @@ var PostgresConnector = class {
250
264
  const client = await this.pool.connect();
251
265
  try {
252
266
  const schemaToUse = schema || "public";
253
- const result = await client.query(`
267
+ const result = await client.query(
268
+ `
254
269
  SELECT
255
270
  routine_name
256
271
  FROM information_schema.routines
257
272
  WHERE routine_schema = $1
258
273
  ORDER BY routine_name
259
- `, [schemaToUse]);
274
+ `,
275
+ [schemaToUse]
276
+ );
260
277
  return result.rows.map((row) => row.routine_name);
261
278
  } finally {
262
279
  client.release();
@@ -269,7 +286,8 @@ var PostgresConnector = class {
269
286
  const client = await this.pool.connect();
270
287
  try {
271
288
  const schemaToUse = schema || "public";
272
- const result = await client.query(`
289
+ const result = await client.query(
290
+ `
273
291
  SELECT
274
292
  routine_name as procedure_name,
275
293
  routine_type,
@@ -292,20 +310,25 @@ var PostgresConnector = class {
292
310
  FROM information_schema.routines
293
311
  WHERE routine_schema = $1
294
312
  AND routine_name = $2
295
- `, [schemaToUse, procedureName]);
313
+ `,
314
+ [schemaToUse, procedureName]
315
+ );
296
316
  if (result.rows.length === 0) {
297
317
  throw new Error(`Stored procedure '${procedureName}' not found in schema '${schemaToUse}'`);
298
318
  }
299
319
  const procedure = result.rows[0];
300
320
  let definition = procedure.definition;
301
321
  try {
302
- const oidResult = await client.query(`
322
+ const oidResult = await client.query(
323
+ `
303
324
  SELECT p.oid, p.prosrc
304
325
  FROM pg_proc p
305
326
  JOIN pg_namespace n ON p.pronamespace = n.oid
306
327
  WHERE p.proname = $1
307
328
  AND n.nspname = $2
308
- `, [procedureName, schemaToUse]);
329
+ `,
330
+ [procedureName, schemaToUse]
331
+ );
309
332
  if (oidResult.rows.length > 0) {
310
333
  if (!definition) {
311
334
  const oid = oidResult.rows[0].oid;
@@ -336,10 +359,6 @@ var PostgresConnector = class {
336
359
  if (!this.pool) {
337
360
  throw new Error("Not connected to database");
338
361
  }
339
- const safetyCheck = this.validateQuery(query);
340
- if (!safetyCheck.isValid) {
341
- throw new Error(safetyCheck.message || "Query validation failed");
342
- }
343
362
  const client = await this.pool.connect();
344
363
  try {
345
364
  return await client.query(query);
@@ -347,16 +366,6 @@ var PostgresConnector = class {
347
366
  client.release();
348
367
  }
349
368
  }
350
- validateQuery(query) {
351
- const normalizedQuery = query.trim().toLowerCase();
352
- if (!normalizedQuery.startsWith("select")) {
353
- return {
354
- isValid: false,
355
- message: "Only SELECT queries are allowed for security reasons."
356
- };
357
- }
358
- return { isValid: true };
359
- }
360
369
  };
361
370
  var postgresConnector = new PostgresConnector();
362
371
  ConnectorRegistry.register(postgresConnector);
@@ -367,7 +376,9 @@ import { DefaultAzureCredential } from "@azure/identity";
367
376
  var SQLServerDSNParser = class {
368
377
  async parse(dsn) {
369
378
  if (!this.isValidDSN(dsn)) {
370
- throw new Error("Invalid SQL Server DSN format. Expected: sqlserver://username:password@host:port/database");
379
+ throw new Error(
380
+ "Invalid SQL Server DSN format. Expected: sqlserver://username:password@host:port/database"
381
+ );
371
382
  }
372
383
  const url = new URL(dsn);
373
384
  const host = url.hostname;
@@ -644,10 +655,12 @@ var SQLServerConnector = class {
644
655
  const parameterResult = await request.query(parameterQuery);
645
656
  let parameterList = "";
646
657
  if (parameterResult.recordset.length > 0) {
647
- parameterList = parameterResult.recordset.map((param) => {
648
- const lengthStr = param.CHARACTER_MAXIMUM_LENGTH > 0 ? `(${param.CHARACTER_MAXIMUM_LENGTH})` : "";
649
- return `${param.PARAMETER_NAME} ${param.PARAMETER_MODE} ${param.DATA_TYPE}${lengthStr}`;
650
- }).join(", ");
658
+ parameterList = parameterResult.recordset.map(
659
+ (param) => {
660
+ const lengthStr = param.CHARACTER_MAXIMUM_LENGTH > 0 ? `(${param.CHARACTER_MAXIMUM_LENGTH})` : "";
661
+ return `${param.PARAMETER_NAME} ${param.PARAMETER_MODE} ${param.DATA_TYPE}${lengthStr}`;
662
+ }
663
+ ).join(", ");
651
664
  }
652
665
  const definitionQuery = `
653
666
  SELECT definition
@@ -679,10 +692,6 @@ var SQLServerConnector = class {
679
692
  if (!this.connection) {
680
693
  throw new Error("Not connected to SQL Server database");
681
694
  }
682
- const safetyCheck = this.validateQuery(query);
683
- if (!safetyCheck.isValid) {
684
- throw new Error(safetyCheck.message || "Query validation failed");
685
- }
686
695
  try {
687
696
  const result = await this.connection.request().query(query);
688
697
  return {
@@ -696,16 +705,6 @@ var SQLServerConnector = class {
696
705
  throw new Error(`Failed to execute query: ${error.message}`);
697
706
  }
698
707
  }
699
- validateQuery(query) {
700
- const normalizedQuery = query.trim().toLowerCase();
701
- if (!normalizedQuery.startsWith("select")) {
702
- return {
703
- isValid: false,
704
- message: "Only SELECT queries are allowed for security reasons."
705
- };
706
- }
707
- return { isValid: true };
708
- }
709
708
  };
710
709
  var sqlServerConnector = new SQLServerConnector();
711
710
  ConnectorRegistry.register(sqlServerConnector);
@@ -731,7 +730,9 @@ var SQLiteDSNParser = class {
731
730
  }
732
731
  return { dbPath };
733
732
  } catch (error) {
734
- throw new Error(`Failed to parse SQLite DSN: ${error instanceof Error ? error.message : String(error)}`);
733
+ throw new Error(
734
+ `Failed to parse SQLite DSN: ${error instanceof Error ? error.message : String(error)}`
735
+ );
735
736
  }
736
737
  }
737
738
  getSampleDSN() {
@@ -792,11 +793,13 @@ var SQLiteConnector = class {
792
793
  throw new Error("Not connected to SQLite database");
793
794
  }
794
795
  try {
795
- const rows = this.db.prepare(`
796
+ const rows = this.db.prepare(
797
+ `
796
798
  SELECT name FROM sqlite_master
797
799
  WHERE type='table' AND name NOT LIKE 'sqlite_%'
798
800
  ORDER BY name
799
- `).all();
801
+ `
802
+ ).all();
800
803
  return rows.map((row) => row.name);
801
804
  } catch (error) {
802
805
  throw error;
@@ -807,10 +810,12 @@ var SQLiteConnector = class {
807
810
  throw new Error("Not connected to SQLite database");
808
811
  }
809
812
  try {
810
- const row = this.db.prepare(`
813
+ const row = this.db.prepare(
814
+ `
811
815
  SELECT name FROM sqlite_master
812
816
  WHERE type='table' AND name = ?
813
- `).get(tableName);
817
+ `
818
+ ).get(tableName);
814
819
  return !!row;
815
820
  } catch (error) {
816
821
  throw error;
@@ -821,7 +826,8 @@ var SQLiteConnector = class {
821
826
  throw new Error("Not connected to SQLite database");
822
827
  }
823
828
  try {
824
- const indexInfoRows = this.db.prepare(`
829
+ const indexInfoRows = this.db.prepare(
830
+ `
825
831
  SELECT
826
832
  name as index_name,
827
833
  CASE
@@ -831,7 +837,8 @@ var SQLiteConnector = class {
831
837
  FROM sqlite_master
832
838
  WHERE type = 'index'
833
839
  AND tbl_name = ?
834
- `).all(tableName);
840
+ `
841
+ ).all(tableName);
835
842
  const tableInfo = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
836
843
  const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
837
844
  const results = [];
@@ -886,16 +893,14 @@ var SQLiteConnector = class {
886
893
  if (!this.db) {
887
894
  throw new Error("Not connected to SQLite database");
888
895
  }
889
- throw new Error("SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database.");
896
+ throw new Error(
897
+ "SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
898
+ );
890
899
  }
891
900
  async executeQuery(query) {
892
901
  if (!this.db) {
893
902
  throw new Error("Not connected to SQLite database");
894
903
  }
895
- const safetyCheck = this.validateQuery(query);
896
- if (!safetyCheck.isValid) {
897
- throw new Error(safetyCheck.message || "Query validation failed");
898
- }
899
904
  try {
900
905
  const rows = this.db.prepare(query).all();
901
906
  return { rows };
@@ -903,16 +908,6 @@ var SQLiteConnector = class {
903
908
  throw error;
904
909
  }
905
910
  }
906
- validateQuery(query) {
907
- const normalizedQuery = query.trim().toLowerCase();
908
- if (!normalizedQuery.startsWith("select")) {
909
- return {
910
- isValid: false,
911
- message: "Only SELECT queries are allowed for security reasons."
912
- };
913
- }
914
- return { isValid: true };
915
- }
916
911
  };
917
912
  var sqliteConnector = new SQLiteConnector();
918
913
  ConnectorRegistry.register(sqliteConnector);
@@ -941,7 +936,9 @@ var MySQLDSNParser = class {
941
936
  });
942
937
  return config;
943
938
  } catch (error) {
944
- throw new Error(`Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`);
939
+ throw new Error(
940
+ `Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`
941
+ );
945
942
  }
946
943
  }
947
944
  getSampleDSN() {
@@ -1003,12 +1000,15 @@ var MySQLConnector = class {
1003
1000
  try {
1004
1001
  const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1005
1002
  const queryParams = schema ? [schema] : [];
1006
- const [rows] = await this.pool.query(`
1003
+ const [rows] = await this.pool.query(
1004
+ `
1007
1005
  SELECT TABLE_NAME
1008
1006
  FROM INFORMATION_SCHEMA.TABLES
1009
1007
  ${schemaClause}
1010
1008
  ORDER BY TABLE_NAME
1011
- `, queryParams);
1009
+ `,
1010
+ queryParams
1011
+ );
1012
1012
  return rows.map((row) => row.TABLE_NAME);
1013
1013
  } catch (error) {
1014
1014
  console.error("Error getting tables:", error);
@@ -1022,12 +1022,15 @@ var MySQLConnector = class {
1022
1022
  try {
1023
1023
  const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1024
1024
  const queryParams = schema ? [schema, tableName] : [tableName];
1025
- const [rows] = await this.pool.query(`
1025
+ const [rows] = await this.pool.query(
1026
+ `
1026
1027
  SELECT COUNT(*) AS COUNT
1027
1028
  FROM INFORMATION_SCHEMA.TABLES
1028
1029
  ${schemaClause}
1029
1030
  AND TABLE_NAME = ?
1030
- `, queryParams);
1031
+ `,
1032
+ queryParams
1033
+ );
1031
1034
  return rows[0].COUNT > 0;
1032
1035
  } catch (error) {
1033
1036
  console.error("Error checking if table exists:", error);
@@ -1041,7 +1044,8 @@ var MySQLConnector = class {
1041
1044
  try {
1042
1045
  const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
1043
1046
  const queryParams = schema ? [schema, tableName] : [tableName];
1044
- const [indexRows] = await this.pool.query(`
1047
+ const [indexRows] = await this.pool.query(
1048
+ `
1045
1049
  SELECT
1046
1050
  INDEX_NAME,
1047
1051
  COLUMN_NAME,
@@ -1055,7 +1059,9 @@ var MySQLConnector = class {
1055
1059
  ORDER BY
1056
1060
  INDEX_NAME,
1057
1061
  SEQ_IN_INDEX
1058
- `, queryParams);
1062
+ `,
1063
+ queryParams
1064
+ );
1059
1065
  const indexMap = /* @__PURE__ */ new Map();
1060
1066
  for (const row of indexRows) {
1061
1067
  const indexName = row.INDEX_NAME;
@@ -1094,7 +1100,8 @@ var MySQLConnector = class {
1094
1100
  try {
1095
1101
  const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1096
1102
  const queryParams = schema ? [schema, tableName] : [tableName];
1097
- const [rows] = await this.pool.query(`
1103
+ const [rows] = await this.pool.query(
1104
+ `
1098
1105
  SELECT
1099
1106
  COLUMN_NAME as column_name,
1100
1107
  DATA_TYPE as data_type,
@@ -1104,7 +1111,9 @@ var MySQLConnector = class {
1104
1111
  ${schemaClause}
1105
1112
  AND TABLE_NAME = ?
1106
1113
  ORDER BY ORDINAL_POSITION
1107
- `, queryParams);
1114
+ `,
1115
+ queryParams
1116
+ );
1108
1117
  return rows;
1109
1118
  } catch (error) {
1110
1119
  console.error("Error getting table schema:", error);
@@ -1118,12 +1127,15 @@ var MySQLConnector = class {
1118
1127
  try {
1119
1128
  const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
1120
1129
  const queryParams = schema ? [schema] : [];
1121
- const [rows] = await this.pool.query(`
1130
+ const [rows] = await this.pool.query(
1131
+ `
1122
1132
  SELECT ROUTINE_NAME
1123
1133
  FROM INFORMATION_SCHEMA.ROUTINES
1124
1134
  ${schemaClause}
1125
1135
  ORDER BY ROUTINE_NAME
1126
- `, queryParams);
1136
+ `,
1137
+ queryParams
1138
+ );
1127
1139
  return rows.map((row) => row.ROUTINE_NAME);
1128
1140
  } catch (error) {
1129
1141
  console.error("Error getting stored procedures:", error);
@@ -1137,7 +1149,8 @@ var MySQLConnector = class {
1137
1149
  try {
1138
1150
  const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
1139
1151
  const queryParams = schema ? [schema, procedureName] : [procedureName];
1140
- const [rows] = await this.pool.query(`
1152
+ const [rows] = await this.pool.query(
1153
+ `
1141
1154
  SELECT
1142
1155
  r.ROUTINE_NAME AS procedure_name,
1143
1156
  CASE
@@ -1161,7 +1174,9 @@ var MySQLConnector = class {
1161
1174
  FROM INFORMATION_SCHEMA.ROUTINES r
1162
1175
  ${schemaClause}
1163
1176
  AND r.ROUTINE_NAME = ?
1164
- `, queryParams);
1177
+ `,
1178
+ queryParams
1179
+ );
1165
1180
  if (rows.length === 0) {
1166
1181
  const schemaName = schema || "current schema";
1167
1182
  throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
@@ -1194,11 +1209,14 @@ var MySQLConnector = class {
1194
1209
  }
1195
1210
  }
1196
1211
  if (!definition) {
1197
- const [bodyRows] = await this.pool.query(`
1212
+ const [bodyRows] = await this.pool.query(
1213
+ `
1198
1214
  SELECT ROUTINE_DEFINITION, ROUTINE_BODY
1199
1215
  FROM INFORMATION_SCHEMA.ROUTINES
1200
1216
  WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
1201
- `, [schemaValue, procedureName]);
1217
+ `,
1218
+ [schemaValue, procedureName]
1219
+ );
1202
1220
  if (bodyRows && bodyRows.length > 0) {
1203
1221
  if (bodyRows[0].ROUTINE_DEFINITION) {
1204
1222
  definition = bodyRows[0].ROUTINE_DEFINITION;
@@ -1233,10 +1251,6 @@ var MySQLConnector = class {
1233
1251
  if (!this.pool) {
1234
1252
  throw new Error("Not connected to database");
1235
1253
  }
1236
- const safetyCheck = this.validateQuery(query);
1237
- if (!safetyCheck.isValid) {
1238
- throw new Error(safetyCheck.message || "Query validation failed");
1239
- }
1240
1254
  try {
1241
1255
  const [rows, fields] = await this.pool.query(query);
1242
1256
  return { rows, fields };
@@ -1245,16 +1259,6 @@ var MySQLConnector = class {
1245
1259
  throw error;
1246
1260
  }
1247
1261
  }
1248
- validateQuery(query) {
1249
- const normalizedQuery = query.trim().toLowerCase();
1250
- if (!normalizedQuery.startsWith("select")) {
1251
- return {
1252
- isValid: false,
1253
- message: "Only SELECT queries are allowed for security reasons."
1254
- };
1255
- }
1256
- return { isValid: true };
1257
- }
1258
1262
  };
1259
1263
  var mysqlConnector = new MySQLConnector();
1260
1264
  ConnectorRegistry.register(mysqlConnector);
@@ -1282,7 +1286,9 @@ var MariadbDSNParser = class {
1282
1286
  });
1283
1287
  return config;
1284
1288
  } catch (error) {
1285
- throw new Error(`Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`);
1289
+ throw new Error(
1290
+ `Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
1291
+ );
1286
1292
  }
1287
1293
  }
1288
1294
  getSampleDSN() {
@@ -1345,12 +1351,15 @@ var MariaDBConnector = class {
1345
1351
  try {
1346
1352
  const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1347
1353
  const queryParams = schema ? [schema] : [];
1348
- const [rows] = await this.pool.query(`
1354
+ const [rows] = await this.pool.query(
1355
+ `
1349
1356
  SELECT TABLE_NAME
1350
1357
  FROM INFORMATION_SCHEMA.TABLES
1351
1358
  ${schemaClause}
1352
1359
  ORDER BY TABLE_NAME
1353
- `, queryParams);
1360
+ `,
1361
+ queryParams
1362
+ );
1354
1363
  return rows.map((row) => row.TABLE_NAME);
1355
1364
  } catch (error) {
1356
1365
  console.error("Error getting tables:", error);
@@ -1364,12 +1373,15 @@ var MariaDBConnector = class {
1364
1373
  try {
1365
1374
  const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1366
1375
  const queryParams = schema ? [schema, tableName] : [tableName];
1367
- const [rows] = await this.pool.query(`
1376
+ const [rows] = await this.pool.query(
1377
+ `
1368
1378
  SELECT COUNT(*) AS COUNT
1369
1379
  FROM INFORMATION_SCHEMA.TABLES
1370
1380
  ${schemaClause}
1371
1381
  AND TABLE_NAME = ?
1372
- `, queryParams);
1382
+ `,
1383
+ queryParams
1384
+ );
1373
1385
  return rows[0].COUNT > 0;
1374
1386
  } catch (error) {
1375
1387
  console.error("Error checking if table exists:", error);
@@ -1383,7 +1395,8 @@ var MariaDBConnector = class {
1383
1395
  try {
1384
1396
  const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
1385
1397
  const queryParams = schema ? [schema, tableName] : [tableName];
1386
- const [indexRows] = await this.pool.query(`
1398
+ const [indexRows] = await this.pool.query(
1399
+ `
1387
1400
  SELECT
1388
1401
  INDEX_NAME,
1389
1402
  COLUMN_NAME,
@@ -1397,7 +1410,9 @@ var MariaDBConnector = class {
1397
1410
  ORDER BY
1398
1411
  INDEX_NAME,
1399
1412
  SEQ_IN_INDEX
1400
- `, queryParams);
1413
+ `,
1414
+ queryParams
1415
+ );
1401
1416
  const indexMap = /* @__PURE__ */ new Map();
1402
1417
  for (const row of indexRows) {
1403
1418
  const indexName = row.INDEX_NAME;
@@ -1436,7 +1451,8 @@ var MariaDBConnector = class {
1436
1451
  try {
1437
1452
  const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1438
1453
  const queryParams = schema ? [schema, tableName] : [tableName];
1439
- const [rows] = await this.pool.query(`
1454
+ const [rows] = await this.pool.query(
1455
+ `
1440
1456
  SELECT
1441
1457
  COLUMN_NAME as column_name,
1442
1458
  DATA_TYPE as data_type,
@@ -1446,7 +1462,9 @@ var MariaDBConnector = class {
1446
1462
  ${schemaClause}
1447
1463
  AND TABLE_NAME = ?
1448
1464
  ORDER BY ORDINAL_POSITION
1449
- `, queryParams);
1465
+ `,
1466
+ queryParams
1467
+ );
1450
1468
  return rows;
1451
1469
  } catch (error) {
1452
1470
  console.error("Error getting table schema:", error);
@@ -1460,12 +1478,15 @@ var MariaDBConnector = class {
1460
1478
  try {
1461
1479
  const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
1462
1480
  const queryParams = schema ? [schema] : [];
1463
- const [rows] = await this.pool.query(`
1481
+ const [rows] = await this.pool.query(
1482
+ `
1464
1483
  SELECT ROUTINE_NAME
1465
1484
  FROM INFORMATION_SCHEMA.ROUTINES
1466
1485
  ${schemaClause}
1467
1486
  ORDER BY ROUTINE_NAME
1468
- `, queryParams);
1487
+ `,
1488
+ queryParams
1489
+ );
1469
1490
  return rows.map((row) => row.ROUTINE_NAME);
1470
1491
  } catch (error) {
1471
1492
  console.error("Error getting stored procedures:", error);
@@ -1479,7 +1500,8 @@ var MariaDBConnector = class {
1479
1500
  try {
1480
1501
  const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
1481
1502
  const queryParams = schema ? [schema, procedureName] : [procedureName];
1482
- const [rows] = await this.pool.query(`
1503
+ const [rows] = await this.pool.query(
1504
+ `
1483
1505
  SELECT
1484
1506
  r.ROUTINE_NAME AS procedure_name,
1485
1507
  CASE
@@ -1503,7 +1525,9 @@ var MariaDBConnector = class {
1503
1525
  FROM INFORMATION_SCHEMA.ROUTINES r
1504
1526
  ${schemaClause}
1505
1527
  AND r.ROUTINE_NAME = ?
1506
- `, queryParams);
1528
+ `,
1529
+ queryParams
1530
+ );
1507
1531
  if (rows.length === 0) {
1508
1532
  const schemaName = schema || "current schema";
1509
1533
  throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
@@ -1536,11 +1560,14 @@ var MariaDBConnector = class {
1536
1560
  }
1537
1561
  }
1538
1562
  if (!definition) {
1539
- const [bodyRows] = await this.pool.query(`
1563
+ const [bodyRows] = await this.pool.query(
1564
+ `
1540
1565
  SELECT ROUTINE_DEFINITION, ROUTINE_BODY
1541
1566
  FROM INFORMATION_SCHEMA.ROUTINES
1542
1567
  WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
1543
- `, [schemaValue, procedureName]);
1568
+ `,
1569
+ [schemaValue, procedureName]
1570
+ );
1544
1571
  if (bodyRows && bodyRows.length > 0) {
1545
1572
  if (bodyRows[0].ROUTINE_DEFINITION) {
1546
1573
  definition = bodyRows[0].ROUTINE_DEFINITION;
@@ -1575,10 +1602,6 @@ var MariaDBConnector = class {
1575
1602
  if (!this.pool) {
1576
1603
  throw new Error("Not connected to database");
1577
1604
  }
1578
- const safetyCheck = this.validateQuery(query);
1579
- if (!safetyCheck.isValid) {
1580
- throw new Error(safetyCheck.message || "Query validation failed");
1581
- }
1582
1605
  try {
1583
1606
  const [rows, fields] = await this.pool.query(query);
1584
1607
  return { rows, fields };
@@ -1587,16 +1610,6 @@ var MariaDBConnector = class {
1587
1610
  throw error;
1588
1611
  }
1589
1612
  }
1590
- validateQuery(query) {
1591
- const normalizedQuery = query.trim().toLowerCase();
1592
- if (!normalizedQuery.startsWith("select")) {
1593
- return {
1594
- isValid: false,
1595
- message: "Only SELECT queries are allowed for security reasons."
1596
- };
1597
- }
1598
- return { isValid: true };
1599
- }
1600
1613
  };
1601
1614
  var mariadbConnector = new MariaDBConnector();
1602
1615
  ConnectorRegistry.register(mariadbConnector);
@@ -1746,6 +1759,16 @@ function isDemoMode() {
1746
1759
  const args = parseCommandLineArgs();
1747
1760
  return args.demo === "true";
1748
1761
  }
1762
+ function isReadOnlyMode() {
1763
+ const args = parseCommandLineArgs();
1764
+ if (args.readonly !== void 0) {
1765
+ return args.readonly === "true";
1766
+ }
1767
+ if (process.env.READONLY !== void 0) {
1768
+ return process.env.READONLY === "true";
1769
+ }
1770
+ return false;
1771
+ }
1749
1772
  function resolveDSN() {
1750
1773
  const args = parseCommandLineArgs();
1751
1774
  if (isDemoMode()) {
@@ -1863,39 +1886,47 @@ function formatErrorResponse(error, code = "ERROR", details) {
1863
1886
  }
1864
1887
  function createToolErrorResponse(error, code = "ERROR", details) {
1865
1888
  return {
1866
- content: [{
1867
- type: "text",
1868
- text: JSON.stringify(formatErrorResponse(error, code, details), null, 2),
1869
- mimeType: "application/json"
1870
- }],
1889
+ content: [
1890
+ {
1891
+ type: "text",
1892
+ text: JSON.stringify(formatErrorResponse(error, code, details), null, 2),
1893
+ mimeType: "application/json"
1894
+ }
1895
+ ],
1871
1896
  isError: true
1872
1897
  };
1873
1898
  }
1874
1899
  function createToolSuccessResponse(data, meta = {}) {
1875
1900
  return {
1876
- content: [{
1877
- type: "text",
1878
- text: JSON.stringify(formatSuccessResponse(data, meta), null, 2),
1879
- mimeType: "application/json"
1880
- }]
1901
+ content: [
1902
+ {
1903
+ type: "text",
1904
+ text: JSON.stringify(formatSuccessResponse(data, meta), null, 2),
1905
+ mimeType: "application/json"
1906
+ }
1907
+ ]
1881
1908
  };
1882
1909
  }
1883
1910
  function createResourceErrorResponse(uri, error, code = "ERROR", details) {
1884
1911
  return {
1885
- contents: [{
1886
- uri,
1887
- text: JSON.stringify(formatErrorResponse(error, code, details), null, 2),
1888
- mimeType: "application/json"
1889
- }]
1912
+ contents: [
1913
+ {
1914
+ uri,
1915
+ text: JSON.stringify(formatErrorResponse(error, code, details), null, 2),
1916
+ mimeType: "application/json"
1917
+ }
1918
+ ]
1890
1919
  };
1891
1920
  }
1892
1921
  function createResourceSuccessResponse(uri, data, meta = {}) {
1893
1922
  return {
1894
- contents: [{
1895
- uri,
1896
- text: JSON.stringify(formatSuccessResponse(data, meta), null, 2),
1897
- mimeType: "application/json"
1898
- }]
1923
+ contents: [
1924
+ {
1925
+ uri,
1926
+ text: JSON.stringify(formatSuccessResponse(data, meta), null, 2),
1927
+ mimeType: "application/json"
1928
+ }
1929
+ ]
1899
1930
  };
1900
1931
  }
1901
1932
  function formatPromptSuccessResponse(text, references = []) {
@@ -2032,11 +2063,7 @@ async function indexesResourceHandler(uri, variables, _extra) {
2032
2063
  const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
2033
2064
  const tableName = variables && variables.tableName ? Array.isArray(variables.tableName) ? variables.tableName[0] : variables.tableName : void 0;
2034
2065
  if (!tableName) {
2035
- return createResourceErrorResponse(
2036
- uri.href,
2037
- "Table name is required",
2038
- "MISSING_TABLE_NAME"
2039
- );
2066
+ return createResourceErrorResponse(uri.href, "Table name is required", "MISSING_TABLE_NAME");
2040
2067
  }
2041
2068
  try {
2042
2069
  if (schemaName) {
@@ -2109,11 +2136,7 @@ async function procedureDetailResourceHandler(uri, variables, _extra) {
2109
2136
  const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
2110
2137
  const procedureName = variables && variables.procedureName ? Array.isArray(variables.procedureName) ? variables.procedureName[0] : variables.procedureName : void 0;
2111
2138
  if (!procedureName) {
2112
- return createResourceErrorResponse(
2113
- uri.href,
2114
- "Procedure name is required",
2115
- "MISSING_PARAMETER"
2116
- );
2139
+ return createResourceErrorResponse(uri.href, "Procedure name is required", "MISSING_PARAMETER");
2117
2140
  }
2118
2141
  try {
2119
2142
  if (schemaName) {
@@ -2148,11 +2171,7 @@ async function procedureDetailResourceHandler(uri, variables, _extra) {
2148
2171
 
2149
2172
  // src/resources/index.ts
2150
2173
  function registerResources(server) {
2151
- server.resource(
2152
- "schemas",
2153
- "db://schemas",
2154
- schemasResourceHandler
2155
- );
2174
+ server.resource("schemas", "db://schemas", schemasResourceHandler);
2156
2175
  server.resource(
2157
2176
  "tables_in_schema",
2158
2177
  new ResourceTemplate("db://schemas/{schemaName}/tables", { list: void 0 }),
@@ -2165,7 +2184,9 @@ function registerResources(server) {
2165
2184
  );
2166
2185
  server.resource(
2167
2186
  "indexes_in_table",
2168
- new ResourceTemplate("db://schemas/{schemaName}/tables/{tableName}/indexes", { list: void 0 }),
2187
+ new ResourceTemplate("db://schemas/{schemaName}/tables/{tableName}/indexes", {
2188
+ list: void 0
2189
+ }),
2169
2190
  indexesResourceHandler
2170
2191
  );
2171
2192
  server.resource(
@@ -2175,24 +2196,42 @@ function registerResources(server) {
2175
2196
  );
2176
2197
  server.resource(
2177
2198
  "procedure_detail_in_schema",
2178
- new ResourceTemplate("db://schemas/{schemaName}/procedures/{procedureName}", { list: void 0 }),
2199
+ new ResourceTemplate("db://schemas/{schemaName}/procedures/{procedureName}", {
2200
+ list: void 0
2201
+ }),
2179
2202
  procedureDetailResourceHandler
2180
2203
  );
2181
2204
  }
2182
2205
 
2183
- // src/tools/run-query.ts
2206
+ // src/tools/execute-sql.ts
2184
2207
  import { z } from "zod";
2185
- var runQuerySchema = {
2208
+
2209
+ // src/utils/allowed-keywords.ts
2210
+ var allowedKeywords = {
2211
+ postgres: ["select", "with", "explain", "analyze", "show"],
2212
+ mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2213
+ mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2214
+ sqlite: ["select", "with", "explain", "analyze", "pragma"],
2215
+ sqlserver: ["select", "with", "explain", "showplan"]
2216
+ };
2217
+
2218
+ // src/tools/execute-sql.ts
2219
+ var executeSqlSchema = {
2186
2220
  query: z.string().describe("SQL query to execute (SELECT only)")
2187
2221
  };
2188
- async function runQueryToolHandler({ query }, _extra) {
2222
+ function isReadOnlyQuery(query, connectorType) {
2223
+ const normalizedQuery = query.trim().toLowerCase();
2224
+ const firstWord = normalizedQuery.split(/\s+/)[0];
2225
+ const keywordList = allowedKeywords[connectorType] || allowedKeywords.default || [];
2226
+ return keywordList.includes(firstWord);
2227
+ }
2228
+ async function executeSqlToolHandler({ query }, _extra) {
2189
2229
  const connector = ConnectorManager.getCurrentConnector();
2190
2230
  try {
2191
- const validationResult = connector.validateQuery(query);
2192
- if (!validationResult.isValid) {
2231
+ if (isReadOnlyMode() && !isReadOnlyQuery(query, connector.id)) {
2193
2232
  return createToolErrorResponse(
2194
- validationResult.message ?? "Unknown validation error",
2195
- "VALIDATION_ERROR"
2233
+ `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`,
2234
+ "READONLY_VIOLATION"
2196
2235
  );
2197
2236
  }
2198
2237
  const result = await connector.executeQuery(query);
@@ -2202,36 +2241,34 @@ async function runQueryToolHandler({ query }, _extra) {
2202
2241
  };
2203
2242
  return createToolSuccessResponse(responseData);
2204
2243
  } catch (error) {
2205
- return createToolErrorResponse(
2206
- error.message,
2207
- "EXECUTION_ERROR"
2208
- );
2244
+ return createToolErrorResponse(error.message, "EXECUTION_ERROR");
2209
2245
  }
2210
2246
  }
2211
2247
 
2212
2248
  // src/tools/list-connectors.ts
2213
2249
  async function listConnectorsToolHandler(_args, _extra) {
2214
2250
  const samples = ConnectorRegistry.getAllSampleDSNs();
2215
- let activeConnectorId = null;
2251
+ let activeConnectorType = null;
2216
2252
  try {
2217
2253
  const activeConnector = ConnectorManager.getCurrentConnector();
2218
- activeConnectorId = activeConnector.id;
2254
+ activeConnectorType = activeConnector.id;
2219
2255
  } catch (error) {
2220
2256
  }
2221
2257
  const isDemo = isDemoMode();
2222
- if (isDemo && !activeConnectorId) {
2223
- activeConnectorId = "sqlite";
2258
+ if (isDemo && !activeConnectorType) {
2259
+ activeConnectorType = "sqlite";
2224
2260
  }
2225
2261
  const sampleObjects = Object.entries(samples).map(([id, dsn]) => ({
2226
2262
  id,
2227
2263
  dsn,
2228
- active: id === activeConnectorId
2264
+ active: id === activeConnectorType
2229
2265
  }));
2230
2266
  const responseData = {
2231
2267
  connectors: sampleObjects,
2232
2268
  count: sampleObjects.length,
2233
- activeConnector: activeConnectorId,
2234
- demoMode: isDemo
2269
+ activeConnector: activeConnectorType,
2270
+ demoMode: isDemo,
2271
+ readonlyMode: isReadOnlyMode()
2235
2272
  };
2236
2273
  return createToolSuccessResponse(responseData);
2237
2274
  }
@@ -2239,10 +2276,10 @@ async function listConnectorsToolHandler(_args, _extra) {
2239
2276
  // src/tools/index.ts
2240
2277
  function registerTools(server) {
2241
2278
  server.tool(
2242
- "run_query",
2243
- "Run a SQL query on the current database",
2244
- runQuerySchema,
2245
- runQueryToolHandler
2279
+ "execute_sql",
2280
+ "Execute a SQL query on the current database",
2281
+ executeSqlSchema,
2282
+ executeSqlToolHandler
2246
2283
  );
2247
2284
  server.tool(
2248
2285
  "list_connectors",
@@ -2258,7 +2295,10 @@ var sqlGeneratorSchema = {
2258
2295
  description: z2.string().describe("Natural language description of the SQL query to generate"),
2259
2296
  schema: z2.string().optional().describe("Optional database schema to use")
2260
2297
  };
2261
- async function sqlGeneratorPromptHandler({ description, schema }, _extra) {
2298
+ async function sqlGeneratorPromptHandler({
2299
+ description,
2300
+ schema
2301
+ }, _extra) {
2262
2302
  try {
2263
2303
  const connector = ConnectorManager.getCurrentConnector();
2264
2304
  let sqlDialect;
@@ -2321,9 +2361,7 @@ async function sqlGeneratorPromptHandler({ description, schema }, _extra) {
2321
2361
  }
2322
2362
  const schemaContext = accessibleSchemas.length > 0 ? `Available tables and their columns:
2323
2363
  ${accessibleSchemas.map(
2324
- (schema2) => `- ${schema2.table}: ${schema2.columns.map(
2325
- (col) => `${col.name} (${col.type})`
2326
- ).join(", ")}`
2364
+ (schema2) => `- ${schema2.table}: ${schema2.columns.map((col) => `${col.name} (${col.type})`).join(", ")}`
2327
2365
  ).join("\n")}` : "No schema information available.";
2328
2366
  const dialectExamples = {
2329
2367
  postgres: [
@@ -2375,7 +2413,11 @@ SELECT COUNT(*) AS count
2375
2413
  FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name"};`;
2376
2414
  } else if (description.toLowerCase().includes("average") || description.toLowerCase().includes("avg")) {
2377
2415
  const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name";
2378
- const numericColumn = accessibleSchemas.length > 0 ? accessibleSchemas[0].columns.find((col) => ["int", "numeric", "decimal", "float", "real", "double"].some((t) => col.type.includes(t)))?.name || "numeric_column" : "numeric_column";
2416
+ const numericColumn = accessibleSchemas.length > 0 ? accessibleSchemas[0].columns.find(
2417
+ (col) => ["int", "numeric", "decimal", "float", "real", "double"].some(
2418
+ (t) => col.type.includes(t)
2419
+ )
2420
+ )?.name || "numeric_column" : "numeric_column";
2379
2421
  const schemaPrefix = schema ? `-- Schema: ${schema}
2380
2422
  ` : "";
2381
2423
  generatedSQL = `${schemaPrefix}-- Average query generated from: "${description}"
@@ -2423,7 +2465,10 @@ var dbExplainerSchema = {
2423
2465
  schema: z3.string().optional().describe("Optional database schema to use"),
2424
2466
  table: z3.string().optional().describe("Optional specific table to explain")
2425
2467
  };
2426
- async function dbExplainerPromptHandler({ schema, table }, _extra) {
2468
+ async function dbExplainerPromptHandler({
2469
+ schema,
2470
+ table
2471
+ }, _extra) {
2427
2472
  try {
2428
2473
  const connector = ConnectorManager.getCurrentConnector();
2429
2474
  if (schema) {
@@ -2477,7 +2522,9 @@ ${determineRelationships(matchingTable, columns)}`;
2477
2522
  }
2478
2523
  try {
2479
2524
  const columns = await connector.getTableSchema(tableName, schema);
2480
- const column = columns.find((c) => c.column_name.toLowerCase() === columnName.toLowerCase());
2525
+ const column = columns.find(
2526
+ (c) => c.column_name.toLowerCase() === columnName.toLowerCase()
2527
+ );
2481
2528
  if (column) {
2482
2529
  const columnDescription = `Column: ${tableName}.${column.column_name}
2483
2530
 
@@ -2568,11 +2615,15 @@ function determineRelationships(tableName, columns) {
2568
2615
  if (idColumns.length > 0) {
2569
2616
  idColumns.forEach((col) => {
2570
2617
  const referencedTable = col.column_name.toLowerCase().replace("_id", "");
2571
- potentialRelationships.push(`May have a relationship with the "${referencedTable}" table (via ${col.column_name})`);
2618
+ potentialRelationships.push(
2619
+ `May have a relationship with the "${referencedTable}" table (via ${col.column_name})`
2620
+ );
2572
2621
  });
2573
2622
  }
2574
2623
  if (columns.some((c) => c.column_name.toLowerCase() === "id")) {
2575
- potentialRelationships.push(`May be referenced by other tables as "${tableName.toLowerCase()}_id"`);
2624
+ potentialRelationships.push(
2625
+ `May be referenced by other tables as "${tableName.toLowerCase()}_id"`
2626
+ );
2576
2627
  }
2577
2628
  return potentialRelationships.length > 0 ? potentialRelationships.join("\n") : "No obvious relationships identified based on column names";
2578
2629
  }
@@ -2660,8 +2711,8 @@ var packageJsonPath = path3.join(__dirname3, "..", "package.json");
2660
2711
  var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
2661
2712
  var SERVER_NAME = "DBHub MCP Server";
2662
2713
  var SERVER_VERSION = packageJson.version;
2663
- function generateBanner(version, isDemo = false) {
2664
- const demoText = isDemo ? " [DEMO MODE]" : "";
2714
+ function generateBanner(version, modes = []) {
2715
+ const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
2665
2716
  return `
2666
2717
  _____ ____ _ _ _
2667
2718
  | __ \\| _ \\| | | | | |
@@ -2670,7 +2721,7 @@ function generateBanner(version, isDemo = false) {
2670
2721
  | |__| | |_) | | | | |_| | |_) |
2671
2722
  |_____/|____/|_| |_|\\__,_|_.__/
2672
2723
 
2673
- v${version}${demoText} - Universal Database MCP Server
2724
+ v${version}${modeText} - Universal Database MCP Server
2674
2725
  `;
2675
2726
  }
2676
2727
  async function main() {
@@ -2706,7 +2757,6 @@ See documentation for more details on configuring database connections.
2706
2757
  console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
2707
2758
  console.error(`DSN source: ${dsnData.source}`);
2708
2759
  if (dsnData.isDemo) {
2709
- console.error("Running in demo mode with sample employee database");
2710
2760
  const initScript = getSqliteInMemorySetupSql();
2711
2761
  await connectorManager.connectWithDSN(dsnData.dsn, initScript);
2712
2762
  } else {
@@ -2715,7 +2765,21 @@ See documentation for more details on configuring database connections.
2715
2765
  const transportData = resolveTransport();
2716
2766
  console.error(`Using transport: ${transportData.type}`);
2717
2767
  console.error(`Transport source: ${transportData.source}`);
2718
- console.error(generateBanner(SERVER_VERSION, dsnData.isDemo));
2768
+ const readonly = isReadOnlyMode();
2769
+ const activeModes = [];
2770
+ const modeDescriptions = [];
2771
+ if (dsnData.isDemo) {
2772
+ activeModes.push("DEMO");
2773
+ modeDescriptions.push("using sample employee database");
2774
+ }
2775
+ if (readonly) {
2776
+ activeModes.push("READ-ONLY");
2777
+ modeDescriptions.push("only read only queries allowed");
2778
+ }
2779
+ if (activeModes.length > 0) {
2780
+ console.error(`Running in ${activeModes.join(" and ")} mode - ${modeDescriptions.join(", ")}`);
2781
+ }
2782
+ console.error(generateBanner(SERVER_VERSION, activeModes));
2719
2783
  if (transportData.type === "sse") {
2720
2784
  const app = express();
2721
2785
  let transport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Universal Database MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -21,8 +21,8 @@
21
21
  "better-sqlite3": "^11.9.0",
22
22
  "dotenv": "^16.4.7",
23
23
  "express": "^4.18.2",
24
- "mssql": "^11.0.1",
25
24
  "mariadb": "^3.4.0",
25
+ "mssql": "^11.0.1",
26
26
  "mysql2": "^3.13.0",
27
27
  "pg": "^8.13.3",
28
28
  "zod": "^3.24.2"
@@ -34,6 +34,7 @@
34
34
  "@types/node": "^22.13.10",
35
35
  "@types/pg": "^8.11.11",
36
36
  "cross-env": "^7.0.3",
37
+ "prettier": "^3.5.3",
37
38
  "ts-node": "^10.9.2",
38
39
  "tsup": "^8.4.0",
39
40
  "tsx": "^4.19.3",