Skip to main content
Glama

server_manage

Destructive

Register, unregister, or permanently delete servers from cloud providers like Hetzner, DigitalOcean, Vultr, and Linode. Supports Coolify, Dokploy, or bare server modes.

Instructions

Manage Kastell servers. Actions: 'add' registers an existing Coolify or bare server to local config (validates API token, optionally verifies Coolify via SSH — pass mode:'bare' for servers without Coolify). 'remove' unregisters a server from local config only (cloud server keeps running). 'destroy' PERMANENTLY DELETES the server from the cloud provider and removes from local config. Requires provider API tokens as environment variables. Destroy is blocked when KASTELL_SAFE_MODE=true. Server mode for 'add' action: 'coolify', 'dokploy', or 'bare'. Default: coolify

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
actionYesAction: 'add' register an existing server, 'remove' unregister from local config (server stays running), 'destroy' permanently delete from cloud provider AND local config
serverNoServer name or IP (required for 'remove' and 'destroy' actions)
providerNoCloud provider: 'hetzner', 'digitalocean', 'vultr', 'linode' (required for 'add' action)
ipNoServer public IP address (required for 'add' action)
nameNoServer name, 3-63 chars, lowercase alphanumeric and hyphens (required for 'add' action)
skipVerifyNoSkip Coolify SSH verification when adding a server (only for 'add' action)
modeNoServer mode for 'add' action: 'coolify', 'dokploy', or 'bare'. Default: coolifycoolify

Implementation Reference

  • Input schema for the server_manage tool defining 'action' (add/remove/destroy), 'server', 'provider', 'ip', 'name', 'skipVerify', and 'mode' parameters with Zod validation.
    export const serverManageSchema = {
      action: z
        .enum(["add", "remove", "destroy"])
        .describe(
          "Action: 'add' register an existing server, 'remove' unregister from local config (server stays running), 'destroy' permanently delete from cloud provider AND local config",
        ),
      server: z
        .string()
        .optional()
        .describe("Server name or IP (required for 'remove' and 'destroy' actions)"),
      provider: z
        .enum(SUPPORTED_PROVIDERS)
        .optional()
        .describe(
          "Cloud provider: 'hetzner', 'digitalocean', 'vultr', 'linode' (required for 'add' action)",
        ),
      ip: z.string().optional().describe("Server public IP address (required for 'add' action)"),
      name: z
        .string()
        .optional()
        .describe(
          "Server name, 3-63 chars, lowercase alphanumeric and hyphens (required for 'add' action)",
        ),
      skipVerify: z
        .boolean()
        .default(false)
        .describe("Skip Coolify SSH verification when adding a server (only for 'add' action)"),
      mode: z
        .enum(["coolify", "dokploy", "bare"])
        .default("coolify")
        .describe("Server mode for 'add' action: 'coolify', 'dokploy', or 'bare'. Default: coolify"),
    };
  • Main handler for server_manage tool that dispatches to addServerRecord, removeServerRecord, or destroyCloudServer based on the action parameter. Handles validation, safe mode checks, and returns structured MCP responses.
    export async function handleServerManage(params: {
      action: "add" | "remove" | "destroy";
      server?: string;
      provider?: string;
      ip?: string;
      name?: string;
      skipVerify?: boolean;
      mode?: "coolify" | "dokploy" | "bare";
    }): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
      try {
        switch (params.action) {
          case "add": {
            if (!params.provider) {
              return mcpError(
                "Missing required parameter: provider",
                "Specify provider: 'hetzner', 'digitalocean', 'vultr', or 'linode'",
              );
            }
            if (!params.ip) {
              return mcpError(
                "Missing required parameter: ip",
                "Specify the server's public IP address",
              );
            }
            if (!params.name) {
              return mcpError(
                "Missing required parameter: name",
                "Specify a server name (3-63 chars, lowercase, alphanumeric and hyphens)",
              );
            }
    
            const mode = params.mode ?? "coolify";
    
            const result = await addServerRecord({
              provider: params.provider,
              ip: params.ip,
              name: params.name,
              skipVerify: params.skipVerify ?? false,
              mode,
            });
    
            if (!result.success) {
              return mcpError(result.error, undefined, [
                { command: "server_info { action: 'list' }", reason: "Check existing servers" },
              ]);
            }
    
            const serverName = result.server.name;
            const suggestedActions =
              mode === "bare"
                ? [
                    {
                      command: `server_info { action: 'status', server: '${serverName}' }`,
                      reason: "Check server status",
                    },
                    {
                      command: `server_secure { action: 'secure-setup', server: '${serverName}' }`,
                      reason: "Harden SSH security + install fail2ban",
                    },
                    {
                      command: `server_logs { action: 'logs', server: '${serverName}' }`,
                      reason: "View server logs",
                    },
                  ]
                : [
                    {
                      command: `server_info { action: 'status', server: '${serverName}' }`,
                      reason: "Check server status",
                    },
                    {
                      command: `server_info { action: 'health', server: '${serverName}' }`,
                      reason: "Check Coolify health",
                    },
                    {
                      command: `server_logs { action: 'logs', server: '${serverName}' }`,
                      reason: "View server logs",
                    },
                  ];
    
            return mcpSuccess({
              success: true,
              message: `Server "${serverName}" added successfully`,
              server: {
                name: serverName,
                ip: result.server.ip,
                provider: result.server.provider,
                id: result.server.id,
                mode,
              },
              platformStatus: result.platformStatus,
              suggested_actions: suggestedActions,
            });
          }
    
          case "remove": {
            if (isSafeMode()) {
              logSafeModeBlock("server-manage", { category: "destructive" });
              return mcpError(
                "Remove is disabled in SAFE_MODE",
                "Set KASTELL_SAFE_MODE=false to enable server removal from local config.",
              );
            }
            if (!params.server) {
              const servers = getServers();
              if (servers.length === 0) {
                return mcpError("No servers found", undefined, [
                  { command: "kastell init", reason: "Deploy a server first" },
                ]);
              }
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: "Missing required parameter: server",
                      available_servers: servers.map((s) => ({ name: s.name, ip: s.ip })),
                      hint: "Specify which server to remove by name or IP",
                    }),
                  },
                ],
                isError: true,
              };
            }
    
            const result = await removeServerRecord(params.server);
    
            if (!result.success) {
              const servers = getServers();
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: result.error,
                      available_servers: servers.map((s) => s.name),
                    }),
                  },
                ],
                isError: true,
              };
            }
    
            return mcpSuccess({
              success: true,
              message: `Server "${result.server!.name}" removed from local config`,
              note: "The cloud server is still running. Use 'destroy' to delete it from the provider.",
              server: {
                name: result.server!.name,
                ip: result.server!.ip,
                provider: result.server!.provider,
              },
              suggested_actions: [
                { command: "server_info { action: 'list' }", reason: "View remaining servers" },
              ],
            });
          }
    
          case "destroy": {
            // SAFE_MODE check
            if (isSafeMode()) {
              logSafeModeBlock("server-destroy", { category: "destructive" });
              return mcpError(
                "Destroy is disabled in SAFE_MODE (enabled by default for safety)",
                "Set KASTELL_SAFE_MODE=false to enable destructive operations. WARNING: This will permanently delete the server from the cloud provider.",
                [
                  {
                    command: `server_manage { action: 'remove', server: '${params.server ?? ""}' }`,
                    reason: "Remove from local config only (non-destructive)",
                  },
                ],
              );
            }
    
            if (!params.server) {
              const servers = getServers();
              if (servers.length === 0) {
                return mcpError("No servers found", undefined, [
                  { command: "kastell init", reason: "Deploy a server first" },
                ]);
              }
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: "Missing required parameter: server",
                      available_servers: servers.map((s) => ({
                        name: s.name,
                        ip: s.ip,
                        provider: s.provider,
                      })),
                      hint: "Specify which server to destroy by name or IP",
                      warning: "This will PERMANENTLY DELETE the server from the cloud provider",
                    }),
                  },
                ],
                isError: true,
              };
            }
    
            const result = await destroyCloudServer(params.server);
    
            if (!result.success) {
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: result.error,
                      ...(result.hint ? { hint: result.hint } : {}),
                      ...(result.server
                        ? {
                            server: {
                              name: result.server.name,
                              ip: result.server.ip,
                              provider: result.server.provider,
                            },
                          }
                        : {}),
                      suggested_actions: [
                        {
                          command: `server_manage { action: 'remove', server: '${params.server}' }`,
                          reason: "Remove from local config only",
                        },
                        { command: "kastell doctor --check-tokens", reason: "Verify API tokens" },
                      ],
                    }),
                  },
                ],
                isError: true,
              };
            }
    
            return mcpSuccess({
              success: true,
              message: `Server "${result.server!.name}" destroyed`,
              cloudDeleted: result.cloudDeleted,
              localRemoved: result.localRemoved,
              ...(result.hint ? { note: result.hint } : {}),
              server: {
                name: result.server!.name,
                ip: result.server!.ip,
                provider: result.server!.provider,
              },
              suggested_actions: [
                { command: "server_info { action: 'list' }", reason: "Verify server was removed" },
              ],
            });
          }
          default: {
            return mcpError(`Unknown action: ${params.action as string}`);
          }
        }
      } catch (error: unknown) {
        return mcpError(sanitizeStderr(getErrorMessage(error)));
      }
    }
  • Registration of the server_manage tool with the MCP server, including description, input schema, annotations, and the async handler that delegates to handleServerManage.
    server.registerTool("server_manage", {
      description:
        "Manage Kastell servers. Actions: 'add' registers an existing Coolify or bare server to local config (validates API token, optionally verifies Coolify via SSH — pass mode:'bare' for servers without Coolify). 'remove' unregisters a server from local config only (cloud server keeps running). 'destroy' PERMANENTLY DELETES the server from the cloud provider and removes from local config. Requires provider API tokens as environment variables. Destroy is blocked when KASTELL_SAFE_MODE=true. Server mode for 'add' action: 'coolify', 'dokploy', or 'bare'. Default: coolify",
      inputSchema: serverManageSchema,
      annotations: {
        title: "Server Management",
        readOnlyHint: false,
        destructiveHint: true,
        idempotentHint: false,
        openWorldHint: true,
      },
    }, async (params) => {
      return handleServerManage(params);
    });
  • Core helper that adds a server record: validates provider/token/IP/name, checks duplicates, validates API token, auto-detects platform, verifies via SSH, and saves to config.
    export async function addServerRecord(params: AddServerParams): Promise<AddServerResult> {
      // Validate provider
      if (!isValidProvider(params.provider)) {
        return {
          success: false,
          error: invalidProviderError(params.provider),
        };
      }
    
      // Get token from explicit param or env
      const token = params.apiToken ?? getProviderToken(params.provider);
      if (!token) {
        return {
          success: false,
          error: `No API token found for provider: ${params.provider}. Set ${params.provider.toUpperCase()}_TOKEN environment variable`,
        };
      }
    
      // Validate IP
      const ipError = validateIpAddress(params.ip);
      if (ipError) {
        return { success: false, error: ipError };
      }
    
      // Check duplicate
      const existing = getServers();
      const duplicate = existing.find((s) => s.ip === params.ip);
      if (duplicate) {
        return {
          success: false,
          error: `Server with IP ${params.ip} already exists: ${duplicate.name}`,
        };
      }
    
      // Validate name
      const nameError = validateServerName(params.name);
      if (nameError) {
        return { success: false, error: nameError };
      }
    
      // Validate API token
      const provider = createProviderWithToken(params.provider, token);
      try {
        const valid = await provider.validateToken(token);
        if (!valid) {
          return { success: false, error: `Invalid API token for ${params.provider}` };
        }
      } catch (error: unknown) {
        return {
          success: false,
          error: `Token validation failed: ${getErrorMessage(error)}`,
        };
      }
    
      let cloudId: string | null = null;
      try {
        cloudId = await provider.findServerByIp(params.ip);
      } catch {
        // Non-fatal: API lookup failure keeps manual-{timestamp} id
      }
    
      // Resolve mode and platform — auto-detect if no explicit mode provided
      let modeStr = params.mode;
      if (!modeStr && !params.skipVerify && checkSshAvailable()) {
        try {
          const detected = await detectPlatform(params.ip);
          modeStr = detected; // "coolify" | "dokploy" | "bare"
        } catch {
          modeStr = "bare"; // safe fallback — bare is least privileged
        }
      }
      modeStr = modeStr || "bare";
      const isBare = modeStr === "bare";
      const platform: Platform | undefined = isBare ? undefined
        : modeStr === "dokploy" ? "dokploy"
        : "coolify";
      const mode: ServerMode = isBare ? "bare" : "coolify";
    
      // Optional platform verification via SSH (skip entirely for bare mode)
      let platformStatus: PlatformStatus = "skipped";
      if (!params.skipVerify && mode !== "bare") {
        const healthPort = platform === "dokploy" ? DOKPLOY_PORT : COOLIFY_PORT;
        const healthPath = platform === "dokploy" ? "/" : "/api/health";
        const containerGrep = platform === "dokploy" ? "dokploy" : "coolify";
    
        if (!checkSshAvailable()) {
          platformStatus = "ssh_unavailable";
        } else {
          try {
            const result = await sshExec(
              params.ip,
              raw(`curl -s -o /dev/null -w '%{http_code}' http://localhost:${healthPort}${healthPath}`),
            );
            if (result.code === 0 && result.stdout.trim().includes("200")) {
              platformStatus = "running";
            } else {
              const dockerResult = await sshExec(
                params.ip,
                raw(`docker ps --format '{{.Names}}' 2>/dev/null | grep -q ${containerGrep} && echo OK`),
              );
              if (dockerResult.code === 0 && dockerResult.stdout.trim().includes("OK")) {
                platformStatus = "containers_detected";
              } else {
                platformStatus = "not_detected";
              }
            }
          } catch (error: unknown) {
            process.stderr.write(`[warn] Platform verification failed: ${getErrorMessage(error)}\n`);
            platformStatus = "verification_failed";
          }
        }
      }
    
      // Save to config
      const record: ServerRecord = {
        id: cloudId ?? `manual-${Date.now()}`,
        name: params.name,
        provider: params.provider,
        ip: params.ip,
        region: "unknown",
        size: "unknown",
        createdAt: new Date().toISOString(),
        mode,
        ...(platform ? { platform } : {}),
      };
    
      await saveServer(record);
    
      return { success: true, server: record, platformStatus };
    }
  • Core helper that permanently deletes a server from the cloud provider API and removes it from local config. Handles manual servers and not-found fallback.
    export async function destroyCloudServer(query: string): Promise<DestroyServerResult> {
      const server = findServer(query);
      if (!server) {
        return {
          success: false,
          cloudDeleted: false,
          localRemoved: false,
          error: `Server not found: ${query}`,
        };
      }
    
      // Manual servers can only be removed, not destroyed
      if (server.id.startsWith("manual-")) {
        return {
          success: false,
          server,
          cloudDeleted: false,
          localRemoved: false,
          error: `Server "${server.name}" was manually added (no cloud provider ID). Use 'remove' action instead.`,
        };
      }
    
      // Get token from env
      const token = getProviderToken(server.provider);
      if (!token) {
        return {
          success: false,
          server,
          cloudDeleted: false,
          localRemoved: false,
          error: `No API token for ${server.provider}. Set ${server.provider.toUpperCase()}_TOKEN environment variable`,
        };
      }
    
      try {
        const provider = createProviderWithToken(server.provider, token);
        await provider.destroyServer(server.id);
        await removeServer(server.id);
        return { success: true, server, cloudDeleted: true, localRemoved: true };
      } catch (error: unknown) {
        const message = getErrorMessage(error);
        const isNotFound =
          message.toLowerCase().includes("not found") || message.toLowerCase().includes("not_found");
    
        if (isNotFound) {
          await removeServer(server.id);
          return {
            success: true,
            server,
            cloudDeleted: false,
            localRemoved: true,
            hint: `Server not found on ${server.provider} (may have been deleted manually). Removed from local config.`,
          };
        }
    
        const hint = mapProviderError(error, server.provider);
        return {
          success: false,
          server,
          cloudDeleted: false,
          localRemoved: false,
          error: message,
          ...(hint ? { hint } : {}),
        };
      }
    }
Behavior5/5

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

Beyond annotations (destructiveHint=true), the description reveals that 'destroy' permanently deletes from cloud, requires provider API tokens, and is blocked when KASTELL_SAFE_MODE=true. This adds critical behavioral context not in annotations.

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 front-loaded with the core purpose and structured by actions. It contains multiple sentences but each adds essential information without redundancy, balancing detail and brevity.

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

Completeness5/5

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

Given the tool's complexity (7 parameters, 3 actions, environment requirements, safety mode), the description comprehensively covers all aspects needed for correct invocation, including prerequisites and blocking conditions.

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

Parameters4/5

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

Schema coverage is 100%, so baseline is 3. The description adds value by explaining operational nuances (e.g., validates API token, optionally verifies Coolify via SSH, mode options with defaults) that go beyond the schema's parameter descriptions.

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 states the tool manages Kastell servers with three distinct actions (add, remove, destroy), each explained concisely. It differentiates from sibling tools by focusing on lifecycle management.

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

Usage Guidelines4/5

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

The description provides explicit guidance for each action, including when to use 'add' (with mode options) and that 'destroy' is blocked in safe mode. However, it does not explicitly contrast with sibling tools like server_audit or server_backup, relying on implicit differentiation.

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/kastelldev/kastell'

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