taurusdb-mcp 0.3.0 → 0.5.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -6
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +66 -0
- package/dist/commands/credentials.d.ts +3 -0
- package/dist/commands/credentials.js +190 -0
- package/dist/index.js +10 -0
- package/dist/security/session-coordinator.d.ts +9 -0
- package/dist/security/session-coordinator.js +46 -0
- package/dist/server.d.ts +8 -1
- package/dist/server.js +11 -1
- package/dist/tools/common/public-formatters.d.ts +8 -0
- package/dist/tools/common/public-formatters.js +4 -0
- package/dist/tools/error-handling.js +10 -1
- package/dist/tools/query.js +18 -13
- package/dist/tools/registry.d.ts +5 -0
- package/dist/tools/registry.js +127 -7
- package/dist/tools/taurus/cloud-context.js +85 -53
- package/dist/tools/taurus/diagnostics.d.ts +1 -0
- package/dist/tools/taurus/diagnostics.js +31 -0
- package/dist/tools/taurus/recycle-bin.js +15 -11
- package/dist/tools/taurus/sql-login.js +40 -20
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -5
- package/scripts/windows-credential-configure.ps1 +51 -0
package/dist/tools/query.js
CHANGED
|
@@ -12,23 +12,24 @@ async function inspectSql(toolName, sql, ctx, deps) {
|
|
|
12
12
|
context: ctx,
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
async function ensureConfirmation(decision, ctx, deps,
|
|
15
|
+
async function ensureConfirmation(decision, ctx, deps, invokeContext, approvalToken) {
|
|
16
16
|
if (!decision.requiresConfirmation) {
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
|
-
const responseMetadata = metadata(taskId, {
|
|
19
|
+
const responseMetadata = metadata(invokeContext.taskId, {
|
|
20
20
|
sql_hash: decision.sqlHash,
|
|
21
21
|
statement_type: statementTypeFromSql(decision.normalizedSql),
|
|
22
22
|
});
|
|
23
|
-
if (
|
|
24
|
-
const validation = await deps.engine.validateConfirmation(
|
|
23
|
+
if (approvalToken) {
|
|
24
|
+
const validation = await deps.engine.validateConfirmation(approvalToken, decision.normalizedSql, ctx);
|
|
25
25
|
if (validation.valid) {
|
|
26
|
+
invokeContext.approvalActor = validation.actor;
|
|
26
27
|
return undefined;
|
|
27
28
|
}
|
|
28
29
|
return formatError({
|
|
29
30
|
code: ErrorCode.CONFIRMATION_INVALID,
|
|
30
|
-
message: validation.reason ?? "
|
|
31
|
-
summary: "The provided
|
|
31
|
+
message: validation.reason ?? "Approval token validation failed.",
|
|
32
|
+
summary: "The provided external approval is invalid for this SQL statement.",
|
|
32
33
|
metadata: responseMetadata,
|
|
33
34
|
details: {
|
|
34
35
|
reason_codes: validation.reasonCodes,
|
|
@@ -37,9 +38,10 @@ async function ensureConfirmation(decision, ctx, deps, taskId, confirmationToken
|
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
const outcome = await deps.engine.handleConfirmation(decision, ctx);
|
|
40
|
-
if (outcome.status === "
|
|
41
|
+
if (outcome.status === "approval_required") {
|
|
41
42
|
return formatConfirmationRequired({
|
|
42
|
-
|
|
43
|
+
approvalRequest: outcome.request,
|
|
44
|
+
requestId: outcome.requestId,
|
|
43
45
|
metadata: responseMetadata,
|
|
44
46
|
riskLevel: decision.riskLevel,
|
|
45
47
|
sqlHash: decision.sqlHash,
|
|
@@ -53,7 +55,7 @@ export const executeReadonlySqlTool = {
|
|
|
53
55
|
inputSchema: {
|
|
54
56
|
...contextInputShape,
|
|
55
57
|
sql: requiredSqlSchema("Readonly SQL to execute."),
|
|
56
|
-
|
|
58
|
+
approval_token: optionalTokenSchema(),
|
|
57
59
|
},
|
|
58
60
|
async handler(input, deps, context) {
|
|
59
61
|
const sql = asRequiredString(input.sql, "sql");
|
|
@@ -76,7 +78,7 @@ export const executeReadonlySqlTool = {
|
|
|
76
78
|
},
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
|
-
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context
|
|
81
|
+
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context, asOptionalString(input.approval_token, "approval_token"));
|
|
80
82
|
if (confirmationResponse) {
|
|
81
83
|
return confirmationResponse;
|
|
82
84
|
}
|
|
@@ -85,6 +87,9 @@ export const executeReadonlySqlTool = {
|
|
|
85
87
|
maxRows: decision.runtimeLimits.maxRows,
|
|
86
88
|
maxColumns: decision.runtimeLimits.maxColumns,
|
|
87
89
|
maxFieldChars: decision.runtimeLimits.maxFieldChars,
|
|
90
|
+
maxResultBytes: decision.runtimeLimits.maxResultBytes,
|
|
91
|
+
maxBlobBytes: decision.runtimeLimits.maxBlobBytes,
|
|
92
|
+
maskAllColumns: decision.runtimeLimits.maskAllColumns,
|
|
88
93
|
});
|
|
89
94
|
return formatSuccess(toPublicQueryResult(result), {
|
|
90
95
|
summary: summarizeRows(result.rowCount, result.truncated),
|
|
@@ -161,7 +166,7 @@ export const executeSqlTool = {
|
|
|
161
166
|
inputSchema: {
|
|
162
167
|
...contextInputShape,
|
|
163
168
|
sql: requiredSqlSchema("Mutation SQL to execute."),
|
|
164
|
-
|
|
169
|
+
approval_token: optionalTokenSchema(),
|
|
165
170
|
},
|
|
166
171
|
async handler(input, deps, context) {
|
|
167
172
|
const sql = asRequiredString(input.sql, "sql");
|
|
@@ -184,7 +189,7 @@ export const executeSqlTool = {
|
|
|
184
189
|
},
|
|
185
190
|
});
|
|
186
191
|
}
|
|
187
|
-
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context
|
|
192
|
+
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context, asOptionalString(input.approval_token, "approval_token"));
|
|
188
193
|
if (confirmationResponse) {
|
|
189
194
|
return confirmationResponse;
|
|
190
195
|
}
|
|
@@ -219,5 +224,5 @@ function optionalTokenSchema() {
|
|
|
219
224
|
.trim()
|
|
220
225
|
.min(1)
|
|
221
226
|
.optional()
|
|
222
|
-
.describe("
|
|
227
|
+
.describe("Externally signed approval token produced by `taurusdb-mcp approve`.");
|
|
223
228
|
}
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
import { type Config } from "taurusdb-core";
|
|
3
4
|
import type { ServerDeps } from "../server.js";
|
|
4
5
|
import { type ToolResponse } from "../utils/formatter.js";
|
|
5
6
|
export type ToolDeps = ServerDeps;
|
|
6
7
|
export type ToolInvokeContext = {
|
|
7
8
|
taskId: string;
|
|
9
|
+
approvalActor?: string;
|
|
8
10
|
};
|
|
9
11
|
export interface ToolDefinition<I extends Record<string, unknown> = Record<string, unknown>, O = unknown> {
|
|
10
12
|
name: string;
|
|
11
13
|
description: string;
|
|
12
14
|
inputSchema: Record<string, unknown>;
|
|
15
|
+
annotations?: ToolAnnotations;
|
|
13
16
|
handler: (input: I, deps: ToolDeps, context: ToolInvokeContext) => Promise<ToolResponse<O>>;
|
|
14
17
|
}
|
|
15
18
|
export declare const commonToolDefinitions: ToolDefinition[];
|
|
16
19
|
export declare const capabilityToolDefinitions: ToolDefinition[];
|
|
20
|
+
export declare const dynamicTargetToolDefinitions: ToolDefinition[];
|
|
17
21
|
export declare const taurusToolDefinitions: ToolDefinition[];
|
|
22
|
+
export declare const mutationToolDefinitions: ToolDefinition[];
|
|
18
23
|
export declare function registerTools(server: McpServer, deps: ToolDeps, config: Config, tools?: ToolDefinition[]): void;
|
package/dist/tools/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { generateTaskId, logger, withTaskContext, } from "taurusdb-core";
|
|
2
3
|
import { executeReadonlySqlTool, executeSqlTool, explainSqlTool, } from "./query.js";
|
|
3
|
-
import { describeTableTool, listDatabasesTool, listTablesTool, } from "./discovery.js";
|
|
4
|
+
import { describeTableTool, listDataSourcesTool, listDatabasesTool, listTablesTool, } from "./discovery.js";
|
|
4
5
|
import { showProcesslistTool } from "./processlist.js";
|
|
5
6
|
import { pingTool } from "./ping.js";
|
|
6
7
|
import { getKernelInfoTool, listTaurusFeaturesTool } from "./taurus/capability.js";
|
|
@@ -29,22 +30,56 @@ function registerOneTool(server, tool, deps) {
|
|
|
29
30
|
const taskId = generateTaskId();
|
|
30
31
|
return withTaskContext(taskId, async () => {
|
|
31
32
|
const startedAt = Date.now();
|
|
33
|
+
const invokeContext = { taskId };
|
|
32
34
|
logger.info({ tool: tool.name }, "Tool invocation started");
|
|
33
35
|
try {
|
|
34
|
-
const
|
|
36
|
+
const invoke = () => tool.handler(rawInput, deps, invokeContext);
|
|
37
|
+
const response = deps.sessionCoordinator
|
|
38
|
+
? SESSION_MUTATION_TOOLS.has(tool.name)
|
|
39
|
+
? await deps.sessionCoordinator.runExclusive(invoke)
|
|
40
|
+
: await deps.sessionCoordinator.runShared(invoke)
|
|
41
|
+
: await invoke();
|
|
42
|
+
try {
|
|
43
|
+
await writeAuditEvent(deps, tool.name, rawInput, response, invokeContext, Date.now() - startedAt);
|
|
44
|
+
}
|
|
45
|
+
catch (auditError) {
|
|
46
|
+
logger.fatal({ err: auditError, tool: tool.name }, "Tool completed but audit persistence failed");
|
|
47
|
+
return toMcpToolResult(formatError({
|
|
48
|
+
code: ErrorCode.AUDIT_FAILED,
|
|
49
|
+
message: "The tool completed but its audit event could not be persisted. Verify database state before retrying.",
|
|
50
|
+
summary: "Audit persistence failed after tool execution.",
|
|
51
|
+
metadata: response.metadata,
|
|
52
|
+
retryable: false,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
35
55
|
logger.info({ tool: tool.name, ok: response.ok, durationMs: Date.now() - startedAt }, "Tool invocation finished");
|
|
36
56
|
return toMcpToolResult(response);
|
|
37
57
|
}
|
|
38
58
|
catch (error) {
|
|
39
59
|
logger.error({ err: error, tool: tool.name }, "Tool invocation failed with unhandled error");
|
|
40
|
-
|
|
60
|
+
const fallback = formatUnhandledToolError(error, taskId);
|
|
61
|
+
try {
|
|
62
|
+
await writeAuditEvent(deps, tool.name, rawInput, fallback.structuredContent, invokeContext, Date.now() - startedAt);
|
|
63
|
+
}
|
|
64
|
+
catch (auditError) {
|
|
65
|
+
logger.fatal({ err: auditError, tool: tool.name }, "Unhandled tool failure could not be audited");
|
|
66
|
+
}
|
|
67
|
+
return fallback;
|
|
41
68
|
}
|
|
42
69
|
});
|
|
43
70
|
};
|
|
44
71
|
if (typeof registrar.registerTool === "function") {
|
|
72
|
+
const readOnly = !PRIVILEGED_TOOL_NAMES.has(tool.name);
|
|
45
73
|
registrar.registerTool(tool.name, {
|
|
46
74
|
description: tool.description,
|
|
47
75
|
inputSchema: tool.inputSchema,
|
|
76
|
+
outputSchema: TOOL_OUTPUT_SHAPE,
|
|
77
|
+
annotations: tool.annotations ?? {
|
|
78
|
+
readOnlyHint: readOnly,
|
|
79
|
+
destructiveHint: DATABASE_MUTATION_TOOLS.has(tool.name),
|
|
80
|
+
idempotentHint: readOnly || IDEMPOTENT_SESSION_MUTATION_TOOLS.has(tool.name),
|
|
81
|
+
openWorldHint: true,
|
|
82
|
+
},
|
|
48
83
|
}, wrappedHandler);
|
|
49
84
|
return;
|
|
50
85
|
}
|
|
@@ -54,39 +89,124 @@ function registerOneTool(server, tool, deps) {
|
|
|
54
89
|
}
|
|
55
90
|
throw new Error("Unsupported MCP SDK version: expected `tool` or `registerTool` on McpServer.");
|
|
56
91
|
}
|
|
92
|
+
const SESSION_MUTATION_TOOLS = new Set([
|
|
93
|
+
"set_cloud_region",
|
|
94
|
+
"begin_sql_login",
|
|
95
|
+
"clear_sql_credentials",
|
|
96
|
+
"set_default_database",
|
|
97
|
+
"select_cloud_taurus_instance",
|
|
98
|
+
]);
|
|
99
|
+
const PRIVILEGED_TOOL_NAMES = new Set([
|
|
100
|
+
...SESSION_MUTATION_TOOLS,
|
|
101
|
+
"execute_sql",
|
|
102
|
+
"restore_recycle_bin_table",
|
|
103
|
+
]);
|
|
104
|
+
const DATABASE_MUTATION_TOOLS = new Set([
|
|
105
|
+
"execute_sql",
|
|
106
|
+
"restore_recycle_bin_table",
|
|
107
|
+
]);
|
|
108
|
+
const IDEMPOTENT_SESSION_MUTATION_TOOLS = new Set([
|
|
109
|
+
"set_cloud_region",
|
|
110
|
+
"clear_sql_credentials",
|
|
111
|
+
"set_default_database",
|
|
112
|
+
"select_cloud_taurus_instance",
|
|
113
|
+
]);
|
|
114
|
+
const TOOL_OUTPUT_SHAPE = {
|
|
115
|
+
ok: z.boolean(),
|
|
116
|
+
summary: z.string(),
|
|
117
|
+
data: z.unknown().optional(),
|
|
118
|
+
error: z.unknown().optional(),
|
|
119
|
+
metadata: z.record(z.unknown()),
|
|
120
|
+
};
|
|
121
|
+
async function writeAuditEvent(deps, tool, input, response, context, durationMs) {
|
|
122
|
+
if (!deps.auditWriter) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const datasource = typeof input.datasource === "string"
|
|
126
|
+
? input.datasource
|
|
127
|
+
: deps.config.defaultDatasource ?? (await deps.profileLoader.getDefault());
|
|
128
|
+
const profile = datasource ? await deps.profileLoader.get(datasource) : undefined;
|
|
129
|
+
const target = datasource
|
|
130
|
+
? deps.profileLoader.getRuntimeTarget(datasource)
|
|
131
|
+
: undefined;
|
|
132
|
+
const errorCode = response.error?.code;
|
|
133
|
+
const decision = response.ok
|
|
134
|
+
? "allowed"
|
|
135
|
+
: errorCode === ErrorCode.BLOCKED_SQL
|
|
136
|
+
? "blocked"
|
|
137
|
+
: errorCode === ErrorCode.CONFIRMATION_REQUIRED
|
|
138
|
+
? "approval_required"
|
|
139
|
+
: errorCode === ErrorCode.CONFIRMATION_INVALID
|
|
140
|
+
? "approval_denied"
|
|
141
|
+
: "failed";
|
|
142
|
+
await deps.auditWriter.write({
|
|
143
|
+
timestamp: new Date().toISOString(),
|
|
144
|
+
task_id: context.taskId,
|
|
145
|
+
tool,
|
|
146
|
+
actor: context.approvalActor ??
|
|
147
|
+
(() => {
|
|
148
|
+
const client = deps.clientIdentityProvider?.();
|
|
149
|
+
return client
|
|
150
|
+
? `mcp:${client.name}@${client.version}`
|
|
151
|
+
: "unattributed-mcp-client";
|
|
152
|
+
})(),
|
|
153
|
+
datasource,
|
|
154
|
+
database: typeof input.database === "string" ? input.database : profile?.database,
|
|
155
|
+
host: profile?.host,
|
|
156
|
+
port: profile?.port,
|
|
157
|
+
project_id: deps.config.cloud.projectId,
|
|
158
|
+
instance_id: target?.instanceId ?? profile?.instanceId ?? deps.config.cloud.instanceId,
|
|
159
|
+
node_id: target?.nodeId ?? profile?.nodeId ?? deps.config.cloud.nodeId,
|
|
160
|
+
sql_hash: response.metadata.sql_hash,
|
|
161
|
+
raw_sql: deps.config.audit.includeRawSql && typeof input.sql === "string"
|
|
162
|
+
? input.sql
|
|
163
|
+
: undefined,
|
|
164
|
+
decision,
|
|
165
|
+
outcome: response.ok ? "success" : "error",
|
|
166
|
+
error_code: errorCode,
|
|
167
|
+
duration_ms: durationMs,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
57
170
|
export const commonToolDefinitions = [
|
|
58
171
|
pingTool,
|
|
172
|
+
listDataSourcesTool,
|
|
59
173
|
listDatabasesTool,
|
|
60
174
|
listTablesTool,
|
|
61
175
|
describeTableTool,
|
|
62
176
|
showProcesslistTool,
|
|
63
177
|
executeReadonlySqlTool,
|
|
64
178
|
explainSqlTool,
|
|
65
|
-
executeSqlTool,
|
|
66
179
|
];
|
|
67
180
|
export const capabilityToolDefinitions = [
|
|
68
181
|
getKernelInfoTool,
|
|
69
182
|
listTaurusFeaturesTool,
|
|
70
|
-
setCloudRegionTool,
|
|
71
183
|
getSessionBindingTool,
|
|
184
|
+
listCloudTaurusInstancesTool,
|
|
185
|
+
];
|
|
186
|
+
export const dynamicTargetToolDefinitions = [
|
|
187
|
+
setCloudRegionTool,
|
|
72
188
|
beginSqlLoginTool,
|
|
73
189
|
clearSqlCredentialsTool,
|
|
74
190
|
setDefaultDatabaseTool,
|
|
75
|
-
listCloudTaurusInstancesTool,
|
|
76
191
|
selectCloudTaurusInstanceTool,
|
|
77
192
|
];
|
|
78
193
|
export const taurusToolDefinitions = [
|
|
79
194
|
explainSqlEnhancedTool,
|
|
80
195
|
flashbackQueryTool,
|
|
81
196
|
listRecycleBinTool,
|
|
197
|
+
];
|
|
198
|
+
export const mutationToolDefinitions = [
|
|
199
|
+
executeSqlTool,
|
|
82
200
|
restoreRecycleBinTableTool,
|
|
83
201
|
];
|
|
84
|
-
function buildDefaultToolDefinitions(
|
|
202
|
+
function buildDefaultToolDefinitions(config) {
|
|
85
203
|
return [
|
|
86
204
|
...commonToolDefinitions,
|
|
87
205
|
...capabilityToolDefinitions,
|
|
206
|
+
...(config.security.dynamicTargetsEnabled ? dynamicTargetToolDefinitions : []),
|
|
88
207
|
...diagnosticToolDefinitions,
|
|
89
208
|
...taurusToolDefinitions,
|
|
209
|
+
...(config.security.mutationsEnabled ? mutationToolDefinitions : []),
|
|
90
210
|
];
|
|
91
211
|
}
|
|
92
212
|
export function registerTools(server, deps, config, tools = buildDefaultToolDefinitions(config)) {
|
|
@@ -6,31 +6,57 @@ import { metadata } from "../common.js";
|
|
|
6
6
|
function buildHuaweiCloudEndpoint(service, region, domainSuffix) {
|
|
7
7
|
return `https://${service}.${region}.${domainSuffix}`;
|
|
8
8
|
}
|
|
9
|
-
function clearCloudSelection(deps) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
function clearCloudSelection(config, deps) {
|
|
10
|
+
config.cloud.projectId = undefined;
|
|
11
|
+
config.cloud.instanceId = undefined;
|
|
12
|
+
config.cloud.nodeId = undefined;
|
|
13
|
+
config.slowSqlSource.taurusApi.projectId = undefined;
|
|
14
|
+
config.slowSqlSource.taurusApi.instanceId = undefined;
|
|
15
|
+
config.slowSqlSource.taurusApi.nodeId = undefined;
|
|
16
|
+
config.slowSqlSource.das.projectId = undefined;
|
|
17
|
+
config.slowSqlSource.das.instanceId = undefined;
|
|
18
|
+
config.metricsSource.ces.projectId = undefined;
|
|
19
|
+
config.metricsSource.ces.instanceId = undefined;
|
|
20
|
+
config.metricsSource.ces.nodeId = undefined;
|
|
21
21
|
deps.profileLoader.clearAllRuntimeTargets();
|
|
22
22
|
}
|
|
23
|
-
async function
|
|
23
|
+
async function snapshotRuntimeTargets(deps) {
|
|
24
|
+
const profiles = await deps.profileLoader.load();
|
|
25
|
+
return new Map([...profiles.keys()].flatMap((name) => {
|
|
26
|
+
const target = deps.profileLoader.getRuntimeTarget(name);
|
|
27
|
+
return target ? [[name, target]] : [];
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function restoreRuntimeTargets(deps, targets) {
|
|
31
|
+
deps.profileLoader.clearAllRuntimeTargets();
|
|
32
|
+
for (const [name, target] of targets) {
|
|
33
|
+
deps.profileLoader.setRuntimeTarget(name, target);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function reloadEngine(deps, nextConfig) {
|
|
24
37
|
const nextEngine = await TaurusDBEngine.create({
|
|
25
|
-
config:
|
|
38
|
+
config: nextConfig,
|
|
26
39
|
profileLoader: deps.profileLoader,
|
|
27
40
|
});
|
|
28
41
|
const previousEngine = deps.engine;
|
|
42
|
+
deps.config = nextConfig;
|
|
29
43
|
deps.engine = nextEngine;
|
|
30
44
|
if (previousEngine?.close) {
|
|
31
45
|
await previousEngine.close();
|
|
32
46
|
}
|
|
33
47
|
}
|
|
48
|
+
async function updateSessionState(deps, mutate) {
|
|
49
|
+
const nextConfig = structuredClone(deps.config);
|
|
50
|
+
const previousTargets = await snapshotRuntimeTargets(deps);
|
|
51
|
+
try {
|
|
52
|
+
await mutate(nextConfig);
|
|
53
|
+
await reloadEngine(deps, nextConfig);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
restoreRuntimeTargets(deps, previousTargets);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
34
60
|
async function resolveBindingDatasource(deps, explicit) {
|
|
35
61
|
const trimmed = typeof explicit === "string" ? explicit.trim() : "";
|
|
36
62
|
if (trimmed) {
|
|
@@ -39,7 +65,7 @@ async function resolveBindingDatasource(deps, explicit) {
|
|
|
39
65
|
return deps.engine.getDefaultDataSource();
|
|
40
66
|
}
|
|
41
67
|
function selectInstanceAddress(input) {
|
|
42
|
-
return input.
|
|
68
|
+
return input.privateIps[0] ?? input.hostnames[0] ?? input.publicIps[0];
|
|
43
69
|
}
|
|
44
70
|
function normalizePort(port) {
|
|
45
71
|
if (typeof port === "number" && Number.isFinite(port)) {
|
|
@@ -71,19 +97,22 @@ export const setCloudRegionTool = {
|
|
|
71
97
|
if (!region) {
|
|
72
98
|
throw new ToolInputError("region is required.");
|
|
73
99
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
await updateSessionState(deps, (nextConfig) => {
|
|
101
|
+
const domainSuffix = nextConfig.cloud.domainSuffix ?? "myhuaweicloud.com";
|
|
102
|
+
nextConfig.cloud.region = region;
|
|
103
|
+
nextConfig.cloud.apiEndpoint = buildHuaweiCloudEndpoint("gaussdb", region, domainSuffix);
|
|
104
|
+
nextConfig.cloud.iamEndpoint = buildHuaweiCloudEndpoint("iam", region, domainSuffix);
|
|
105
|
+
nextConfig.cloud.kmsEndpoint = buildHuaweiCloudEndpoint("kms", region, domainSuffix);
|
|
106
|
+
nextConfig.slowSqlSource.taurusApi.endpoint = buildHuaweiCloudEndpoint("gaussdb", region, domainSuffix);
|
|
107
|
+
nextConfig.slowSqlSource.das.endpoint = buildHuaweiCloudEndpoint("das", region, domainSuffix);
|
|
108
|
+
nextConfig.metricsSource.ces.endpoint = buildHuaweiCloudEndpoint("ces", region, domainSuffix);
|
|
109
|
+
clearCloudSelection(nextConfig, deps);
|
|
110
|
+
});
|
|
83
111
|
return formatSuccess({
|
|
84
112
|
region,
|
|
85
113
|
api_endpoint: deps.config.cloud.apiEndpoint,
|
|
86
114
|
iam_endpoint: deps.config.cloud.iamEndpoint,
|
|
115
|
+
kms_endpoint: deps.config.cloud.kmsEndpoint,
|
|
87
116
|
}, {
|
|
88
117
|
summary: `Cloud region switched to ${region}.`,
|
|
89
118
|
metadata: metadata(context.taskId),
|
|
@@ -136,14 +165,15 @@ export const setDefaultDatabaseTool = {
|
|
|
136
165
|
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
137
166
|
}
|
|
138
167
|
const currentTarget = deps.profileLoader.getRuntimeTarget(datasource);
|
|
139
|
-
deps
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
168
|
+
await updateSessionState(deps, () => {
|
|
169
|
+
deps.profileLoader.setRuntimeTarget(datasource, {
|
|
170
|
+
host: currentTarget?.host ?? profile.host,
|
|
171
|
+
port: currentTarget?.port ?? profile.port,
|
|
172
|
+
instanceId: currentTarget?.instanceId,
|
|
173
|
+
nodeId: currentTarget?.nodeId,
|
|
174
|
+
database,
|
|
175
|
+
});
|
|
145
176
|
});
|
|
146
|
-
await reloadEngine(deps);
|
|
147
177
|
return formatSuccess({
|
|
148
178
|
datasource,
|
|
149
179
|
database,
|
|
@@ -181,8 +211,9 @@ export const clearSqlCredentialsTool = {
|
|
|
181
211
|
if (!profile) {
|
|
182
212
|
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
183
213
|
}
|
|
184
|
-
deps
|
|
185
|
-
|
|
214
|
+
await updateSessionState(deps, () => {
|
|
215
|
+
deps.profileLoader.clearRuntimeUser(datasource);
|
|
216
|
+
});
|
|
186
217
|
return formatSuccess({
|
|
187
218
|
datasource,
|
|
188
219
|
}, {
|
|
@@ -278,29 +309,30 @@ export const selectCloudTaurusInstanceTool = {
|
|
|
278
309
|
if (!matched) {
|
|
279
310
|
throw new ToolInputError(`No TaurusDB cloud instance matched id ${instanceId}.`);
|
|
280
311
|
}
|
|
281
|
-
deps.config.cloud.projectId = projectId;
|
|
282
|
-
deps.config.cloud.instanceId = matched.id;
|
|
283
|
-
deps.config.cloud.nodeId = matched.primaryNodeId;
|
|
284
|
-
deps.config.slowSqlSource.taurusApi.projectId = projectId;
|
|
285
|
-
deps.config.slowSqlSource.taurusApi.instanceId = matched.id;
|
|
286
|
-
deps.config.slowSqlSource.taurusApi.nodeId = matched.primaryNodeId;
|
|
287
|
-
deps.config.slowSqlSource.das.projectId = projectId;
|
|
288
|
-
deps.config.slowSqlSource.das.instanceId = matched.id;
|
|
289
|
-
deps.config.metricsSource.ces.projectId = projectId;
|
|
290
|
-
deps.config.metricsSource.ces.instanceId = matched.id;
|
|
291
|
-
deps.config.metricsSource.ces.nodeId = matched.primaryNodeId;
|
|
292
312
|
const boundDatasource = await resolveBindingDatasource(deps, typeof input.datasource === "string" ? input.datasource : undefined);
|
|
293
313
|
const selectedHost = selectInstanceAddress(matched);
|
|
294
314
|
const selectedPort = normalizePort(matched.port);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
315
|
+
await updateSessionState(deps, (nextConfig) => {
|
|
316
|
+
nextConfig.cloud.projectId = projectId;
|
|
317
|
+
nextConfig.cloud.instanceId = matched.id;
|
|
318
|
+
nextConfig.cloud.nodeId = matched.primaryNodeId;
|
|
319
|
+
nextConfig.slowSqlSource.taurusApi.projectId = projectId;
|
|
320
|
+
nextConfig.slowSqlSource.taurusApi.instanceId = matched.id;
|
|
321
|
+
nextConfig.slowSqlSource.taurusApi.nodeId = matched.primaryNodeId;
|
|
322
|
+
nextConfig.slowSqlSource.das.projectId = projectId;
|
|
323
|
+
nextConfig.slowSqlSource.das.instanceId = matched.id;
|
|
324
|
+
nextConfig.metricsSource.ces.projectId = projectId;
|
|
325
|
+
nextConfig.metricsSource.ces.instanceId = matched.id;
|
|
326
|
+
nextConfig.metricsSource.ces.nodeId = matched.primaryNodeId;
|
|
327
|
+
if (boundDatasource && selectedHost) {
|
|
328
|
+
deps.profileLoader.setRuntimeTarget(boundDatasource, {
|
|
329
|
+
host: selectedHost,
|
|
330
|
+
port: selectedPort,
|
|
331
|
+
instanceId: matched.id,
|
|
332
|
+
nodeId: matched.primaryNodeId,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
});
|
|
304
336
|
return formatSuccess({
|
|
305
337
|
project_id: projectId,
|
|
306
338
|
instance_id: matched.id,
|
|
@@ -6,4 +6,5 @@ export declare const diagnoseSlowQueryTool: ToolDefinition;
|
|
|
6
6
|
export declare const diagnoseConnectionSpikeTool: ToolDefinition;
|
|
7
7
|
export declare const diagnoseLockContentionTool: ToolDefinition;
|
|
8
8
|
export declare const diagnoseStoragePressureTool: ToolDefinition;
|
|
9
|
+
export declare const diagnoseReplicationLagTool: ToolDefinition;
|
|
9
10
|
export declare const diagnosticToolDefinitions: ToolDefinition[];
|
|
@@ -303,6 +303,36 @@ export const diagnoseStoragePressureTool = {
|
|
|
303
303
|
}
|
|
304
304
|
},
|
|
305
305
|
};
|
|
306
|
+
export const diagnoseReplicationLagTool = {
|
|
307
|
+
name: "diagnose_replication_lag",
|
|
308
|
+
description: "Diagnose current replica delay and replication worker state using a bounded, readonly replication-status snapshot.",
|
|
309
|
+
inputSchema: {
|
|
310
|
+
...diagnosticBaseInputShape,
|
|
311
|
+
replica_id: diagnosticString("Optional replica or source server id to focus on."),
|
|
312
|
+
channel: diagnosticString("Optional replication channel name to focus on."),
|
|
313
|
+
},
|
|
314
|
+
async handler(input, deps, context) {
|
|
315
|
+
try {
|
|
316
|
+
const ctx = await resolveContext(input, deps, context, true);
|
|
317
|
+
const diagnosticInput = {
|
|
318
|
+
...parseBaseInput(input),
|
|
319
|
+
replicaId: asOptionalString(input.replica_id, "replica_id"),
|
|
320
|
+
channel: asOptionalString(input.channel, "channel"),
|
|
321
|
+
};
|
|
322
|
+
const result = await deps.engine.diagnoseReplicationLag(diagnosticInput, ctx);
|
|
323
|
+
return formatSuccess(toPublicDiagnosticResult(result), {
|
|
324
|
+
summary: summarizeDiagnostic("Replication-lag diagnosis", result.status),
|
|
325
|
+
metadata: metadata(context.taskId),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
return formatToolError(error, {
|
|
330
|
+
action: "diagnose_replication_lag",
|
|
331
|
+
metadata: metadata(context.taskId),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
};
|
|
306
336
|
export const diagnosticToolDefinitions = [
|
|
307
337
|
diagnoseServiceLatencyTool,
|
|
308
338
|
diagnoseDbHotspotTool,
|
|
@@ -310,6 +340,7 @@ export const diagnosticToolDefinitions = [
|
|
|
310
340
|
diagnoseSlowQueryTool,
|
|
311
341
|
diagnoseConnectionSpikeTool,
|
|
312
342
|
diagnoseLockContentionTool,
|
|
343
|
+
diagnoseReplicationLagTool,
|
|
313
344
|
diagnoseStoragePressureTool,
|
|
314
345
|
];
|
|
315
346
|
function diagnosticString(description) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { buildRestoreRecycleBinTableSql, } from "taurusdb-core";
|
|
2
|
+
import { buildRestoreRecycleBinTableSql, normalizeSql, sqlHash, } from "taurusdb-core";
|
|
3
3
|
import { ErrorCode, formatConfirmationRequired, formatError, formatSuccess, } from "../../utils/formatter.js";
|
|
4
4
|
import { formatToolError } from "../error-handling.js";
|
|
5
5
|
import { asOptionalString, asRequiredString, contextInputShape, metadata, resolveContext, summarizeMutation, summarizeRows, toPublicMutationResult, toPublicQueryResult, } from "../common.js";
|
|
@@ -70,12 +70,12 @@ export const restoreRecycleBinTableTool = {
|
|
|
70
70
|
.min(1)
|
|
71
71
|
.optional()
|
|
72
72
|
.describe("Destination table. Required for insert_select; optional with native_restore when renaming on restore."),
|
|
73
|
-
|
|
73
|
+
approval_token: z
|
|
74
74
|
.string()
|
|
75
75
|
.trim()
|
|
76
76
|
.min(1)
|
|
77
77
|
.optional()
|
|
78
|
-
.describe("
|
|
78
|
+
.describe("Externally signed approval token produced by `taurusdb-mcp approve`."),
|
|
79
79
|
},
|
|
80
80
|
async handler(input, deps, context) {
|
|
81
81
|
const restoreInput = {
|
|
@@ -89,15 +89,16 @@ export const restoreRecycleBinTableTool = {
|
|
|
89
89
|
const sql = buildRestoreRecycleBinTableSql(restoreInput);
|
|
90
90
|
const responseMetadata = metadata(context.taskId, {
|
|
91
91
|
statement_type: restoreInput.method === "insert_select" ? "insert" : "unknown",
|
|
92
|
+
sql_hash: sqlHash(normalizeSql(sql)),
|
|
92
93
|
});
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
const validation = await deps.engine.validateConfirmation(
|
|
94
|
+
const approvalToken = asOptionalString(input.approval_token, "approval_token");
|
|
95
|
+
if (approvalToken) {
|
|
96
|
+
const validation = await deps.engine.validateConfirmation(approvalToken, sql, ctx);
|
|
96
97
|
if (!validation.valid) {
|
|
97
98
|
return formatError({
|
|
98
99
|
code: ErrorCode.CONFIRMATION_INVALID,
|
|
99
|
-
message: validation.reason ?? "
|
|
100
|
-
summary: "The provided
|
|
100
|
+
message: validation.reason ?? "Approval token validation failed.",
|
|
101
|
+
summary: "The provided external approval is invalid for this recycle bin restore.",
|
|
101
102
|
metadata: responseMetadata,
|
|
102
103
|
details: {
|
|
103
104
|
reason_codes: validation.reasonCodes,
|
|
@@ -105,19 +106,21 @@ export const restoreRecycleBinTableTool = {
|
|
|
105
106
|
},
|
|
106
107
|
});
|
|
107
108
|
}
|
|
109
|
+
context.approvalActor = validation.actor;
|
|
108
110
|
}
|
|
109
111
|
else {
|
|
110
|
-
const
|
|
112
|
+
const approval = await deps.engine.issueConfirmation({
|
|
111
113
|
sql,
|
|
112
114
|
context: ctx,
|
|
113
115
|
riskLevel: "high",
|
|
114
116
|
});
|
|
115
117
|
return formatConfirmationRequired({
|
|
116
|
-
|
|
118
|
+
approvalRequest: approval.request,
|
|
119
|
+
requestId: approval.requestId,
|
|
117
120
|
metadata: responseMetadata,
|
|
118
121
|
riskLevel: "high",
|
|
119
122
|
summary: "Recycle bin restore requires explicit confirmation.",
|
|
120
|
-
message: "
|
|
123
|
+
message: "An external operator must sign approval_request before retrying with approval_token.",
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
126
|
const result = await deps.engine.restoreRecycleBinTable(restoreInput, ctx);
|
|
@@ -132,6 +135,7 @@ export const restoreRecycleBinTableTool = {
|
|
|
132
135
|
summary: summarizeMutation(result.affectedRows),
|
|
133
136
|
metadata: metadata(context.taskId, {
|
|
134
137
|
statement_type: restoreInput.method === "insert_select" ? "insert" : "unknown",
|
|
138
|
+
sql_hash: sqlHash(normalizeSql(sql)),
|
|
135
139
|
duration_ms: result.durationMs,
|
|
136
140
|
}),
|
|
137
141
|
});
|