@bytebase/dbhub 0.13.0 → 0.13.2
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 +4 -3
- package/dist/{chunk-WSLDVMBA.js → chunk-KBVJEDZF.js} +144 -92
- package/dist/index.js +91 -59
- package/dist/public/index.html +1 -1
- package/dist/{registry-GJGPWR3I.js → registry-AWAIN6WO.js} +1 -2
- package/package.json +20 -19
- package/dist/builtin-tools-SVENYBIA.js +0 -10
- package/dist/chunk-D23WQQY7.js +0 -13
- package/dist/chunk-VWZF5OAJ.js +0 -112
- package/dist/custom-tool-registry-EW3KOBGC.js +0 -7
package/README.md
CHANGED
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
MCP Clients MCP Server Databases
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
DBHub is a
|
|
33
|
+
DBHub is a Minimal Database MCP Server implementing the Model Context Protocol (MCP) server interface. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
|
|
34
34
|
|
|
35
|
-
- **
|
|
35
|
+
- **Minimal Design**: Just two general MCP tools (execute_sql, search_objects) for token-efficient operations, plus support for custom tools
|
|
36
|
+
- **Multi-Database Support**: Single interface for PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite
|
|
36
37
|
- **Secure Access**: Read-only mode, SSH tunneling, and SSL/TLS encryption support
|
|
37
|
-
- **
|
|
38
|
+
- **Multiple Connections**: Connect to multiple databases simultaneously with TOML configuration
|
|
38
39
|
- **Production-Ready**: Row limiting, lock timeout control, and connection pooling
|
|
39
40
|
- **MCP Native**: Full implementation of Model Context Protocol with comprehensive tools
|
|
40
41
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/tools/builtin-tools.ts
|
|
2
|
+
var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
|
|
3
|
+
var BUILTIN_TOOL_SEARCH_OBJECTS = "search_objects";
|
|
4
|
+
var BUILTIN_TOOLS = [
|
|
5
|
+
BUILTIN_TOOL_EXECUTE_SQL,
|
|
6
|
+
BUILTIN_TOOL_SEARCH_OBJECTS
|
|
7
|
+
];
|
|
5
8
|
|
|
6
9
|
// src/connectors/interface.ts
|
|
7
10
|
var _ConnectorRegistry = class _ConnectorRegistry {
|
|
@@ -1588,37 +1591,64 @@ function mapArgumentsToArray(parameters, args) {
|
|
|
1588
1591
|
});
|
|
1589
1592
|
}
|
|
1590
1593
|
|
|
1591
|
-
// src/tools/
|
|
1592
|
-
var
|
|
1593
|
-
constructor() {
|
|
1594
|
-
this.
|
|
1595
|
-
this.initialized = false;
|
|
1594
|
+
// src/tools/registry.ts
|
|
1595
|
+
var ToolRegistry = class {
|
|
1596
|
+
constructor(config) {
|
|
1597
|
+
this.toolsBySource = this.buildRegistry(config);
|
|
1596
1598
|
}
|
|
1597
1599
|
/**
|
|
1598
|
-
*
|
|
1599
|
-
* @param toolConfigs Tool definitions from TOML config
|
|
1600
|
-
* @throws Error if validation fails
|
|
1600
|
+
* Check if a tool name is a built-in tool
|
|
1601
1601
|
*/
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1602
|
+
isBuiltinTool(toolName) {
|
|
1603
|
+
return BUILTIN_TOOLS.includes(toolName);
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Validate a custom tool parameter definition
|
|
1607
|
+
*/
|
|
1608
|
+
validateParameter(toolName, param) {
|
|
1609
|
+
if (!param.name || param.name.trim() === "") {
|
|
1610
|
+
throw new Error(`Tool '${toolName}' has parameter missing 'name' field`);
|
|
1605
1611
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1612
|
+
if (!param.type) {
|
|
1613
|
+
throw new Error(
|
|
1614
|
+
`Tool '${toolName}', parameter '${param.name}' missing 'type' field`
|
|
1615
|
+
);
|
|
1610
1616
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1617
|
+
const validTypes = ["string", "integer", "float", "boolean", "array"];
|
|
1618
|
+
if (!validTypes.includes(param.type)) {
|
|
1619
|
+
throw new Error(
|
|
1620
|
+
`Tool '${toolName}', parameter '${param.name}' has invalid type '${param.type}'. Valid types: ${validTypes.join(", ")}`
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
if (!param.description || param.description.trim() === "") {
|
|
1624
|
+
throw new Error(
|
|
1625
|
+
`Tool '${toolName}', parameter '${param.name}' missing 'description' field`
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
if (param.allowed_values) {
|
|
1629
|
+
if (!Array.isArray(param.allowed_values)) {
|
|
1630
|
+
throw new Error(
|
|
1631
|
+
`Tool '${toolName}', parameter '${param.name}': allowed_values must be an array`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
if (param.allowed_values.length === 0) {
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
`Tool '${toolName}', parameter '${param.name}': allowed_values cannot be empty`
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (param.default !== void 0 && param.allowed_values) {
|
|
1641
|
+
if (!param.allowed_values.includes(param.default)) {
|
|
1642
|
+
throw new Error(
|
|
1643
|
+
`Tool '${toolName}', parameter '${param.name}': default value '${param.default}' is not in allowed_values: ${param.allowed_values.join(", ")}`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1613
1646
|
}
|
|
1614
|
-
this.initialized = true;
|
|
1615
1647
|
}
|
|
1616
1648
|
/**
|
|
1617
|
-
* Validate a tool configuration
|
|
1618
|
-
* @param toolConfig Tool configuration to validate
|
|
1619
|
-
* @throws Error if validation fails
|
|
1649
|
+
* Validate a custom tool configuration
|
|
1620
1650
|
*/
|
|
1621
|
-
|
|
1651
|
+
validateCustomTool(toolConfig, availableSources) {
|
|
1622
1652
|
if (!toolConfig.name || toolConfig.name.trim() === "") {
|
|
1623
1653
|
throw new Error("Tool definition missing required field: name");
|
|
1624
1654
|
}
|
|
@@ -1637,7 +1667,6 @@ var CustomToolRegistry = class {
|
|
|
1637
1667
|
`Tool '${toolConfig.name}' missing required field: statement`
|
|
1638
1668
|
);
|
|
1639
1669
|
}
|
|
1640
|
-
const availableSources = ConnectorManager.getAvailableSourceIds();
|
|
1641
1670
|
if (!availableSources.includes(toolConfig.source)) {
|
|
1642
1671
|
throw new Error(
|
|
1643
1672
|
`Tool '${toolConfig.name}' references unknown source '${toolConfig.source}'. Available sources: ${availableSources.join(", ")}`
|
|
@@ -1650,11 +1679,6 @@ var CustomToolRegistry = class {
|
|
|
1650
1679
|
);
|
|
1651
1680
|
}
|
|
1652
1681
|
}
|
|
1653
|
-
if (this.tools.some((t) => t.name === toolConfig.name)) {
|
|
1654
|
-
throw new Error(
|
|
1655
|
-
`Duplicate tool name '${toolConfig.name}'. Tool names must be unique.`
|
|
1656
|
-
);
|
|
1657
|
-
}
|
|
1658
1682
|
const sourceConfig = ConnectorManager.getSourceConfig(toolConfig.source);
|
|
1659
1683
|
const connectorType = sourceConfig.type;
|
|
1660
1684
|
try {
|
|
@@ -1673,85 +1697,109 @@ var CustomToolRegistry = class {
|
|
|
1673
1697
|
this.validateParameter(toolConfig.name, param);
|
|
1674
1698
|
}
|
|
1675
1699
|
}
|
|
1676
|
-
this.tools.push(toolConfig);
|
|
1677
1700
|
}
|
|
1678
1701
|
/**
|
|
1679
|
-
*
|
|
1680
|
-
* @param toolName Name of the tool (for error messages)
|
|
1681
|
-
* @param param Parameter configuration to validate
|
|
1682
|
-
* @throws Error if validation fails
|
|
1702
|
+
* Build the internal registry mapping sources to their enabled tools
|
|
1683
1703
|
*/
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
);
|
|
1698
|
-
}
|
|
1699
|
-
if (!param.description || param.description.trim() === "") {
|
|
1700
|
-
throw new Error(
|
|
1701
|
-
`Tool '${toolName}', parameter '${param.name}' missing 'description' field`
|
|
1702
|
-
);
|
|
1703
|
-
}
|
|
1704
|
-
if (param.allowed_values) {
|
|
1705
|
-
if (!Array.isArray(param.allowed_values)) {
|
|
1706
|
-
throw new Error(
|
|
1707
|
-
`Tool '${toolName}', parameter '${param.name}': allowed_values must be an array`
|
|
1708
|
-
);
|
|
1709
|
-
}
|
|
1710
|
-
if (param.allowed_values.length === 0) {
|
|
1711
|
-
throw new Error(
|
|
1712
|
-
`Tool '${toolName}', parameter '${param.name}': allowed_values cannot be empty`
|
|
1713
|
-
);
|
|
1704
|
+
buildRegistry(config) {
|
|
1705
|
+
const registry = /* @__PURE__ */ new Map();
|
|
1706
|
+
const availableSources = config.sources.map((s) => s.id);
|
|
1707
|
+
const customToolNames = /* @__PURE__ */ new Set();
|
|
1708
|
+
for (const tool of config.tools || []) {
|
|
1709
|
+
if (!this.isBuiltinTool(tool.name)) {
|
|
1710
|
+
this.validateCustomTool(tool, availableSources);
|
|
1711
|
+
if (customToolNames.has(tool.name)) {
|
|
1712
|
+
throw new Error(
|
|
1713
|
+
`Duplicate tool name '${tool.name}'. Tool names must be unique.`
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
customToolNames.add(tool.name);
|
|
1714
1717
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1718
|
+
const existing = registry.get(tool.source) || [];
|
|
1719
|
+
existing.push(tool);
|
|
1720
|
+
registry.set(tool.source, existing);
|
|
1721
|
+
}
|
|
1722
|
+
for (const source of config.sources) {
|
|
1723
|
+
if (!registry.has(source.id)) {
|
|
1724
|
+
const defaultTools = BUILTIN_TOOLS.map((name) => {
|
|
1725
|
+
if (name === "execute_sql") {
|
|
1726
|
+
return { name: "execute_sql", source: source.id };
|
|
1727
|
+
} else {
|
|
1728
|
+
return { name: "search_objects", source: source.id };
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
registry.set(source.id, defaultTools);
|
|
1721
1732
|
}
|
|
1722
1733
|
}
|
|
1734
|
+
return registry;
|
|
1723
1735
|
}
|
|
1724
1736
|
/**
|
|
1725
|
-
* Get all
|
|
1726
|
-
* @returns Array of tool configurations
|
|
1737
|
+
* Get all enabled tool configs for a specific source
|
|
1727
1738
|
*/
|
|
1728
|
-
|
|
1729
|
-
return
|
|
1739
|
+
getEnabledToolConfigs(sourceId) {
|
|
1740
|
+
return this.toolsBySource.get(sourceId) || [];
|
|
1730
1741
|
}
|
|
1731
1742
|
/**
|
|
1732
|
-
* Get a specific
|
|
1733
|
-
*
|
|
1734
|
-
* @returns Tool configuration or undefined if not found
|
|
1743
|
+
* Get built-in tool configuration for a specific source
|
|
1744
|
+
* Returns undefined if tool is not enabled or not a built-in
|
|
1735
1745
|
*/
|
|
1736
|
-
|
|
1737
|
-
|
|
1746
|
+
getBuiltinToolConfig(toolName, sourceId) {
|
|
1747
|
+
if (!this.isBuiltinTool(toolName)) {
|
|
1748
|
+
return void 0;
|
|
1749
|
+
}
|
|
1750
|
+
const tools = this.getEnabledToolConfigs(sourceId);
|
|
1751
|
+
return tools.find((t) => t.name === toolName);
|
|
1738
1752
|
}
|
|
1739
1753
|
/**
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1754
|
+
* Get all unique tools across all sources (for tools/list response)
|
|
1755
|
+
* Returns the union of all enabled tools
|
|
1742
1756
|
*/
|
|
1743
|
-
|
|
1744
|
-
|
|
1757
|
+
getAllTools() {
|
|
1758
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1759
|
+
const result = [];
|
|
1760
|
+
for (const tools of this.toolsBySource.values()) {
|
|
1761
|
+
for (const tool of tools) {
|
|
1762
|
+
if (!seen.has(tool.name)) {
|
|
1763
|
+
seen.add(tool.name);
|
|
1764
|
+
result.push(tool);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return result;
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Get all custom tools (non-builtin) across all sources
|
|
1772
|
+
*/
|
|
1773
|
+
getCustomTools() {
|
|
1774
|
+
return this.getAllTools().filter((tool) => !this.isBuiltinTool(tool.name));
|
|
1745
1775
|
}
|
|
1746
1776
|
/**
|
|
1747
|
-
*
|
|
1777
|
+
* Get all built-in tool names that are enabled across any source
|
|
1748
1778
|
*/
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
this.
|
|
1779
|
+
getEnabledBuiltinToolNames() {
|
|
1780
|
+
const enabledBuiltins = /* @__PURE__ */ new Set();
|
|
1781
|
+
for (const tools of this.toolsBySource.values()) {
|
|
1782
|
+
for (const tool of tools) {
|
|
1783
|
+
if (this.isBuiltinTool(tool.name)) {
|
|
1784
|
+
enabledBuiltins.add(tool.name);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return Array.from(enabledBuiltins);
|
|
1752
1789
|
}
|
|
1753
1790
|
};
|
|
1754
|
-
var
|
|
1791
|
+
var globalRegistry = null;
|
|
1792
|
+
function initializeToolRegistry(config) {
|
|
1793
|
+
globalRegistry = new ToolRegistry(config);
|
|
1794
|
+
}
|
|
1795
|
+
function getToolRegistry() {
|
|
1796
|
+
if (!globalRegistry) {
|
|
1797
|
+
throw new Error(
|
|
1798
|
+
"Tool registry not initialized. Call initializeToolRegistry first."
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
return globalRegistry;
|
|
1802
|
+
}
|
|
1755
1803
|
|
|
1756
1804
|
export {
|
|
1757
1805
|
ConnectorRegistry,
|
|
@@ -1766,8 +1814,12 @@ export {
|
|
|
1766
1814
|
resolvePort,
|
|
1767
1815
|
redactDSN,
|
|
1768
1816
|
resolveSourceConfigs,
|
|
1817
|
+
BUILTIN_TOOL_EXECUTE_SQL,
|
|
1818
|
+
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
1769
1819
|
buildDSNFromSource,
|
|
1770
1820
|
ConnectorManager,
|
|
1771
1821
|
mapArgumentsToArray,
|
|
1772
|
-
|
|
1822
|
+
ToolRegistry,
|
|
1823
|
+
initializeToolRegistry,
|
|
1824
|
+
getToolRegistry
|
|
1773
1825
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
3
|
+
BUILTIN_TOOL_EXECUTE_SQL,
|
|
4
|
+
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
6
5
|
ConnectorManager,
|
|
7
6
|
ConnectorRegistry,
|
|
8
7
|
SafeURL,
|
|
9
8
|
buildDSNFromSource,
|
|
10
|
-
customToolRegistry,
|
|
11
9
|
getDatabaseTypeFromDSN,
|
|
12
10
|
getDefaultPortForType,
|
|
11
|
+
getToolRegistry,
|
|
13
12
|
isDemoMode,
|
|
14
13
|
mapArgumentsToArray,
|
|
15
14
|
obfuscateDSNPassword,
|
|
@@ -19,11 +18,7 @@ import {
|
|
|
19
18
|
resolveSourceConfigs,
|
|
20
19
|
resolveTransport,
|
|
21
20
|
stripCommentsAndStrings
|
|
22
|
-
} from "./chunk-
|
|
23
|
-
import {
|
|
24
|
-
BUILTIN_TOOL_EXECUTE_SQL,
|
|
25
|
-
BUILTIN_TOOL_SEARCH_OBJECTS
|
|
26
|
-
} from "./chunk-D23WQQY7.js";
|
|
21
|
+
} from "./chunk-KBVJEDZF.js";
|
|
27
22
|
|
|
28
23
|
// src/connectors/postgres/index.ts
|
|
29
24
|
import pg from "pg";
|
|
@@ -1332,6 +1327,16 @@ Expected: ${expectedFormat}`
|
|
|
1332
1327
|
if (connectionTimeoutSeconds !== void 0) {
|
|
1333
1328
|
config2.connectTimeout = connectionTimeoutSeconds * 1e3;
|
|
1334
1329
|
}
|
|
1330
|
+
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
1331
|
+
config2.authPlugins = {
|
|
1332
|
+
mysql_clear_password: () => () => {
|
|
1333
|
+
return Buffer.from(url.password + "\0");
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
if (config2.ssl === void 0) {
|
|
1337
|
+
config2.ssl = { rejectUnauthorized: false };
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1335
1340
|
return config2;
|
|
1336
1341
|
} catch (error) {
|
|
1337
1342
|
throw new Error(
|
|
@@ -1735,6 +1740,11 @@ Expected: ${expectedFormat}`
|
|
|
1735
1740
|
}
|
|
1736
1741
|
}
|
|
1737
1742
|
});
|
|
1743
|
+
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
1744
|
+
if (config2.ssl === void 0) {
|
|
1745
|
+
config2.ssl = { rejectUnauthorized: false };
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1738
1748
|
return config2;
|
|
1739
1749
|
} catch (error) {
|
|
1740
1750
|
throw new Error(
|
|
@@ -2298,7 +2308,8 @@ import { z as z2 } from "zod";
|
|
|
2298
2308
|
var searchDatabaseObjectsSchema = {
|
|
2299
2309
|
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Type of database object to search for"),
|
|
2300
2310
|
pattern: z2.string().optional().default("%").describe("Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."),
|
|
2301
|
-
schema: z2.string().optional().describe("Filter results to a specific schema/database"),
|
|
2311
|
+
schema: z2.string().optional().describe("Filter results to a specific schema/database (exact match)"),
|
|
2312
|
+
table: z2.string().optional().describe("Filter to a specific table (exact match). Requires schema parameter. Only applies to columns and indexes."),
|
|
2302
2313
|
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Level of detail to return: names (minimal), summary (with metadata), full (complete structure)"),
|
|
2303
2314
|
limit: z2.number().int().positive().max(1e3).default(100).describe("Maximum number of results to return (default: 100, max: 1000)")
|
|
2304
2315
|
};
|
|
@@ -2421,7 +2432,7 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
2421
2432
|
}
|
|
2422
2433
|
return results;
|
|
2423
2434
|
}
|
|
2424
|
-
async function searchColumns(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2435
|
+
async function searchColumns(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
2425
2436
|
const regex = likePatternToRegex(pattern);
|
|
2426
2437
|
const results = [];
|
|
2427
2438
|
let schemasToSearch;
|
|
@@ -2433,8 +2444,13 @@ async function searchColumns(connector, pattern, schemaFilter, detailLevel, limi
|
|
|
2433
2444
|
for (const schemaName of schemasToSearch) {
|
|
2434
2445
|
if (results.length >= limit) break;
|
|
2435
2446
|
try {
|
|
2436
|
-
|
|
2437
|
-
|
|
2447
|
+
let tablesToSearch;
|
|
2448
|
+
if (tableFilter) {
|
|
2449
|
+
tablesToSearch = [tableFilter];
|
|
2450
|
+
} else {
|
|
2451
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
2452
|
+
}
|
|
2453
|
+
for (const tableName of tablesToSearch) {
|
|
2438
2454
|
if (results.length >= limit) break;
|
|
2439
2455
|
try {
|
|
2440
2456
|
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
@@ -2516,7 +2532,7 @@ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, l
|
|
|
2516
2532
|
}
|
|
2517
2533
|
return results;
|
|
2518
2534
|
}
|
|
2519
|
-
async function searchIndexes(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2535
|
+
async function searchIndexes(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
2520
2536
|
const regex = likePatternToRegex(pattern);
|
|
2521
2537
|
const results = [];
|
|
2522
2538
|
let schemasToSearch;
|
|
@@ -2528,8 +2544,13 @@ async function searchIndexes(connector, pattern, schemaFilter, detailLevel, limi
|
|
|
2528
2544
|
for (const schemaName of schemasToSearch) {
|
|
2529
2545
|
if (results.length >= limit) break;
|
|
2530
2546
|
try {
|
|
2531
|
-
|
|
2532
|
-
|
|
2547
|
+
let tablesToSearch;
|
|
2548
|
+
if (tableFilter) {
|
|
2549
|
+
tablesToSearch = [tableFilter];
|
|
2550
|
+
} else {
|
|
2551
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
2552
|
+
}
|
|
2553
|
+
for (const tableName of tablesToSearch) {
|
|
2533
2554
|
if (results.length >= limit) break;
|
|
2534
2555
|
try {
|
|
2535
2556
|
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
@@ -2569,11 +2590,26 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
2569
2590
|
object_type,
|
|
2570
2591
|
pattern = "%",
|
|
2571
2592
|
schema,
|
|
2593
|
+
table,
|
|
2572
2594
|
detail_level = "names",
|
|
2573
2595
|
limit = 100
|
|
2574
2596
|
} = args;
|
|
2575
2597
|
try {
|
|
2576
2598
|
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
2599
|
+
if (table) {
|
|
2600
|
+
if (!schema) {
|
|
2601
|
+
return createToolErrorResponse(
|
|
2602
|
+
"The 'table' parameter requires 'schema' to be specified",
|
|
2603
|
+
"SCHEMA_REQUIRED"
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
if (!["column", "index"].includes(object_type)) {
|
|
2607
|
+
return createToolErrorResponse(
|
|
2608
|
+
`The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`,
|
|
2609
|
+
"INVALID_TABLE_FILTER"
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2577
2613
|
if (schema) {
|
|
2578
2614
|
const schemas = await connector.getSchemas();
|
|
2579
2615
|
if (!schemas.includes(schema)) {
|
|
@@ -2592,13 +2628,13 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
2592
2628
|
results = await searchTables(connector, pattern, schema, detail_level, limit);
|
|
2593
2629
|
break;
|
|
2594
2630
|
case "column":
|
|
2595
|
-
results = await searchColumns(connector, pattern, schema, detail_level, limit);
|
|
2631
|
+
results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
|
|
2596
2632
|
break;
|
|
2597
2633
|
case "procedure":
|
|
2598
2634
|
results = await searchProcedures(connector, pattern, schema, detail_level, limit);
|
|
2599
2635
|
break;
|
|
2600
2636
|
case "index":
|
|
2601
|
-
results = await searchIndexes(connector, pattern, schema, detail_level, limit);
|
|
2637
|
+
results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
|
|
2602
2638
|
break;
|
|
2603
2639
|
default:
|
|
2604
2640
|
return createToolErrorResponse(
|
|
@@ -2610,6 +2646,7 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
2610
2646
|
object_type,
|
|
2611
2647
|
pattern,
|
|
2612
2648
|
schema,
|
|
2649
|
+
table,
|
|
2613
2650
|
detail_level,
|
|
2614
2651
|
count: results.length,
|
|
2615
2652
|
results,
|
|
@@ -2679,9 +2716,8 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
2679
2716
|
// In readonly mode, queries are more predictable (though still not strictly idempotent due to data changes)
|
|
2680
2717
|
// In write mode, queries are definitely not idempotent
|
|
2681
2718
|
idempotentHint: false,
|
|
2682
|
-
//
|
|
2683
|
-
|
|
2684
|
-
openWorldHint: isReadonly
|
|
2719
|
+
// Database operations are always against internal/closed systems, not open-world
|
|
2720
|
+
openWorldHint: false
|
|
2685
2721
|
};
|
|
2686
2722
|
return {
|
|
2687
2723
|
name: toolName,
|
|
@@ -2711,21 +2747,22 @@ function customParamsToToolParams(params) {
|
|
|
2711
2747
|
description: param.description
|
|
2712
2748
|
}));
|
|
2713
2749
|
}
|
|
2714
|
-
function
|
|
2715
|
-
const tools = [];
|
|
2716
|
-
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2717
|
-
const dbType = sourceConfig.type;
|
|
2718
|
-
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2719
|
-
const isDefault = sourceIds[0] === sourceId;
|
|
2750
|
+
function buildExecuteSqlTool(sourceId) {
|
|
2720
2751
|
const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
|
|
2721
2752
|
const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
|
|
2722
|
-
|
|
2753
|
+
return {
|
|
2723
2754
|
name: executeSqlMetadata.name,
|
|
2724
2755
|
description: executeSqlMetadata.description,
|
|
2725
2756
|
parameters: executeSqlParameters
|
|
2726
|
-
}
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
function buildSearchObjectsTool(sourceId) {
|
|
2760
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2761
|
+
const dbType = sourceConfig.type;
|
|
2762
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2763
|
+
const isDefault = sourceIds[0] === sourceId;
|
|
2727
2764
|
const searchMetadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
|
|
2728
|
-
|
|
2765
|
+
return {
|
|
2729
2766
|
name: searchMetadata.name,
|
|
2730
2767
|
description: searchMetadata.description,
|
|
2731
2768
|
parameters: [
|
|
@@ -2760,19 +2797,27 @@ function getToolsForSource(sourceId) {
|
|
|
2760
2797
|
description: "Maximum number of results to return (default: 100, max: 1000)"
|
|
2761
2798
|
}
|
|
2762
2799
|
]
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
function buildCustomTool(toolConfig) {
|
|
2803
|
+
return {
|
|
2804
|
+
name: toolConfig.name,
|
|
2805
|
+
description: toolConfig.description,
|
|
2806
|
+
parameters: customParamsToToolParams(toolConfig.parameters)
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
function getToolsForSource(sourceId) {
|
|
2810
|
+
const registry = getToolRegistry();
|
|
2811
|
+
const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
|
|
2812
|
+
return enabledToolConfigs.map((toolConfig) => {
|
|
2813
|
+
if (toolConfig.name === "execute_sql") {
|
|
2814
|
+
return buildExecuteSqlTool(sourceId);
|
|
2815
|
+
} else if (toolConfig.name === "search_objects") {
|
|
2816
|
+
return buildSearchObjectsTool(sourceId);
|
|
2817
|
+
} else {
|
|
2818
|
+
return buildCustomTool(toolConfig);
|
|
2773
2819
|
}
|
|
2774
|
-
}
|
|
2775
|
-
return tools;
|
|
2820
|
+
});
|
|
2776
2821
|
}
|
|
2777
2822
|
|
|
2778
2823
|
// src/tools/custom-tool-handler.ts
|
|
@@ -2894,7 +2939,7 @@ function registerTools(server) {
|
|
|
2894
2939
|
}
|
|
2895
2940
|
const registry = getToolRegistry();
|
|
2896
2941
|
for (const sourceId of sourceIds) {
|
|
2897
|
-
const enabledTools = registry.
|
|
2942
|
+
const enabledTools = registry.getEnabledToolConfigs(sourceId);
|
|
2898
2943
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2899
2944
|
const dbType = sourceConfig.type;
|
|
2900
2945
|
const isDefault = sourceIds[0] === sourceId;
|
|
@@ -2933,7 +2978,7 @@ function registerSearchObjectsTool(server, sourceId, dbType, isDefault) {
|
|
|
2933
2978
|
readOnlyHint: true,
|
|
2934
2979
|
destructiveHint: false,
|
|
2935
2980
|
idempotentHint: true,
|
|
2936
|
-
openWorldHint:
|
|
2981
|
+
openWorldHint: false
|
|
2937
2982
|
}
|
|
2938
2983
|
},
|
|
2939
2984
|
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
@@ -3201,7 +3246,7 @@ function generateBanner(version, modes = []) {
|
|
|
3201
3246
|
| |__| | |_) | | | | |_| | |_) |
|
|
3202
3247
|
|_____/|____/|_| |_|\\__,_|_.__/
|
|
3203
3248
|
|
|
3204
|
-
v${version}${modeText} -
|
|
3249
|
+
v${version}${modeText} - Minimal Database MCP Server
|
|
3205
3250
|
`;
|
|
3206
3251
|
}
|
|
3207
3252
|
async function main() {
|
|
@@ -3241,25 +3286,12 @@ See documentation for more details on configuring database connections.
|
|
|
3241
3286
|
console.error(` - ${source.id}: ${redactDSN(dsn)}`);
|
|
3242
3287
|
}
|
|
3243
3288
|
await connectorManager.connectWithSources(sources);
|
|
3244
|
-
const { initializeToolRegistry } = await import("./registry-
|
|
3289
|
+
const { initializeToolRegistry } = await import("./registry-AWAIN6WO.js");
|
|
3245
3290
|
initializeToolRegistry({
|
|
3246
3291
|
sources: sourceConfigsData.sources,
|
|
3247
3292
|
tools: sourceConfigsData.tools
|
|
3248
3293
|
});
|
|
3249
3294
|
console.error("Tool registry initialized");
|
|
3250
|
-
if (sourceConfigsData.tools && sourceConfigsData.tools.length > 0) {
|
|
3251
|
-
const { customToolRegistry: customToolRegistry2 } = await import("./custom-tool-registry-EW3KOBGC.js");
|
|
3252
|
-
const { BUILTIN_TOOLS } = await import("./builtin-tools-SVENYBIA.js");
|
|
3253
|
-
if (!customToolRegistry2.isInitialized()) {
|
|
3254
|
-
const customTools = sourceConfigsData.tools.filter(
|
|
3255
|
-
(tool) => !BUILTIN_TOOLS.includes(tool.name)
|
|
3256
|
-
);
|
|
3257
|
-
if (customTools.length > 0) {
|
|
3258
|
-
customToolRegistry2.initialize(customTools);
|
|
3259
|
-
console.error(`Custom tool registry initialized with ${customTools.length} tool(s)`);
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
3295
|
const createServer = () => {
|
|
3264
3296
|
const server = new McpServer({
|
|
3265
3297
|
name: SERVER_NAME,
|
package/dist/public/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>DBHub -
|
|
7
|
+
<title>DBHub - Minimal Database MCP Server</title>
|
|
8
8
|
<script type="module" crossorigin src="/assets/index-hd88eD9m.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-gVrYRID4.css">
|
|
10
10
|
</head>
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.13.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.13.2",
|
|
4
|
+
"mcpName": "io.github.bytebase/dbhub",
|
|
5
|
+
"description": "Minimal Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
7
8
|
"url": "https://github.com/bytebase/dbhub.git"
|
|
@@ -16,6 +17,21 @@
|
|
|
16
17
|
"LICENSE",
|
|
17
18
|
"README.md"
|
|
18
19
|
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "pnpm run generate:api-types && tsup && cd frontend && pnpm run build",
|
|
22
|
+
"build:backend": "pnpm run generate:api-types && tsup",
|
|
23
|
+
"build:frontend": "cd frontend && pnpm run build",
|
|
24
|
+
"start": "node dist/index.js",
|
|
25
|
+
"dev": "concurrently --kill-others \"pnpm run dev:backend\" \"pnpm run dev:frontend\"",
|
|
26
|
+
"dev:backend": "NODE_ENV=development tsx src/index.ts --transport=http",
|
|
27
|
+
"dev:frontend": "cd frontend && pnpm run dev",
|
|
28
|
+
"crossdev": "cross-env NODE_ENV=development tsx src/index.ts",
|
|
29
|
+
"generate:api-types": "openapi-typescript src/api/openapi.yaml -o src/api/openapi.d.ts",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:unit": "vitest run --project unit",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"test:integration": "vitest run --project integration"
|
|
34
|
+
},
|
|
19
35
|
"keywords": [],
|
|
20
36
|
"author": "",
|
|
21
37
|
"license": "MIT",
|
|
@@ -67,20 +83,5 @@
|
|
|
67
83
|
},
|
|
68
84
|
"include": [
|
|
69
85
|
"src/**/*"
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
"build": "pnpm run generate:api-types && tsup && cd frontend && pnpm run build",
|
|
73
|
-
"build:backend": "pnpm run generate:api-types && tsup",
|
|
74
|
-
"build:frontend": "cd frontend && pnpm run build",
|
|
75
|
-
"start": "node dist/index.js",
|
|
76
|
-
"dev": "concurrently --kill-others \"pnpm run dev:backend\" \"pnpm run dev:frontend\"",
|
|
77
|
-
"dev:backend": "NODE_ENV=development tsx src/index.ts --transport=http",
|
|
78
|
-
"dev:frontend": "cd frontend && pnpm run dev",
|
|
79
|
-
"crossdev": "cross-env NODE_ENV=development tsx src/index.ts",
|
|
80
|
-
"generate:api-types": "openapi-typescript src/api/openapi.yaml -o src/api/openapi.d.ts",
|
|
81
|
-
"test": "vitest run",
|
|
82
|
-
"test:unit": "vitest run --project unit",
|
|
83
|
-
"test:watch": "vitest",
|
|
84
|
-
"test:integration": "vitest run --project integration"
|
|
85
|
-
}
|
|
86
|
-
}
|
|
86
|
+
]
|
|
87
|
+
}
|
package/dist/chunk-D23WQQY7.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// src/tools/builtin-tools.ts
|
|
2
|
-
var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
|
|
3
|
-
var BUILTIN_TOOL_SEARCH_OBJECTS = "search_objects";
|
|
4
|
-
var BUILTIN_TOOLS = [
|
|
5
|
-
BUILTIN_TOOL_EXECUTE_SQL,
|
|
6
|
-
BUILTIN_TOOL_SEARCH_OBJECTS
|
|
7
|
-
];
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
BUILTIN_TOOL_EXECUTE_SQL,
|
|
11
|
-
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
12
|
-
BUILTIN_TOOLS
|
|
13
|
-
};
|
package/dist/chunk-VWZF5OAJ.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BUILTIN_TOOLS
|
|
3
|
-
} from "./chunk-D23WQQY7.js";
|
|
4
|
-
|
|
5
|
-
// src/tools/registry.ts
|
|
6
|
-
var ToolRegistry = class {
|
|
7
|
-
constructor(config) {
|
|
8
|
-
this.toolsBySource = this.buildRegistry(config);
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Check if a tool name is a built-in tool
|
|
12
|
-
*/
|
|
13
|
-
isBuiltinTool(toolName) {
|
|
14
|
-
return BUILTIN_TOOLS.includes(toolName);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Build the internal registry mapping sources to their enabled tools
|
|
18
|
-
*/
|
|
19
|
-
buildRegistry(config) {
|
|
20
|
-
const registry = /* @__PURE__ */ new Map();
|
|
21
|
-
for (const tool of config.tools || []) {
|
|
22
|
-
const existing = registry.get(tool.source) || [];
|
|
23
|
-
existing.push(tool);
|
|
24
|
-
registry.set(tool.source, existing);
|
|
25
|
-
}
|
|
26
|
-
for (const source of config.sources) {
|
|
27
|
-
if (!registry.has(source.id)) {
|
|
28
|
-
const defaultTools = BUILTIN_TOOLS.map((name) => {
|
|
29
|
-
if (name === "execute_sql") {
|
|
30
|
-
return { name: "execute_sql", source: source.id };
|
|
31
|
-
} else {
|
|
32
|
-
return { name: "search_objects", source: source.id };
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
registry.set(source.id, defaultTools);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return registry;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Get all tools enabled for a specific source
|
|
42
|
-
*/
|
|
43
|
-
getToolsForSource(sourceId) {
|
|
44
|
-
return this.toolsBySource.get(sourceId) || [];
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Get built-in tool configuration for a specific source
|
|
48
|
-
* Returns undefined if tool is not enabled or not a built-in
|
|
49
|
-
*/
|
|
50
|
-
getBuiltinToolConfig(toolName, sourceId) {
|
|
51
|
-
if (!this.isBuiltinTool(toolName)) {
|
|
52
|
-
return void 0;
|
|
53
|
-
}
|
|
54
|
-
const tools = this.getToolsForSource(sourceId);
|
|
55
|
-
return tools.find((t) => t.name === toolName);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Get all unique tools across all sources (for tools/list response)
|
|
59
|
-
* Returns the union of all enabled tools
|
|
60
|
-
*/
|
|
61
|
-
getAllTools() {
|
|
62
|
-
const seen = /* @__PURE__ */ new Set();
|
|
63
|
-
const result = [];
|
|
64
|
-
for (const tools of this.toolsBySource.values()) {
|
|
65
|
-
for (const tool of tools) {
|
|
66
|
-
if (!seen.has(tool.name)) {
|
|
67
|
-
seen.add(tool.name);
|
|
68
|
-
result.push(tool);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Get all custom tools (non-builtin) across all sources
|
|
76
|
-
*/
|
|
77
|
-
getCustomTools() {
|
|
78
|
-
return this.getAllTools().filter((tool) => !this.isBuiltinTool(tool.name));
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Get all built-in tool names that are enabled across any source
|
|
82
|
-
*/
|
|
83
|
-
getEnabledBuiltinToolNames() {
|
|
84
|
-
const enabledBuiltins = /* @__PURE__ */ new Set();
|
|
85
|
-
for (const tools of this.toolsBySource.values()) {
|
|
86
|
-
for (const tool of tools) {
|
|
87
|
-
if (this.isBuiltinTool(tool.name)) {
|
|
88
|
-
enabledBuiltins.add(tool.name);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return Array.from(enabledBuiltins);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
var globalRegistry = null;
|
|
96
|
-
function initializeToolRegistry(config) {
|
|
97
|
-
globalRegistry = new ToolRegistry(config);
|
|
98
|
-
}
|
|
99
|
-
function getToolRegistry() {
|
|
100
|
-
if (!globalRegistry) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
"Tool registry not initialized. Call initializeToolRegistry first."
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
return globalRegistry;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export {
|
|
109
|
-
ToolRegistry,
|
|
110
|
-
initializeToolRegistry,
|
|
111
|
-
getToolRegistry
|
|
112
|
-
};
|