local_ydb_remove_dynamic_nodes
Removes extra dynamic tenant nodes one at a time, verifying node list disappearance when the node IC port is resolvable.
Instructions
Remove extra dynamic tenant nodes one at a time and verify nodelist disappearance when the node IC port can be resolved.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| profile | No | Named profile from local-ydb.config.json. Defaults to config.defaultProfile. | |
| configPath | No | Explicit local-ydb config file path to load for this tool call. Useful when the MCP server should pick up a different config without restart. | |
| confirm | No | Must be true to execute planned commands. Omit or false for plan-only output. | |
| count | No | Number of extra dynamic nodes to remove. Defaults to 1. | |
| startIndex | No | Minimum suffix to consider removable. Defaults to 2. | |
| containers | No | Explicit extra dynamic-node container names to remove. | |
| nodeIds | No | Explicit YDB dynamic-node IDs to remove. IDs must resolve to extra dynamic-node containers; the profile's base dynamic node is not removable through this option. |
Implementation Reference
- Main handler function for removeDynamicNodes. Lines 71-123: orchestrates the removal of dynamic tenant nodes. It determines targets (via removableDynamicNodeTargets), builds docker rm -f commands, runs them sequentially, verifies node port absence from nodelist, and finally checks the tenant scheme. Supports dry-run mode when confirm is false.
export async function removeDynamicNodes(ctx: ToolkitContext, options: RemoveDynamicNodesOptions = {}): Promise<RemoveDynamicNodesResponse> { const targets = await removableDynamicNodeTargets(ctx, options); const specs = targets.map((target) => bash(`docker rm -f ${shellQuote(target.container)}`, { timeoutMs: 60_000, description: `Remove dynamic tenant node ${target.container}` })); const rollback = [ "Recreate removed nodes with local_ydb_add_dynamic_nodes using matching suffixes and ports if needed." ]; const verification = [ "authenticated viewer/json/nodelist no longer includes each removed node IC port", `scheme ls ${ctx.profile.tenantPath}` ]; if (!options.confirm) { return { summary: `Remove ${targets.length} dynamic node${targets.length === 1 ? "" : "s"} from ${ctx.profile.tenantPath}. Not executed because confirm=true was not provided.`, executed: false, risk: "high", plannedCommands: specs.map((spec) => ctx.client.display(spec)), rollback, verification, nodes: targets }; } const results: CommandResult[] = []; const nodeChecks: DynamicNodeCheck[] = []; let completedNodes = 0; for (const target of targets) { const result = await ctx.client.run(bash(`docker rm -f ${shellQuote(target.container)}`, { timeoutMs: 60_000, description: `Remove dynamic tenant node ${target.container}` })); results.push(result); if (!result.ok) { return removeDynamicNodesResponse(ctx, targets, nodeChecks, results, rollback, verification, completedNodes); } const icPort = target.icPort; if (typeof icPort === "number") { const check = await waitForDynamicNodePortAbsence(ctx, { ...target, icPort }); nodeChecks.push(check); if (!check.ok) { return removeDynamicNodesResponse(ctx, targets, nodeChecks, results, rollback, verification, completedNodes); } } completedNodes += 1; } results.push(await ctx.client.run(ydbCli(ctx.profile, ["scheme", "ls", ctx.profile.tenantPath], ctx.profile.tenantPath, "Verify tenant metadata"))); return removeDynamicNodesResponse(ctx, targets, nodeChecks, results, rollback, verification, completedNodes); } - removableDynamicNodeTargets helper: resolves which dynamic nodes to remove based on options (nodeIds, containers, or count/startIndex). Validates inputs, sorts targets by descending index, and inspects containers to get IC ports.
async function removableDynamicNodeTargets(ctx: ToolkitContext, options: RemoveDynamicNodesOptions): Promise<DynamicNodeTarget[]> { const startIndex = options.startIndex ?? 2; if (startIndex < 2) { throw new Error("startIndex must be 2 or greater to avoid the profile dynamicContainer"); } if (options.nodeIds && options.nodeIds.length > 0 && options.containers && options.containers.length > 0) { throw new Error("Specify either nodeIds or containers, not both"); } if (options.nodeIds && options.nodeIds.length > 0 && options.count !== undefined) { throw new Error("count cannot be used with nodeIds"); } const containers = await ctx.client.dockerPs(); const available = containers .map((container) => extraDynamicNodeTarget(ctx.profile, container.names)) .filter((target): target is DynamicNodeTarget => Boolean(target)) .filter((target) => target.index >= startIndex); let targets: DynamicNodeTarget[]; if (options.nodeIds && options.nodeIds.length > 0) { const requestedNodeIds = validateNodeIds(options.nodeIds); const inspectByContainer = await inspectDynamicNodeTargets(ctx, available.map((target) => target.container)); targets = await targetsForNodeIds(ctx, available, inspectByContainer, requestedNodeIds); return targets.sort((left, right) => right.index - left.index); } else if (options.containers && options.containers.length > 0) { const requested = new Set(options.containers); targets = available.filter((target) => requested.has(target.container)); if (targets.length !== requested.size) { const resolved = new Set(targets.map((target) => target.container)); const missing = Array.from(requested).filter((container) => !resolved.has(container)); throw new Error(`Requested dynamic-node containers were not found or were not removable extras: ${missing.join(", ")}`); } } else { const count = options.count ?? 1; assertPositiveInteger("count", count); if (count > 10) { throw new Error("count must be 10 or less"); } targets = available .sort((left, right) => right.index - left.index) .slice(0, count); if (targets.length < count) { throw new Error(`Requested ${count} removable dynamic nodes but found ${targets.length}`); } } const inspectByContainer = await inspectDynamicNodeTargets(ctx, targets.map((target) => target.container)); return targets .sort((left, right) => right.index - left.index) .map((target) => ({ ...target, icPort: inspectByContainer.get(target.container)?.icPort ?? target.icPort })); } - waitForDynamicNodePortAbsence helper: polls the nodelist up to 5 times (2s apart) to confirm a removed node's IC port is no longer present, ensuring the node has fully disappeared.
async function waitForDynamicNodePortAbsence(ctx: ToolkitContext, target: DynamicNodeTarget & { icPort: number }): Promise<DynamicNodeCheck> { let observedPorts: number[] = []; let error: string | undefined; for (let attempt = 1; attempt <= 5; attempt += 1) { const check = await nodesCheck(ctx); observedPorts = observedNodePorts(check.nodes); error = check.error; if (!observedPorts.includes(target.icPort)) { return { container: target.container, icPort: target.icPort, ok: true, attempts: attempt, observedPorts }; } if (attempt < 5) { await delay(2_000); } } return { container: target.container, icPort: target.icPort, ok: false, attempts: 5, observedPorts, error }; } - removeDynamicNodesResponse helper: constructs the RemoveDynamicNodesResponse object with summary, execution status, planned commands, rollback instructions, verification steps, results, nodes, and checks.
function removeDynamicNodesResponse( ctx: ToolkitContext, targets: DynamicNodeTarget[], nodeChecks: DynamicNodeCheck[], results: CommandResult[], rollback: string[], verification: string[], completedNodes: number ): RemoveDynamicNodesResponse { return { summary: `Remove ${targets.length} dynamic node${targets.length === 1 ? "" : "s"} from ${ctx.profile.tenantPath}. Executed ${results.filter((result) => result.ok).length}/${results.length} commands; verified ${completedNodes}/${targets.length} nodes.`, executed: true, risk: "high", plannedCommands: targets.map((target) => ctx.client.display(bash(`docker rm -f ${shellQuote(target.container)}`, { timeoutMs: 60_000, description: `Remove dynamic tenant node ${target.container}` }))), rollback, verification, results, nodes: targets, nodeChecks }; } - RemoveDynamicNodesArgs Zod schema: extends MutatingArgs with optional count, startIndex, containers (string array), and nodeIds (positive int array, max 10).
export const RemoveDynamicNodesArgs = MutatingArgs.extend({ count: z.number().int().positive().max(10).optional(), startIndex: z.number().int().min(2).optional(), containers: z.array(z.string()).optional(), nodeIds: z.array(z.number().int().positive()).max(10).optional(), }); - removeDynamicNodesSchema: defines the JSON Schema input schema for the tool, including profile, configPath, confirm, count (1-10), startIndex (min 2), containers (string array), and nodeIds (integer array, max 10).
export function removeDynamicNodesSchema(): Tool["inputSchema"] { return { type: "object", properties: { profile: profileProperty(), configPath: configPathProperty(), confirm: confirmProperty(), count: { type: "integer", minimum: 1, maximum: 10, description: "Number of extra dynamic nodes to remove. Defaults to 1.", }, startIndex: { type: "integer", minimum: 2, description: "Minimum suffix to consider removable. Defaults to 2.", }, containers: { type: "array", items: { type: "string" }, description: "Explicit extra dynamic-node container names to remove.", }, nodeIds: { type: "array", items: { type: "integer", minimum: 1 }, maxItems: 10, description: "Explicit YDB dynamic-node IDs to remove. IDs must resolve to extra dynamic-node containers; the profile's base dynamic node is not removable through this option.", }, }, additionalProperties: false, }; } - RemoveDynamicNodesOptions TypeScript interface: extends MutatingOptions with optional count, startIndex, containers, and nodeIds.
export interface RemoveDynamicNodesOptions extends MutatingOptions { count?: number; startIndex?: number; containers?: string[]; nodeIds?: number[]; } - RemoveDynamicNodesResponse TypeScript interface: extends OperationResponse with nodes (DynamicNodeTarget[]) and nodeChecks.
export interface RemoveDynamicNodesResponse extends OperationResponse { nodes: DynamicNodeTarget[]; nodeChecks?: DynamicNodeCheck[]; } - packages/mcp-server/src/tools/registry.ts:414-424 (registration)Tool registration in registry.ts: defines the tool with name 'local_ydb_remove_dynamic_nodes', description, inputSchema from removeDynamicNodesSchema(), mutating annotations, and handler wiring using withContext(RemoveDynamicNodesArgs, ...) calling the import removeDynamicNodes.
defineTool({ group: "dynamic nodes", name: "local_ydb_remove_dynamic_nodes", description: "Remove extra dynamic tenant nodes one at a time and verify nodelist disappearance when the node IC port can be resolved.", inputSchema: removeDynamicNodesSchema(), annotations: mutatingAnnotations({ destructive: true }), handler: withContext(RemoveDynamicNodesArgs, (context, parsed) => removeDynamicNodes(context, parsed), ), }),