@bytebase/dbhub 0.0.6 → 0.0.8
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 +118 -61
- package/dist/config/demo-loader.js +45 -0
- package/dist/config/env.js +18 -1
- package/dist/connectors/manager.js +2 -2
- package/dist/connectors/mysql/index.js +169 -0
- package/dist/connectors/sqlite/index.js +104 -100
- package/dist/index.js +2 -1
- package/dist/prompts/index.js +2 -2
- package/dist/server.js +18 -7
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<a href="https://dbhub.ai/" target="_blank">
|
|
3
3
|
<picture>
|
|
4
|
-
<img src="https://raw.githubusercontent.com/bytebase/dbhub/main/
|
|
4
|
+
<img src="https://raw.githubusercontent.com/bytebase/dbhub/main/resources/images/logo-full.svg" width="50%">
|
|
5
5
|
</picture>
|
|
6
6
|
</a>
|
|
7
7
|
</p>
|
|
@@ -14,11 +14,11 @@ DBHub is a universal database gateway implementing the Model Context Protocol (M
|
|
|
14
14
|
| | | | | |
|
|
15
15
|
| Claude Desktop +--->+ +--->+ PostgreSQL |
|
|
16
16
|
| | | | | |
|
|
17
|
-
| Cursor +--->+ DBHub +--->+
|
|
17
|
+
| Cursor +--->+ DBHub +--->+ SQL Server |
|
|
18
18
|
| | | | | |
|
|
19
19
|
| Other MCP +--->+ +--->+ SQLite |
|
|
20
20
|
| Clients | | | | |
|
|
21
|
-
| | | +--->+
|
|
21
|
+
| | | +--->+ MySQL |
|
|
22
22
|
| | | | | |
|
|
23
23
|
| | | +--->+ Other Databases |
|
|
24
24
|
| | | | | |
|
|
@@ -26,18 +26,35 @@ DBHub is a universal database gateway implementing the Model Context Protocol (M
|
|
|
26
26
|
MCP Clients MCP Server Databases
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Supported Matrix
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
### Database Resources
|
|
32
|
+
|
|
33
|
+
| Resource | URI Format | PostgreSQL | MySQL | SQL Server | SQLite |
|
|
34
|
+
|------------------------|:----------:|:----------:|:-----:|:----------:|:------:|
|
|
35
|
+
| Tables | `db://tables` | ✅ | ✅ | ✅ | ✅ |
|
|
36
|
+
| Schema | `db://schema/{tableName}` | ✅ | ✅ | ✅ | ✅ |
|
|
37
|
+
|
|
38
|
+
### Database Tools
|
|
39
|
+
|
|
40
|
+
| Tool | Command Name | PostgreSQL | MySQL | SQL Server | SQLite |
|
|
41
|
+
|------------------------|:------------------:|:----------:|:-----:|:----------:|:------:|
|
|
42
|
+
| Execute Query | `run_query` | ✅ | ✅ | ✅ | ✅ |
|
|
43
|
+
| List Connectors | `list_connectors` | ✅ | ✅ | ✅ | ✅ |
|
|
44
|
+
|
|
45
|
+
### Prompt Capabilities
|
|
46
|
+
|
|
47
|
+
| Prompt | Command Name | PostgreSQL | MySQL | SQL Server | SQLite |
|
|
48
|
+
|------------------------|:----------------:|:----------:|:-----:|:----------:|:------:|
|
|
49
|
+
| Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ |
|
|
50
|
+
| Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ |
|
|
35
51
|
|
|
36
52
|
## Installation
|
|
37
53
|
|
|
38
54
|
### Docker
|
|
39
55
|
|
|
40
56
|
```bash
|
|
57
|
+
# PostgreSQL example
|
|
41
58
|
docker run --rm --init \
|
|
42
59
|
--name dbhub \
|
|
43
60
|
--publish 8080:8080 \
|
|
@@ -47,17 +64,96 @@ docker run --rm --init \
|
|
|
47
64
|
--dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
48
65
|
```
|
|
49
66
|
|
|
67
|
+
```bash
|
|
68
|
+
# Demo mode with sample employee database
|
|
69
|
+
docker run --rm --init \
|
|
70
|
+
--name dbhub \
|
|
71
|
+
--publish 8080:8080 \
|
|
72
|
+
bytebase/dbhub \
|
|
73
|
+
--transport sse \
|
|
74
|
+
--port 8080 \
|
|
75
|
+
--demo
|
|
76
|
+
```
|
|
77
|
+
|
|
50
78
|
### NPM
|
|
51
79
|
|
|
52
80
|
```bash
|
|
81
|
+
# PostgreSQL example
|
|
53
82
|
npx @bytebase/dbhub --transport sse --port 8080 --dsn "postgres://user:password@localhost:5432/dbname"
|
|
54
83
|
```
|
|
55
84
|
|
|
85
|
+
```bash
|
|
86
|
+
# Demo mode with sample employee database
|
|
87
|
+
npx @bytebase/dbhub --transport sse --port 8080 --demo
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Claude Desktop
|
|
91
|
+
|
|
92
|
+

|
|
93
|
+
|
|
94
|
+
- Claude Desktop only supports `stdio` transport https://github.com/orgs/modelcontextprotocol/discussions/16
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
// claude_desktop_config.json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"dbhub-postgres-docker": {
|
|
101
|
+
"command": "docker",
|
|
102
|
+
"args": [
|
|
103
|
+
"run",
|
|
104
|
+
"-i",
|
|
105
|
+
"--rm",
|
|
106
|
+
"bytebase/dbhub",
|
|
107
|
+
"--transport",
|
|
108
|
+
"stdio",
|
|
109
|
+
"--dsn",
|
|
110
|
+
// Use host.docker.internal as the host if connecting to the local db
|
|
111
|
+
"postgres://user:password@host.docker.internal:5432/dbname?sslmode=disable"
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"dbhub-postgres-npx": {
|
|
115
|
+
"command": "npx",
|
|
116
|
+
"args": [
|
|
117
|
+
"-y",
|
|
118
|
+
"@bytebase/dbhub",
|
|
119
|
+
"--transport",
|
|
120
|
+
"stdio",
|
|
121
|
+
"--dsn",
|
|
122
|
+
"postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
"dbhub-demo": {
|
|
126
|
+
"command": "npx",
|
|
127
|
+
"args": [
|
|
128
|
+
"-y",
|
|
129
|
+
"@bytebase/dbhub",
|
|
130
|
+
"--transport",
|
|
131
|
+
"stdio",
|
|
132
|
+
"--demo"
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Cursor
|
|
140
|
+
|
|
141
|
+

|
|
142
|
+
|
|
143
|
+
- Cursor supports both `stdio` and `sse`.
|
|
144
|
+
- Follow [Cursor MCP guide](https://docs.cursor.com/context/model-context-protocol) and make sure to use [Agent](https://docs.cursor.com/chat/agent) mode.
|
|
145
|
+
|
|
56
146
|
## Usage
|
|
57
147
|
|
|
58
148
|
### Configure your database connection
|
|
59
149
|
|
|
60
|
-
|
|
150
|
+
You can use DBHub in demo mode with a sample employee database for testing:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pnpm dev --demo
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
For real databases, a Database Source Name (DSN) is required. You can provide this in several ways:
|
|
61
157
|
|
|
62
158
|
- **Command line argument** (highest priority):
|
|
63
159
|
|
|
@@ -79,6 +175,15 @@ Database Source Name (DSN) is required to connect to your database. You can prov
|
|
|
79
175
|
DSN=postgres://user:password@localhost:5432/dbname?sslmode=disable
|
|
80
176
|
```
|
|
81
177
|
|
|
178
|
+
DBHub supports the following database connection string formats:
|
|
179
|
+
|
|
180
|
+
| Database | DSN Format | Example |
|
|
181
|
+
|------------|-----------------------------------------------------------|--------------------------------------------------------|
|
|
182
|
+
| PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
|
|
183
|
+
| SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:` | `sqlite:///path/to/database.db` or `sqlite::memory:` |
|
|
184
|
+
| SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname` |
|
|
185
|
+
| MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname` |
|
|
186
|
+
|
|
82
187
|
### Transport
|
|
83
188
|
|
|
84
189
|
- **stdio** (default) - for direct integration with tools like Claude Desktop:
|
|
@@ -96,60 +201,11 @@ Database Source Name (DSN) is required to connect to your database. You can prov
|
|
|
96
201
|
|
|
97
202
|
| Option | Description | Default |
|
|
98
203
|
| :-------- | :-------------------------------------------------------------- | :---------------------------------- |
|
|
99
|
-
|
|
|
204
|
+
| demo | Run in demo mode with sample employee database | `false` |
|
|
205
|
+
| dsn | Database connection string | Required if not in demo mode |
|
|
100
206
|
| transport | Transport mode: `stdio` or `sse` | `stdio` |
|
|
101
207
|
| port | HTTP server port (only applicable when using `--transport=sse`) | `8080` |
|
|
102
208
|
|
|
103
|
-
### Claude Desktop
|
|
104
|
-
|
|
105
|
-

|
|
106
|
-
|
|
107
|
-
- Claude Desktop only supports `stdio` transport https://github.com/orgs/modelcontextprotocol/discussions/16
|
|
108
|
-
|
|
109
|
-
#### Docker
|
|
110
|
-
|
|
111
|
-
```json
|
|
112
|
-
// claude_desktop_config.json
|
|
113
|
-
{
|
|
114
|
-
"mcpServers": {
|
|
115
|
-
"dbhub": {
|
|
116
|
-
"command": "docker",
|
|
117
|
-
"args": [
|
|
118
|
-
"run",
|
|
119
|
-
"-i",
|
|
120
|
-
"--rm",
|
|
121
|
-
"bytebase/dbhub",
|
|
122
|
-
"--transport",
|
|
123
|
-
"stdio",
|
|
124
|
-
"--dsn",
|
|
125
|
-
// Use host.docker.internal as the host if connecting to the local db
|
|
126
|
-
"postgres://user:password@host.docker.internal:5432/dbname?sslmode=disable"
|
|
127
|
-
]
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
#### NPX
|
|
134
|
-
|
|
135
|
-
```json
|
|
136
|
-
// claude_desktop_config.json
|
|
137
|
-
{
|
|
138
|
-
"mcpServers": {
|
|
139
|
-
"dbhub": {
|
|
140
|
-
"command": "npx",
|
|
141
|
-
"args": [
|
|
142
|
-
"-y",
|
|
143
|
-
"@bytebase/dbhub",
|
|
144
|
-
"--transport",
|
|
145
|
-
"stdio",
|
|
146
|
-
"--dsn",
|
|
147
|
-
"postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
148
|
-
]
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
209
|
|
|
154
210
|
## Development
|
|
155
211
|
|
|
@@ -176,6 +232,7 @@ Database Source Name (DSN) is required to connect to your database. You can prov
|
|
|
176
232
|
#### stdio
|
|
177
233
|
|
|
178
234
|
```bash
|
|
235
|
+
# PostgreSQL example
|
|
179
236
|
TRANSPORT=stdio DSN="postgres://user:password@localhost:5432/dbname?sslmode=disable" npx @modelcontextprotocol/inspector node /path/to/dbhub/dist/index.js
|
|
180
237
|
```
|
|
181
238
|
|
|
@@ -191,4 +248,4 @@ npx @modelcontextprotocol/inspector
|
|
|
191
248
|
|
|
192
249
|
Connect to the DBHub server `/sse` endpoint
|
|
193
250
|
|
|
194
|
-

|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo data loader for SQLite in-memory database
|
|
3
|
+
*
|
|
4
|
+
* This module loads the sample employee database into the SQLite in-memory database
|
|
5
|
+
* when the --demo flag is specified.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
// Create __dirname equivalent for ES modules
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
// Path to sample data files
|
|
14
|
+
const DEMO_DATA_DIR = path.join(__dirname, '..', '..', 'resources', 'employee-sqlite');
|
|
15
|
+
/**
|
|
16
|
+
* Load SQL file contents
|
|
17
|
+
*/
|
|
18
|
+
export function loadSqlFile(fileName) {
|
|
19
|
+
const filePath = path.join(DEMO_DATA_DIR, fileName);
|
|
20
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get SQLite DSN for in-memory database
|
|
24
|
+
*/
|
|
25
|
+
export function getInMemorySqliteDSN() {
|
|
26
|
+
return 'sqlite::memory:';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Load SQL files sequentially
|
|
30
|
+
*/
|
|
31
|
+
export function getSqliteInMemorySetupSql() {
|
|
32
|
+
// First, load the schema
|
|
33
|
+
let sql = loadSqlFile('employee.sql');
|
|
34
|
+
// Replace .read directives with the actual file contents
|
|
35
|
+
// This is necessary because in-memory SQLite can't use .read
|
|
36
|
+
const readRegex = /\.read\s+([a-zA-Z0-9_]+\.sql)/g;
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = readRegex.exec(sql)) !== null) {
|
|
39
|
+
const includePath = match[1];
|
|
40
|
+
const includeContent = loadSqlFile(includePath);
|
|
41
|
+
// Replace the .read line with the file contents
|
|
42
|
+
sql = sql.replace(match[0], includeContent);
|
|
43
|
+
}
|
|
44
|
+
return sql;
|
|
45
|
+
}
|
package/dist/config/env.js
CHANGED
|
@@ -62,6 +62,14 @@ export function loadEnvFiles() {
|
|
|
62
62
|
}
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if demo mode is enabled from command line args
|
|
67
|
+
* Returns true if --demo flag is provided
|
|
68
|
+
*/
|
|
69
|
+
export function isDemoMode() {
|
|
70
|
+
const args = parseCommandLineArgs();
|
|
71
|
+
return args.demo === 'true';
|
|
72
|
+
}
|
|
65
73
|
/**
|
|
66
74
|
* Resolve DSN from command line args, environment variables, or .env files
|
|
67
75
|
* Returns the DSN and its source, or null if not found
|
|
@@ -69,7 +77,16 @@ export function loadEnvFiles() {
|
|
|
69
77
|
export function resolveDSN() {
|
|
70
78
|
// Get command line arguments
|
|
71
79
|
const args = parseCommandLineArgs();
|
|
72
|
-
//
|
|
80
|
+
// Check for demo mode first (highest priority)
|
|
81
|
+
if (isDemoMode()) {
|
|
82
|
+
// Will use in-memory SQLite with demo data
|
|
83
|
+
return {
|
|
84
|
+
dsn: 'sqlite::memory:',
|
|
85
|
+
source: 'demo mode',
|
|
86
|
+
isDemo: true
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// 1. Check command line arguments
|
|
73
90
|
if (args.dsn) {
|
|
74
91
|
return { dsn: args.dsn, source: 'command line argument' };
|
|
75
92
|
}
|
|
@@ -15,7 +15,7 @@ export class ConnectorManager {
|
|
|
15
15
|
/**
|
|
16
16
|
* Initialize and connect to the database using a DSN
|
|
17
17
|
*/
|
|
18
|
-
async connectWithDSN(dsn) {
|
|
18
|
+
async connectWithDSN(dsn, initScript) {
|
|
19
19
|
// First try to find a connector that can handle this DSN
|
|
20
20
|
let connector = ConnectorRegistry.getConnectorForDSN(dsn);
|
|
21
21
|
if (!connector) {
|
|
@@ -23,7 +23,7 @@ export class ConnectorManager {
|
|
|
23
23
|
}
|
|
24
24
|
this.activeConnector = connector;
|
|
25
25
|
// Connect to the database
|
|
26
|
-
await this.activeConnector.connect(dsn);
|
|
26
|
+
await this.activeConnector.connect(dsn, initScript);
|
|
27
27
|
this.connected = true;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
2
|
+
import { ConnectorRegistry } from '../interface.js';
|
|
3
|
+
/**
|
|
4
|
+
* MySQL DSN Parser
|
|
5
|
+
* Handles DSN strings like: mysql://user:password@localhost:3306/dbname
|
|
6
|
+
*/
|
|
7
|
+
class MySQLDSNParser {
|
|
8
|
+
parse(dsn) {
|
|
9
|
+
// Basic validation
|
|
10
|
+
if (!this.isValidDSN(dsn)) {
|
|
11
|
+
throw new Error(`Invalid MySQL DSN: ${dsn}`);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(dsn);
|
|
15
|
+
const config = {
|
|
16
|
+
host: url.hostname,
|
|
17
|
+
port: url.port ? parseInt(url.port) : 3306,
|
|
18
|
+
database: url.pathname.substring(1), // Remove leading '/'
|
|
19
|
+
user: url.username,
|
|
20
|
+
password: url.password,
|
|
21
|
+
};
|
|
22
|
+
// Handle query parameters
|
|
23
|
+
url.searchParams.forEach((value, key) => {
|
|
24
|
+
if (key === 'ssl') {
|
|
25
|
+
config.ssl = value === 'true' ? {} : undefined;
|
|
26
|
+
}
|
|
27
|
+
// Add other parameters as needed
|
|
28
|
+
});
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error(`Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
getSampleDSN() {
|
|
36
|
+
return 'mysql://root:password@localhost:3306/mysql';
|
|
37
|
+
}
|
|
38
|
+
isValidDSN(dsn) {
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(dsn);
|
|
41
|
+
return url.protocol === 'mysql:';
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* MySQL Connector Implementation
|
|
50
|
+
*/
|
|
51
|
+
export class MySQLConnector {
|
|
52
|
+
constructor() {
|
|
53
|
+
this.id = 'mysql';
|
|
54
|
+
this.name = 'MySQL';
|
|
55
|
+
this.dsnParser = new MySQLDSNParser();
|
|
56
|
+
this.pool = null;
|
|
57
|
+
}
|
|
58
|
+
async connect(dsn) {
|
|
59
|
+
try {
|
|
60
|
+
const config = this.dsnParser.parse(dsn);
|
|
61
|
+
this.pool = mysql.createPool(config);
|
|
62
|
+
// Test the connection
|
|
63
|
+
const [rows] = await this.pool.query('SELECT 1');
|
|
64
|
+
console.error("Successfully connected to MySQL database");
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error("Failed to connect to MySQL database:", err);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async disconnect() {
|
|
72
|
+
if (this.pool) {
|
|
73
|
+
await this.pool.end();
|
|
74
|
+
this.pool = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async getTables() {
|
|
78
|
+
if (!this.pool) {
|
|
79
|
+
throw new Error('Not connected to database');
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
// Get all tables from the current database
|
|
83
|
+
const [rows] = await this.pool.query(`
|
|
84
|
+
SELECT table_name
|
|
85
|
+
FROM information_schema.tables
|
|
86
|
+
WHERE table_schema = DATABASE()
|
|
87
|
+
ORDER BY table_name
|
|
88
|
+
`);
|
|
89
|
+
return rows.map(row => row.table_name);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error("Error getting tables:", error);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async tableExists(tableName) {
|
|
97
|
+
if (!this.pool) {
|
|
98
|
+
throw new Error('Not connected to database');
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const [rows] = await this.pool.query(`
|
|
102
|
+
SELECT COUNT(*) as count
|
|
103
|
+
FROM information_schema.tables
|
|
104
|
+
WHERE table_schema = DATABASE()
|
|
105
|
+
AND table_name = ?
|
|
106
|
+
`, [tableName]);
|
|
107
|
+
return rows[0].count > 0;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error("Error checking if table exists:", error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async getTableSchema(tableName) {
|
|
115
|
+
if (!this.pool) {
|
|
116
|
+
throw new Error('Not connected to database');
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
// Get table columns
|
|
120
|
+
const [rows] = await this.pool.query(`
|
|
121
|
+
SELECT
|
|
122
|
+
column_name,
|
|
123
|
+
data_type,
|
|
124
|
+
is_nullable,
|
|
125
|
+
column_default
|
|
126
|
+
FROM information_schema.columns
|
|
127
|
+
WHERE table_schema = DATABASE()
|
|
128
|
+
AND table_name = ?
|
|
129
|
+
ORDER BY ordinal_position
|
|
130
|
+
`, [tableName]);
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.error("Error getting table schema:", error);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async executeQuery(query) {
|
|
139
|
+
if (!this.pool) {
|
|
140
|
+
throw new Error('Not connected to database');
|
|
141
|
+
}
|
|
142
|
+
const safetyCheck = this.validateQuery(query);
|
|
143
|
+
if (!safetyCheck.isValid) {
|
|
144
|
+
throw new Error(safetyCheck.message || "Query validation failed");
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const [rows, fields] = await this.pool.query(query);
|
|
148
|
+
return { rows, fields };
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error("Error executing query:", error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
validateQuery(query) {
|
|
156
|
+
// Basic check to prevent non-SELECT queries
|
|
157
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
158
|
+
if (!normalizedQuery.startsWith('select')) {
|
|
159
|
+
return {
|
|
160
|
+
isValid: false,
|
|
161
|
+
message: "Only SELECT queries are allowed for security reasons."
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { isValid: true };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Create and register the connector
|
|
168
|
+
const mysqlConnector = new MySQLConnector();
|
|
169
|
+
ConnectorRegistry.register(mysqlConnector);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite Connector Implementation
|
|
2
|
+
* SQLite Connector Implementation
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Implements SQLite database connectivity for DBHub
|
|
5
5
|
* To use this connector:
|
|
6
|
-
* 1.
|
|
7
|
-
* 2.
|
|
8
|
-
* 3. Set DSN=sqlite:///path/to/database.db in your .env file
|
|
6
|
+
* 1. Set DSN=sqlite:///path/to/database.db in your .env file
|
|
7
|
+
* 2. Or set DB_CONNECTOR_TYPE=sqlite for default in-memory database
|
|
9
8
|
*/
|
|
9
|
+
import { ConnectorRegistry } from '../interface.js';
|
|
10
|
+
import sqlite3 from 'sqlite3';
|
|
10
11
|
/**
|
|
11
12
|
* SQLite DSN Parser
|
|
12
13
|
* Handles DSN strings like:
|
|
@@ -63,129 +64,132 @@ export class SQLiteConnector {
|
|
|
63
64
|
this.id = 'sqlite';
|
|
64
65
|
this.name = 'SQLite';
|
|
65
66
|
this.dsnParser = new SQLiteDSNParser();
|
|
66
|
-
this.
|
|
67
|
+
this.db = null;
|
|
68
|
+
this.dbPath = ':memory:'; // Default to in-memory database
|
|
67
69
|
}
|
|
68
|
-
async connect(dsn) {
|
|
70
|
+
async connect(dsn, initScript) {
|
|
69
71
|
const config = this.dsnParser.parse(dsn);
|
|
70
72
|
this.dbPath = config.dbPath;
|
|
71
|
-
// Example implementation (requires sqlite3 package)
|
|
72
|
-
/*
|
|
73
73
|
return new Promise((resolve, reject) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
this.db = new sqlite3.Database(this.dbPath, (err) => {
|
|
75
|
+
if (err) {
|
|
76
|
+
console.error("Failed to connect to SQLite database:", err);
|
|
77
|
+
reject(err);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Can't use console.log here because it will break the stdio transport
|
|
81
|
+
console.error("Successfully connected to SQLite database");
|
|
82
|
+
// If an initialization script is provided, run it
|
|
83
|
+
if (initScript) {
|
|
84
|
+
this.db.exec(initScript, (err) => {
|
|
85
|
+
if (err) {
|
|
86
|
+
console.error("Failed to initialize database with script:", err);
|
|
87
|
+
reject(err);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.error("Successfully initialized database with script");
|
|
91
|
+
resolve();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
resolve();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
84
100
|
});
|
|
85
|
-
*/
|
|
86
|
-
throw new Error('SQLite connector not implemented yet');
|
|
87
101
|
}
|
|
88
102
|
async disconnect() {
|
|
89
103
|
// Close the SQLite connection
|
|
90
|
-
/*
|
|
91
104
|
if (this.db) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
this.db.close((err) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
reject(err);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.db = null;
|
|
112
|
+
resolve();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
100
115
|
});
|
|
101
|
-
});
|
|
102
116
|
}
|
|
103
|
-
|
|
104
|
-
throw new Error('SQLite connector not implemented yet');
|
|
117
|
+
return Promise.resolve();
|
|
105
118
|
}
|
|
106
119
|
async getTables() {
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
if (!this.db) {
|
|
121
|
+
throw new Error("Not connected to SQLite database");
|
|
122
|
+
}
|
|
109
123
|
return new Promise((resolve, reject) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
);
|
|
124
|
+
this.db.all(`SELECT name FROM sqlite_master
|
|
125
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
126
|
+
ORDER BY name`, (err, rows) => {
|
|
127
|
+
if (err) {
|
|
128
|
+
reject(err);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
resolve(rows.map(row => row.name));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
122
134
|
});
|
|
123
|
-
*/
|
|
124
|
-
throw new Error('SQLite connector not implemented yet');
|
|
125
135
|
}
|
|
126
136
|
async tableExists(tableName) {
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
if (!this.db) {
|
|
138
|
+
throw new Error("Not connected to SQLite database");
|
|
139
|
+
}
|
|
129
140
|
return new Promise((resolve, reject) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
);
|
|
141
|
+
this.db.get(`SELECT name FROM sqlite_master
|
|
142
|
+
WHERE type='table' AND name = ?`, [tableName], (err, row) => {
|
|
143
|
+
if (err) {
|
|
144
|
+
reject(err);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
resolve(!!row);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
142
150
|
});
|
|
143
|
-
*/
|
|
144
|
-
throw new Error('SQLite connector not implemented yet');
|
|
145
151
|
}
|
|
146
152
|
async getTableSchema(tableName) {
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
if (!this.db) {
|
|
154
|
+
throw new Error("Not connected to SQLite database");
|
|
155
|
+
}
|
|
149
156
|
return new Promise((resolve, reject) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
);
|
|
157
|
+
this.db.all(`PRAGMA table_info(${tableName})`, (err, rows) => {
|
|
158
|
+
if (err) {
|
|
159
|
+
reject(err);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Convert SQLite schema format to our standard TableColumn format
|
|
163
|
+
const columns = rows.map(row => ({
|
|
164
|
+
column_name: row.name,
|
|
165
|
+
data_type: row.type,
|
|
166
|
+
is_nullable: row.notnull === 0 ? 'YES' : 'NO', // In SQLite, 0 means nullable
|
|
167
|
+
column_default: row.dflt_value
|
|
168
|
+
}));
|
|
169
|
+
resolve(columns);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
167
172
|
});
|
|
168
|
-
*/
|
|
169
|
-
throw new Error('SQLite connector not implemented yet');
|
|
170
173
|
}
|
|
171
174
|
async executeQuery(query) {
|
|
172
|
-
|
|
175
|
+
if (!this.db) {
|
|
176
|
+
throw new Error("Not connected to SQLite database");
|
|
177
|
+
}
|
|
178
|
+
// Validate query for safety
|
|
173
179
|
const safetyCheck = this.validateQuery(query);
|
|
174
180
|
if (!safetyCheck.isValid) {
|
|
175
181
|
throw new Error(safetyCheck.message || "Query validation failed");
|
|
176
182
|
}
|
|
177
|
-
/*
|
|
178
183
|
return new Promise((resolve, reject) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
this.db.all(query, (err, rows) => {
|
|
185
|
+
if (err) {
|
|
186
|
+
reject(err);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
resolve({ rows });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
186
192
|
});
|
|
187
|
-
*/
|
|
188
|
-
throw new Error('SQLite connector not implemented yet');
|
|
189
193
|
}
|
|
190
194
|
validateQuery(query) {
|
|
191
195
|
// Basic check to prevent non-SELECT queries
|
|
@@ -199,6 +203,6 @@ export class SQLiteConnector {
|
|
|
199
203
|
return { isValid: true };
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
// Register the SQLite connector
|
|
207
|
+
const sqliteConnector = new SQLiteConnector();
|
|
208
|
+
ConnectorRegistry.register(sqliteConnector);
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Import connector modules to register them
|
|
3
3
|
import './connectors/postgres/index.js'; // Register PostgreSQL connector
|
|
4
4
|
import './connectors/sqlserver/index.js'; // Register SQL Server connector
|
|
5
|
-
|
|
5
|
+
import './connectors/sqlite/index.js'; // SQLite connector
|
|
6
|
+
import './connectors/mysql/index.js'; // MySQL connector
|
|
6
7
|
// Import main function from server.ts
|
|
7
8
|
import { main } from './server.js';
|
|
8
9
|
/**
|
package/dist/prompts/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { dbExplainerPromptHandler, dbExplainerSchema } from './db-explainer.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export function registerPrompts(server) {
|
|
7
7
|
// Register SQL Generator prompt
|
|
8
|
-
server.prompt("
|
|
8
|
+
server.prompt("generate_sql", "Generate SQL queries from natural language descriptions", sqlGeneratorSchema, sqlGeneratorPromptHandler);
|
|
9
9
|
// Register Database Explainer prompt
|
|
10
|
-
server.prompt("
|
|
10
|
+
server.prompt("explain_db", "Get explanations about database tables, columns, and structures", dbExplainerSchema, dbExplainerPromptHandler);
|
|
11
11
|
}
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
import { ConnectorManager } from './connectors/manager.js';
|
|
9
9
|
import { ConnectorRegistry } from './connectors/interface.js';
|
|
10
10
|
import { resolveDSN, resolveTransport, resolvePort } from './config/env.js';
|
|
11
|
+
import { getSqliteInMemorySetupSql } from './config/demo-loader.js';
|
|
11
12
|
import { registerResources } from './resources/index.js';
|
|
12
13
|
import { registerTools } from './tools/index.js';
|
|
13
14
|
import { registerPrompts } from './prompts/index.js';
|
|
@@ -23,7 +24,8 @@ export const SERVER_VERSION = packageJson.version;
|
|
|
23
24
|
/**
|
|
24
25
|
* Generate ASCII art banner with version information
|
|
25
26
|
*/
|
|
26
|
-
export function generateBanner(version) {
|
|
27
|
+
export function generateBanner(version, isDemo = false) {
|
|
28
|
+
const demoText = isDemo ? " [DEMO MODE]" : "";
|
|
27
29
|
return `
|
|
28
30
|
_____ ____ _ _ _
|
|
29
31
|
| __ \\| _ \\| | | | | |
|
|
@@ -32,7 +34,7 @@ export function generateBanner(version) {
|
|
|
32
34
|
| |__| | |_) | | | | |_| | |_) |
|
|
33
35
|
|_____/|____/|_| |_|\\__,_|_.__/
|
|
34
36
|
|
|
35
|
-
v${version} - Universal Database MCP Server
|
|
37
|
+
v${version}${demoText} - Universal Database MCP Server
|
|
36
38
|
`;
|
|
37
39
|
}
|
|
38
40
|
/**
|
|
@@ -51,9 +53,10 @@ export async function main() {
|
|
|
51
53
|
ERROR: Database connection string (DSN) is required.
|
|
52
54
|
Please provide the DSN in one of these ways (in order of priority):
|
|
53
55
|
|
|
54
|
-
1.
|
|
55
|
-
2.
|
|
56
|
-
3.
|
|
56
|
+
1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
|
|
57
|
+
2. Command line argument: --dsn="your-connection-string"
|
|
58
|
+
3. Environment variable: export DSN="your-connection-string"
|
|
59
|
+
4. .env file: DSN=your-connection-string
|
|
57
60
|
|
|
58
61
|
Example formats:
|
|
59
62
|
${sampleFormats}
|
|
@@ -75,13 +78,21 @@ See documentation for more details on configuring database connections.
|
|
|
75
78
|
const connectorManager = new ConnectorManager();
|
|
76
79
|
console.error(`Connecting with DSN: ${dsnData.dsn}`);
|
|
77
80
|
console.error(`DSN source: ${dsnData.source}`);
|
|
78
|
-
|
|
81
|
+
// If in demo mode, load the employee database
|
|
82
|
+
if (dsnData.isDemo) {
|
|
83
|
+
console.error('Running in demo mode with sample employee database');
|
|
84
|
+
const initScript = getSqliteInMemorySetupSql();
|
|
85
|
+
await connectorManager.connectWithDSN(dsnData.dsn, initScript);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
await connectorManager.connectWithDSN(dsnData.dsn);
|
|
89
|
+
}
|
|
79
90
|
// Resolve transport type
|
|
80
91
|
const transportData = resolveTransport();
|
|
81
92
|
console.error(`Using transport: ${transportData.type}`);
|
|
82
93
|
console.error(`Transport source: ${transportData.source}`);
|
|
83
94
|
// Print ASCII art banner with version and slogan
|
|
84
|
-
console.error(generateBanner(SERVER_VERSION));
|
|
95
|
+
console.error(generateBanner(SERVER_VERSION, dsnData.isDemo));
|
|
85
96
|
// Set up transport based on type
|
|
86
97
|
if (transportData.type === 'sse') {
|
|
87
98
|
// Set up Express server for SSE transport
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Universal Database MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
20
|
+
"@types/sqlite3": "^5.1.0",
|
|
20
21
|
"dotenv": "^16.4.7",
|
|
21
22
|
"express": "^4.18.2",
|
|
22
23
|
"mssql": "^11.0.1",
|
|
24
|
+
"mysql2": "^3.13.0",
|
|
23
25
|
"pg": "^8.13.3",
|
|
26
|
+
"sqlite3": "^5.1.7",
|
|
24
27
|
"zod": "^3.24.2"
|
|
25
28
|
},
|
|
26
29
|
"devDependencies": {
|