taurusdb-mcp 0.2.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/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/error-handling.js +3 -3
- package/dist/tools/query.js +8 -8
- package/dist/tools/registry.d.ts +2 -7
- package/dist/tools/registry.js +7 -17
- package/dist/tools/taurus/cloud-context.d.ts +0 -2
- package/dist/tools/taurus/cloud-context.js +2 -116
- 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/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) {
|
|
@@ -116,11 +116,11 @@ export function formatToolError(error, context) {
|
|
|
116
116
|
metadata: context.metadata,
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
|
+
logger.error({ err: error, action: context.action }, "Tool execution failed unexpectedly");
|
|
119
120
|
return formatError({
|
|
120
121
|
code: ErrorCode.CONNECTION_FAILED,
|
|
121
|
-
message:
|
|
122
|
+
message: `${context.action} failed unexpectedly.`,
|
|
122
123
|
summary: `${context.action} failed unexpectedly.`,
|
|
123
124
|
metadata: context.metadata,
|
|
124
|
-
details: detailsOf(error),
|
|
125
125
|
});
|
|
126
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."
|
|
@@ -184,11 +184,11 @@ export const executeSqlTool = {
|
|
|
184
184
|
},
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
|
-
const confirmationResponse = await ensureConfirmation(
|
|
187
|
+
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context.taskId, asOptionalString(input.confirmation_token, "confirmation_token"));
|
|
188
188
|
if (confirmationResponse) {
|
|
189
189
|
return confirmationResponse;
|
|
190
190
|
}
|
|
191
|
-
const result = await deps.engine.executeMutation(
|
|
191
|
+
const result = await deps.engine.executeMutation(decision.normalizedSql, ctx, {
|
|
192
192
|
timeoutMs: decision.runtimeLimits.timeoutMs,
|
|
193
193
|
});
|
|
194
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,9 +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
17
|
export declare const taurusToolDefinitions: ToolDefinition[];
|
|
23
|
-
export declare function registerTools(server: McpServer, deps: ToolDeps, config: Config,
|
|
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 { clearSqlCredentialsTool, getSessionBindingTool, selectCloudTaurusInstanceTool,
|
|
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,9 +68,8 @@ export const capabilityToolDefinitions = [
|
|
|
67
68
|
getKernelInfoTool,
|
|
68
69
|
listTaurusFeaturesTool,
|
|
69
70
|
setCloudRegionTool,
|
|
70
|
-
setCloudAccessKeysTool,
|
|
71
71
|
getSessionBindingTool,
|
|
72
|
-
|
|
72
|
+
beginSqlLoginTool,
|
|
73
73
|
clearSqlCredentialsTool,
|
|
74
74
|
setDefaultDatabaseTool,
|
|
75
75
|
listCloudTaurusInstancesTool,
|
|
@@ -81,8 +81,7 @@ export const taurusToolDefinitions = [
|
|
|
81
81
|
listRecycleBinTool,
|
|
82
82
|
restoreRecycleBinTableTool,
|
|
83
83
|
];
|
|
84
|
-
function buildDefaultToolDefinitions(_config
|
|
85
|
-
void probe;
|
|
84
|
+
function buildDefaultToolDefinitions(_config) {
|
|
86
85
|
return [
|
|
87
86
|
...commonToolDefinitions,
|
|
88
87
|
...capabilityToolDefinitions,
|
|
@@ -90,17 +89,8 @@ function buildDefaultToolDefinitions(_config, probe) {
|
|
|
90
89
|
...taurusToolDefinitions,
|
|
91
90
|
];
|
|
92
91
|
}
|
|
93
|
-
function
|
|
94
|
-
return Array.isArray(value);
|
|
95
|
-
}
|
|
96
|
-
export function registerTools(server, deps, config, probeOrTools, maybeTools) {
|
|
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,8 +1,6 @@
|
|
|
1
1
|
import type { ToolDefinition } from "../registry.js";
|
|
2
2
|
export declare const setCloudRegionTool: ToolDefinition;
|
|
3
|
-
export declare const setCloudAccessKeysTool: ToolDefinition;
|
|
4
3
|
export declare const setDefaultDatabaseTool: ToolDefinition;
|
|
5
|
-
export declare const setSqlCredentialsTool: ToolDefinition;
|
|
6
4
|
export declare const clearSqlCredentialsTool: ToolDefinition;
|
|
7
5
|
export declare const getSessionBindingTool: ToolDefinition;
|
|
8
6
|
export declare const selectCloudTaurusInstanceTool: ToolDefinition;
|
|
@@ -97,47 +97,6 @@ export const setCloudRegionTool = {
|
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
99
|
};
|
|
100
|
-
export const setCloudAccessKeysTool = {
|
|
101
|
-
name: "set_cloud_access_keys",
|
|
102
|
-
description: "Update the active Huawei Cloud AK/SK for the current MCP session and clear stale token or instance bindings.",
|
|
103
|
-
inputSchema: {
|
|
104
|
-
access_key_id: z.string().trim().min(1).describe("Huawei Cloud access key id."),
|
|
105
|
-
secret_access_key: z.string().trim().min(1).describe("Huawei Cloud secret access key."),
|
|
106
|
-
security_token: z.string().trim().min(1).optional().describe("Optional temporary security token when using temporary AK/SK."),
|
|
107
|
-
},
|
|
108
|
-
async handler(input, deps, context) {
|
|
109
|
-
try {
|
|
110
|
-
const accessKeyId = typeof input.access_key_id === "string" ? input.access_key_id.trim() : "";
|
|
111
|
-
const secretAccessKey = typeof input.secret_access_key === "string" ? input.secret_access_key.trim() : "";
|
|
112
|
-
const securityToken = typeof input.security_token === "string" ? input.security_token.trim() : undefined;
|
|
113
|
-
if (!accessKeyId || !secretAccessKey) {
|
|
114
|
-
throw new ToolInputError("access_key_id and secret_access_key are required.");
|
|
115
|
-
}
|
|
116
|
-
deps.config.cloud.accessKeyId = accessKeyId;
|
|
117
|
-
deps.config.cloud.secretAccessKey = secretAccessKey;
|
|
118
|
-
deps.config.cloud.securityToken = securityToken;
|
|
119
|
-
deps.config.cloud.authToken = undefined;
|
|
120
|
-
deps.config.slowSqlSource.taurusApi.authToken = undefined;
|
|
121
|
-
deps.config.slowSqlSource.das.authToken = undefined;
|
|
122
|
-
deps.config.metricsSource.ces.authToken = undefined;
|
|
123
|
-
clearCloudSelection(deps);
|
|
124
|
-
await reloadEngine(deps);
|
|
125
|
-
return formatSuccess({
|
|
126
|
-
access_key_id_suffix: accessKeyId.slice(-4),
|
|
127
|
-
uses_security_token: Boolean(securityToken),
|
|
128
|
-
}, {
|
|
129
|
-
summary: "Cloud access keys updated for the current session.",
|
|
130
|
-
metadata: metadata(context.taskId),
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
return formatToolError(error, {
|
|
135
|
-
action: "set_cloud_access_keys",
|
|
136
|
-
metadata: metadata(context.taskId),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
100
|
export const setDefaultDatabaseTool = {
|
|
142
101
|
name: "set_default_database",
|
|
143
102
|
description: "Set the default database for a datasource in the current MCP session so later tool calls can omit input.database.",
|
|
@@ -201,71 +160,6 @@ export const setDefaultDatabaseTool = {
|
|
|
201
160
|
}
|
|
202
161
|
},
|
|
203
162
|
};
|
|
204
|
-
export const setSqlCredentialsTool = {
|
|
205
|
-
name: "set_sql_credentials",
|
|
206
|
-
description: "Set the SQL username and password for a datasource in the current MCP session without writing credentials to disk.",
|
|
207
|
-
inputSchema: {
|
|
208
|
-
username: z
|
|
209
|
-
.string()
|
|
210
|
-
.trim()
|
|
211
|
-
.min(1)
|
|
212
|
-
.describe("Database username to use for the selected datasource in this session."),
|
|
213
|
-
password: z
|
|
214
|
-
.string()
|
|
215
|
-
.trim()
|
|
216
|
-
.min(1)
|
|
217
|
-
.describe("Database password to use for the selected datasource in this session."),
|
|
218
|
-
datasource: z
|
|
219
|
-
.string()
|
|
220
|
-
.trim()
|
|
221
|
-
.min(1)
|
|
222
|
-
.optional()
|
|
223
|
-
.describe("Optional datasource template to bind. Defaults to the current default datasource."),
|
|
224
|
-
},
|
|
225
|
-
async handler(input, deps, context) {
|
|
226
|
-
try {
|
|
227
|
-
const username = typeof input.username === "string" ? input.username.trim() : "";
|
|
228
|
-
const password = typeof input.password === "string" ? input.password.trim() : "";
|
|
229
|
-
if (!username || !password) {
|
|
230
|
-
throw new ToolInputError("username and password are required.");
|
|
231
|
-
}
|
|
232
|
-
const datasource = await resolveBindingDatasource(deps, typeof input.datasource === "string" ? input.datasource : undefined);
|
|
233
|
-
if (!datasource) {
|
|
234
|
-
throw new ToolInputError("No datasource selected. Configure a default datasource or pass datasource explicitly.");
|
|
235
|
-
}
|
|
236
|
-
const profile = await deps.profileLoader.get(datasource);
|
|
237
|
-
if (!profile) {
|
|
238
|
-
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
239
|
-
}
|
|
240
|
-
const currentTarget = deps.profileLoader.getRuntimeTarget(datasource);
|
|
241
|
-
deps.profileLoader.setRuntimeTarget(datasource, {
|
|
242
|
-
host: currentTarget?.host ?? profile.host,
|
|
243
|
-
port: currentTarget?.port ?? profile.port,
|
|
244
|
-
database: currentTarget?.database ?? profile.database,
|
|
245
|
-
instanceId: currentTarget?.instanceId,
|
|
246
|
-
nodeId: currentTarget?.nodeId,
|
|
247
|
-
user: {
|
|
248
|
-
username,
|
|
249
|
-
password: { type: "plain", value: password },
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
await reloadEngine(deps);
|
|
253
|
-
return formatSuccess({
|
|
254
|
-
datasource,
|
|
255
|
-
username,
|
|
256
|
-
}, {
|
|
257
|
-
summary: `SQL credentials for ${datasource} updated in the current session.`,
|
|
258
|
-
metadata: metadata(context.taskId),
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
return formatToolError(error, {
|
|
263
|
-
action: "set_sql_credentials",
|
|
264
|
-
metadata: metadata(context.taskId),
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
163
|
export const clearSqlCredentialsTool = {
|
|
270
164
|
name: "clear_sql_credentials",
|
|
271
165
|
description: "Clear any session-scoped SQL credential override for a datasource and fall back to the configured profile credentials.",
|
|
@@ -287,15 +181,7 @@ export const clearSqlCredentialsTool = {
|
|
|
287
181
|
if (!profile) {
|
|
288
182
|
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
289
183
|
}
|
|
290
|
-
|
|
291
|
-
deps.profileLoader.setRuntimeTarget(datasource, {
|
|
292
|
-
host: currentTarget?.host ?? profile.host,
|
|
293
|
-
port: currentTarget?.port ?? profile.port,
|
|
294
|
-
database: currentTarget?.database ?? profile.database,
|
|
295
|
-
instanceId: currentTarget?.instanceId,
|
|
296
|
-
nodeId: currentTarget?.nodeId,
|
|
297
|
-
user: profile.user,
|
|
298
|
-
});
|
|
184
|
+
deps.profileLoader.clearRuntimeUser(datasource);
|
|
299
185
|
await reloadEngine(deps);
|
|
300
186
|
return formatSuccess({
|
|
301
187
|
datasource,
|
|
@@ -339,7 +225,7 @@ export const getSessionBindingTool = {
|
|
|
339
225
|
host: profile.host,
|
|
340
226
|
port: profile.port,
|
|
341
227
|
database: profile.database,
|
|
342
|
-
username_masked: maskUsername(profile.user
|
|
228
|
+
username_masked: maskUsername(profile.user?.username),
|
|
343
229
|
runtime_override: {
|
|
344
230
|
instance_id: target?.instanceId,
|
|
345
231
|
node_id: target?.nodeId,
|
|
@@ -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"
|