taurusdb-mcp 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -2
- package/dist/index.js +0 -0
- package/dist/security/local-credential-login.d.ts +33 -0
- package/dist/security/local-credential-login.js +156 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.js +21 -20
- package/dist/tools/common/public-formatters.d.ts +0 -1
- package/dist/tools/common/public-formatters.js +0 -1
- package/dist/tools/error-handling.js +8 -4
- package/dist/tools/query.js +8 -9
- package/dist/tools/registry.d.ts +3 -7
- package/dist/tools/registry.js +18 -28
- package/dist/tools/taurus/cloud-context.d.ts +3 -1
- package/dist/tools/taurus/cloud-context.js +142 -23
- package/dist/tools/taurus/sql-login.d.ts +2 -0
- package/dist/tools/taurus/sql-login.js +70 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
TaurusDB MCP server for MCP clients such as:
|
|
4
4
|
|
|
5
|
-
- Claude
|
|
5
|
+
- Claude Code
|
|
6
|
+
- Codex
|
|
6
7
|
- Cursor
|
|
7
8
|
- VS Code
|
|
8
9
|
|
|
@@ -15,7 +16,43 @@ npm install taurusdb-mcp
|
|
|
15
16
|
Run directly with:
|
|
16
17
|
|
|
17
18
|
```bash
|
|
18
|
-
npx taurusdb-mcp --version
|
|
19
|
+
npx -y taurusdb-mcp --version
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use this command in MCP client configs:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["-y", "taurusdb-mcp"]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Claude Code:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
claude mcp add huaweicloud-taurusdb \
|
|
35
|
+
--transport stdio \
|
|
36
|
+
-e TAURUSDB_CLOUD_REGION=<your-region> \
|
|
37
|
+
-e TAURUSDB_CLOUD_ACCESS_KEY_ID=<your-ak> \
|
|
38
|
+
-e TAURUSDB_CLOUD_SECRET_ACCESS_KEY=<your-sk> \
|
|
39
|
+
-e TAURUSDB_SQL_DATABASE=<your-database> \
|
|
40
|
+
-e TAURUSDB_SQL_USER=<your-readonly-user> \
|
|
41
|
+
-e TAURUSDB_SQL_PASSWORD=<your-readonly-password> \
|
|
42
|
+
-- npx -y taurusdb-mcp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Codex:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
codex mcp add huaweicloud-taurusdb \
|
|
49
|
+
--env TAURUSDB_CLOUD_REGION=<your-region> \
|
|
50
|
+
--env TAURUSDB_CLOUD_ACCESS_KEY_ID=<your-ak> \
|
|
51
|
+
--env TAURUSDB_CLOUD_SECRET_ACCESS_KEY=<your-sk> \
|
|
52
|
+
--env TAURUSDB_SQL_DATABASE=<your-database> \
|
|
53
|
+
--env TAURUSDB_SQL_USER=<your-readonly-user> \
|
|
54
|
+
--env TAURUSDB_SQL_PASSWORD=<your-readonly-password> \
|
|
55
|
+
-- npx -y taurusdb-mcp
|
|
19
56
|
```
|
|
20
57
|
|
|
21
58
|
Initialize local MCP client config:
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type SqlLoginCredentials = {
|
|
2
|
+
datasource: string;
|
|
3
|
+
username: string;
|
|
4
|
+
password: string;
|
|
5
|
+
};
|
|
6
|
+
export type SqlLoginRequest = {
|
|
7
|
+
datasource: string;
|
|
8
|
+
bind: (credentials: SqlLoginCredentials) => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export type IssuedSqlLogin = {
|
|
11
|
+
loginUrl: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
};
|
|
14
|
+
export interface CredentialLoginService {
|
|
15
|
+
issueSqlLogin(request: SqlLoginRequest): Promise<IssuedSqlLogin>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export type LocalCredentialLoginServiceOptions = {
|
|
19
|
+
now?: () => number;
|
|
20
|
+
tokenTtlMs?: number;
|
|
21
|
+
};
|
|
22
|
+
export declare class LocalCredentialLoginService implements CredentialLoginService {
|
|
23
|
+
private readonly now;
|
|
24
|
+
private readonly tokenTtlMs;
|
|
25
|
+
private readonly pending;
|
|
26
|
+
private server;
|
|
27
|
+
private port;
|
|
28
|
+
constructor(options?: LocalCredentialLoginServiceOptions);
|
|
29
|
+
issueSqlLogin(request: SqlLoginRequest): Promise<IssuedSqlLogin>;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
private ensureStarted;
|
|
32
|
+
private handle;
|
|
33
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer, } from "node:http";
|
|
3
|
+
const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
4
|
+
const MAX_REQUEST_BODY_BYTES = 16 * 1024;
|
|
5
|
+
function page(title, message, form = false) {
|
|
6
|
+
const formHtml = form
|
|
7
|
+
? `<form method="post">
|
|
8
|
+
<label>Database username <input name="username" autocomplete="username" required></label>
|
|
9
|
+
<label>Database password <input name="password" type="password" autocomplete="current-password" required></label>
|
|
10
|
+
<button type="submit">Connect</button>
|
|
11
|
+
</form>`
|
|
12
|
+
: "";
|
|
13
|
+
return `<!doctype html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
|
+
<title>${title}</title>
|
|
19
|
+
<style>
|
|
20
|
+
body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 4rem auto; padding: 0 1rem; }
|
|
21
|
+
form { display: grid; gap: 1rem; }
|
|
22
|
+
label { display: grid; gap: .35rem; }
|
|
23
|
+
input, button { font: inherit; padding: .65rem; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<h1>${title}</h1>
|
|
28
|
+
<p>${message}</p>
|
|
29
|
+
${formHtml}
|
|
30
|
+
</body>
|
|
31
|
+
</html>`;
|
|
32
|
+
}
|
|
33
|
+
function respond(res, status, body) {
|
|
34
|
+
res.writeHead(status, {
|
|
35
|
+
"cache-control": "no-store",
|
|
36
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'",
|
|
37
|
+
"content-type": "text/html; charset=utf-8",
|
|
38
|
+
"referrer-policy": "no-referrer",
|
|
39
|
+
"x-content-type-options": "nosniff",
|
|
40
|
+
});
|
|
41
|
+
res.end(body);
|
|
42
|
+
}
|
|
43
|
+
async function readBody(req) {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
let size = 0;
|
|
46
|
+
for await (const chunk of req) {
|
|
47
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
48
|
+
size += buffer.length;
|
|
49
|
+
if (size > MAX_REQUEST_BODY_BYTES) {
|
|
50
|
+
throw new Error("Request body is too large.");
|
|
51
|
+
}
|
|
52
|
+
chunks.push(buffer);
|
|
53
|
+
}
|
|
54
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
55
|
+
}
|
|
56
|
+
export class LocalCredentialLoginService {
|
|
57
|
+
now;
|
|
58
|
+
tokenTtlMs;
|
|
59
|
+
pending = new Map();
|
|
60
|
+
server;
|
|
61
|
+
port;
|
|
62
|
+
constructor(options = {}) {
|
|
63
|
+
this.now = options.now ?? Date.now;
|
|
64
|
+
this.tokenTtlMs = options.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
|
|
65
|
+
}
|
|
66
|
+
async issueSqlLogin(request) {
|
|
67
|
+
await this.ensureStarted();
|
|
68
|
+
const token = randomBytes(32).toString("base64url");
|
|
69
|
+
const expiresAtMs = this.now() + this.tokenTtlMs;
|
|
70
|
+
this.pending.set(token, { ...request, expiresAtMs });
|
|
71
|
+
return {
|
|
72
|
+
loginUrl: `http://127.0.0.1:${this.port}/sql-login/${token}`,
|
|
73
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async close() {
|
|
77
|
+
this.pending.clear();
|
|
78
|
+
const server = this.server;
|
|
79
|
+
this.server = undefined;
|
|
80
|
+
this.port = undefined;
|
|
81
|
+
if (!server) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await new Promise((resolve, reject) => {
|
|
85
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async ensureStarted() {
|
|
89
|
+
if (this.server) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const server = createServer((req, res) => {
|
|
93
|
+
void this.handle(req, res);
|
|
94
|
+
});
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
server.once("error", reject);
|
|
97
|
+
server.listen(0, "127.0.0.1", () => {
|
|
98
|
+
server.off("error", reject);
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
const address = server.address();
|
|
103
|
+
if (!address || typeof address === "string") {
|
|
104
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
105
|
+
throw new Error("Unable to resolve local credential login address.");
|
|
106
|
+
}
|
|
107
|
+
this.server = server;
|
|
108
|
+
this.port = address.port;
|
|
109
|
+
}
|
|
110
|
+
async handle(req, res) {
|
|
111
|
+
try {
|
|
112
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
113
|
+
const match = url.pathname.match(/^\/sql-login\/([^/]+)$/);
|
|
114
|
+
const token = match?.[1];
|
|
115
|
+
const pending = token ? this.pending.get(token) : undefined;
|
|
116
|
+
if (!token || !pending || pending.expiresAtMs <= this.now()) {
|
|
117
|
+
if (token) {
|
|
118
|
+
this.pending.delete(token);
|
|
119
|
+
}
|
|
120
|
+
respond(res, 410, page("Invalid login link", "This login link is invalid or has expired."));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (req.method === "GET") {
|
|
124
|
+
respond(res, 200, page("Connect to TaurusDB", "Enter your database credentials.", true));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (req.method !== "POST") {
|
|
128
|
+
respond(res, 405, page("Unsupported request", "Use the login form to continue."));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const form = new URLSearchParams(await readBody(req));
|
|
132
|
+
const username = form.get("username")?.trim() ?? "";
|
|
133
|
+
const password = form.get("password") ?? "";
|
|
134
|
+
if (!username || !password) {
|
|
135
|
+
respond(res, 400, page("Connect to TaurusDB", "Username and password are required.", true));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.pending.delete(token);
|
|
139
|
+
try {
|
|
140
|
+
await pending.bind({
|
|
141
|
+
datasource: pending.datasource,
|
|
142
|
+
username,
|
|
143
|
+
password,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
respond(res, 500, page("Connection failed", "Credentials could not be bound to this session."));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
respond(res, 200, page("Connected", "Database credentials are now active for this MCP session."));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
respond(res, 400, page("Invalid request", "The login request could not be processed."));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { TaurusDBEngine, type
|
|
2
|
+
import { TaurusDBEngine, type Config, type RuntimeTargetProfileLoader } from "taurusdb-core";
|
|
3
|
+
import { type CredentialLoginService } from "./security/local-credential-login.js";
|
|
3
4
|
export interface ServerDeps {
|
|
4
5
|
config: Config;
|
|
5
6
|
profileLoader: RuntimeTargetProfileLoader;
|
|
6
7
|
engine: TaurusDBEngine;
|
|
8
|
+
credentialLogin: CredentialLoginService;
|
|
7
9
|
pingResponse: string;
|
|
8
|
-
startupProbe?: CapabilitySnapshot;
|
|
9
10
|
}
|
|
10
11
|
export declare function bootstrapDependencies(): Promise<ServerDeps>;
|
|
11
12
|
export declare function createServer(deps: ServerDeps): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -4,33 +4,17 @@ import { createSqlProfileLoader, getConfig, RuntimeOverrideProfileLoader, redact
|
|
|
4
4
|
import { registerTools } from "./tools/registry.js";
|
|
5
5
|
import { logger } from "taurusdb-core";
|
|
6
6
|
import { VERSION } from "./version.js";
|
|
7
|
+
import { LocalCredentialLoginService, } from "./security/local-credential-login.js";
|
|
7
8
|
export async function bootstrapDependencies() {
|
|
8
9
|
const config = getConfig();
|
|
9
10
|
const profileLoader = new RuntimeOverrideProfileLoader(createSqlProfileLoader({ config }));
|
|
10
11
|
const engine = await TaurusDBEngine.create({ config, profileLoader });
|
|
11
|
-
const defaultDatasource = await engine.getDefaultDataSource();
|
|
12
|
-
let startupProbe;
|
|
13
|
-
if (defaultDatasource) {
|
|
14
|
-
try {
|
|
15
|
-
const bootstrapContext = await engine.resolveContext({
|
|
16
|
-
datasource: defaultDatasource,
|
|
17
|
-
readonly: true,
|
|
18
|
-
}, "task_bootstrap_probe");
|
|
19
|
-
startupProbe = await engine.probeCapabilities(bootstrapContext);
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
logger.warn({
|
|
23
|
-
err: error,
|
|
24
|
-
defaultDatasource,
|
|
25
|
-
}, "Capability probe failed during bootstrap; TaurusDB-specific tools will stay disabled");
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
12
|
return {
|
|
29
13
|
config,
|
|
30
14
|
profileLoader,
|
|
31
15
|
engine,
|
|
16
|
+
credentialLogin: new LocalCredentialLoginService(),
|
|
32
17
|
pingResponse: "pong",
|
|
33
|
-
startupProbe,
|
|
34
18
|
};
|
|
35
19
|
}
|
|
36
20
|
export function createServer(deps) {
|
|
@@ -38,7 +22,25 @@ export function createServer(deps) {
|
|
|
38
22
|
name: "huaweicloud-taurusdb",
|
|
39
23
|
version: VERSION,
|
|
40
24
|
});
|
|
41
|
-
|
|
25
|
+
let cleanupPromise;
|
|
26
|
+
const cleanup = () => {
|
|
27
|
+
cleanupPromise ??= Promise.all([
|
|
28
|
+
deps.credentialLogin.close(),
|
|
29
|
+
deps.engine.close(),
|
|
30
|
+
]).then(() => undefined);
|
|
31
|
+
return cleanupPromise;
|
|
32
|
+
};
|
|
33
|
+
server.server.onclose = () => {
|
|
34
|
+
void cleanup().catch((error) => {
|
|
35
|
+
logger.error({ err: error }, "Failed to close MCP session dependencies");
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
const closeServer = server.close.bind(server);
|
|
39
|
+
server.close = async () => {
|
|
40
|
+
await closeServer();
|
|
41
|
+
await cleanup();
|
|
42
|
+
};
|
|
43
|
+
registerTools(server, deps, deps.config);
|
|
42
44
|
return server;
|
|
43
45
|
}
|
|
44
46
|
export async function startMcpServer() {
|
|
@@ -52,7 +54,6 @@ export async function startMcpServer() {
|
|
|
52
54
|
logger.info({
|
|
53
55
|
profileCount: datasources.length,
|
|
54
56
|
defaultDatasource: defaultDatasource ?? null,
|
|
55
|
-
taurusdbProbe: deps.startupProbe?.kernelInfo?.isTaurusDB ?? null,
|
|
56
57
|
}, "SQL profiles resolved");
|
|
57
58
|
logger.info({ server: "huaweicloud-taurusdb", version: VERSION }, "Starting MCP server");
|
|
58
59
|
await server.connect(new StdioServerTransport());
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectionPoolError, DatasourceResolutionError, FlashbackNoViewError, SchemaIntrospectionError, UnsupportedFeatureError, } from "taurusdb-core";
|
|
1
|
+
import { ConnectionPoolError, DatasourceResolutionError, FlashbackNoViewError, logger, SchemaIntrospectionError, UnsupportedFeatureError, } from "taurusdb-core";
|
|
2
2
|
import { ErrorCode, formatError, } from "../utils/formatter.js";
|
|
3
3
|
export class ToolInputError extends Error {
|
|
4
4
|
constructor(message) {
|
|
@@ -75,15 +75,19 @@ export function formatToolError(error, context) {
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
if (error instanceof UnsupportedFeatureError) {
|
|
78
|
+
const parameterHint = error.parameterHint
|
|
79
|
+
? ` Enable or verify ${error.parameterHint} on the target instance if this feature should be available.`
|
|
80
|
+
: "";
|
|
78
81
|
return formatError({
|
|
79
82
|
code: ErrorCode.UNSUPPORTED_FEATURE,
|
|
80
|
-
message: error.message
|
|
83
|
+
message: `${error.message}${parameterHint}`,
|
|
81
84
|
summary: "The requested TaurusDB feature is not available on this instance.",
|
|
82
85
|
metadata: context.metadata,
|
|
83
86
|
details: {
|
|
84
87
|
feature: error.feature,
|
|
85
88
|
required_version: error.requiredVersion,
|
|
86
89
|
current_version: error.currentVersion,
|
|
90
|
+
parameter_hint: error.parameterHint,
|
|
87
91
|
},
|
|
88
92
|
});
|
|
89
93
|
}
|
|
@@ -112,11 +116,11 @@ export function formatToolError(error, context) {
|
|
|
112
116
|
metadata: context.metadata,
|
|
113
117
|
});
|
|
114
118
|
}
|
|
119
|
+
logger.error({ err: error, action: context.action }, "Tool execution failed unexpectedly");
|
|
115
120
|
return formatError({
|
|
116
121
|
code: ErrorCode.CONNECTION_FAILED,
|
|
117
|
-
message:
|
|
122
|
+
message: `${context.action} failed unexpectedly.`,
|
|
118
123
|
summary: `${context.action} failed unexpectedly.`,
|
|
119
124
|
metadata: context.metadata,
|
|
120
|
-
details: detailsOf(error),
|
|
121
125
|
});
|
|
122
126
|
}
|
package/dist/tools/query.js
CHANGED
|
@@ -12,16 +12,16 @@ async function inspectSql(toolName, sql, ctx, deps) {
|
|
|
12
12
|
context: ctx,
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
async function ensureConfirmation(
|
|
15
|
+
async function ensureConfirmation(decision, ctx, deps, taskId, confirmationToken) {
|
|
16
16
|
if (!decision.requiresConfirmation) {
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
19
|
const responseMetadata = metadata(taskId, {
|
|
20
20
|
sql_hash: decision.sqlHash,
|
|
21
|
-
statement_type: statementTypeFromSql(
|
|
21
|
+
statement_type: statementTypeFromSql(decision.normalizedSql),
|
|
22
22
|
});
|
|
23
23
|
if (confirmationToken) {
|
|
24
|
-
const validation = await deps.engine.validateConfirmation(confirmationToken,
|
|
24
|
+
const validation = await deps.engine.validateConfirmation(confirmationToken, decision.normalizedSql, ctx);
|
|
25
25
|
if (validation.valid) {
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
@@ -76,11 +76,11 @@ export const executeReadonlySqlTool = {
|
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
-
const confirmationResponse = await ensureConfirmation(
|
|
79
|
+
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context.taskId, asOptionalString(input.confirmation_token, "confirmation_token"));
|
|
80
80
|
if (confirmationResponse) {
|
|
81
81
|
return confirmationResponse;
|
|
82
82
|
}
|
|
83
|
-
const result = await deps.engine.executeReadonly(
|
|
83
|
+
const result = await deps.engine.executeReadonly(decision.normalizedSql, ctx, {
|
|
84
84
|
timeoutMs: decision.runtimeLimits.timeoutMs,
|
|
85
85
|
maxRows: decision.runtimeLimits.maxRows,
|
|
86
86
|
maxColumns: decision.runtimeLimits.maxColumns,
|
|
@@ -133,7 +133,7 @@ export const explainSqlTool = {
|
|
|
133
133
|
},
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
|
-
const result = await deps.engine.explain(
|
|
136
|
+
const result = await deps.engine.explain(decision.normalizedSql, ctx);
|
|
137
137
|
return formatSuccess(toPublicExplainResult(result, decision), {
|
|
138
138
|
summary: decision.requiresConfirmation
|
|
139
139
|
? "Explain generated. Executing this SQL would require explicit confirmation."
|
|
@@ -163,7 +163,6 @@ export const executeSqlTool = {
|
|
|
163
163
|
sql: requiredSqlSchema("Mutation SQL to execute."),
|
|
164
164
|
confirmation_token: optionalTokenSchema(),
|
|
165
165
|
},
|
|
166
|
-
exposeWhen: (config) => config.enableMutations,
|
|
167
166
|
async handler(input, deps, context) {
|
|
168
167
|
const sql = asRequiredString(input.sql, "sql");
|
|
169
168
|
const statementType = statementTypeFromSql(sql);
|
|
@@ -185,11 +184,11 @@ export const executeSqlTool = {
|
|
|
185
184
|
},
|
|
186
185
|
});
|
|
187
186
|
}
|
|
188
|
-
const confirmationResponse = await ensureConfirmation(
|
|
187
|
+
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context.taskId, asOptionalString(input.confirmation_token, "confirmation_token"));
|
|
189
188
|
if (confirmationResponse) {
|
|
190
189
|
return confirmationResponse;
|
|
191
190
|
}
|
|
192
|
-
const result = await deps.engine.executeMutation(
|
|
191
|
+
const result = await deps.engine.executeMutation(decision.normalizedSql, ctx, {
|
|
193
192
|
timeoutMs: decision.runtimeLimits.timeoutMs,
|
|
194
193
|
});
|
|
195
194
|
return formatSuccess(toPublicMutationResult(result), {
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type Config } from "taurusdb-core";
|
|
3
3
|
import type { ServerDeps } from "../server.js";
|
|
4
4
|
import { type ToolResponse } from "../utils/formatter.js";
|
|
5
5
|
export type ToolDeps = ServerDeps;
|
|
6
|
-
export interface ToolRegistrationProbe {
|
|
7
|
-
kernelInfo?: KernelInfo;
|
|
8
|
-
features?: FeatureMatrix;
|
|
9
|
-
}
|
|
10
6
|
export type ToolInvokeContext = {
|
|
11
7
|
taskId: string;
|
|
12
8
|
};
|
|
@@ -15,8 +11,8 @@ export interface ToolDefinition<I extends Record<string, unknown> = Record<strin
|
|
|
15
11
|
description: string;
|
|
16
12
|
inputSchema: Record<string, unknown>;
|
|
17
13
|
handler: (input: I, deps: ToolDeps, context: ToolInvokeContext) => Promise<ToolResponse<O>>;
|
|
18
|
-
exposeWhen?: (config: Config) => boolean;
|
|
19
14
|
}
|
|
20
15
|
export declare const commonToolDefinitions: ToolDefinition[];
|
|
21
16
|
export declare const capabilityToolDefinitions: ToolDefinition[];
|
|
22
|
-
export declare
|
|
17
|
+
export declare const taurusToolDefinitions: ToolDefinition[];
|
|
18
|
+
export declare function registerTools(server: McpServer, deps: ToolDeps, config: Config, tools?: ToolDefinition[]): void;
|
package/dist/tools/registry.js
CHANGED
|
@@ -4,7 +4,8 @@ import { describeTableTool, listDatabasesTool, listTablesTool, } from "./discove
|
|
|
4
4
|
import { showProcesslistTool } from "./processlist.js";
|
|
5
5
|
import { pingTool } from "./ping.js";
|
|
6
6
|
import { getKernelInfoTool, listTaurusFeaturesTool } from "./taurus/capability.js";
|
|
7
|
-
import {
|
|
7
|
+
import { clearSqlCredentialsTool, getSessionBindingTool, selectCloudTaurusInstanceTool, setCloudRegionTool, setDefaultDatabaseTool, } from "./taurus/cloud-context.js";
|
|
8
|
+
import { beginSqlLoginTool } from "./taurus/sql-login.js";
|
|
8
9
|
import { listCloudTaurusInstancesTool } from "./taurus/cloud-instances.js";
|
|
9
10
|
import { diagnosticToolDefinitions } from "./taurus/diagnostics.js";
|
|
10
11
|
import { explainSqlEnhancedTool } from "./taurus/explain.js";
|
|
@@ -12,10 +13,10 @@ import { flashbackQueryTool } from "./taurus/flashback.js";
|
|
|
12
13
|
import { listRecycleBinTool, restoreRecycleBinTableTool } from "./taurus/recycle-bin.js";
|
|
13
14
|
import { ErrorCode, formatError, toMcpToolResult, } from "../utils/formatter.js";
|
|
14
15
|
function formatUnhandledToolError(error, taskId) {
|
|
15
|
-
|
|
16
|
+
void error;
|
|
16
17
|
const response = formatError({
|
|
17
18
|
code: ErrorCode.CONNECTION_FAILED,
|
|
18
|
-
message,
|
|
19
|
+
message: "Tool execution failed unexpectedly.",
|
|
19
20
|
summary: "Tool execution failed unexpectedly.",
|
|
20
21
|
metadata: { task_id: taskId },
|
|
21
22
|
retryable: false,
|
|
@@ -67,40 +68,29 @@ export const capabilityToolDefinitions = [
|
|
|
67
68
|
getKernelInfoTool,
|
|
68
69
|
listTaurusFeaturesTool,
|
|
69
70
|
setCloudRegionTool,
|
|
70
|
-
|
|
71
|
+
getSessionBindingTool,
|
|
72
|
+
beginSqlLoginTool,
|
|
73
|
+
clearSqlCredentialsTool,
|
|
74
|
+
setDefaultDatabaseTool,
|
|
71
75
|
listCloudTaurusInstancesTool,
|
|
72
76
|
selectCloudTaurusInstanceTool,
|
|
73
77
|
];
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
export const taurusToolDefinitions = [
|
|
79
|
+
explainSqlEnhancedTool,
|
|
80
|
+
flashbackQueryTool,
|
|
81
|
+
listRecycleBinTool,
|
|
82
|
+
restoreRecycleBinTableTool,
|
|
83
|
+
];
|
|
84
|
+
function buildDefaultToolDefinitions(_config) {
|
|
85
|
+
return [
|
|
76
86
|
...commonToolDefinitions,
|
|
77
87
|
...capabilityToolDefinitions,
|
|
78
88
|
...diagnosticToolDefinitions,
|
|
89
|
+
...taurusToolDefinitions,
|
|
79
90
|
];
|
|
80
|
-
if (probe?.features) {
|
|
81
|
-
if (probe.features.ndp_pushdown.available || probe.features.parallel_query.available) {
|
|
82
|
-
tools.push(explainSqlEnhancedTool);
|
|
83
|
-
}
|
|
84
|
-
if (probe.features.flashback_query.available) {
|
|
85
|
-
tools.push(flashbackQueryTool);
|
|
86
|
-
}
|
|
87
|
-
if (probe.features.recycle_bin.available) {
|
|
88
|
-
tools.push(listRecycleBinTool, restoreRecycleBinTableTool);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return tools;
|
|
92
|
-
}
|
|
93
|
-
function isToolDefinitionArray(value) {
|
|
94
|
-
return Array.isArray(value);
|
|
95
91
|
}
|
|
96
|
-
export function registerTools(server, deps, config,
|
|
97
|
-
const probe = isToolDefinitionArray(probeOrTools) ? undefined : probeOrTools;
|
|
98
|
-
const tools = maybeTools
|
|
99
|
-
?? (isToolDefinitionArray(probeOrTools) ? probeOrTools : buildDefaultToolDefinitions(config, probe));
|
|
92
|
+
export function registerTools(server, deps, config, tools = buildDefaultToolDefinitions(config)) {
|
|
100
93
|
for (const tool of tools) {
|
|
101
|
-
if (tool.exposeWhen && !tool.exposeWhen(config)) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
94
|
registerOneTool(server, tool, deps);
|
|
105
95
|
}
|
|
106
96
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ToolDefinition } from "../registry.js";
|
|
2
2
|
export declare const setCloudRegionTool: ToolDefinition;
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const setDefaultDatabaseTool: ToolDefinition;
|
|
4
|
+
export declare const clearSqlCredentialsTool: ToolDefinition;
|
|
5
|
+
export declare const getSessionBindingTool: ToolDefinition;
|
|
4
6
|
export declare const selectCloudTaurusInstanceTool: ToolDefinition;
|
|
@@ -50,6 +50,15 @@ function normalizePort(port) {
|
|
|
50
50
|
}
|
|
51
51
|
return undefined;
|
|
52
52
|
}
|
|
53
|
+
function maskUsername(username) {
|
|
54
|
+
if (!username) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (username.length <= 2) {
|
|
58
|
+
return `${username[0]}*`;
|
|
59
|
+
}
|
|
60
|
+
return `${username.slice(0, 1)}***${username.slice(-1)}`;
|
|
61
|
+
}
|
|
53
62
|
export const setCloudRegionTool = {
|
|
54
63
|
name: "set_cloud_region",
|
|
55
64
|
description: "Update the active Huawei Cloud region for the current MCP session and reset any stale cloud project or instance selections.",
|
|
@@ -88,42 +97,152 @@ export const setCloudRegionTool = {
|
|
|
88
97
|
}
|
|
89
98
|
},
|
|
90
99
|
};
|
|
91
|
-
export const
|
|
92
|
-
name: "
|
|
93
|
-
description: "
|
|
100
|
+
export const setDefaultDatabaseTool = {
|
|
101
|
+
name: "set_default_database",
|
|
102
|
+
description: "Set the default database for a datasource in the current MCP session so later tool calls can omit input.database.",
|
|
94
103
|
inputSchema: {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
database: z
|
|
105
|
+
.string()
|
|
106
|
+
.trim()
|
|
107
|
+
.min(1)
|
|
108
|
+
.describe("Database name to bind as the session default for the selected datasource."),
|
|
109
|
+
datasource: z
|
|
110
|
+
.string()
|
|
111
|
+
.trim()
|
|
112
|
+
.min(1)
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("Optional datasource template to bind. Defaults to the current default datasource."),
|
|
98
115
|
},
|
|
99
116
|
async handler(input, deps, context) {
|
|
100
117
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (!accessKeyId || !secretAccessKey) {
|
|
105
|
-
throw new ToolInputError("access_key_id and secret_access_key are required.");
|
|
118
|
+
const database = typeof input.database === "string" ? input.database.trim() : "";
|
|
119
|
+
if (!database) {
|
|
120
|
+
throw new ToolInputError("database is required.");
|
|
106
121
|
}
|
|
107
|
-
deps.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
deps.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
const datasource = await resolveBindingDatasource(deps, typeof input.datasource === "string" ? input.datasource : undefined);
|
|
123
|
+
if (!datasource) {
|
|
124
|
+
throw new ToolInputError("No datasource selected. Configure a default datasource or pass datasource explicitly.");
|
|
125
|
+
}
|
|
126
|
+
const ctx = await deps.engine.resolveContext({
|
|
127
|
+
datasource,
|
|
128
|
+
readonly: true,
|
|
129
|
+
}, context.taskId);
|
|
130
|
+
const databases = await deps.engine.listDatabases(ctx);
|
|
131
|
+
if (!databases.some((item) => item.name === database)) {
|
|
132
|
+
throw new ToolInputError(`Database "${database}" was not found on datasource "${datasource}". Run list_databases first and choose one of the returned names.`);
|
|
133
|
+
}
|
|
134
|
+
const profile = await deps.profileLoader.get(datasource);
|
|
135
|
+
if (!profile) {
|
|
136
|
+
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
137
|
+
}
|
|
138
|
+
const currentTarget = deps.profileLoader.getRuntimeTarget(datasource);
|
|
139
|
+
deps.profileLoader.setRuntimeTarget(datasource, {
|
|
140
|
+
host: currentTarget?.host ?? profile.host,
|
|
141
|
+
port: currentTarget?.port ?? profile.port,
|
|
142
|
+
instanceId: currentTarget?.instanceId,
|
|
143
|
+
nodeId: currentTarget?.nodeId,
|
|
144
|
+
database,
|
|
145
|
+
});
|
|
115
146
|
await reloadEngine(deps);
|
|
116
147
|
return formatSuccess({
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
datasource,
|
|
149
|
+
database,
|
|
150
|
+
}, {
|
|
151
|
+
summary: `Default database for ${datasource} set to ${database} in the current session.`,
|
|
152
|
+
metadata: metadata(context.taskId),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return formatToolError(error, {
|
|
157
|
+
action: "set_default_database",
|
|
158
|
+
metadata: metadata(context.taskId),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
export const clearSqlCredentialsTool = {
|
|
164
|
+
name: "clear_sql_credentials",
|
|
165
|
+
description: "Clear any session-scoped SQL credential override for a datasource and fall back to the configured profile credentials.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
datasource: z
|
|
168
|
+
.string()
|
|
169
|
+
.trim()
|
|
170
|
+
.min(1)
|
|
171
|
+
.optional()
|
|
172
|
+
.describe("Optional datasource template to clear. Defaults to the current default datasource."),
|
|
173
|
+
},
|
|
174
|
+
async handler(input, deps, context) {
|
|
175
|
+
try {
|
|
176
|
+
const datasource = await resolveBindingDatasource(deps, typeof input.datasource === "string" ? input.datasource : undefined);
|
|
177
|
+
if (!datasource) {
|
|
178
|
+
throw new ToolInputError("No datasource selected. Configure a default datasource or pass datasource explicitly.");
|
|
179
|
+
}
|
|
180
|
+
const profile = await deps.profileLoader.get(datasource);
|
|
181
|
+
if (!profile) {
|
|
182
|
+
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
183
|
+
}
|
|
184
|
+
deps.profileLoader.clearRuntimeUser(datasource);
|
|
185
|
+
await reloadEngine(deps);
|
|
186
|
+
return formatSuccess({
|
|
187
|
+
datasource,
|
|
188
|
+
}, {
|
|
189
|
+
summary: `Session SQL credential override cleared for ${datasource}.`,
|
|
190
|
+
metadata: metadata(context.taskId),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return formatToolError(error, {
|
|
195
|
+
action: "clear_sql_credentials",
|
|
196
|
+
metadata: metadata(context.taskId),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
export const getSessionBindingTool = {
|
|
202
|
+
name: "get_session_binding",
|
|
203
|
+
description: "Return the current session binding for a datasource, including selected instance, host, database, and whether SQL credentials are overridden in memory.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
datasource: z
|
|
206
|
+
.string()
|
|
207
|
+
.trim()
|
|
208
|
+
.min(1)
|
|
209
|
+
.optional()
|
|
210
|
+
.describe("Optional datasource template to inspect. Defaults to the current default datasource."),
|
|
211
|
+
},
|
|
212
|
+
async handler(input, deps, context) {
|
|
213
|
+
try {
|
|
214
|
+
const datasource = await resolveBindingDatasource(deps, typeof input.datasource === "string" ? input.datasource : undefined);
|
|
215
|
+
if (!datasource) {
|
|
216
|
+
throw new ToolInputError("No datasource selected. Configure a default datasource or pass datasource explicitly.");
|
|
217
|
+
}
|
|
218
|
+
const profile = await deps.profileLoader.get(datasource);
|
|
219
|
+
if (!profile) {
|
|
220
|
+
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
221
|
+
}
|
|
222
|
+
const target = deps.profileLoader.getRuntimeTarget(datasource);
|
|
223
|
+
return formatSuccess({
|
|
224
|
+
datasource,
|
|
225
|
+
host: profile.host,
|
|
226
|
+
port: profile.port,
|
|
227
|
+
database: profile.database,
|
|
228
|
+
username_masked: maskUsername(profile.user?.username),
|
|
229
|
+
runtime_override: {
|
|
230
|
+
instance_id: target?.instanceId,
|
|
231
|
+
node_id: target?.nodeId,
|
|
232
|
+
host: target?.host,
|
|
233
|
+
port: target?.port,
|
|
234
|
+
database: target?.database,
|
|
235
|
+
has_sql_credentials_override: Boolean(target?.user),
|
|
236
|
+
username_masked: maskUsername(target?.user?.username),
|
|
237
|
+
},
|
|
119
238
|
}, {
|
|
120
|
-
summary:
|
|
239
|
+
summary: `Returned current session binding for ${datasource}.`,
|
|
121
240
|
metadata: metadata(context.taskId),
|
|
122
241
|
});
|
|
123
242
|
}
|
|
124
243
|
catch (error) {
|
|
125
244
|
return formatToolError(error, {
|
|
126
|
-
action: "
|
|
245
|
+
action: "get_session_binding",
|
|
127
246
|
metadata: metadata(context.taskId),
|
|
128
247
|
});
|
|
129
248
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { TaurusDBEngine } from "taurusdb-core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { formatSuccess } from "../../utils/formatter.js";
|
|
4
|
+
import { metadata } from "../common.js";
|
|
5
|
+
import { formatToolError, ToolInputError } from "../error-handling.js";
|
|
6
|
+
export const beginSqlLoginTool = {
|
|
7
|
+
name: "begin_sql_login",
|
|
8
|
+
description: "Create a short-lived local login URL where the user can enter SQL credentials without sending the password through the Agent or MCP tool arguments.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
datasource: z
|
|
11
|
+
.string()
|
|
12
|
+
.trim()
|
|
13
|
+
.min(1)
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Optional datasource template. Defaults to the current default datasource."),
|
|
16
|
+
},
|
|
17
|
+
async handler(input, deps, context) {
|
|
18
|
+
try {
|
|
19
|
+
const explicit = typeof input.datasource === "string" ? input.datasource.trim() : "";
|
|
20
|
+
const datasource = explicit || (await deps.engine.getDefaultDataSource());
|
|
21
|
+
if (!datasource) {
|
|
22
|
+
throw new ToolInputError("No datasource selected. Configure a default datasource or pass datasource explicitly.");
|
|
23
|
+
}
|
|
24
|
+
const profile = await deps.profileLoader.get(datasource);
|
|
25
|
+
if (!profile) {
|
|
26
|
+
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
27
|
+
}
|
|
28
|
+
const issued = await deps.credentialLogin.issueSqlLogin({
|
|
29
|
+
datasource,
|
|
30
|
+
bind: async ({ username, password }) => {
|
|
31
|
+
const currentTarget = deps.profileLoader.getRuntimeTarget(datasource);
|
|
32
|
+
deps.profileLoader.setRuntimeTarget(datasource, {
|
|
33
|
+
host: currentTarget?.host ?? profile.host,
|
|
34
|
+
port: currentTarget?.port ?? profile.port,
|
|
35
|
+
database: currentTarget?.database ?? profile.database,
|
|
36
|
+
instanceId: currentTarget?.instanceId,
|
|
37
|
+
nodeId: currentTarget?.nodeId,
|
|
38
|
+
user: {
|
|
39
|
+
username,
|
|
40
|
+
password: { type: "plain", value: password },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const nextEngine = await TaurusDBEngine.create({
|
|
44
|
+
config: deps.config,
|
|
45
|
+
profileLoader: deps.profileLoader,
|
|
46
|
+
});
|
|
47
|
+
const previousEngine = deps.engine;
|
|
48
|
+
deps.engine = nextEngine;
|
|
49
|
+
if (previousEngine?.close) {
|
|
50
|
+
await previousEngine.close();
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return formatSuccess({
|
|
55
|
+
datasource,
|
|
56
|
+
login_url: issued.loginUrl,
|
|
57
|
+
expires_at: issued.expiresAt,
|
|
58
|
+
}, {
|
|
59
|
+
summary: `Created a secure local SQL login link for ${datasource}.`,
|
|
60
|
+
metadata: metadata(context.taskId),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return formatToolError(error, {
|
|
65
|
+
action: "begin_sql_login",
|
|
66
|
+
metadata: metadata(context.taskId),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taurusdb-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "TaurusDB MCP server for Claude, Cursor, VS Code, and other MCP clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"test": "node --test tests/*.test.mjs"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"taurusdb-core": "0.
|
|
35
|
+
"taurusdb-core": "0.3.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
37
37
|
"mysql2": "^3.22.1",
|
|
38
38
|
"zod": "^3.24.1"
|