Skip to main content
Glama

local_ydb_remove_dynamic_nodes

Destructive

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

TableJSON Schema
NameRequiredDescriptionDefault
profileNoNamed profile from local-ydb.config.json. Defaults to config.defaultProfile.
configPathNoExplicit local-ydb config file path to load for this tool call. Useful when the MCP server should pick up a different config without restart.
confirmNoMust be true to execute planned commands. Omit or false for plan-only output.
countNoNumber of extra dynamic nodes to remove. Defaults to 1.
startIndexNoMinimum suffix to consider removable. Defaults to 2.
containersNoExplicit extra dynamic-node container names to remove.
nodeIdsNoExplicit 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[];
    }
  • 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),
      ),
    }),
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations correctly mark the tool as destructive. The description adds behavioral context by stating it removes nodes 'one at a time' and verifies node-list disappearance when the IC port is resolvable. This goes beyond what annotations provide, though it could mention that changes are irreversible.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, front-loaded sentence that conveys the core action and a notable behavioral condition. While dense, it avoids unnecessary words. The content earns its place, though a slight split into two sentences could improve readability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having 7 parameters and no output schema, the description fails to explain what happens after removal, potential side effects, or how to revert. It also omits details about the verification success criteria. The tool's destructive nature demands more context to be complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% parameter description coverage, so every parameter's meaning is already documented. The description does not add further detail beyond the general removal purpose. Baseline 3 is appropriate since the schema fully handles parameter semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly specifies the action 'remove', the resource 'extra dynamic tenant nodes', and the verification step regarding the node IC port. It effectively distinguishes from sibling tools like 'local_ydb_add_dynamic_nodes' by focusing on removal and verification.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description lacks explicit guidance on when to use this tool versus alternatives. It does not state prerequisites, scenarios where removal is appropriate, or direct the agent to other tools for related operations. The agent receives no when-to-use or when-not-to-use advice.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/astandrik/local-ydb-toolkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server