@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.
Files changed (3) hide show
  1. package/README.md +43 -23
  2. package/dist/index.js +480 -19
  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,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 | 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:` |
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
  [![Star History Chart](https://api.star-history.com/svg?repos=bytebase/dbhub&type=Date)](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 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
  }
@@ -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 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,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
- query: z.string().describe("SQL query to execute (SELECT only)")
2681
+ sql: z.string().describe("SQL query to execute (SELECT only)")
2221
2682
  };
2222
- function isReadOnlyQuery(query, connectorType) {
2223
- const normalizedQuery = query.trim().toLowerCase();
2224
- const firstWord = normalizedQuery.split(/\s+/)[0];
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({ query }, _extra) {
2689
+ async function executeSqlToolHandler({ sql: sql2 }, _extra) {
2229
2690
  const connector = ConnectorManager.getCurrentConnector();
2230
2691
  try {
2231
- if (isReadOnlyMode() && !isReadOnlyQuery(query, connector.id)) {
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.executeQuery(query);
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.2",
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",