@bytebase/dbhub 0.3.2 → 0.4.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 +43 -23
- package/dist/index.js +480 -19
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ DBHub is a universal database gateway implementing the Model Context Protocol (M
|
|
|
22
22
|
| | | | | |
|
|
23
23
|
| | | +--->+ MariaDB |
|
|
24
24
|
| | | | | |
|
|
25
|
+
| | | +--->+ Oracle |
|
|
26
|
+
| | | | | |
|
|
25
27
|
+------------------+ +--------------+ +------------------+
|
|
26
28
|
MCP Clients MCP Server Databases
|
|
27
29
|
```
|
|
@@ -36,28 +38,28 @@ https://demo.dbhub.ai/sse connects a [sample employee database](https://github.c
|
|
|
36
38
|
|
|
37
39
|
### Database Resources
|
|
38
40
|
|
|
39
|
-
| Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
|
|
40
|
-
| --------------------------- | ------------------------------------------------------ | :--------: | :---: | :-----: | :--------: | :----: |
|
|
41
|
-
| schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
42
|
-
| tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
43
|
-
| table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
44
|
-
| indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
45
|
-
| procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
46
|
-
| procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
41
|
+
| Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite | Oracle |
|
|
42
|
+
| --------------------------- | ------------------------------------------------------ | :--------: | :---: | :-----: | :--------: | :----: | :----: |
|
|
43
|
+
| schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
44
|
+
| tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
45
|
+
| table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
46
|
+
| indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
47
|
+
| procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
|
48
|
+
| procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
|
47
49
|
|
|
48
50
|
### Database Tools
|
|
49
51
|
|
|
50
|
-
| Tool | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
|
|
51
|
-
| --------------- | ----------------- | :--------: | :---: | :-----: | :--------: | ------ |
|
|
52
|
-
| Execute SQL | `execute_sql` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
53
|
-
| List Connectors | `list_connectors` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
52
|
+
| Tool | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite | Oracle |
|
|
53
|
+
| --------------- | ----------------- | :--------: | :---: | :-----: | :--------: | ------ | :----: |
|
|
54
|
+
| Execute SQL | `execute_sql` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
55
|
+
| List Connectors | `list_connectors` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
54
56
|
|
|
55
57
|
### Prompt Capabilities
|
|
56
58
|
|
|
57
|
-
| Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
|
|
58
|
-
| ------------------- | -------------- | :--------: | :---: | :-----: | :--------: | ------ |
|
|
59
|
-
| Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
60
|
-
| Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
59
|
+
| Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite | Oracle |
|
|
60
|
+
| ------------------- | -------------- | :--------: | :---: | :-----: | :--------: | ------ | :----: |
|
|
61
|
+
| Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
62
|
+
| Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
61
63
|
|
|
62
64
|
## Installation
|
|
63
65
|
|
|
@@ -85,6 +87,17 @@ docker run --rm --init \
|
|
|
85
87
|
--demo
|
|
86
88
|
```
|
|
87
89
|
|
|
90
|
+
```bash
|
|
91
|
+
# Oracle example
|
|
92
|
+
docker run --rm --init \
|
|
93
|
+
--name dbhub \
|
|
94
|
+
--publish 8080:8080 \
|
|
95
|
+
bytebase/dbhub \
|
|
96
|
+
--transport sse \
|
|
97
|
+
--port 8080 \
|
|
98
|
+
--dsn "oracle://username:password@localhost:1521/service_name"
|
|
99
|
+
```
|
|
100
|
+
|
|
88
101
|
### NPM
|
|
89
102
|
|
|
90
103
|
```bash
|
|
@@ -199,13 +212,14 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
|
|
|
199
212
|
|
|
200
213
|
DBHub supports the following database connection string formats:
|
|
201
214
|
|
|
202
|
-
| Database | DSN Format
|
|
203
|
-
| ---------- |
|
|
204
|
-
| MySQL | `mysql://[user]:[password]@[host]:[port]/[database]`
|
|
205
|
-
| MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]`
|
|
206
|
-
| PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]`
|
|
207
|
-
| SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]`
|
|
208
|
-
| SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:`
|
|
215
|
+
| Database | DSN Format | Example |
|
|
216
|
+
| ---------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
217
|
+
| MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname` |
|
|
218
|
+
| MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname` |
|
|
219
|
+
| PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
|
|
220
|
+
| SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname` |
|
|
221
|
+
| SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite::memory:` |
|
|
222
|
+
| Oracle | `oracle://[user]:[password]@[host]:[port]/[service_name]` | `oracle://username:password@localhost:1521/service_name` |
|
|
209
223
|
|
|
210
224
|
#### SQL Server
|
|
211
225
|
|
|
@@ -281,6 +295,12 @@ npx @modelcontextprotocol/inspector
|
|
|
281
295
|
|
|
282
296
|
Connect to the DBHub server `/sse` endpoint
|
|
283
297
|
|
|
298
|
+
## Contributors
|
|
299
|
+
|
|
300
|
+
<a href="https://github.com/bytebase/dbhub/graphs/contributors">
|
|
301
|
+
<img src="https://contrib.rocks/image?repo=bytebase/dbhub" />
|
|
302
|
+
</a>
|
|
303
|
+
|
|
284
304
|
## Star History
|
|
285
305
|
|
|
286
306
|
[](https://www.star-history.com/#bytebase/dbhub&Date)
|
package/dist/index.js
CHANGED
|
@@ -355,13 +355,13 @@ var PostgresConnector = class {
|
|
|
355
355
|
client.release();
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
|
-
async
|
|
358
|
+
async executeSQL(sql2) {
|
|
359
359
|
if (!this.pool) {
|
|
360
360
|
throw new Error("Not connected to database");
|
|
361
361
|
}
|
|
362
362
|
const client = await this.pool.connect();
|
|
363
363
|
try {
|
|
364
|
-
return await client.query(
|
|
364
|
+
return await client.query(sql2);
|
|
365
365
|
} finally {
|
|
366
366
|
client.release();
|
|
367
367
|
}
|
|
@@ -389,7 +389,7 @@ var SQLServerDSNParser = class {
|
|
|
389
389
|
const options = {};
|
|
390
390
|
for (const [key, value] of url.searchParams.entries()) {
|
|
391
391
|
if (key === "encrypt") {
|
|
392
|
-
options.encrypt = value;
|
|
392
|
+
options.encrypt = value === "true" ? true : value === "false" ? false : value;
|
|
393
393
|
} else if (key === "trustServerCertificate") {
|
|
394
394
|
options.trustServerCertificate = value === "true";
|
|
395
395
|
} else if (key === "connectTimeout") {
|
|
@@ -688,12 +688,12 @@ var SQLServerConnector = class {
|
|
|
688
688
|
throw new Error(`Failed to get stored procedure details: ${error.message}`);
|
|
689
689
|
}
|
|
690
690
|
}
|
|
691
|
-
async
|
|
691
|
+
async executeSQL(sql2) {
|
|
692
692
|
if (!this.connection) {
|
|
693
693
|
throw new Error("Not connected to SQL Server database");
|
|
694
694
|
}
|
|
695
695
|
try {
|
|
696
|
-
const result = await this.connection.request().query(
|
|
696
|
+
const result = await this.connection.request().query(sql2);
|
|
697
697
|
return {
|
|
698
698
|
rows: result.recordset || [],
|
|
699
699
|
fields: result.recordset && result.recordset.length > 0 ? Object.keys(result.recordset[0]).map((key) => ({
|
|
@@ -897,12 +897,12 @@ var SQLiteConnector = class {
|
|
|
897
897
|
"SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
|
|
898
898
|
);
|
|
899
899
|
}
|
|
900
|
-
async
|
|
900
|
+
async executeSQL(sql2) {
|
|
901
901
|
if (!this.db) {
|
|
902
902
|
throw new Error("Not connected to SQLite database");
|
|
903
903
|
}
|
|
904
904
|
try {
|
|
905
|
-
const rows = this.db.prepare(
|
|
905
|
+
const rows = this.db.prepare(sql2).all();
|
|
906
906
|
return { rows };
|
|
907
907
|
} catch (error) {
|
|
908
908
|
throw error;
|
|
@@ -1247,12 +1247,12 @@ var MySQLConnector = class {
|
|
|
1247
1247
|
const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
|
|
1248
1248
|
return rows[0].DB;
|
|
1249
1249
|
}
|
|
1250
|
-
async
|
|
1250
|
+
async executeSQL(sql2) {
|
|
1251
1251
|
if (!this.pool) {
|
|
1252
1252
|
throw new Error("Not connected to database");
|
|
1253
1253
|
}
|
|
1254
1254
|
try {
|
|
1255
|
-
const [rows, fields] = await this.pool.query(
|
|
1255
|
+
const [rows, fields] = await this.pool.query(sql2);
|
|
1256
1256
|
return { rows, fields };
|
|
1257
1257
|
} catch (error) {
|
|
1258
1258
|
console.error("Error executing query:", error);
|
|
@@ -1598,12 +1598,12 @@ var MariaDBConnector = class {
|
|
|
1598
1598
|
const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
|
|
1599
1599
|
return rows[0].DB;
|
|
1600
1600
|
}
|
|
1601
|
-
async
|
|
1601
|
+
async executeSQL(sql2) {
|
|
1602
1602
|
if (!this.pool) {
|
|
1603
1603
|
throw new Error("Not connected to database");
|
|
1604
1604
|
}
|
|
1605
1605
|
try {
|
|
1606
|
-
const [rows, fields] = await this.pool.query(
|
|
1606
|
+
const [rows, fields] = await this.pool.query(sql2);
|
|
1607
1607
|
return { rows, fields };
|
|
1608
1608
|
} catch (error) {
|
|
1609
1609
|
console.error("Error executing query:", error);
|
|
@@ -1614,6 +1614,466 @@ var MariaDBConnector = class {
|
|
|
1614
1614
|
var mariadbConnector = new MariaDBConnector();
|
|
1615
1615
|
ConnectorRegistry.register(mariadbConnector);
|
|
1616
1616
|
|
|
1617
|
+
// src/connectors/oracle/index.ts
|
|
1618
|
+
import oracledb from "oracledb";
|
|
1619
|
+
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
|
|
1620
|
+
var OracleConnector = class {
|
|
1621
|
+
// constructor(config: ConnectionConfig) { // Removed config
|
|
1622
|
+
constructor() {
|
|
1623
|
+
// Connector ID and Name are part of the Connector interface
|
|
1624
|
+
this.id = "oracle";
|
|
1625
|
+
this.name = "Oracle Database";
|
|
1626
|
+
this.pool = null;
|
|
1627
|
+
this.currentSchema = null;
|
|
1628
|
+
// Oracle DSN Parser implementation
|
|
1629
|
+
this.dsnParser = {
|
|
1630
|
+
parse: async (dsn) => {
|
|
1631
|
+
if (!this.dsnParser.isValidDSN(dsn)) {
|
|
1632
|
+
throw new Error(`Invalid Oracle DSN: ${dsn}`);
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
const url = new URL(dsn);
|
|
1636
|
+
const username = url.username;
|
|
1637
|
+
const password = url.password;
|
|
1638
|
+
const host = url.hostname;
|
|
1639
|
+
const port = url.port ? parseInt(url.port, 10) : 1521;
|
|
1640
|
+
let serviceName = url.pathname;
|
|
1641
|
+
if (serviceName.startsWith("/")) {
|
|
1642
|
+
serviceName = serviceName.substring(1);
|
|
1643
|
+
}
|
|
1644
|
+
const connectString = `${host}:${port}/${serviceName}`;
|
|
1645
|
+
const config = {
|
|
1646
|
+
user: username,
|
|
1647
|
+
password,
|
|
1648
|
+
connectString,
|
|
1649
|
+
poolMin: 0,
|
|
1650
|
+
poolMax: 10,
|
|
1651
|
+
poolIncrement: 1
|
|
1652
|
+
};
|
|
1653
|
+
url.searchParams.forEach((value, key) => {
|
|
1654
|
+
switch (key.toLowerCase()) {
|
|
1655
|
+
case "poolmin":
|
|
1656
|
+
config.poolMin = parseInt(value, 10);
|
|
1657
|
+
break;
|
|
1658
|
+
case "poolmax":
|
|
1659
|
+
config.poolMax = parseInt(value, 10);
|
|
1660
|
+
break;
|
|
1661
|
+
case "poolincrement":
|
|
1662
|
+
config.poolIncrement = parseInt(value, 10);
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
return config;
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
throw new Error(`Failed to parse Oracle DSN: ${error instanceof Error ? error.message : String(error)}`);
|
|
1669
|
+
}
|
|
1670
|
+
},
|
|
1671
|
+
getSampleDSN: () => {
|
|
1672
|
+
return "oracle://username:password@host:1521/service_name";
|
|
1673
|
+
},
|
|
1674
|
+
isValidDSN: (dsn) => {
|
|
1675
|
+
try {
|
|
1676
|
+
const url = new URL(dsn);
|
|
1677
|
+
return url.protocol === "oracle:";
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
return false;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
oracledb.autoCommit = true;
|
|
1684
|
+
}
|
|
1685
|
+
async connect(dsn, initializationScript) {
|
|
1686
|
+
try {
|
|
1687
|
+
const config = await this.dsnParser.parse(dsn);
|
|
1688
|
+
this.pool = await oracledb.createPool(config);
|
|
1689
|
+
const conn = await this.getConnection();
|
|
1690
|
+
try {
|
|
1691
|
+
const result = await conn.execute("SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') as SCHEMA FROM DUAL");
|
|
1692
|
+
if (result.rows && result.rows.length > 0) {
|
|
1693
|
+
this.currentSchema = result.rows[0].SCHEMA;
|
|
1694
|
+
}
|
|
1695
|
+
if (initializationScript) {
|
|
1696
|
+
await conn.execute(initializationScript);
|
|
1697
|
+
}
|
|
1698
|
+
} finally {
|
|
1699
|
+
await conn.close();
|
|
1700
|
+
}
|
|
1701
|
+
console.error("Successfully connected to Oracle database");
|
|
1702
|
+
if (this.currentSchema) {
|
|
1703
|
+
console.error(`Current schema: ${this.currentSchema}`);
|
|
1704
|
+
}
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
console.error("Failed to connect to Oracle database:", error);
|
|
1707
|
+
throw error;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
async disconnect() {
|
|
1711
|
+
if (this.pool) {
|
|
1712
|
+
try {
|
|
1713
|
+
await this.pool.close();
|
|
1714
|
+
this.pool = null;
|
|
1715
|
+
this.currentSchema = null;
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
console.error("Error disconnecting from Oracle:", error);
|
|
1718
|
+
throw error;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
async getSchemas() {
|
|
1723
|
+
try {
|
|
1724
|
+
const conn = await this.getConnection();
|
|
1725
|
+
try {
|
|
1726
|
+
const result = await conn.execute(
|
|
1727
|
+
`SELECT USERNAME AS SCHEMA_NAME
|
|
1728
|
+
FROM ALL_USERS
|
|
1729
|
+
ORDER BY USERNAME`
|
|
1730
|
+
);
|
|
1731
|
+
return result.rows?.map((row) => row.SCHEMA_NAME) || [];
|
|
1732
|
+
} finally {
|
|
1733
|
+
await conn.close();
|
|
1734
|
+
}
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
console.error("Error getting schemas from Oracle:", error);
|
|
1737
|
+
throw error;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
async getTables(schemaName) {
|
|
1741
|
+
try {
|
|
1742
|
+
const conn = await this.getConnection();
|
|
1743
|
+
try {
|
|
1744
|
+
const schema = schemaName || this.currentSchema;
|
|
1745
|
+
const result = await conn.execute(
|
|
1746
|
+
`SELECT TABLE_NAME
|
|
1747
|
+
FROM ALL_TABLES
|
|
1748
|
+
WHERE OWNER = :schema
|
|
1749
|
+
ORDER BY TABLE_NAME`,
|
|
1750
|
+
{ schema: schema?.toUpperCase() }
|
|
1751
|
+
);
|
|
1752
|
+
return result.rows?.map((row) => row.TABLE_NAME) || [];
|
|
1753
|
+
} finally {
|
|
1754
|
+
await conn.close();
|
|
1755
|
+
}
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
console.error("Error getting tables from Oracle:", error);
|
|
1758
|
+
throw error;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
async getTableColumns(tableName, schemaName) {
|
|
1762
|
+
try {
|
|
1763
|
+
const conn = await this.getConnection();
|
|
1764
|
+
try {
|
|
1765
|
+
const schema = schemaName || this.currentSchema;
|
|
1766
|
+
const result = await conn.execute(
|
|
1767
|
+
`SELECT
|
|
1768
|
+
COLUMN_NAME,
|
|
1769
|
+
DATA_TYPE,
|
|
1770
|
+
NULLABLE as IS_NULLABLE,
|
|
1771
|
+
DATA_DEFAULT as COLUMN_DEFAULT
|
|
1772
|
+
FROM ALL_TAB_COLUMNS
|
|
1773
|
+
WHERE OWNER = :schema
|
|
1774
|
+
AND TABLE_NAME = :tableName
|
|
1775
|
+
ORDER BY COLUMN_ID`,
|
|
1776
|
+
{
|
|
1777
|
+
schema: schema?.toUpperCase(),
|
|
1778
|
+
tableName: tableName.toUpperCase()
|
|
1779
|
+
}
|
|
1780
|
+
);
|
|
1781
|
+
return result.rows?.map((row) => ({
|
|
1782
|
+
column_name: row.COLUMN_NAME,
|
|
1783
|
+
data_type: row.DATA_TYPE,
|
|
1784
|
+
is_nullable: row.IS_NULLABLE === "Y" ? "YES" : "NO",
|
|
1785
|
+
column_default: row.COLUMN_DEFAULT
|
|
1786
|
+
})) || [];
|
|
1787
|
+
} finally {
|
|
1788
|
+
await conn.close();
|
|
1789
|
+
}
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
console.error("Error getting columns from Oracle:", error);
|
|
1792
|
+
throw error;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
// Method to ensure boolean return type
|
|
1796
|
+
ensureBoolean(value) {
|
|
1797
|
+
return value === true;
|
|
1798
|
+
}
|
|
1799
|
+
async getTableIndexes(tableName, schemaName) {
|
|
1800
|
+
try {
|
|
1801
|
+
const conn = await this.getConnection();
|
|
1802
|
+
try {
|
|
1803
|
+
const schema = schemaName || this.currentSchema;
|
|
1804
|
+
const indexesResult = await conn.execute(
|
|
1805
|
+
`SELECT
|
|
1806
|
+
i.INDEX_NAME,
|
|
1807
|
+
i.UNIQUENESS
|
|
1808
|
+
FROM ALL_INDEXES i
|
|
1809
|
+
WHERE i.OWNER = :schema
|
|
1810
|
+
AND i.TABLE_NAME = :tableName`,
|
|
1811
|
+
{
|
|
1812
|
+
schema: schema?.toUpperCase(),
|
|
1813
|
+
tableName: tableName.toUpperCase()
|
|
1814
|
+
}
|
|
1815
|
+
);
|
|
1816
|
+
if (!indexesResult.rows || indexesResult.rows.length === 0) {
|
|
1817
|
+
return [];
|
|
1818
|
+
}
|
|
1819
|
+
const indexes = [];
|
|
1820
|
+
for (const idx of indexesResult.rows) {
|
|
1821
|
+
const indexRow = idx;
|
|
1822
|
+
const indexName = indexRow.INDEX_NAME;
|
|
1823
|
+
const isUnique = indexRow.UNIQUENESS === "UNIQUE";
|
|
1824
|
+
const columnsResult = await conn.execute(
|
|
1825
|
+
`SELECT
|
|
1826
|
+
COLUMN_NAME
|
|
1827
|
+
FROM ALL_IND_COLUMNS
|
|
1828
|
+
WHERE INDEX_OWNER = :schema
|
|
1829
|
+
AND INDEX_NAME = :indexName
|
|
1830
|
+
ORDER BY COLUMN_POSITION`,
|
|
1831
|
+
{
|
|
1832
|
+
schema: schema?.toUpperCase(),
|
|
1833
|
+
indexName
|
|
1834
|
+
}
|
|
1835
|
+
);
|
|
1836
|
+
const columnNames = columnsResult.rows?.map((row) => row.COLUMN_NAME) || [];
|
|
1837
|
+
const pkResult = await conn.execute(
|
|
1838
|
+
`SELECT COUNT(*) AS IS_PK
|
|
1839
|
+
FROM ALL_CONSTRAINTS
|
|
1840
|
+
WHERE CONSTRAINT_TYPE = 'P'
|
|
1841
|
+
AND OWNER = :schema
|
|
1842
|
+
AND TABLE_NAME = :tableName
|
|
1843
|
+
AND INDEX_NAME = :indexName`,
|
|
1844
|
+
{
|
|
1845
|
+
schema: schema?.toUpperCase(),
|
|
1846
|
+
tableName: tableName.toUpperCase(),
|
|
1847
|
+
indexName
|
|
1848
|
+
}
|
|
1849
|
+
);
|
|
1850
|
+
const isPrimary = pkResult.rows && pkResult.rows.length > 0 && pkResult.rows[0].IS_PK > 0;
|
|
1851
|
+
indexes.push({
|
|
1852
|
+
index_name: indexName,
|
|
1853
|
+
column_names: columnNames,
|
|
1854
|
+
is_unique: isUnique,
|
|
1855
|
+
is_primary: !!isPrimary
|
|
1856
|
+
// Ensure boolean
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
return indexes;
|
|
1860
|
+
} finally {
|
|
1861
|
+
await conn.close();
|
|
1862
|
+
}
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
console.error("Error getting indexes from Oracle:", error);
|
|
1865
|
+
throw error;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
async tableExists(tableName, schemaName) {
|
|
1869
|
+
try {
|
|
1870
|
+
const conn = await this.getConnection();
|
|
1871
|
+
try {
|
|
1872
|
+
const schema = schemaName || this.currentSchema;
|
|
1873
|
+
const result = await conn.execute(
|
|
1874
|
+
`SELECT COUNT(*) AS COUNT
|
|
1875
|
+
FROM ALL_TABLES
|
|
1876
|
+
WHERE OWNER = :schema
|
|
1877
|
+
AND TABLE_NAME = :tableName`,
|
|
1878
|
+
{
|
|
1879
|
+
schema: schema?.toUpperCase(),
|
|
1880
|
+
tableName: tableName.toUpperCase()
|
|
1881
|
+
}
|
|
1882
|
+
);
|
|
1883
|
+
return !!(result.rows && result.rows.length > 0 && result.rows[0].COUNT > 0);
|
|
1884
|
+
} finally {
|
|
1885
|
+
await conn.close();
|
|
1886
|
+
}
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
console.error("Error checking table existence in Oracle:", error);
|
|
1889
|
+
throw error;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
async getTableSchema(tableName, schema) {
|
|
1893
|
+
return this.getTableColumns(tableName, schema);
|
|
1894
|
+
}
|
|
1895
|
+
async getStoredProcedures(schema) {
|
|
1896
|
+
try {
|
|
1897
|
+
const conn = await this.getConnection();
|
|
1898
|
+
try {
|
|
1899
|
+
const schemaName = schema || this.currentSchema;
|
|
1900
|
+
const result = await conn.execute(
|
|
1901
|
+
`SELECT OBJECT_NAME
|
|
1902
|
+
FROM ALL_OBJECTS
|
|
1903
|
+
WHERE OWNER = :schema
|
|
1904
|
+
AND OBJECT_TYPE IN ('PROCEDURE', 'FUNCTION')
|
|
1905
|
+
ORDER BY OBJECT_NAME`,
|
|
1906
|
+
{ schema: schemaName?.toUpperCase() }
|
|
1907
|
+
);
|
|
1908
|
+
return result.rows?.map((row) => row.OBJECT_NAME) || [];
|
|
1909
|
+
} finally {
|
|
1910
|
+
await conn.close();
|
|
1911
|
+
}
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
console.error("Error getting stored procedures from Oracle:", error);
|
|
1914
|
+
throw error;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
async getStoredProcedureDetail(procedureName, schema) {
|
|
1918
|
+
try {
|
|
1919
|
+
const conn = await this.getConnection();
|
|
1920
|
+
try {
|
|
1921
|
+
const schemaName = schema || this.currentSchema;
|
|
1922
|
+
const typeResult = await conn.execute(
|
|
1923
|
+
`SELECT OBJECT_TYPE
|
|
1924
|
+
FROM ALL_OBJECTS
|
|
1925
|
+
WHERE OWNER = :schema
|
|
1926
|
+
AND OBJECT_NAME = :procName`,
|
|
1927
|
+
{
|
|
1928
|
+
schema: schemaName?.toUpperCase(),
|
|
1929
|
+
procName: procedureName.toUpperCase()
|
|
1930
|
+
}
|
|
1931
|
+
);
|
|
1932
|
+
if (!typeResult.rows || typeResult.rows.length === 0) {
|
|
1933
|
+
throw new Error(`Procedure or function ${procedureName} not found`);
|
|
1934
|
+
}
|
|
1935
|
+
const objectType = typeResult.rows[0].OBJECT_TYPE;
|
|
1936
|
+
const isProcedure = objectType === "PROCEDURE";
|
|
1937
|
+
const sourceResult = await conn.execute(
|
|
1938
|
+
`SELECT TEXT
|
|
1939
|
+
FROM ALL_SOURCE
|
|
1940
|
+
WHERE OWNER = :schema
|
|
1941
|
+
AND NAME = :procName
|
|
1942
|
+
AND TYPE = :objectType
|
|
1943
|
+
ORDER BY LINE`,
|
|
1944
|
+
{
|
|
1945
|
+
schema: schemaName?.toUpperCase(),
|
|
1946
|
+
procName: procedureName.toUpperCase(),
|
|
1947
|
+
objectType
|
|
1948
|
+
}
|
|
1949
|
+
);
|
|
1950
|
+
let definition = "";
|
|
1951
|
+
if (sourceResult.rows && sourceResult.rows.length > 0) {
|
|
1952
|
+
definition = sourceResult.rows.map((row) => row.TEXT).join("");
|
|
1953
|
+
}
|
|
1954
|
+
const paramsResult = await conn.execute(
|
|
1955
|
+
`SELECT
|
|
1956
|
+
ARGUMENT_NAME,
|
|
1957
|
+
IN_OUT,
|
|
1958
|
+
DATA_TYPE,
|
|
1959
|
+
DATA_LENGTH,
|
|
1960
|
+
DATA_PRECISION,
|
|
1961
|
+
DATA_SCALE
|
|
1962
|
+
FROM ALL_ARGUMENTS
|
|
1963
|
+
WHERE OWNER = :schema
|
|
1964
|
+
AND OBJECT_NAME = :procName
|
|
1965
|
+
AND POSITION > 0
|
|
1966
|
+
ORDER BY SEQUENCE`,
|
|
1967
|
+
{
|
|
1968
|
+
schema: schemaName?.toUpperCase(),
|
|
1969
|
+
procName: procedureName.toUpperCase()
|
|
1970
|
+
}
|
|
1971
|
+
);
|
|
1972
|
+
let parameterList = "";
|
|
1973
|
+
let returnType = "";
|
|
1974
|
+
if (paramsResult.rows && paramsResult.rows.length > 0) {
|
|
1975
|
+
const params = paramsResult.rows.map((row) => {
|
|
1976
|
+
const argRow = row;
|
|
1977
|
+
if (argRow.IN_OUT === "OUT" && !isProcedure) {
|
|
1978
|
+
returnType = formatOracleDataType(
|
|
1979
|
+
argRow.DATA_TYPE,
|
|
1980
|
+
argRow.DATA_LENGTH,
|
|
1981
|
+
argRow.DATA_PRECISION,
|
|
1982
|
+
argRow.DATA_SCALE
|
|
1983
|
+
);
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
const paramType = formatOracleDataType(
|
|
1987
|
+
argRow.DATA_TYPE,
|
|
1988
|
+
argRow.DATA_LENGTH,
|
|
1989
|
+
argRow.DATA_PRECISION,
|
|
1990
|
+
argRow.DATA_SCALE
|
|
1991
|
+
);
|
|
1992
|
+
return `${argRow.ARGUMENT_NAME} ${argRow.IN_OUT} ${paramType}`;
|
|
1993
|
+
}).filter(Boolean);
|
|
1994
|
+
parameterList = params.join(", ");
|
|
1995
|
+
}
|
|
1996
|
+
return {
|
|
1997
|
+
procedure_name: procedureName,
|
|
1998
|
+
procedure_type: isProcedure ? "procedure" : "function",
|
|
1999
|
+
language: "PL/SQL",
|
|
2000
|
+
parameter_list: parameterList,
|
|
2001
|
+
return_type: returnType || void 0,
|
|
2002
|
+
definition: definition || void 0
|
|
2003
|
+
};
|
|
2004
|
+
} finally {
|
|
2005
|
+
await conn.close();
|
|
2006
|
+
}
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
console.error("Error getting stored procedure details from Oracle:", error);
|
|
2009
|
+
throw error;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
async executeSQL(sql2, params) {
|
|
2013
|
+
try {
|
|
2014
|
+
const conn = await this.getConnection();
|
|
2015
|
+
try {
|
|
2016
|
+
let bindParams = void 0;
|
|
2017
|
+
if (params && params.length > 0) {
|
|
2018
|
+
bindParams = {};
|
|
2019
|
+
for (let i = 0; i < params.length; i++) {
|
|
2020
|
+
bindParams[`param${i + 1}`] = params[i];
|
|
2021
|
+
}
|
|
2022
|
+
let paramIndex = 1;
|
|
2023
|
+
sql2 = sql2.replace(/\?/g, () => `:param${paramIndex++}`);
|
|
2024
|
+
}
|
|
2025
|
+
const options = {
|
|
2026
|
+
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
|
2027
|
+
autoCommit: true
|
|
2028
|
+
};
|
|
2029
|
+
const result = await conn.execute(sql2, bindParams || {}, options);
|
|
2030
|
+
return {
|
|
2031
|
+
rows: result.rows || [],
|
|
2032
|
+
rowCount: result.rows?.length || 0,
|
|
2033
|
+
fields: result.metaData?.map((col) => ({
|
|
2034
|
+
name: col.name,
|
|
2035
|
+
type: col.dbType?.toString() || "UNKNOWN"
|
|
2036
|
+
})) || []
|
|
2037
|
+
};
|
|
2038
|
+
} finally {
|
|
2039
|
+
await conn.close();
|
|
2040
|
+
}
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
console.error("Error executing query in Oracle:", error);
|
|
2043
|
+
throw error;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
// Helper method to get a connection from the pool
|
|
2047
|
+
async getConnection() {
|
|
2048
|
+
if (!this.pool) {
|
|
2049
|
+
throw new Error("Connection pool not initialized. Call connect() first.");
|
|
2050
|
+
}
|
|
2051
|
+
return this.pool.getConnection();
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
function formatOracleDataType(dataType, dataLength, dataPrecision, dataScale) {
|
|
2055
|
+
if (!dataType) {
|
|
2056
|
+
return "UNKNOWN";
|
|
2057
|
+
}
|
|
2058
|
+
switch (dataType.toUpperCase()) {
|
|
2059
|
+
case "VARCHAR2":
|
|
2060
|
+
case "CHAR":
|
|
2061
|
+
case "NVARCHAR2":
|
|
2062
|
+
case "NCHAR":
|
|
2063
|
+
return `${dataType}(${dataLength || ""})`;
|
|
2064
|
+
case "NUMBER":
|
|
2065
|
+
if (dataPrecision !== void 0 && dataScale !== void 0) {
|
|
2066
|
+
return `NUMBER(${dataPrecision}, ${dataScale})`;
|
|
2067
|
+
} else if (dataPrecision !== void 0) {
|
|
2068
|
+
return `NUMBER(${dataPrecision})`;
|
|
2069
|
+
}
|
|
2070
|
+
return "NUMBER";
|
|
2071
|
+
default:
|
|
2072
|
+
return dataType;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
ConnectorRegistry.register(new OracleConnector());
|
|
2076
|
+
|
|
1617
2077
|
// src/server.ts
|
|
1618
2078
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1619
2079
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
@@ -2212,29 +2672,30 @@ var allowedKeywords = {
|
|
|
2212
2672
|
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2213
2673
|
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2214
2674
|
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
2215
|
-
sqlserver: ["select", "with", "explain", "showplan"]
|
|
2675
|
+
sqlserver: ["select", "with", "explain", "showplan"],
|
|
2676
|
+
oracle: ["select", "with", "explain"]
|
|
2216
2677
|
};
|
|
2217
2678
|
|
|
2218
2679
|
// src/tools/execute-sql.ts
|
|
2219
2680
|
var executeSqlSchema = {
|
|
2220
|
-
|
|
2681
|
+
sql: z.string().describe("SQL query to execute (SELECT only)")
|
|
2221
2682
|
};
|
|
2222
|
-
function
|
|
2223
|
-
const
|
|
2224
|
-
const firstWord =
|
|
2683
|
+
function isReadOnlySQL(sql2, connectorType) {
|
|
2684
|
+
const normalizedSQL = sql2.trim().toLowerCase();
|
|
2685
|
+
const firstWord = normalizedSQL.split(/\s+/)[0];
|
|
2225
2686
|
const keywordList = allowedKeywords[connectorType] || allowedKeywords.default || [];
|
|
2226
2687
|
return keywordList.includes(firstWord);
|
|
2227
2688
|
}
|
|
2228
|
-
async function executeSqlToolHandler({
|
|
2689
|
+
async function executeSqlToolHandler({ sql: sql2 }, _extra) {
|
|
2229
2690
|
const connector = ConnectorManager.getCurrentConnector();
|
|
2230
2691
|
try {
|
|
2231
|
-
if (isReadOnlyMode() && !
|
|
2692
|
+
if (isReadOnlyMode() && !isReadOnlySQL(sql2, connector.id)) {
|
|
2232
2693
|
return createToolErrorResponse(
|
|
2233
2694
|
`Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`,
|
|
2234
2695
|
"READONLY_VIOLATION"
|
|
2235
2696
|
);
|
|
2236
2697
|
}
|
|
2237
|
-
const result = await connector.
|
|
2698
|
+
const result = await connector.executeSQL(sql2);
|
|
2238
2699
|
const responseData = {
|
|
2239
2700
|
rows: result.rows,
|
|
2240
2701
|
count: result.rows.length
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Universal Database MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -24,10 +24,12 @@
|
|
|
24
24
|
"mariadb": "^3.4.0",
|
|
25
25
|
"mssql": "^11.0.1",
|
|
26
26
|
"mysql2": "^3.13.0",
|
|
27
|
+
"oracledb": "^6.5.1",
|
|
27
28
|
"pg": "^8.13.3",
|
|
28
29
|
"zod": "^3.24.2"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
32
|
+
"@types/oracledb": "^6.6.0",
|
|
31
33
|
"@types/better-sqlite3": "^7.6.12",
|
|
32
34
|
"@types/express": "^4.17.21",
|
|
33
35
|
"@types/mssql": "^9.1.7",
|