@bytebase/dbhub 0.13.0 → 0.13.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.
- package/README.md +4 -3
- package/dist/{chunk-WSLDVMBA.js → chunk-KBVJEDZF.js} +144 -92
- package/dist/index.js +76 -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";
|
|
@@ -2298,7 +2293,8 @@ import { z as z2 } from "zod";
|
|
|
2298
2293
|
var searchDatabaseObjectsSchema = {
|
|
2299
2294
|
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Type of database object to search for"),
|
|
2300
2295
|
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"),
|
|
2296
|
+
schema: z2.string().optional().describe("Filter results to a specific schema/database (exact match)"),
|
|
2297
|
+
table: z2.string().optional().describe("Filter to a specific table (exact match). Requires schema parameter. Only applies to columns and indexes."),
|
|
2302
2298
|
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Level of detail to return: names (minimal), summary (with metadata), full (complete structure)"),
|
|
2303
2299
|
limit: z2.number().int().positive().max(1e3).default(100).describe("Maximum number of results to return (default: 100, max: 1000)")
|
|
2304
2300
|
};
|
|
@@ -2421,7 +2417,7 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
2421
2417
|
}
|
|
2422
2418
|
return results;
|
|
2423
2419
|
}
|
|
2424
|
-
async function searchColumns(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2420
|
+
async function searchColumns(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
2425
2421
|
const regex = likePatternToRegex(pattern);
|
|
2426
2422
|
const results = [];
|
|
2427
2423
|
let schemasToSearch;
|
|
@@ -2433,8 +2429,13 @@ async function searchColumns(connector, pattern, schemaFilter, detailLevel, limi
|
|
|
2433
2429
|
for (const schemaName of schemasToSearch) {
|
|
2434
2430
|
if (results.length >= limit) break;
|
|
2435
2431
|
try {
|
|
2436
|
-
|
|
2437
|
-
|
|
2432
|
+
let tablesToSearch;
|
|
2433
|
+
if (tableFilter) {
|
|
2434
|
+
tablesToSearch = [tableFilter];
|
|
2435
|
+
} else {
|
|
2436
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
2437
|
+
}
|
|
2438
|
+
for (const tableName of tablesToSearch) {
|
|
2438
2439
|
if (results.length >= limit) break;
|
|
2439
2440
|
try {
|
|
2440
2441
|
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
@@ -2516,7 +2517,7 @@ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, l
|
|
|
2516
2517
|
}
|
|
2517
2518
|
return results;
|
|
2518
2519
|
}
|
|
2519
|
-
async function searchIndexes(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2520
|
+
async function searchIndexes(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
2520
2521
|
const regex = likePatternToRegex(pattern);
|
|
2521
2522
|
const results = [];
|
|
2522
2523
|
let schemasToSearch;
|
|
@@ -2528,8 +2529,13 @@ async function searchIndexes(connector, pattern, schemaFilter, detailLevel, limi
|
|
|
2528
2529
|
for (const schemaName of schemasToSearch) {
|
|
2529
2530
|
if (results.length >= limit) break;
|
|
2530
2531
|
try {
|
|
2531
|
-
|
|
2532
|
-
|
|
2532
|
+
let tablesToSearch;
|
|
2533
|
+
if (tableFilter) {
|
|
2534
|
+
tablesToSearch = [tableFilter];
|
|
2535
|
+
} else {
|
|
2536
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
2537
|
+
}
|
|
2538
|
+
for (const tableName of tablesToSearch) {
|
|
2533
2539
|
if (results.length >= limit) break;
|
|
2534
2540
|
try {
|
|
2535
2541
|
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
@@ -2569,11 +2575,26 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
2569
2575
|
object_type,
|
|
2570
2576
|
pattern = "%",
|
|
2571
2577
|
schema,
|
|
2578
|
+
table,
|
|
2572
2579
|
detail_level = "names",
|
|
2573
2580
|
limit = 100
|
|
2574
2581
|
} = args;
|
|
2575
2582
|
try {
|
|
2576
2583
|
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
2584
|
+
if (table) {
|
|
2585
|
+
if (!schema) {
|
|
2586
|
+
return createToolErrorResponse(
|
|
2587
|
+
"The 'table' parameter requires 'schema' to be specified",
|
|
2588
|
+
"SCHEMA_REQUIRED"
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2591
|
+
if (!["column", "index"].includes(object_type)) {
|
|
2592
|
+
return createToolErrorResponse(
|
|
2593
|
+
`The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`,
|
|
2594
|
+
"INVALID_TABLE_FILTER"
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2577
2598
|
if (schema) {
|
|
2578
2599
|
const schemas = await connector.getSchemas();
|
|
2579
2600
|
if (!schemas.includes(schema)) {
|
|
@@ -2592,13 +2613,13 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
2592
2613
|
results = await searchTables(connector, pattern, schema, detail_level, limit);
|
|
2593
2614
|
break;
|
|
2594
2615
|
case "column":
|
|
2595
|
-
results = await searchColumns(connector, pattern, schema, detail_level, limit);
|
|
2616
|
+
results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
|
|
2596
2617
|
break;
|
|
2597
2618
|
case "procedure":
|
|
2598
2619
|
results = await searchProcedures(connector, pattern, schema, detail_level, limit);
|
|
2599
2620
|
break;
|
|
2600
2621
|
case "index":
|
|
2601
|
-
results = await searchIndexes(connector, pattern, schema, detail_level, limit);
|
|
2622
|
+
results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
|
|
2602
2623
|
break;
|
|
2603
2624
|
default:
|
|
2604
2625
|
return createToolErrorResponse(
|
|
@@ -2610,6 +2631,7 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
2610
2631
|
object_type,
|
|
2611
2632
|
pattern,
|
|
2612
2633
|
schema,
|
|
2634
|
+
table,
|
|
2613
2635
|
detail_level,
|
|
2614
2636
|
count: results.length,
|
|
2615
2637
|
results,
|
|
@@ -2679,9 +2701,8 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
2679
2701
|
// In readonly mode, queries are more predictable (though still not strictly idempotent due to data changes)
|
|
2680
2702
|
// In write mode, queries are definitely not idempotent
|
|
2681
2703
|
idempotentHint: false,
|
|
2682
|
-
//
|
|
2683
|
-
|
|
2684
|
-
openWorldHint: isReadonly
|
|
2704
|
+
// Database operations are always against internal/closed systems, not open-world
|
|
2705
|
+
openWorldHint: false
|
|
2685
2706
|
};
|
|
2686
2707
|
return {
|
|
2687
2708
|
name: toolName,
|
|
@@ -2711,21 +2732,22 @@ function customParamsToToolParams(params) {
|
|
|
2711
2732
|
description: param.description
|
|
2712
2733
|
}));
|
|
2713
2734
|
}
|
|
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;
|
|
2735
|
+
function buildExecuteSqlTool(sourceId) {
|
|
2720
2736
|
const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
|
|
2721
2737
|
const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
|
|
2722
|
-
|
|
2738
|
+
return {
|
|
2723
2739
|
name: executeSqlMetadata.name,
|
|
2724
2740
|
description: executeSqlMetadata.description,
|
|
2725
2741
|
parameters: executeSqlParameters
|
|
2726
|
-
}
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
function buildSearchObjectsTool(sourceId) {
|
|
2745
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2746
|
+
const dbType = sourceConfig.type;
|
|
2747
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2748
|
+
const isDefault = sourceIds[0] === sourceId;
|
|
2727
2749
|
const searchMetadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
|
|
2728
|
-
|
|
2750
|
+
return {
|
|
2729
2751
|
name: searchMetadata.name,
|
|
2730
2752
|
description: searchMetadata.description,
|
|
2731
2753
|
parameters: [
|
|
@@ -2760,19 +2782,27 @@ function getToolsForSource(sourceId) {
|
|
|
2760
2782
|
description: "Maximum number of results to return (default: 100, max: 1000)"
|
|
2761
2783
|
}
|
|
2762
2784
|
]
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
function buildCustomTool(toolConfig) {
|
|
2788
|
+
return {
|
|
2789
|
+
name: toolConfig.name,
|
|
2790
|
+
description: toolConfig.description,
|
|
2791
|
+
parameters: customParamsToToolParams(toolConfig.parameters)
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
function getToolsForSource(sourceId) {
|
|
2795
|
+
const registry = getToolRegistry();
|
|
2796
|
+
const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
|
|
2797
|
+
return enabledToolConfigs.map((toolConfig) => {
|
|
2798
|
+
if (toolConfig.name === "execute_sql") {
|
|
2799
|
+
return buildExecuteSqlTool(sourceId);
|
|
2800
|
+
} else if (toolConfig.name === "search_objects") {
|
|
2801
|
+
return buildSearchObjectsTool(sourceId);
|
|
2802
|
+
} else {
|
|
2803
|
+
return buildCustomTool(toolConfig);
|
|
2773
2804
|
}
|
|
2774
|
-
}
|
|
2775
|
-
return tools;
|
|
2805
|
+
});
|
|
2776
2806
|
}
|
|
2777
2807
|
|
|
2778
2808
|
// src/tools/custom-tool-handler.ts
|
|
@@ -2894,7 +2924,7 @@ function registerTools(server) {
|
|
|
2894
2924
|
}
|
|
2895
2925
|
const registry = getToolRegistry();
|
|
2896
2926
|
for (const sourceId of sourceIds) {
|
|
2897
|
-
const enabledTools = registry.
|
|
2927
|
+
const enabledTools = registry.getEnabledToolConfigs(sourceId);
|
|
2898
2928
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2899
2929
|
const dbType = sourceConfig.type;
|
|
2900
2930
|
const isDefault = sourceIds[0] === sourceId;
|
|
@@ -2933,7 +2963,7 @@ function registerSearchObjectsTool(server, sourceId, dbType, isDefault) {
|
|
|
2933
2963
|
readOnlyHint: true,
|
|
2934
2964
|
destructiveHint: false,
|
|
2935
2965
|
idempotentHint: true,
|
|
2936
|
-
openWorldHint:
|
|
2966
|
+
openWorldHint: false
|
|
2937
2967
|
}
|
|
2938
2968
|
},
|
|
2939
2969
|
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
@@ -3201,7 +3231,7 @@ function generateBanner(version, modes = []) {
|
|
|
3201
3231
|
| |__| | |_) | | | | |_| | |_) |
|
|
3202
3232
|
|_____/|____/|_| |_|\\__,_|_.__/
|
|
3203
3233
|
|
|
3204
|
-
v${version}${modeText} -
|
|
3234
|
+
v${version}${modeText} - Minimal Database MCP Server
|
|
3205
3235
|
`;
|
|
3206
3236
|
}
|
|
3207
3237
|
async function main() {
|
|
@@ -3241,25 +3271,12 @@ See documentation for more details on configuring database connections.
|
|
|
3241
3271
|
console.error(` - ${source.id}: ${redactDSN(dsn)}`);
|
|
3242
3272
|
}
|
|
3243
3273
|
await connectorManager.connectWithSources(sources);
|
|
3244
|
-
const { initializeToolRegistry } = await import("./registry-
|
|
3274
|
+
const { initializeToolRegistry } = await import("./registry-AWAIN6WO.js");
|
|
3245
3275
|
initializeToolRegistry({
|
|
3246
3276
|
sources: sourceConfigsData.sources,
|
|
3247
3277
|
tools: sourceConfigsData.tools
|
|
3248
3278
|
});
|
|
3249
3279
|
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
3280
|
const createServer = () => {
|
|
3264
3281
|
const server = new McpServer({
|
|
3265
3282
|
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.1",
|
|
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
|
-
};
|