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.
@@ -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, taskId, confirmationToken) {
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 (confirmationToken) {
24
- const validation = await deps.engine.validateConfirmation(confirmationToken, decision.normalizedSql, ctx);
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 ?? "Confirmation token validation failed.",
31
- summary: "The provided confirmation token is invalid for this SQL statement.",
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 === "token_issued") {
41
+ if (outcome.status === "approval_required") {
41
42
  return formatConfirmationRequired({
42
- confirmationToken: outcome.token,
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
- confirmation_token: optionalTokenSchema(),
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.taskId, asOptionalString(input.confirmation_token, "confirmation_token"));
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
- confirmation_token: optionalTokenSchema(),
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.taskId, asOptionalString(input.confirmation_token, "confirmation_token"));
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("Confirmation token returned by a previous guarded call when required.");
227
+ .describe("Externally signed approval token produced by `taurusdb-mcp approve`.");
223
228
  }
@@ -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;
@@ -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 response = await tool.handler(rawInput, deps, { taskId });
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
- return formatUnhandledToolError(error, taskId);
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(_config) {
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
- deps.config.cloud.projectId = undefined;
11
- deps.config.cloud.instanceId = undefined;
12
- deps.config.cloud.nodeId = undefined;
13
- deps.config.slowSqlSource.taurusApi.projectId = undefined;
14
- deps.config.slowSqlSource.taurusApi.instanceId = undefined;
15
- deps.config.slowSqlSource.taurusApi.nodeId = undefined;
16
- deps.config.slowSqlSource.das.projectId = undefined;
17
- deps.config.slowSqlSource.das.instanceId = undefined;
18
- deps.config.metricsSource.ces.projectId = undefined;
19
- deps.config.metricsSource.ces.instanceId = undefined;
20
- deps.config.metricsSource.ces.nodeId = undefined;
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 reloadEngine(deps) {
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: deps.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.publicIps[0] ?? input.privateIps[0] ?? input.hostnames[0];
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
- const domainSuffix = deps.config.cloud.domainSuffix ?? "myhuaweicloud.com";
75
- deps.config.cloud.region = region;
76
- deps.config.cloud.apiEndpoint = buildHuaweiCloudEndpoint("gaussdb", region, domainSuffix);
77
- deps.config.cloud.iamEndpoint = buildHuaweiCloudEndpoint("iam", region, domainSuffix);
78
- deps.config.slowSqlSource.taurusApi.endpoint = buildHuaweiCloudEndpoint("gaussdb", region, domainSuffix);
79
- deps.config.slowSqlSource.das.endpoint = buildHuaweiCloudEndpoint("das", region, domainSuffix);
80
- deps.config.metricsSource.ces.endpoint = buildHuaweiCloudEndpoint("ces", region, domainSuffix);
81
- clearCloudSelection(deps);
82
- await reloadEngine(deps);
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.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,
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.profileLoader.clearRuntimeUser(datasource);
185
- await reloadEngine(deps);
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
- if (boundDatasource && selectedHost) {
296
- deps.profileLoader.setRuntimeTarget(boundDatasource, {
297
- host: selectedHost,
298
- port: selectedPort,
299
- instanceId: matched.id,
300
- nodeId: matched.primaryNodeId,
301
- });
302
- }
303
- await reloadEngine(deps);
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
- confirmation_token: z
73
+ approval_token: z
74
74
  .string()
75
75
  .trim()
76
76
  .min(1)
77
77
  .optional()
78
- .describe("Confirmation token returned by the first guarded restore call."),
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 confirmationToken = asOptionalString(input.confirmation_token, "confirmation_token");
94
- if (confirmationToken) {
95
- const validation = await deps.engine.validateConfirmation(confirmationToken, sql, ctx);
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 ?? "Confirmation token validation failed.",
100
- summary: "The provided confirmation token is invalid for this recycle bin restore.",
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 token = await deps.engine.issueConfirmation({
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
- confirmationToken: token.token,
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: "Re-run restore_recycle_bin_table with the same input and confirmation_token to continue.",
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
  });