@bytebase/dbhub 0.9.0 → 0.10.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/README.md CHANGED
@@ -24,6 +24,8 @@ DBHub is a universal database gateway implementing the Model Context Protocol (M
24
24
  | | | | | |
25
25
  | Cursor +--->+ DBHub +--->+ SQL Server |
26
26
  | | | | | |
27
+ | Other Clients +--->+ +--->+ SQLite |
28
+ | | | | | |
27
29
  | | | +--->+ MySQL |
28
30
  | | | | | |
29
31
  | | | +--->+ MariaDB |
@@ -42,27 +44,27 @@ https://demo.dbhub.ai/message connects a [sample employee database](https://gith
42
44
 
43
45
  ### Database Resources
44
46
 
45
- | Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server |
46
- | --------------------------- | ------------------------------------------------------ | :--------: | :---: | :-----: | :--------: |
47
- | schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ |
48
- | tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ |
49
- | table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ |
50
- | indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ |
51
- | procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ✅ |
52
- | procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ✅ |
47
+ | Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
48
+ | --------------------------- | ------------------------------------------------------ | :--------: | :---: | :-----: | :--------: | :----: |
49
+ | schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ | ✅ |
50
+ | tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ | ✅ |
51
+ | table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ | ✅ |
52
+ | indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ | ✅ |
53
+ | procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ✅ | ❌ |
54
+ | procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ✅ | ❌ |
53
55
 
54
56
  ### Database Tools
55
57
 
56
- | Tool | Command Name | Description | PostgreSQL | MySQL | MariaDB | SQL Server |
57
- | ----------- | ------------- | ------------------------------------------------------------------- | :--------: | :---: | :-----: | :--------: |
58
- | Execute SQL | `execute_sql` | Execute single or multiple SQL statements (separated by semicolons) | ✅ | ✅ | ✅ | ✅ |
58
+ | Tool | Command Name | Description | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
59
+ | ----------- | ------------- | ------------------------------------------------------------------- | :--------: | :---: | :-----: | :--------: | ------ |
60
+ | Execute SQL | `execute_sql` | Execute single or multiple SQL statements (separated by semicolons) | ✅ | ✅ | ✅ | ✅ | ✅ |
59
61
 
60
62
  ### Prompt Capabilities
61
63
 
62
- | Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server |
63
- | ------------------- | -------------- | :--------: | :---: | :-----: | :--------: |
64
- | Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ |
65
- | Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ |
64
+ | Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
65
+ | ------------------- | -------------- | :--------: | :---: | :-----: | :--------: | ------ |
66
+ | Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ | ✅ |
67
+ | Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ | ✅ |
66
68
 
67
69
  ## Installation
68
70
 
@@ -79,6 +81,16 @@ docker run --rm --init \
79
81
  --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
80
82
  ```
81
83
 
84
+ ```bash
85
+ # Demo mode with sample employee database
86
+ docker run --rm --init \
87
+ --name dbhub \
88
+ --publish 8080:8080 \
89
+ bytebase/dbhub \
90
+ --transport http \
91
+ --port 8080 \
92
+ --demo
93
+ ```
82
94
 
83
95
 
84
96
  ### NPM
@@ -88,6 +100,12 @@ docker run --rm --init \
88
100
  npx @bytebase/dbhub --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
89
101
  ```
90
102
 
103
+ ```bash
104
+ # Demo mode with sample employee database
105
+ npx @bytebase/dbhub --transport http --port 8080 --demo
106
+ ```
107
+
108
+ > Note: The demo mode includes a bundled SQLite sample "employee" database with tables for employees, departments, salaries, and more.
91
109
 
92
110
  ### Claude Desktop
93
111
 
@@ -124,6 +142,10 @@ npx @bytebase/dbhub --transport http --port 8080 --dsn "postgres://user:password
124
142
  "postgres://user:password@localhost:5432/dbname?sslmode=disable"
125
143
  ]
126
144
  },
145
+ "dbhub-demo": {
146
+ "command": "npx",
147
+ "args": ["-y", "@bytebase/dbhub", "--transport", "stdio", "--demo"]
148
+ }
127
149
  }
128
150
  }
129
151
  ```
@@ -149,6 +171,7 @@ You can specify the SSL mode using the `sslmode` parameter in your DSN string:
149
171
  | MySQL | ✅ | ✅ | Certificate verification |
150
172
  | MariaDB | ✅ | ✅ | Certificate verification |
151
173
  | SQL Server | ✅ | ✅ | Certificate verification |
174
+ | SQLite | ❌ | ❌ | N/A (file-based) |
152
175
 
153
176
  **SSL Mode Options:**
154
177
 
@@ -185,6 +208,12 @@ This provides an additional layer of security when connecting to production data
185
208
 
186
209
  ### Configure your database connection
187
210
 
211
+ You can use DBHub in demo mode with a sample employee database for testing:
212
+
213
+ ```bash
214
+ npx @bytebase/dbhub --demo
215
+ ```
216
+
188
217
  > [!WARNING]
189
218
  > If your user/password contains special characters, you need to escape them first. (e.g. `pass#word` should be escaped as `pass%23word`)
190
219
 
@@ -221,6 +250,7 @@ DBHub supports the following database connection string formats:
221
250
  | MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname?sslmode=disable` |
222
251
  | PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
223
252
  | SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname?sslmode=disable` |
253
+ | SQLite | `sqlite:///[path/to/file]` or `sqlite:///:memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite:///:memory:` |
224
254
 
225
255
 
226
256
  #### SQL Server
@@ -246,13 +276,15 @@ Extra query parameters:
246
276
 
247
277
  ### Command line options
248
278
 
249
- | Option | Environment Variable | Description | Default |
250
- | --------- | -------------------- | ---------------------------------------------------------------- | -------- |
251
- | dsn | `DSN` | Database connection string | Required |
252
- | transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
253
- | port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
254
- | readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
279
+ | Option | Environment Variable | Description | Default |
280
+ | --------- | -------------------- | ---------------------------------------------------------------- | ---------------------------- |
281
+ | dsn | `DSN` | Database connection string | Required if not in demo mode |
282
+ | transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
283
+ | port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
284
+ | readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
285
+ | demo | N/A | Run in demo mode with sample employee database | `false` |
255
286
 
287
+ 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.
256
288
 
257
289
  ## Development
258
290
 
@@ -310,6 +342,8 @@ pnpm test src/connectors/__tests__/mysql.integration.test.ts
310
342
  pnpm test src/connectors/__tests__/mariadb.integration.test.ts
311
343
  # Run only SQL Server integration tests
312
344
  pnpm test src/connectors/__tests__/sqlserver.integration.test.ts
345
+ # Run only SQLite integration tests
346
+ pnpm test src/connectors/__tests__/sqlite.integration.test.ts
313
347
  # Run JSON RPC integration tests
314
348
  pnpm test src/__tests__/json-rpc-integration.test.ts
315
349
  ```
package/dist/index.js CHANGED
@@ -159,6 +159,9 @@ function obfuscateDSNPassword(dsn) {
159
159
  return dsn;
160
160
  }
161
161
  const protocol = protocolMatch[1];
162
+ if (protocol === "sqlite") {
163
+ return dsn;
164
+ }
162
165
  const protocolPart = dsn.split("://")[1];
163
166
  if (!protocolPart) {
164
167
  return dsn;
@@ -876,6 +879,246 @@ var SQLServerConnector = class {
876
879
  var sqlServerConnector = new SQLServerConnector();
877
880
  ConnectorRegistry.register(sqlServerConnector);
878
881
 
882
+ // src/connectors/sqlite/index.ts
883
+ import Database from "better-sqlite3";
884
+ var SQLiteDSNParser = class {
885
+ async parse(dsn) {
886
+ if (!this.isValidDSN(dsn)) {
887
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
888
+ const expectedFormat = this.getSampleDSN();
889
+ throw new Error(
890
+ `Invalid SQLite DSN format.
891
+ Provided: ${obfuscatedDSN}
892
+ Expected: ${expectedFormat}`
893
+ );
894
+ }
895
+ try {
896
+ const url = new SafeURL(dsn);
897
+ let dbPath;
898
+ if (url.hostname === "" && url.pathname === "/:memory:") {
899
+ dbPath = ":memory:";
900
+ } else {
901
+ if (url.pathname.startsWith("//")) {
902
+ dbPath = url.pathname.substring(2);
903
+ } else {
904
+ dbPath = url.pathname;
905
+ }
906
+ }
907
+ return { dbPath };
908
+ } catch (error) {
909
+ throw new Error(
910
+ `Failed to parse SQLite DSN: ${error instanceof Error ? error.message : String(error)}`
911
+ );
912
+ }
913
+ }
914
+ getSampleDSN() {
915
+ return "sqlite:///path/to/database.db";
916
+ }
917
+ isValidDSN(dsn) {
918
+ try {
919
+ return dsn.startsWith("sqlite://");
920
+ } catch (error) {
921
+ return false;
922
+ }
923
+ }
924
+ };
925
+ var SQLiteConnector = class {
926
+ constructor() {
927
+ this.id = "sqlite";
928
+ this.name = "SQLite";
929
+ this.dsnParser = new SQLiteDSNParser();
930
+ this.db = null;
931
+ this.dbPath = ":memory:";
932
+ }
933
+ // Default to in-memory database
934
+ async connect(dsn, initScript) {
935
+ const config = await this.dsnParser.parse(dsn);
936
+ this.dbPath = config.dbPath;
937
+ try {
938
+ this.db = new Database(this.dbPath);
939
+ console.error("Successfully connected to SQLite database");
940
+ if (initScript) {
941
+ this.db.exec(initScript);
942
+ console.error("Successfully initialized database with script");
943
+ }
944
+ } catch (error) {
945
+ console.error("Failed to connect to SQLite database:", error);
946
+ throw error;
947
+ }
948
+ }
949
+ async disconnect() {
950
+ if (this.db) {
951
+ try {
952
+ this.db.close();
953
+ this.db = null;
954
+ } catch (error) {
955
+ throw error;
956
+ }
957
+ }
958
+ return Promise.resolve();
959
+ }
960
+ async getSchemas() {
961
+ if (!this.db) {
962
+ throw new Error("Not connected to SQLite database");
963
+ }
964
+ return ["main"];
965
+ }
966
+ async getTables(schema) {
967
+ if (!this.db) {
968
+ throw new Error("Not connected to SQLite database");
969
+ }
970
+ try {
971
+ const rows = this.db.prepare(
972
+ `
973
+ SELECT name FROM sqlite_master
974
+ WHERE type='table' AND name NOT LIKE 'sqlite_%'
975
+ ORDER BY name
976
+ `
977
+ ).all();
978
+ return rows.map((row) => row.name);
979
+ } catch (error) {
980
+ throw error;
981
+ }
982
+ }
983
+ async tableExists(tableName, schema) {
984
+ if (!this.db) {
985
+ throw new Error("Not connected to SQLite database");
986
+ }
987
+ try {
988
+ const row = this.db.prepare(
989
+ `
990
+ SELECT name FROM sqlite_master
991
+ WHERE type='table' AND name = ?
992
+ `
993
+ ).get(tableName);
994
+ return !!row;
995
+ } catch (error) {
996
+ throw error;
997
+ }
998
+ }
999
+ async getTableIndexes(tableName, schema) {
1000
+ if (!this.db) {
1001
+ throw new Error("Not connected to SQLite database");
1002
+ }
1003
+ try {
1004
+ const indexInfoRows = this.db.prepare(
1005
+ `
1006
+ SELECT
1007
+ name as index_name,
1008
+ 0 as is_unique
1009
+ FROM sqlite_master
1010
+ WHERE type = 'index'
1011
+ AND tbl_name = ?
1012
+ `
1013
+ ).all(tableName);
1014
+ const indexListRows = this.db.prepare(`PRAGMA index_list(${tableName})`).all();
1015
+ const indexUniqueMap = /* @__PURE__ */ new Map();
1016
+ for (const indexListRow of indexListRows) {
1017
+ indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
1018
+ }
1019
+ const tableInfo = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
1020
+ const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
1021
+ const results = [];
1022
+ for (const indexInfo of indexInfoRows) {
1023
+ const indexDetailRows = this.db.prepare(`PRAGMA index_info(${indexInfo.index_name})`).all();
1024
+ const columnNames = indexDetailRows.map((row) => row.name);
1025
+ results.push({
1026
+ index_name: indexInfo.index_name,
1027
+ column_names: columnNames,
1028
+ is_unique: indexUniqueMap.get(indexInfo.index_name) || false,
1029
+ is_primary: false
1030
+ });
1031
+ }
1032
+ if (pkColumns.length > 0) {
1033
+ results.push({
1034
+ index_name: "PRIMARY",
1035
+ column_names: pkColumns,
1036
+ is_unique: true,
1037
+ is_primary: true
1038
+ });
1039
+ }
1040
+ return results;
1041
+ } catch (error) {
1042
+ throw error;
1043
+ }
1044
+ }
1045
+ async getTableSchema(tableName, schema) {
1046
+ if (!this.db) {
1047
+ throw new Error("Not connected to SQLite database");
1048
+ }
1049
+ try {
1050
+ const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
1051
+ const columns = rows.map((row) => ({
1052
+ column_name: row.name,
1053
+ data_type: row.type,
1054
+ // In SQLite, primary key columns are automatically NOT NULL even if notnull=0
1055
+ is_nullable: row.notnull === 1 || row.pk > 0 ? "NO" : "YES",
1056
+ column_default: row.dflt_value
1057
+ }));
1058
+ return columns;
1059
+ } catch (error) {
1060
+ throw error;
1061
+ }
1062
+ }
1063
+ async getStoredProcedures(schema) {
1064
+ if (!this.db) {
1065
+ throw new Error("Not connected to SQLite database");
1066
+ }
1067
+ return [];
1068
+ }
1069
+ async getStoredProcedureDetail(procedureName, schema) {
1070
+ if (!this.db) {
1071
+ throw new Error("Not connected to SQLite database");
1072
+ }
1073
+ throw new Error(
1074
+ "SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
1075
+ );
1076
+ }
1077
+ async executeSQL(sql2) {
1078
+ if (!this.db) {
1079
+ throw new Error("Not connected to SQLite database");
1080
+ }
1081
+ try {
1082
+ const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
1083
+ if (statements.length === 1) {
1084
+ const trimmedStatement = statements[0].toLowerCase().trim();
1085
+ 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"));
1086
+ if (isReadStatement) {
1087
+ const rows = this.db.prepare(statements[0]).all();
1088
+ return { rows };
1089
+ } else {
1090
+ this.db.prepare(statements[0]).run();
1091
+ return { rows: [] };
1092
+ }
1093
+ } else {
1094
+ const readStatements = [];
1095
+ const writeStatements = [];
1096
+ for (const statement of statements) {
1097
+ const trimmedStatement = statement.toLowerCase().trim();
1098
+ 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"))) {
1099
+ readStatements.push(statement);
1100
+ } else {
1101
+ writeStatements.push(statement);
1102
+ }
1103
+ }
1104
+ if (writeStatements.length > 0) {
1105
+ this.db.exec(writeStatements.join("; "));
1106
+ }
1107
+ let allRows = [];
1108
+ for (const statement of readStatements) {
1109
+ const result = this.db.prepare(statement).all();
1110
+ allRows.push(...result);
1111
+ }
1112
+ return { rows: allRows };
1113
+ }
1114
+ } catch (error) {
1115
+ throw error;
1116
+ }
1117
+ }
1118
+ };
1119
+ var sqliteConnector = new SQLiteConnector();
1120
+ ConnectorRegistry.register(sqliteConnector);
1121
+
879
1122
  // src/connectors/mysql/index.ts
880
1123
  import mysql from "mysql2/promise";
881
1124
  var MySQLDSNParser = class {
@@ -1637,9 +1880,9 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
1637
1880
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1638
1881
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1639
1882
  import express from "express";
1640
- import path2 from "path";
1883
+ import path3 from "path";
1641
1884
  import { readFileSync } from "fs";
1642
- import { fileURLToPath as fileURLToPath2 } from "url";
1885
+ import { fileURLToPath as fileURLToPath3 } from "url";
1643
1886
 
1644
1887
  // src/connectors/manager.ts
1645
1888
  var managerInstance = null;
@@ -1773,6 +2016,10 @@ function loadEnvFiles() {
1773
2016
  }
1774
2017
  return null;
1775
2018
  }
2019
+ function isDemoMode() {
2020
+ const args = parseCommandLineArgs();
2021
+ return args.demo === "true";
2022
+ }
1776
2023
  function isReadOnlyMode() {
1777
2024
  const args = parseCommandLineArgs();
1778
2025
  if (args.readonly !== void 0) {
@@ -1785,6 +2032,13 @@ function isReadOnlyMode() {
1785
2032
  }
1786
2033
  function resolveDSN() {
1787
2034
  const args = parseCommandLineArgs();
2035
+ if (isDemoMode()) {
2036
+ return {
2037
+ dsn: "sqlite:///:memory:",
2038
+ source: "demo mode",
2039
+ isDemo: true
2040
+ };
2041
+ }
1788
2042
  if (args.dsn) {
1789
2043
  return { dsn: args.dsn, source: "command line argument" };
1790
2044
  }
@@ -1833,6 +2087,45 @@ function redactDSN(dsn) {
1833
2087
  }
1834
2088
  }
1835
2089
 
2090
+ // src/config/demo-loader.ts
2091
+ import fs2 from "fs";
2092
+ import path2 from "path";
2093
+ import { fileURLToPath as fileURLToPath2 } from "url";
2094
+ var __filename2 = fileURLToPath2(import.meta.url);
2095
+ var __dirname2 = path2.dirname(__filename2);
2096
+ var DEMO_DATA_DIR;
2097
+ var projectRootPath = path2.join(__dirname2, "..", "..", "..");
2098
+ var projectResourcesPath = path2.join(projectRootPath, "resources", "employee-sqlite");
2099
+ var distPath = path2.join(__dirname2, "resources", "employee-sqlite");
2100
+ if (fs2.existsSync(projectResourcesPath)) {
2101
+ DEMO_DATA_DIR = projectResourcesPath;
2102
+ } else if (fs2.existsSync(distPath)) {
2103
+ DEMO_DATA_DIR = distPath;
2104
+ } else {
2105
+ DEMO_DATA_DIR = path2.join(process.cwd(), "resources", "employee-sqlite");
2106
+ if (!fs2.existsSync(DEMO_DATA_DIR)) {
2107
+ throw new Error(`Could not find employee-sqlite resources in any of the expected locations:
2108
+ - ${projectResourcesPath}
2109
+ - ${distPath}
2110
+ - ${DEMO_DATA_DIR}`);
2111
+ }
2112
+ }
2113
+ function loadSqlFile(fileName) {
2114
+ const filePath = path2.join(DEMO_DATA_DIR, fileName);
2115
+ return fs2.readFileSync(filePath, "utf8");
2116
+ }
2117
+ function getSqliteInMemorySetupSql() {
2118
+ let sql2 = loadSqlFile("employee.sql");
2119
+ const readRegex = /\.read\s+([a-zA-Z0-9_]+\.sql)/g;
2120
+ let match;
2121
+ while ((match = readRegex.exec(sql2)) !== null) {
2122
+ const includePath = match[1];
2123
+ const includeContent = loadSqlFile(includePath);
2124
+ sql2 = sql2.replace(match[0], includeContent);
2125
+ }
2126
+ return sql2;
2127
+ }
2128
+
1836
2129
  // src/resources/index.ts
1837
2130
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1838
2131
 
@@ -2185,6 +2478,7 @@ var allowedKeywords = {
2185
2478
  postgres: ["select", "with", "explain", "analyze", "show"],
2186
2479
  mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2187
2480
  mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2481
+ sqlite: ["select", "with", "explain", "analyze", "pragma"],
2188
2482
  sqlserver: ["select", "with", "explain", "showplan"]
2189
2483
  };
2190
2484
 
@@ -2252,6 +2546,9 @@ async function sqlGeneratorPromptHandler({
2252
2546
  case "postgres":
2253
2547
  sqlDialect = "postgres";
2254
2548
  break;
2549
+ case "sqlite":
2550
+ sqlDialect = "sqlite";
2551
+ break;
2255
2552
  case "mysql":
2256
2553
  sqlDialect = "mysql";
2257
2554
  break;
@@ -2312,6 +2609,11 @@ ${accessibleSchemas.map(
2312
2609
  "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
2313
2610
  "SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
2314
2611
  ],
2612
+ sqlite: [
2613
+ "SELECT * FROM users WHERE created_at > datetime('now', '-1 day')",
2614
+ "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
2615
+ "SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
2616
+ ],
2315
2617
  mysql: [
2316
2618
  "SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 DAY",
2317
2619
  "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
@@ -2643,9 +2945,9 @@ function registerPrompts(server) {
2643
2945
  }
2644
2946
 
2645
2947
  // src/server.ts
2646
- var __filename2 = fileURLToPath2(import.meta.url);
2647
- var __dirname2 = path2.dirname(__filename2);
2648
- var packageJsonPath = path2.join(__dirname2, "..", "package.json");
2948
+ var __filename3 = fileURLToPath3(import.meta.url);
2949
+ var __dirname3 = path3.dirname(__filename3);
2950
+ var packageJsonPath = path3.join(__dirname3, "..", "package.json");
2649
2951
  var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
2650
2952
  var SERVER_NAME = "DBHub MCP Server";
2651
2953
  var SERVER_VERSION = packageJson.version;
@@ -2672,7 +2974,8 @@ async function main() {
2672
2974
  ERROR: Database connection string (DSN) is required.
2673
2975
  Please provide the DSN in one of these ways (in order of priority):
2674
2976
 
2675
- 1. Command line argument: --dsn="your-connection-string"
2977
+ 1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
2978
+ 2. Command line argument: --dsn="your-connection-string"
2676
2979
  3. Environment variable: export DSN="your-connection-string"
2677
2980
  4. .env file: DSN=your-connection-string
2678
2981
 
@@ -2696,13 +2999,22 @@ See documentation for more details on configuring database connections.
2696
2999
  const connectorManager = new ConnectorManager();
2697
3000
  console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
2698
3001
  console.error(`DSN source: ${dsnData.source}`);
2699
- await connectorManager.connectWithDSN(dsnData.dsn);
3002
+ if (dsnData.isDemo) {
3003
+ const initScript = getSqliteInMemorySetupSql();
3004
+ await connectorManager.connectWithDSN(dsnData.dsn, initScript);
3005
+ } else {
3006
+ await connectorManager.connectWithDSN(dsnData.dsn);
3007
+ }
2700
3008
  const transportData = resolveTransport();
2701
3009
  console.error(`Using transport: ${transportData.type}`);
2702
3010
  console.error(`Transport source: ${transportData.source}`);
2703
3011
  const readonly = isReadOnlyMode();
2704
3012
  const activeModes = [];
2705
3013
  const modeDescriptions = [];
3014
+ if (dsnData.isDemo) {
3015
+ activeModes.push("DEMO");
3016
+ modeDescriptions.push("using sample employee database");
3017
+ }
2706
3018
  if (readonly) {
2707
3019
  activeModes.push("READ-ONLY");
2708
3020
  modeDescriptions.push("only read only queries allowed");
@@ -0,0 +1,117 @@
1
+ -- Sample employee database
2
+ -- See changelog table for details
3
+ -- Copyright (C) 2007,2008, MySQL AB
4
+ --
5
+ -- Original data created by Fusheng Wang and Carlo Zaniolo
6
+ -- http://www.cs.aau.dk/TimeCenter/software.htm
7
+ -- http://www.cs.aau.dk/TimeCenter/Data/employeeTemporalDataSet.zip
8
+ --
9
+ -- Current schema by Giuseppe Maxia
10
+ -- Data conversion from XML to relational by Patrick Crews
11
+ -- SQLite adaptation by Claude Code
12
+ --
13
+ -- This work is licensed under the
14
+ -- Creative Commons Attribution-Share Alike 3.0 Unported License.
15
+ -- To view a copy of this license, visit
16
+ -- http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
17
+ -- Creative Commons, 171 Second Street, Suite 300, San Francisco,
18
+ -- California, 94105, USA.
19
+ --
20
+ -- DISCLAIMER
21
+ -- To the best of our knowledge, this data is fabricated, and
22
+ -- it does not correspond to real people.
23
+ -- Any similarity to existing people is purely coincidental.
24
+ --
25
+
26
+ PRAGMA foreign_keys = ON;
27
+
28
+ SELECT 'CREATING DATABASE STRUCTURE' as 'INFO';
29
+
30
+ DROP TABLE IF EXISTS dept_emp;
31
+ DROP TABLE IF EXISTS dept_manager;
32
+ DROP TABLE IF EXISTS title;
33
+ DROP TABLE IF EXISTS salary;
34
+ DROP TABLE IF EXISTS employee;
35
+ DROP TABLE IF EXISTS department;
36
+ DROP VIEW IF EXISTS dept_emp_latest_date;
37
+ DROP VIEW IF EXISTS current_dept_emp;
38
+
39
+ CREATE TABLE employee (
40
+ emp_no INTEGER NOT NULL,
41
+ birth_date DATE NOT NULL,
42
+ first_name TEXT NOT NULL,
43
+ last_name TEXT NOT NULL,
44
+ gender TEXT NOT NULL CHECK (gender IN ('M','F')),
45
+ hire_date DATE NOT NULL,
46
+ PRIMARY KEY (emp_no)
47
+ );
48
+
49
+ CREATE TABLE department (
50
+ dept_no TEXT NOT NULL,
51
+ dept_name TEXT NOT NULL,
52
+ PRIMARY KEY (dept_no),
53
+ UNIQUE (dept_name)
54
+ );
55
+
56
+ CREATE TABLE dept_manager (
57
+ emp_no INTEGER NOT NULL,
58
+ dept_no TEXT NOT NULL,
59
+ from_date DATE NOT NULL,
60
+ to_date DATE NOT NULL,
61
+ FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE,
62
+ FOREIGN KEY (dept_no) REFERENCES department (dept_no) ON DELETE CASCADE,
63
+ PRIMARY KEY (emp_no,dept_no)
64
+ );
65
+
66
+ CREATE TABLE dept_emp (
67
+ emp_no INTEGER NOT NULL,
68
+ dept_no TEXT NOT NULL,
69
+ from_date DATE NOT NULL,
70
+ to_date DATE NOT NULL,
71
+ FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE,
72
+ FOREIGN KEY (dept_no) REFERENCES department (dept_no) ON DELETE CASCADE,
73
+ PRIMARY KEY (emp_no,dept_no)
74
+ );
75
+
76
+ CREATE TABLE title (
77
+ emp_no INTEGER NOT NULL,
78
+ title TEXT NOT NULL,
79
+ from_date DATE NOT NULL,
80
+ to_date DATE,
81
+ FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE,
82
+ PRIMARY KEY (emp_no,title,from_date)
83
+ );
84
+
85
+ CREATE TABLE salary (
86
+ emp_no INTEGER NOT NULL,
87
+ amount INTEGER NOT NULL,
88
+ from_date DATE NOT NULL,
89
+ to_date DATE NOT NULL,
90
+ FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE,
91
+ PRIMARY KEY (emp_no,from_date)
92
+ );
93
+
94
+ CREATE VIEW dept_emp_latest_date AS
95
+ SELECT emp_no, MAX(from_date) AS from_date, MAX(to_date) AS to_date
96
+ FROM dept_emp
97
+ GROUP BY emp_no;
98
+
99
+ -- shows only the current department for each employee
100
+ CREATE VIEW current_dept_emp AS
101
+ SELECT l.emp_no, dept_no, l.from_date, l.to_date
102
+ FROM dept_emp d
103
+ INNER JOIN dept_emp_latest_date l
104
+ ON d.emp_no=l.emp_no AND d.from_date=l.from_date AND l.to_date = d.to_date;
105
+
106
+ SELECT 'LOADING department' as 'INFO';
107
+ .read load_department.sql
108
+ SELECT 'LOADING employee' as 'INFO';
109
+ .read load_employee.sql
110
+ SELECT 'LOADING dept_emp' as 'INFO';
111
+ .read load_dept_emp.sql
112
+ SELECT 'LOADING dept_manager' as 'INFO';
113
+ .read load_dept_manager.sql
114
+ SELECT 'LOADING title' as 'INFO';
115
+ .read load_title.sql
116
+ SELECT 'LOADING salary' as 'INFO';
117
+ .read load_salary1.sql