@dbx-app/mcp-server 0.4.32 → 0.4.34
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 +25 -3
- package/bin/dbx-mcp-server.js +67 -0
- package/package.json +21 -13
- package/server.json +2 -2
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -467
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ MCP server for [DBX](https://github.com/t8y2/dbx) — lets AI agents (Claude Cod
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Zero config** — Automatically reads your DBX connections (including passwords from system keyring)
|
|
10
|
-
- **
|
|
10
|
+
- **10 tools** — List/add/remove connections, list tables, describe table, get schema context, execute SQL, execute Redis commands, open tables, and execute-and-show results
|
|
11
11
|
- **Connection pooling** — Reuses database connections across queries
|
|
12
12
|
- **Direct execution** — PostgreSQL, MySQL, SQLite, and compatible databases (Doris, StarRocks, etc.) can run without opening DBX
|
|
13
13
|
- **Writes enabled by default** — regular `INSERT` / `UPDATE` / `DELETE` statements work out of the box, while dangerous SQL stays blocked unless explicitly enabled
|
|
@@ -27,6 +27,24 @@ Or run directly:
|
|
|
27
27
|
npx @dbx-app/mcp-server
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
The npm package installs a small Node.js launcher plus a precompiled Rust binary for the current platform. It does not install `better-sqlite3`, compile native modules, or require Rust/Cargo locally. Do not use `--no-optional`, because the platform binary is delivered through npm optional dependencies.
|
|
31
|
+
|
|
32
|
+
### Offline / native binary
|
|
33
|
+
|
|
34
|
+
For an offline machine, download the `dbx-mcp` binary matching the operating system and CPU from the DBX release assets, then configure the MCP client to run that file directly:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"dbx": {
|
|
40
|
+
"command": "/path/to/dbx-mcp"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The native binary works without Node.js and without opening the DBX desktop app for supported direct database connections. Set `DBX_DATA_DIR` when the DBX database is stored outside the default location. `dbx_open_table` and other desktop bridge features still require a running DBX app.
|
|
47
|
+
|
|
30
48
|
### 2. Configure Claude Code
|
|
31
49
|
|
|
32
50
|
Add to your project's `.mcp.json`:
|
|
@@ -173,7 +191,8 @@ PostgreSQL, MySQL, SQLite, Doris, StarRocks, and Redshift queries run directly f
|
|
|
173
191
|
## Requirements
|
|
174
192
|
|
|
175
193
|
- [DBX](https://github.com/t8y2/dbx) installed with at least one connection configured
|
|
176
|
-
- Node.js
|
|
194
|
+
- Node.js 18.18.0 or newer (only used by the npm launcher)
|
|
195
|
+
- Rust, Cargo, Python, and native build tools are not required
|
|
177
196
|
|
|
178
197
|
## License
|
|
179
198
|
|
|
@@ -272,6 +291,7 @@ dbx query local "select 1" --json
|
|
|
272
291
|
| `dbx_execute_query` | 执行 SQL 查询(最多返回 100 行) |
|
|
273
292
|
| `dbx_execute_redis_command` | 在 Redis 连接上执行 Redis 命令 |
|
|
274
293
|
| `dbx_open_table` | 在 DBX 桌面端打开指定表 |
|
|
294
|
+
| `dbx_execute_and_show` | 执行查询并在 DBX 中展示结果 |
|
|
275
295
|
|
|
276
296
|
### SQL 安全
|
|
277
297
|
|
|
@@ -330,4 +350,6 @@ PostgreSQL、MySQL、SQLite、Doris、StarRocks、Redshift 查询可由 MCP Serv
|
|
|
330
350
|
### 系统要求
|
|
331
351
|
|
|
332
352
|
- 已安装 [DBX](https://github.com/t8y2/dbx) 并配置了至少一个数据库连接
|
|
333
|
-
- Node.js
|
|
353
|
+
- Node.js 18.18.0 或更高版本(仅用于 npm 启动器)
|
|
354
|
+
- 不需要安装 Rust、Cargo、Python 或本地编译工具
|
|
355
|
+
- 离线直接运行原生二进制时不需要安装 Node.js
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const platformPackages = {
|
|
10
|
+
"darwin-arm64": ["@dbx-app/mcp-darwin-arm64", "dbx-mcp"],
|
|
11
|
+
"darwin-x64": ["@dbx-app/mcp-darwin-x64", "dbx-mcp"],
|
|
12
|
+
"linux-arm64": ["@dbx-app/mcp-linux-arm64-gnu", "dbx-mcp"],
|
|
13
|
+
"linux-x64": ["@dbx-app/mcp-linux-x64-gnu", "dbx-mcp"],
|
|
14
|
+
"win32-arm64": ["@dbx-app/mcp-win32-arm64", "dbx-mcp.exe"],
|
|
15
|
+
"win32-x64": ["@dbx-app/mcp-win32-x64", "dbx-mcp.exe"],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function resolveBinary() {
|
|
19
|
+
if (process.env.DBX_MCP_BINARY) {
|
|
20
|
+
return process.env.DBX_MCP_BINARY;
|
|
21
|
+
}
|
|
22
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
23
|
+
const target = platformPackages[platform];
|
|
24
|
+
if (!target) {
|
|
25
|
+
throw new Error(`DBX MCP does not provide a Rust binary for ${platform}.`);
|
|
26
|
+
}
|
|
27
|
+
const [packageName, binaryName] = target;
|
|
28
|
+
let manifest;
|
|
29
|
+
try {
|
|
30
|
+
manifest = require.resolve(`${packageName}/package.json`);
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`The optional package ${packageName} was not installed. Reinstall @dbx-app/mcp-server without --no-optional.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const binary = join(dirname(manifest), "bin", binaryName);
|
|
37
|
+
if (!existsSync(binary)) {
|
|
38
|
+
throw new Error(`The DBX MCP binary is missing from ${packageName}.`);
|
|
39
|
+
}
|
|
40
|
+
return binary;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (process.argv[2] === "--verify-platform") {
|
|
45
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
46
|
+
if (!platformPackages[platform]) {
|
|
47
|
+
throw new Error(`DBX MCP does not provide a Rust binary for ${platform}.`);
|
|
48
|
+
}
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
const binary = resolveBinary();
|
|
52
|
+
const child = spawn(binary, process.argv.slice(2), { stdio: "inherit", env: process.env });
|
|
53
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
54
|
+
process.on(signal, () => child.kill(signal));
|
|
55
|
+
}
|
|
56
|
+
child.on("error", (error) => {
|
|
57
|
+
console.error(`Failed to start DBX MCP: ${error.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
child.on("exit", (code, signal) => {
|
|
61
|
+
if (signal) process.kill(process.pid, signal);
|
|
62
|
+
else process.exit(code ?? 1);
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbx-app/mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.34",
|
|
4
4
|
"description": "MCP server for DBX — query databases from Claude Code, Cursor, and other AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
@@ -19,31 +19,39 @@
|
|
|
19
19
|
"directory": "packages/mcp-server"
|
|
20
20
|
},
|
|
21
21
|
"bin": {
|
|
22
|
-
"dbx-mcp-server": "
|
|
23
|
-
"mcp-server": "
|
|
22
|
+
"dbx-mcp-server": "bin/dbx-mcp-server.js",
|
|
23
|
+
"mcp-server": "bin/dbx-mcp-server.js"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
-
"
|
|
26
|
+
"bin",
|
|
27
27
|
"server.json"
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
30
|
-
"
|
|
31
|
-
"@
|
|
32
|
-
"
|
|
33
|
-
"@dbx-app/
|
|
30
|
+
"optionalDependencies": {
|
|
31
|
+
"@dbx-app/mcp-darwin-arm64": "0.4.34",
|
|
32
|
+
"@dbx-app/mcp-darwin-x64": "0.4.34",
|
|
33
|
+
"@dbx-app/mcp-linux-arm64-gnu": "0.4.34",
|
|
34
|
+
"@dbx-app/mcp-linux-x64-gnu": "0.4.34",
|
|
35
|
+
"@dbx-app/mcp-win32-arm64": "0.4.34",
|
|
36
|
+
"@dbx-app/mcp-win32-x64": "0.4.34"
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
36
40
|
"@types/node": "^22.15.21",
|
|
37
41
|
"tsx": "^4.19.4",
|
|
38
|
-
"typescript": "^5.8.3"
|
|
42
|
+
"typescript": "^5.8.3",
|
|
43
|
+
"vitest": "^4.1.8",
|
|
44
|
+
"zod": "^3.25.20",
|
|
45
|
+
"@dbx-app/node-core": "^0.4.34"
|
|
39
46
|
},
|
|
40
47
|
"engines": {
|
|
41
|
-
"node": ">=
|
|
48
|
+
"node": ">=18.18.0"
|
|
42
49
|
},
|
|
43
50
|
"mcpName": "io.github.t8y2/dbx",
|
|
44
51
|
"scripts": {
|
|
45
|
-
"start": "
|
|
46
|
-
"test": "
|
|
47
|
-
"build": "pnpm
|
|
52
|
+
"start": "cargo run -p dbx-mcp --",
|
|
53
|
+
"test": "cargo build -p dbx-mcp && pnpm build:legacy && vitest run --config vitest.config.ts",
|
|
54
|
+
"build": "pnpm build:legacy",
|
|
55
|
+
"build:legacy": "pnpm --filter @dbx-app/node-core build && tsc"
|
|
48
56
|
}
|
|
49
57
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/t8y2/dbx",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.4.
|
|
9
|
+
"version": "0.4.34",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@dbx-app/mcp-server",
|
|
14
|
-
"version": "0.4.
|
|
14
|
+
"version": "0.4.34",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { type Backend } from "@dbx-app/node-core";
|
|
4
|
-
export declare const DBX_MCP_PACKAGE_VERSION: string;
|
|
5
|
-
export declare const DBX_CONNECTION_TYPE_DESCRIPTION = "Database type: postgres, mysql, sqlite, rqlite, cloudflare-d1, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, weaviate, chromadb, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, prestosql, hive, spark, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, oscar, tdengine, iotdb, xugu, zookeeper, jdbc, access, mq";
|
|
6
|
-
export declare function createDbxMcpServer(backend: Backend, options?: {
|
|
7
|
-
isWebMode?: boolean;
|
|
8
|
-
}): McpServer;
|
package/dist/index.js
DELETED
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { buildSchemaContext, createBackend, evaluateRedisCommandSafety, evaluateMongoAggregateSafety, evaluateSqlSafety, formatCell, formatSchemaContext, isMainModule, mdTable, notifyReload, parseMongoAggregateCommand, assessProductionSql, isLikelyMongoMutation, isProductionDatabase, postBridge, logSqlDiagnostic, sqlSafetyFromEnv, splitSqlStatements, supportsHashLineComments, } from "@dbx-app/node-core";
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const packageJson = require("../package.json");
|
|
9
|
-
export const DBX_MCP_PACKAGE_VERSION = packageJson.version ?? "0.0.0";
|
|
10
|
-
function text(s) {
|
|
11
|
-
return { content: [{ type: "text", text: s }] };
|
|
12
|
-
}
|
|
13
|
-
function toolError(code, message) {
|
|
14
|
-
return { ...text(`${code}: ${message}`), isError: true };
|
|
15
|
-
}
|
|
16
|
-
function withDatabase(config, database) {
|
|
17
|
-
return database === undefined ? config : { ...config, database };
|
|
18
|
-
}
|
|
19
|
-
function metadataScope(config, database, schema) {
|
|
20
|
-
if (config.db_type !== "dameng") {
|
|
21
|
-
return { config: withDatabase(config, database), schema };
|
|
22
|
-
}
|
|
23
|
-
// Dameng exposes tables under user-owned schemas rather than separate
|
|
24
|
-
// databases. Accept the legacy database argument as a schema, and default to
|
|
25
|
-
// the login user when neither argument is provided.
|
|
26
|
-
const resolvedSchema = schema?.trim() || database?.trim() || config.username?.trim() || undefined;
|
|
27
|
-
return { config, schema: resolvedSchema };
|
|
28
|
-
}
|
|
29
|
-
function connectionIdentity(config) {
|
|
30
|
-
return `${config.name} (${config.id}) [${config.db_type} @ ${config.host}:${config.port}]`;
|
|
31
|
-
}
|
|
32
|
-
function labeledText(config, body) {
|
|
33
|
-
return text(`[${connectionIdentity(config)}]\n${body}`);
|
|
34
|
-
}
|
|
35
|
-
function formatQueryToolResult(result, title) {
|
|
36
|
-
const prefix = title ? `${title}\n` : "";
|
|
37
|
-
if (result.columns.length === 0)
|
|
38
|
-
return text(`${prefix}Query executed. ${result.row_count} row(s) affected.`);
|
|
39
|
-
const rows = result.rows.map((r) => result.columns.map((c) => formatCell(r[c])));
|
|
40
|
-
return text(`${prefix}${mdTable(result.columns, rows)}\n\n${result.row_count} row(s)`);
|
|
41
|
-
}
|
|
42
|
-
function redisDbFromValue(value) {
|
|
43
|
-
const trimmed = value?.trim();
|
|
44
|
-
if (!trimmed)
|
|
45
|
-
return undefined;
|
|
46
|
-
const db = Number(trimmed);
|
|
47
|
-
return Number.isInteger(db) && db >= 0 ? db : undefined;
|
|
48
|
-
}
|
|
49
|
-
function defaultRedisDb(config, scope, db) {
|
|
50
|
-
return db ?? redisDbFromValue(scope.database) ?? redisDbFromValue(config.database) ?? 0;
|
|
51
|
-
}
|
|
52
|
-
function formatRedisCommandValue(value) {
|
|
53
|
-
if (typeof value === "string")
|
|
54
|
-
return value;
|
|
55
|
-
return JSON.stringify(value, null, 2) ?? String(value);
|
|
56
|
-
}
|
|
57
|
-
function formatRedisCommandToolResult(result) {
|
|
58
|
-
return text(`Command: ${result.command}\nSafety: ${result.safety}\n\n${formatRedisCommandValue(result.value)}`);
|
|
59
|
-
}
|
|
60
|
-
export const DBX_CONNECTION_TYPE_DESCRIPTION = "Database type: postgres, mysql, sqlite, rqlite, cloudflare-d1, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, weaviate, chromadb, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, prestosql, hive, spark, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, oscar, tdengine, iotdb, xugu, zookeeper, jdbc, access, mq";
|
|
61
|
-
const FILE_CAPABLE_CONNECTION_TYPES = new Set(["sqlite", "duckdb", "access", "h2"]);
|
|
62
|
-
function scopedValue(value) {
|
|
63
|
-
const trimmed = value?.trim();
|
|
64
|
-
return trimmed ? trimmed : undefined;
|
|
65
|
-
}
|
|
66
|
-
function mcpScopeFromEnv() {
|
|
67
|
-
return {
|
|
68
|
-
connectionId: scopedValue(process.env.DBX_MCP_SCOPE_CONNECTION_ID),
|
|
69
|
-
connectionName: scopedValue(process.env.DBX_MCP_SCOPE_CONNECTION_NAME),
|
|
70
|
-
database: scopedValue(process.env.DBX_MCP_SCOPE_DATABASE),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function scopeEnabled(scope) {
|
|
74
|
-
return !!(scope.connectionId || scope.connectionName);
|
|
75
|
-
}
|
|
76
|
-
function connectionMatchesScope(config, scope) {
|
|
77
|
-
return (!!scope.connectionId && config.id === scope.connectionId) || (!!scope.connectionName && config.name === scope.connectionName);
|
|
78
|
-
}
|
|
79
|
-
async function loadScopedConnections(backend, scope) {
|
|
80
|
-
const connections = await backend.loadConnections();
|
|
81
|
-
if (!scopeEnabled(scope))
|
|
82
|
-
return connections;
|
|
83
|
-
return connections.filter((config) => connectionMatchesScope(config, scope));
|
|
84
|
-
}
|
|
85
|
-
async function resolveConnection(backend, scope, requestedId, requestedName) {
|
|
86
|
-
// connection_id takes priority over connection_name when both are provided.
|
|
87
|
-
if (requestedId?.trim()) {
|
|
88
|
-
const connections = await backend.loadConnections();
|
|
89
|
-
const config = connections.find((c) => c.id === requestedId.trim());
|
|
90
|
-
if (!config)
|
|
91
|
-
return { error: toolError("CONNECTION_NOT_FOUND", `Connection with id "${requestedId}" not found.`) };
|
|
92
|
-
// In scoped mode, verify the resolved connection is within the scope.
|
|
93
|
-
if (scopeEnabled(scope) && !connectionMatchesScope(config, scope)) {
|
|
94
|
-
return { error: toolError("CONNECTION_OUT_OF_SCOPE", `Connection "${requestedId}" is outside this DBX AI session scope.`) };
|
|
95
|
-
}
|
|
96
|
-
return { config };
|
|
97
|
-
}
|
|
98
|
-
if (!scopeEnabled(scope)) {
|
|
99
|
-
if (!requestedName?.trim())
|
|
100
|
-
return { error: toolError("CONNECTION_NOT_FOUND", "Connection name is required.") };
|
|
101
|
-
const connections = await backend.loadConnections();
|
|
102
|
-
const matching = connections.filter((c) => c.name.toLowerCase() === requestedName.trim().toLowerCase());
|
|
103
|
-
if (matching.length === 0)
|
|
104
|
-
return { error: toolError("CONNECTION_NOT_FOUND", `Connection "${requestedName}" not found.`) };
|
|
105
|
-
if (matching.length > 1) {
|
|
106
|
-
const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
|
|
107
|
-
return {
|
|
108
|
-
error: toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${requestedName}". Please specify connection_id:\n${lines.join("\n")}`),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return { config: matching[0] };
|
|
112
|
-
}
|
|
113
|
-
const [scopedConfig] = await loadScopedConnections(backend, scope);
|
|
114
|
-
if (!scopedConfig)
|
|
115
|
-
return { error: toolError("CONNECTION_NOT_FOUND", "Scoped DBX connection was not found.") };
|
|
116
|
-
if (requestedName?.trim() && requestedName !== scopedConfig.name && requestedName !== scopedConfig.id) {
|
|
117
|
-
return { error: toolError("CONNECTION_OUT_OF_SCOPE", `Connection "${requestedName}" is outside this DBX AI session scope.`) };
|
|
118
|
-
}
|
|
119
|
-
return { config: scopedConfig };
|
|
120
|
-
}
|
|
121
|
-
export function createDbxMcpServer(backend, options = {}) {
|
|
122
|
-
const isWebMode = options.isWebMode ?? !!process.env.DBX_WEB_URL;
|
|
123
|
-
const scope = mcpScopeFromEnv();
|
|
124
|
-
const scoped = scopeEnabled(scope);
|
|
125
|
-
const server = new McpServer({
|
|
126
|
-
name: "dbx",
|
|
127
|
-
version: DBX_MCP_PACKAGE_VERSION,
|
|
128
|
-
});
|
|
129
|
-
server.tool("dbx_list_connections", "List all database connections configured in DBX", {}, async () => {
|
|
130
|
-
const connections = await loadScopedConnections(backend, scope);
|
|
131
|
-
if (connections.length === 0)
|
|
132
|
-
return text("No connections configured in DBX.");
|
|
133
|
-
const rows = connections.map((c) => [c.id, c.name, c.db_type, c.host, String(c.port), c.database || ""]);
|
|
134
|
-
return text(mdTable(["ID", "Name", "Type", "Host", "Port", "Database"], rows));
|
|
135
|
-
});
|
|
136
|
-
server.tool("dbx_list_tables", "List tables and views for a database connection", {
|
|
137
|
-
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
138
|
-
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
139
|
-
database: z.string().optional().describe("Database name; for Dameng this is also accepted as a schema alias"),
|
|
140
|
-
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL, login user for Dameng)"),
|
|
141
|
-
}, async ({ connection_id, connection_name, database, schema }) => {
|
|
142
|
-
const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
|
|
143
|
-
if (error)
|
|
144
|
-
return error;
|
|
145
|
-
const resolvedConfig = config;
|
|
146
|
-
const scopeValue = metadataScope(resolvedConfig, database ?? scope.database, schema);
|
|
147
|
-
const tables = await backend.listTables(scopeValue.config, scopeValue.schema);
|
|
148
|
-
if (tables.length === 0)
|
|
149
|
-
return text("No tables found.");
|
|
150
|
-
const rows = tables.map((t) => [t.name, t.type]);
|
|
151
|
-
return labeledText(resolvedConfig, mdTable(["Table", "Type"], rows));
|
|
152
|
-
});
|
|
153
|
-
server.tool("dbx_describe_table", "Get column definitions for a table", {
|
|
154
|
-
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
155
|
-
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
156
|
-
table: z.string().describe("Table name"),
|
|
157
|
-
database: z.string().optional().describe("Database name; for Dameng this is also accepted as a schema alias"),
|
|
158
|
-
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL, login user for Dameng)"),
|
|
159
|
-
}, async ({ connection_id, connection_name, table, database, schema }) => {
|
|
160
|
-
const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
|
|
161
|
-
if (error)
|
|
162
|
-
return error;
|
|
163
|
-
const resolvedConfig = config;
|
|
164
|
-
const scopeValue = metadataScope(resolvedConfig, database ?? scope.database, schema);
|
|
165
|
-
const columns = await backend.describeTable(scopeValue.config, table, scopeValue.schema);
|
|
166
|
-
if (columns.length === 0)
|
|
167
|
-
return text("No columns found.");
|
|
168
|
-
const rows = columns.map((c) => [c.is_primary_key ? `${c.name} (PK)` : c.name, c.data_type, c.is_nullable ? "YES" : "NO", c.column_default ?? "", c.comment ?? ""]);
|
|
169
|
-
return labeledText(resolvedConfig, mdTable(["Column", "Type", "Nullable", "Default", "Comment"], rows));
|
|
170
|
-
});
|
|
171
|
-
server.tool("dbx_execute_query", "Execute a SQL query on a database connection (max 100 rows returned)", {
|
|
172
|
-
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
173
|
-
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
174
|
-
database: z.string().optional().describe("Database name"),
|
|
175
|
-
sql: z.string().describe("SQL query to execute"),
|
|
176
|
-
}, async ({ connection_id, connection_name, database, sql }) => {
|
|
177
|
-
logSqlDiagnostic("dbx_execute_query", sql, { connection_id, connection_name, database });
|
|
178
|
-
const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
|
|
179
|
-
if (error)
|
|
180
|
-
return error;
|
|
181
|
-
const scopedConfig = config;
|
|
182
|
-
if (scopedConfig.db_type === "redis") {
|
|
183
|
-
return toolError("REDIS_COMMAND_REQUIRED", "Redis connections do not accept SQL through dbx_execute_query. Use dbx_execute_redis_command with a Redis command such as GET key or INFO.");
|
|
184
|
-
}
|
|
185
|
-
if (scopedConfig.db_type !== "mongodb") {
|
|
186
|
-
const hashLineComments = supportsHashLineComments(scopedConfig.db_type);
|
|
187
|
-
const safety = evaluateSqlSafety(sql, { ...sqlSafetyFromEnv(), allowMultipleStatements: true, hashLineComments });
|
|
188
|
-
if (!safety.allowed)
|
|
189
|
-
return toolError("SQL_BLOCKED", safety.reason ?? "SQL blocked.");
|
|
190
|
-
const production = assessProductionSql(sql, scopedConfig, database ?? scope.database ?? scopedConfig.database);
|
|
191
|
-
if (production.active && production.isMutation) {
|
|
192
|
-
return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot execute writes against a production database. Return the SQL for a user to review and run in DBX.");
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
else if (isProductionDatabase(scopedConfig, database ?? scope.database ?? scopedConfig.database) && isLikelyMongoMutation(sql)) {
|
|
196
|
-
return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot execute writes against a production database. Return the command for a user to review and run in DBX.");
|
|
197
|
-
}
|
|
198
|
-
// MongoDB shell commands don't fit the SQL safety evaluator; the backend
|
|
199
|
-
// (node-core executeQuery) applies command-aware read/write gating.
|
|
200
|
-
try {
|
|
201
|
-
const statements = scopedConfig.db_type === "mongodb" ? [sql] : splitSqlStatements(sql, { hashLineComments: supportsHashLineComments(scopedConfig.db_type) });
|
|
202
|
-
const results = [];
|
|
203
|
-
for (const statement of statements) {
|
|
204
|
-
results.push(await backend.executeQuery(withDatabase(scopedConfig, database ?? scope.database), statement));
|
|
205
|
-
}
|
|
206
|
-
if (results.length === 1)
|
|
207
|
-
return labeledText(scopedConfig, formatQueryToolResult(results[0]).content[0].text);
|
|
208
|
-
return labeledText(scopedConfig, results.map((result, index) => formatQueryToolResult(result, `Statement ${index + 1}`).content[0].text).join("\n\n"));
|
|
209
|
-
}
|
|
210
|
-
catch (e) {
|
|
211
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
212
|
-
return toolError("QUERY_ERROR", msg);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
server.tool("dbx_execute_redis_command", "Execute a Redis command on a Redis connection", {
|
|
216
|
-
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
217
|
-
connection_name: z.string().optional().describe("Name of the DBX Redis connection"),
|
|
218
|
-
db: z.number().int().min(0).optional().describe("Redis logical database number (default: scoped/default database or 0)"),
|
|
219
|
-
command: z.string().describe("Redis command to execute, for example: GET mykey, INFO, or DBSIZE"),
|
|
220
|
-
}, async ({ connection_id, connection_name, db, command }) => {
|
|
221
|
-
const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
|
|
222
|
-
if (error)
|
|
223
|
-
return error;
|
|
224
|
-
const scopedConfig = config;
|
|
225
|
-
if (scopedConfig.db_type !== "redis") {
|
|
226
|
-
return toolError("INVALID_CONNECTION_TYPE", `Connection "${scopedConfig.name}" is ${scopedConfig.db_type}, not Redis.`);
|
|
227
|
-
}
|
|
228
|
-
if (!backend.executeRedisCommand) {
|
|
229
|
-
return toolError("UNSUPPORTED_BACKEND", "This DBX backend does not support Redis command execution.");
|
|
230
|
-
}
|
|
231
|
-
const safety = evaluateRedisCommandSafety(command, sqlSafetyFromEnv());
|
|
232
|
-
if (!safety.allowed)
|
|
233
|
-
return toolError("REDIS_COMMAND_BLOCKED", safety.reason ?? "Redis command blocked.");
|
|
234
|
-
if (isProductionDatabase(scopedConfig, String(defaultRedisDb(scopedConfig, scope, db))) && safety.safety !== "allowed") {
|
|
235
|
-
return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot execute write or dangerous Redis commands against a production database.");
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
const result = await backend.executeRedisCommand(scopedConfig, defaultRedisDb(scopedConfig, scope, db), command, {
|
|
239
|
-
skipSafetyCheck: safety.skipSafetyCheck,
|
|
240
|
-
});
|
|
241
|
-
return labeledText(scopedConfig, formatRedisCommandToolResult(result).content[0].text);
|
|
242
|
-
}
|
|
243
|
-
catch (e) {
|
|
244
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
245
|
-
return toolError("REDIS_COMMAND_ERROR", msg);
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
server.tool("dbx_get_schema_context", "Get compact table and column context for writing SQL", {
|
|
249
|
-
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
250
|
-
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
251
|
-
database: z.string().optional().describe("Database name"),
|
|
252
|
-
schema: z.string().optional().describe("Schema name (default: public for PostgreSQL)"),
|
|
253
|
-
tables: z.array(z.string()).optional().describe("Specific table names to include"),
|
|
254
|
-
max_tables: z.number().int().min(1).max(20).default(8).describe("Maximum number of tables to include"),
|
|
255
|
-
}, async ({ connection_id, connection_name, database, schema, tables, max_tables }) => {
|
|
256
|
-
const { config, error } = await resolveConnection(backend, scope, connection_id, connection_name);
|
|
257
|
-
if (error)
|
|
258
|
-
return error;
|
|
259
|
-
const resolvedConfig = config;
|
|
260
|
-
const context = await buildSchemaContext(backend, withDatabase(resolvedConfig, database ?? scope.database), {
|
|
261
|
-
schema,
|
|
262
|
-
tables,
|
|
263
|
-
maxTables: max_tables,
|
|
264
|
-
});
|
|
265
|
-
if (context.tables.length === 0)
|
|
266
|
-
return text("No matching tables found.");
|
|
267
|
-
return labeledText(resolvedConfig, formatSchemaContext(context));
|
|
268
|
-
});
|
|
269
|
-
if (!scoped) {
|
|
270
|
-
server.tool("dbx_add_connection", "Add a new database connection to DBX", {
|
|
271
|
-
name: z.string().describe("Connection name"),
|
|
272
|
-
db_type: z.string().describe(DBX_CONNECTION_TYPE_DESCRIPTION),
|
|
273
|
-
host: z.string().describe("Database host; for cloudflare-d1, use the Cloudflare Account ID"),
|
|
274
|
-
port: z.number().optional().describe("Database port (TDengine defaults to 6041, IoTDB defaults to 6667, XuguDB defaults to 5138)"),
|
|
275
|
-
username: z.string().default("").describe("Username"),
|
|
276
|
-
password: z.string().default("").describe("Password; for cloudflare-d1, use the API Token"),
|
|
277
|
-
database: z.string().optional().describe("Default database name; for cloudflare-d1, use the D1 Database ID"),
|
|
278
|
-
ssl: z.boolean().default(false).describe("Enable SSL"),
|
|
279
|
-
driver_profile: z.string().optional().describe("Driver profile (e.g. 'gbase8a', 'gbase8s')"),
|
|
280
|
-
}, async ({ name, db_type, host, port, username, password, database, ssl, driver_profile }) => {
|
|
281
|
-
const existing = await backend.findConnection(name);
|
|
282
|
-
if (existing)
|
|
283
|
-
return text(`Connection "${name}" already exists.`);
|
|
284
|
-
const DEFAULT_PORTS = {
|
|
285
|
-
kwdb: 26257,
|
|
286
|
-
rqlite: 4001,
|
|
287
|
-
"cloudflare-d1": 443,
|
|
288
|
-
tdengine: 6041,
|
|
289
|
-
oscar: 2003,
|
|
290
|
-
iotdb: 6667,
|
|
291
|
-
xugu: 5138,
|
|
292
|
-
};
|
|
293
|
-
const resolvedPort = port ?? DEFAULT_PORTS[db_type] ?? (FILE_CAPABLE_CONNECTION_TYPES.has(db_type) ? 0 : undefined);
|
|
294
|
-
if (resolvedPort === undefined)
|
|
295
|
-
return text("Port is required for this database type.");
|
|
296
|
-
const config = await backend.addConnection({
|
|
297
|
-
name,
|
|
298
|
-
db_type,
|
|
299
|
-
host,
|
|
300
|
-
port: resolvedPort,
|
|
301
|
-
username,
|
|
302
|
-
password,
|
|
303
|
-
database,
|
|
304
|
-
ssl,
|
|
305
|
-
driver_profile,
|
|
306
|
-
ssh_enabled: false,
|
|
307
|
-
});
|
|
308
|
-
await notifyReload();
|
|
309
|
-
return text(`Connection "${config.name}" added (id: ${config.id}).`);
|
|
310
|
-
});
|
|
311
|
-
server.tool("dbx_remove_connection", "Remove a database connection from DBX", {
|
|
312
|
-
connection_name: z.string().describe("Name of the connection to remove"),
|
|
313
|
-
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to remove by id instead of name)"),
|
|
314
|
-
}, async ({ connection_name, connection_id }) => {
|
|
315
|
-
if (connection_id?.trim()) {
|
|
316
|
-
if (backend.removeConnectionById) {
|
|
317
|
-
const removed = await backend.removeConnectionById(connection_id.trim());
|
|
318
|
-
if (!removed)
|
|
319
|
-
return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
|
|
320
|
-
await notifyReload();
|
|
321
|
-
return text(`Connection with id "${connection_id}" removed.`);
|
|
322
|
-
}
|
|
323
|
-
// Fallback: resolve by id then remove by name
|
|
324
|
-
const connections = await backend.loadConnections();
|
|
325
|
-
const config = connections.find((c) => c.id === connection_id.trim());
|
|
326
|
-
if (!config)
|
|
327
|
-
return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
|
|
328
|
-
const removed = await backend.removeConnection(config.name);
|
|
329
|
-
if (!removed)
|
|
330
|
-
return toolError("CONNECTION_NOT_FOUND", `Connection "${config.name}" could not be removed.`);
|
|
331
|
-
await notifyReload();
|
|
332
|
-
return text(`Connection "${config.name}" (id: ${config.id}) removed.`);
|
|
333
|
-
}
|
|
334
|
-
const allConnections = await backend.loadConnections();
|
|
335
|
-
const matching = allConnections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
|
|
336
|
-
if (matching.length === 0)
|
|
337
|
-
return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
|
|
338
|
-
if (matching.length > 1) {
|
|
339
|
-
const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
|
|
340
|
-
return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
|
|
341
|
-
}
|
|
342
|
-
const removed = await backend.removeConnection(connection_name);
|
|
343
|
-
if (!removed)
|
|
344
|
-
return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
|
|
345
|
-
await notifyReload();
|
|
346
|
-
return text(`Connection "${connection_name}" removed.`);
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
// Desktop-only tools: open table and execute-and-show require the Tauri bridge
|
|
350
|
-
if (!isWebMode && !scoped) {
|
|
351
|
-
server.tool("dbx_open_table", "Open a table in DBX desktop app UI. Requires DBX to be running.", {
|
|
352
|
-
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
353
|
-
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
354
|
-
table: z.string().describe("Table name to open"),
|
|
355
|
-
database: z.string().optional().describe("Database name"),
|
|
356
|
-
schema: z.string().optional().describe("Schema name"),
|
|
357
|
-
}, async ({ connection_id, connection_name, table, database, schema }) => {
|
|
358
|
-
let config;
|
|
359
|
-
if (connection_id?.trim()) {
|
|
360
|
-
const connections = await backend.loadConnections();
|
|
361
|
-
config = connections.find((c) => c.id === connection_id.trim());
|
|
362
|
-
if (!config)
|
|
363
|
-
return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
|
|
364
|
-
}
|
|
365
|
-
else if (connection_name?.trim()) {
|
|
366
|
-
const connections = await backend.loadConnections();
|
|
367
|
-
const matching = connections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
|
|
368
|
-
if (matching.length === 0)
|
|
369
|
-
return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
|
|
370
|
-
if (matching.length > 1) {
|
|
371
|
-
const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
|
|
372
|
-
return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
|
|
373
|
-
}
|
|
374
|
-
config = matching[0];
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
return toolError("CONNECTION_NOT_FOUND", "Either connection_id or connection_name is required.");
|
|
378
|
-
}
|
|
379
|
-
return bridgeRequest("/open-table", { connection_id: config.id, connection_name: config.name, table, database, schema }, `Opened ${table} in DBX`);
|
|
380
|
-
});
|
|
381
|
-
server.tool("dbx_execute_and_show", "Execute a SQL query in DBX desktop app UI and show results there. Requires DBX to be running.", {
|
|
382
|
-
connection_id: z.string().optional().describe("Unique ID of the DBX connection (use this to disambiguate when multiple connections share the same name)"),
|
|
383
|
-
connection_name: z.string().optional().describe("Name of the DBX connection"),
|
|
384
|
-
sql: z.string().describe("SQL query to execute"),
|
|
385
|
-
database: z.string().optional().describe("Database name"),
|
|
386
|
-
}, async ({ connection_id, connection_name, sql, database }) => {
|
|
387
|
-
let config;
|
|
388
|
-
if (connection_id?.trim()) {
|
|
389
|
-
const connections = await backend.loadConnections();
|
|
390
|
-
config = connections.find((c) => c.id === connection_id.trim());
|
|
391
|
-
if (!config)
|
|
392
|
-
return toolError("CONNECTION_NOT_FOUND", `Connection with id "${connection_id}" not found.`);
|
|
393
|
-
}
|
|
394
|
-
else if (connection_name?.trim()) {
|
|
395
|
-
const connections = await backend.loadConnections();
|
|
396
|
-
const matching = connections.filter((c) => c.name.toLowerCase() === connection_name.toLowerCase());
|
|
397
|
-
if (matching.length === 0)
|
|
398
|
-
return toolError("CONNECTION_NOT_FOUND", `Connection "${connection_name}" not found.`);
|
|
399
|
-
if (matching.length > 1) {
|
|
400
|
-
const lines = matching.map((c) => `- ${c.id}: ${c.db_type} @ ${c.host}:${c.port}`);
|
|
401
|
-
return toolError("AMBIGUOUS_CONNECTION", `Multiple connections found with name "${connection_name}". Please specify connection_id:\n${lines.join("\n")}`);
|
|
402
|
-
}
|
|
403
|
-
config = matching[0];
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
return toolError("CONNECTION_NOT_FOUND", "Either connection_id or connection_name is required.");
|
|
407
|
-
}
|
|
408
|
-
const safetyOptions = sqlSafetyFromEnv();
|
|
409
|
-
if (config?.db_type === "mongodb") {
|
|
410
|
-
const aggregate = parseMongoAggregateCommand(sql);
|
|
411
|
-
if (aggregate) {
|
|
412
|
-
const safety = evaluateMongoAggregateSafety(aggregate, safetyOptions);
|
|
413
|
-
if (!safety.allowed)
|
|
414
|
-
return toolError("SQL_BLOCKED", safety.reason ?? "Query blocked.");
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
const hashLineComments = supportsHashLineComments(config?.db_type);
|
|
419
|
-
const safety = evaluateSqlSafety(sql, { ...safetyOptions, allowMultipleStatements: true, hashLineComments });
|
|
420
|
-
if (!safety.allowed)
|
|
421
|
-
return toolError("SQL_BLOCKED", safety.reason ?? "SQL blocked.");
|
|
422
|
-
}
|
|
423
|
-
if (config?.db_type === "mongodb") {
|
|
424
|
-
if (isProductionDatabase(config, database ?? scope.database ?? config.database) && isLikelyMongoMutation(sql)) {
|
|
425
|
-
return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot send writes against a production database to DBX.");
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
const production = assessProductionSql(sql, config, database ?? scope.database ?? config.database);
|
|
430
|
-
if (production.active && production.isMutation) {
|
|
431
|
-
return toolError("PRODUCTION_WRITE_BLOCKED", "MCP cannot send writes against a production database to DBX.");
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
// MongoDB shell commands bypass the SQL safety evaluator; pass MCP
|
|
435
|
-
// safety flags to the desktop executor for command-aware gating.
|
|
436
|
-
logSqlDiagnostic("dbx_execute_in_app", sql, { connection_id: config.id, connection_name: config.name, database });
|
|
437
|
-
return bridgeRequest("/execute-query", {
|
|
438
|
-
connection_id: config.id,
|
|
439
|
-
connection_name: config.name,
|
|
440
|
-
sql,
|
|
441
|
-
database,
|
|
442
|
-
allow_writes: safetyOptions.allowWrites,
|
|
443
|
-
allow_dangerous: safetyOptions.allowDangerous,
|
|
444
|
-
}, "Query sent to DBX");
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
return server;
|
|
448
|
-
}
|
|
449
|
-
async function bridgeRequest(path, body, successMsg) {
|
|
450
|
-
const res = await postBridge(path, body);
|
|
451
|
-
if (res.ok)
|
|
452
|
-
return text(successMsg);
|
|
453
|
-
const message = res.text.startsWith("DBX is not running") ? res.text : `Failed: ${res.text}`;
|
|
454
|
-
return toolError("DBX_NOT_RUNNING", message);
|
|
455
|
-
}
|
|
456
|
-
async function main() {
|
|
457
|
-
const backend = await createBackend();
|
|
458
|
-
const server = createDbxMcpServer(backend);
|
|
459
|
-
const transport = new StdioServerTransport();
|
|
460
|
-
await server.connect(transport);
|
|
461
|
-
}
|
|
462
|
-
if (isMainModule(import.meta.url, process.argv[1])) {
|
|
463
|
-
main().catch((e) => {
|
|
464
|
-
console.error("MCP Server failed to start:", e);
|
|
465
|
-
process.exit(1);
|
|
466
|
-
});
|
|
467
|
-
}
|