@bytebase/dbhub 0.3.3 → 0.4.1

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 +69 -23
  2. package/dist/index.js +502 -18
  3. 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,28 @@ 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
+
101
+ ```bash
102
+ # Oracle example with thick mode for connecting to 11g or older
103
+ docker run --rm --init \
104
+ --name dbhub \
105
+ --publish 8080:8080 \
106
+ bytebase/dbhub-oracle-thick \
107
+ --transport sse \
108
+ --port 8080 \
109
+ --dsn "oracle://username:password@localhost:1521/service_name"
110
+ ```
111
+
88
112
  ### NPM
89
113
 
90
114
  ```bash
@@ -199,13 +223,35 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
199
223
 
200
224
  DBHub supports the following database connection string formats:
201
225
 
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:` |
226
+ | Database | DSN Format | Example |
227
+ | ---------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
228
+ | MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname` |
229
+ | MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname` |
230
+ | PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
231
+ | SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname` |
232
+ | SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite::memory:` |
233
+ | Oracle | `oracle://[user]:[password]@[host]:[port]/[service_name]` | `oracle://username:password@localhost:1521/service_name` |
234
+
235
+ #### Oracle
236
+
237
+ If you see the error "NJS-138: connections to this database server version are not supported by node-oracledb in Thin mode", you need to use Thick mode as described below.
238
+
239
+ ##### Docker
240
+
241
+ Use `bytebase/dbhub-oracle-thick` instead of `bytebase/dbhub` docker image.
242
+
243
+ ##### npx
244
+
245
+ 1. Download and install [Oracle Instant Client](https://www.oracle.com/database/technologies/instant-client/downloads.html) for your platform
246
+ 1. Set the `ORACLE_LIB_DIR` environment variable to the path of your Oracle Instant Client:
247
+
248
+ ```bash
249
+ # Set environment variable to Oracle Instant Client directory
250
+ export ORACLE_LIB_DIR=/path/to/instantclient_19_8
251
+
252
+ # Then run DBHub
253
+ npx @bytebase/dbhub --dsn "oracle://username:password@localhost:1521/service_name"
254
+ ```
209
255
 
210
256
  #### SQL Server
211
257
 
package/dist/index.js CHANGED
@@ -355,13 +355,13 @@ var PostgresConnector = class {
355
355
  client.release();
356
356
  }
357
357
  }
358
- async executeQuery(query) {
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(query);
364
+ return await client.query(sql2);
365
365
  } finally {
366
366
  client.release();
367
367
  }
@@ -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 executeQuery(query) {
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(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 executeQuery(query) {
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(query).all();
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 executeQuery(query) {
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(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 executeQuery(query) {
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(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,489 @@ 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
+ try {
1684
+ if (process.env.ORACLE_LIB_DIR) {
1685
+ oracledb.initOracleClient({ libDir: process.env.ORACLE_LIB_DIR });
1686
+ console.error("Oracle client initialized in Thick mode");
1687
+ } else {
1688
+ console.error("ORACLE_LIB_DIR not specified, will use Thin mode by default");
1689
+ }
1690
+ } catch (err) {
1691
+ console.error("Failed to initialize Oracle client:", err);
1692
+ }
1693
+ oracledb.autoCommit = true;
1694
+ }
1695
+ async connect(dsn, initializationScript) {
1696
+ try {
1697
+ const config = await this.dsnParser.parse(dsn);
1698
+ this.pool = await oracledb.createPool(config);
1699
+ const conn = await this.getConnection();
1700
+ try {
1701
+ const result = await conn.execute("SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') as SCHEMA FROM DUAL");
1702
+ if (result.rows && result.rows.length > 0) {
1703
+ this.currentSchema = result.rows[0].SCHEMA;
1704
+ }
1705
+ if (initializationScript) {
1706
+ await conn.execute(initializationScript);
1707
+ }
1708
+ } finally {
1709
+ await conn.close();
1710
+ }
1711
+ console.error("Successfully connected to Oracle database");
1712
+ if (this.currentSchema) {
1713
+ console.error(`Current schema: ${this.currentSchema}`);
1714
+ }
1715
+ } catch (error) {
1716
+ console.error("Failed to connect to Oracle database:", error);
1717
+ if (error instanceof Error && error.message.includes("NJS-138")) {
1718
+ const enhancedError = new Error(
1719
+ `${error.message}
1720
+
1721
+ This error occurs when your Oracle database version is not supported by node-oracledb in Thin mode.
1722
+ To resolve this, you need to use Thick mode:
1723
+ 1. Download Oracle Instant Client from https://www.oracle.com/database/technologies/instant-client/downloads.html
1724
+ 2. Set ORACLE_LIB_DIR environment variable to the path of your Oracle Instant Client
1725
+ Example: export ORACLE_LIB_DIR=/path/to/instantclient_19_8
1726
+ 3. Restart DBHub`
1727
+ );
1728
+ throw enhancedError;
1729
+ }
1730
+ throw error;
1731
+ }
1732
+ }
1733
+ async disconnect() {
1734
+ if (this.pool) {
1735
+ try {
1736
+ await this.pool.close();
1737
+ this.pool = null;
1738
+ this.currentSchema = null;
1739
+ } catch (error) {
1740
+ console.error("Error disconnecting from Oracle:", error);
1741
+ throw error;
1742
+ }
1743
+ }
1744
+ }
1745
+ async getSchemas() {
1746
+ try {
1747
+ const conn = await this.getConnection();
1748
+ try {
1749
+ const result = await conn.execute(
1750
+ `SELECT USERNAME AS SCHEMA_NAME
1751
+ FROM ALL_USERS
1752
+ ORDER BY USERNAME`
1753
+ );
1754
+ return result.rows?.map((row) => row.SCHEMA_NAME) || [];
1755
+ } finally {
1756
+ await conn.close();
1757
+ }
1758
+ } catch (error) {
1759
+ console.error("Error getting schemas from Oracle:", error);
1760
+ throw error;
1761
+ }
1762
+ }
1763
+ async getTables(schemaName) {
1764
+ try {
1765
+ const conn = await this.getConnection();
1766
+ try {
1767
+ const schema = schemaName || this.currentSchema;
1768
+ const result = await conn.execute(
1769
+ `SELECT TABLE_NAME
1770
+ FROM ALL_TABLES
1771
+ WHERE OWNER = :schema
1772
+ ORDER BY TABLE_NAME`,
1773
+ { schema: schema?.toUpperCase() }
1774
+ );
1775
+ return result.rows?.map((row) => row.TABLE_NAME) || [];
1776
+ } finally {
1777
+ await conn.close();
1778
+ }
1779
+ } catch (error) {
1780
+ console.error("Error getting tables from Oracle:", error);
1781
+ throw error;
1782
+ }
1783
+ }
1784
+ async getTableColumns(tableName, schemaName) {
1785
+ try {
1786
+ const conn = await this.getConnection();
1787
+ try {
1788
+ const schema = schemaName || this.currentSchema;
1789
+ const result = await conn.execute(
1790
+ `SELECT
1791
+ COLUMN_NAME,
1792
+ DATA_TYPE,
1793
+ NULLABLE as IS_NULLABLE,
1794
+ DATA_DEFAULT as COLUMN_DEFAULT
1795
+ FROM ALL_TAB_COLUMNS
1796
+ WHERE OWNER = :schema
1797
+ AND TABLE_NAME = :tableName
1798
+ ORDER BY COLUMN_ID`,
1799
+ {
1800
+ schema: schema?.toUpperCase(),
1801
+ tableName: tableName.toUpperCase()
1802
+ }
1803
+ );
1804
+ return result.rows?.map((row) => ({
1805
+ column_name: row.COLUMN_NAME,
1806
+ data_type: row.DATA_TYPE,
1807
+ is_nullable: row.IS_NULLABLE === "Y" ? "YES" : "NO",
1808
+ column_default: row.COLUMN_DEFAULT
1809
+ })) || [];
1810
+ } finally {
1811
+ await conn.close();
1812
+ }
1813
+ } catch (error) {
1814
+ console.error("Error getting columns from Oracle:", error);
1815
+ throw error;
1816
+ }
1817
+ }
1818
+ // Method to ensure boolean return type
1819
+ ensureBoolean(value) {
1820
+ return value === true;
1821
+ }
1822
+ async getTableIndexes(tableName, schemaName) {
1823
+ try {
1824
+ const conn = await this.getConnection();
1825
+ try {
1826
+ const schema = schemaName || this.currentSchema;
1827
+ const indexesResult = await conn.execute(
1828
+ `SELECT
1829
+ i.INDEX_NAME,
1830
+ i.UNIQUENESS
1831
+ FROM ALL_INDEXES i
1832
+ WHERE i.OWNER = :schema
1833
+ AND i.TABLE_NAME = :tableName`,
1834
+ {
1835
+ schema: schema?.toUpperCase(),
1836
+ tableName: tableName.toUpperCase()
1837
+ }
1838
+ );
1839
+ if (!indexesResult.rows || indexesResult.rows.length === 0) {
1840
+ return [];
1841
+ }
1842
+ const indexes = [];
1843
+ for (const idx of indexesResult.rows) {
1844
+ const indexRow = idx;
1845
+ const indexName = indexRow.INDEX_NAME;
1846
+ const isUnique = indexRow.UNIQUENESS === "UNIQUE";
1847
+ const columnsResult = await conn.execute(
1848
+ `SELECT
1849
+ COLUMN_NAME
1850
+ FROM ALL_IND_COLUMNS
1851
+ WHERE INDEX_OWNER = :schema
1852
+ AND INDEX_NAME = :indexName
1853
+ ORDER BY COLUMN_POSITION`,
1854
+ {
1855
+ schema: schema?.toUpperCase(),
1856
+ indexName
1857
+ }
1858
+ );
1859
+ const columnNames = columnsResult.rows?.map((row) => row.COLUMN_NAME) || [];
1860
+ const pkResult = await conn.execute(
1861
+ `SELECT COUNT(*) AS IS_PK
1862
+ FROM ALL_CONSTRAINTS
1863
+ WHERE CONSTRAINT_TYPE = 'P'
1864
+ AND OWNER = :schema
1865
+ AND TABLE_NAME = :tableName
1866
+ AND INDEX_NAME = :indexName`,
1867
+ {
1868
+ schema: schema?.toUpperCase(),
1869
+ tableName: tableName.toUpperCase(),
1870
+ indexName
1871
+ }
1872
+ );
1873
+ const isPrimary = pkResult.rows && pkResult.rows.length > 0 && pkResult.rows[0].IS_PK > 0;
1874
+ indexes.push({
1875
+ index_name: indexName,
1876
+ column_names: columnNames,
1877
+ is_unique: isUnique,
1878
+ is_primary: !!isPrimary
1879
+ // Ensure boolean
1880
+ });
1881
+ }
1882
+ return indexes;
1883
+ } finally {
1884
+ await conn.close();
1885
+ }
1886
+ } catch (error) {
1887
+ console.error("Error getting indexes from Oracle:", error);
1888
+ throw error;
1889
+ }
1890
+ }
1891
+ async tableExists(tableName, schemaName) {
1892
+ try {
1893
+ const conn = await this.getConnection();
1894
+ try {
1895
+ const schema = schemaName || this.currentSchema;
1896
+ const result = await conn.execute(
1897
+ `SELECT COUNT(*) AS COUNT
1898
+ FROM ALL_TABLES
1899
+ WHERE OWNER = :schema
1900
+ AND TABLE_NAME = :tableName`,
1901
+ {
1902
+ schema: schema?.toUpperCase(),
1903
+ tableName: tableName.toUpperCase()
1904
+ }
1905
+ );
1906
+ return !!(result.rows && result.rows.length > 0 && result.rows[0].COUNT > 0);
1907
+ } finally {
1908
+ await conn.close();
1909
+ }
1910
+ } catch (error) {
1911
+ console.error("Error checking table existence in Oracle:", error);
1912
+ throw error;
1913
+ }
1914
+ }
1915
+ async getTableSchema(tableName, schema) {
1916
+ return this.getTableColumns(tableName, schema);
1917
+ }
1918
+ async getStoredProcedures(schema) {
1919
+ try {
1920
+ const conn = await this.getConnection();
1921
+ try {
1922
+ const schemaName = schema || this.currentSchema;
1923
+ const result = await conn.execute(
1924
+ `SELECT OBJECT_NAME
1925
+ FROM ALL_OBJECTS
1926
+ WHERE OWNER = :schema
1927
+ AND OBJECT_TYPE IN ('PROCEDURE', 'FUNCTION')
1928
+ ORDER BY OBJECT_NAME`,
1929
+ { schema: schemaName?.toUpperCase() }
1930
+ );
1931
+ return result.rows?.map((row) => row.OBJECT_NAME) || [];
1932
+ } finally {
1933
+ await conn.close();
1934
+ }
1935
+ } catch (error) {
1936
+ console.error("Error getting stored procedures from Oracle:", error);
1937
+ throw error;
1938
+ }
1939
+ }
1940
+ async getStoredProcedureDetail(procedureName, schema) {
1941
+ try {
1942
+ const conn = await this.getConnection();
1943
+ try {
1944
+ const schemaName = schema || this.currentSchema;
1945
+ const typeResult = await conn.execute(
1946
+ `SELECT OBJECT_TYPE
1947
+ FROM ALL_OBJECTS
1948
+ WHERE OWNER = :schema
1949
+ AND OBJECT_NAME = :procName`,
1950
+ {
1951
+ schema: schemaName?.toUpperCase(),
1952
+ procName: procedureName.toUpperCase()
1953
+ }
1954
+ );
1955
+ if (!typeResult.rows || typeResult.rows.length === 0) {
1956
+ throw new Error(`Procedure or function ${procedureName} not found`);
1957
+ }
1958
+ const objectType = typeResult.rows[0].OBJECT_TYPE;
1959
+ const isProcedure = objectType === "PROCEDURE";
1960
+ const sourceResult = await conn.execute(
1961
+ `SELECT TEXT
1962
+ FROM ALL_SOURCE
1963
+ WHERE OWNER = :schema
1964
+ AND NAME = :procName
1965
+ AND TYPE = :objectType
1966
+ ORDER BY LINE`,
1967
+ {
1968
+ schema: schemaName?.toUpperCase(),
1969
+ procName: procedureName.toUpperCase(),
1970
+ objectType
1971
+ }
1972
+ );
1973
+ let definition = "";
1974
+ if (sourceResult.rows && sourceResult.rows.length > 0) {
1975
+ definition = sourceResult.rows.map((row) => row.TEXT).join("");
1976
+ }
1977
+ const paramsResult = await conn.execute(
1978
+ `SELECT
1979
+ ARGUMENT_NAME,
1980
+ IN_OUT,
1981
+ DATA_TYPE,
1982
+ DATA_LENGTH,
1983
+ DATA_PRECISION,
1984
+ DATA_SCALE
1985
+ FROM ALL_ARGUMENTS
1986
+ WHERE OWNER = :schema
1987
+ AND OBJECT_NAME = :procName
1988
+ AND POSITION > 0
1989
+ ORDER BY SEQUENCE`,
1990
+ {
1991
+ schema: schemaName?.toUpperCase(),
1992
+ procName: procedureName.toUpperCase()
1993
+ }
1994
+ );
1995
+ let parameterList = "";
1996
+ let returnType = "";
1997
+ if (paramsResult.rows && paramsResult.rows.length > 0) {
1998
+ const params = paramsResult.rows.map((row) => {
1999
+ const argRow = row;
2000
+ if (argRow.IN_OUT === "OUT" && !isProcedure) {
2001
+ returnType = formatOracleDataType(
2002
+ argRow.DATA_TYPE,
2003
+ argRow.DATA_LENGTH,
2004
+ argRow.DATA_PRECISION,
2005
+ argRow.DATA_SCALE
2006
+ );
2007
+ return null;
2008
+ }
2009
+ const paramType = formatOracleDataType(
2010
+ argRow.DATA_TYPE,
2011
+ argRow.DATA_LENGTH,
2012
+ argRow.DATA_PRECISION,
2013
+ argRow.DATA_SCALE
2014
+ );
2015
+ return `${argRow.ARGUMENT_NAME} ${argRow.IN_OUT} ${paramType}`;
2016
+ }).filter(Boolean);
2017
+ parameterList = params.join(", ");
2018
+ }
2019
+ return {
2020
+ procedure_name: procedureName,
2021
+ procedure_type: isProcedure ? "procedure" : "function",
2022
+ language: "PL/SQL",
2023
+ parameter_list: parameterList,
2024
+ return_type: returnType || void 0,
2025
+ definition: definition || void 0
2026
+ };
2027
+ } finally {
2028
+ await conn.close();
2029
+ }
2030
+ } catch (error) {
2031
+ console.error("Error getting stored procedure details from Oracle:", error);
2032
+ throw error;
2033
+ }
2034
+ }
2035
+ async executeSQL(sql2, params) {
2036
+ try {
2037
+ const conn = await this.getConnection();
2038
+ try {
2039
+ let bindParams = void 0;
2040
+ if (params && params.length > 0) {
2041
+ bindParams = {};
2042
+ for (let i = 0; i < params.length; i++) {
2043
+ bindParams[`param${i + 1}`] = params[i];
2044
+ }
2045
+ let paramIndex = 1;
2046
+ sql2 = sql2.replace(/\?/g, () => `:param${paramIndex++}`);
2047
+ }
2048
+ const options = {
2049
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
2050
+ autoCommit: true
2051
+ };
2052
+ const result = await conn.execute(sql2, bindParams || {}, options);
2053
+ return {
2054
+ rows: result.rows || [],
2055
+ rowCount: result.rows?.length || 0,
2056
+ fields: result.metaData?.map((col) => ({
2057
+ name: col.name,
2058
+ type: col.dbType?.toString() || "UNKNOWN"
2059
+ })) || []
2060
+ };
2061
+ } finally {
2062
+ await conn.close();
2063
+ }
2064
+ } catch (error) {
2065
+ console.error("Error executing query in Oracle:", error);
2066
+ throw error;
2067
+ }
2068
+ }
2069
+ // Helper method to get a connection from the pool
2070
+ async getConnection() {
2071
+ if (!this.pool) {
2072
+ throw new Error("Connection pool not initialized. Call connect() first.");
2073
+ }
2074
+ return this.pool.getConnection();
2075
+ }
2076
+ };
2077
+ function formatOracleDataType(dataType, dataLength, dataPrecision, dataScale) {
2078
+ if (!dataType) {
2079
+ return "UNKNOWN";
2080
+ }
2081
+ switch (dataType.toUpperCase()) {
2082
+ case "VARCHAR2":
2083
+ case "CHAR":
2084
+ case "NVARCHAR2":
2085
+ case "NCHAR":
2086
+ return `${dataType}(${dataLength || ""})`;
2087
+ case "NUMBER":
2088
+ if (dataPrecision !== void 0 && dataScale !== void 0) {
2089
+ return `NUMBER(${dataPrecision}, ${dataScale})`;
2090
+ } else if (dataPrecision !== void 0) {
2091
+ return `NUMBER(${dataPrecision})`;
2092
+ }
2093
+ return "NUMBER";
2094
+ default:
2095
+ return dataType;
2096
+ }
2097
+ }
2098
+ ConnectorRegistry.register(new OracleConnector());
2099
+
1617
2100
  // src/server.ts
1618
2101
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
1619
2102
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
@@ -2212,29 +2695,30 @@ var allowedKeywords = {
2212
2695
  mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2213
2696
  mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2214
2697
  sqlite: ["select", "with", "explain", "analyze", "pragma"],
2215
- sqlserver: ["select", "with", "explain", "showplan"]
2698
+ sqlserver: ["select", "with", "explain", "showplan"],
2699
+ oracle: ["select", "with", "explain"]
2216
2700
  };
2217
2701
 
2218
2702
  // src/tools/execute-sql.ts
2219
2703
  var executeSqlSchema = {
2220
- query: z.string().describe("SQL query to execute (SELECT only)")
2704
+ sql: z.string().describe("SQL query to execute (SELECT only)")
2221
2705
  };
2222
- function isReadOnlyQuery(query, connectorType) {
2223
- const normalizedQuery = query.trim().toLowerCase();
2224
- const firstWord = normalizedQuery.split(/\s+/)[0];
2706
+ function isReadOnlySQL(sql2, connectorType) {
2707
+ const normalizedSQL = sql2.trim().toLowerCase();
2708
+ const firstWord = normalizedSQL.split(/\s+/)[0];
2225
2709
  const keywordList = allowedKeywords[connectorType] || allowedKeywords.default || [];
2226
2710
  return keywordList.includes(firstWord);
2227
2711
  }
2228
- async function executeSqlToolHandler({ query }, _extra) {
2712
+ async function executeSqlToolHandler({ sql: sql2 }, _extra) {
2229
2713
  const connector = ConnectorManager.getCurrentConnector();
2230
2714
  try {
2231
- if (isReadOnlyMode() && !isReadOnlyQuery(query, connector.id)) {
2715
+ if (isReadOnlyMode() && !isReadOnlySQL(sql2, connector.id)) {
2232
2716
  return createToolErrorResponse(
2233
2717
  `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`,
2234
2718
  "READONLY_VIOLATION"
2235
2719
  );
2236
2720
  }
2237
- const result = await connector.executeQuery(query);
2721
+ const result = await connector.executeSQL(sql2);
2238
2722
  const responseData = {
2239
2723
  rows: result.rows,
2240
2724
  count: result.rows.length
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
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",