server_manage
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
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | Action: 'add' register an existing server, 'remove' unregister from local config (server stays running), 'destroy' permanently delete from cloud provider AND local config | |
| server | No | Server name or IP (required for 'remove' and 'destroy' actions) | |
| provider | No | Cloud provider: 'hetzner', 'digitalocean', 'vultr', 'linode' (required for 'add' action) | |
| ip | No | Server public IP address (required for 'add' action) | |
| name | No | Server name, 3-63 chars, lowercase alphanumeric and hyphens (required for 'add' action) | |
| skipVerify | No | Skip Coolify SSH verification when adding a server (only for 'add' action) | |
| mode | No | Server mode for 'add' action: 'coolify', 'dokploy', or 'bare'. Default: coolify | coolify |
Implementation Reference
- src/mcp/tools/serverManage.ts:14-45 (schema)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"), }; - src/mcp/tools/serverManage.ts:47-303 (handler)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))); } } - src/mcp/server.ts:91-104 (registration)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); }); - src/core/manage.ts:65-194 (helper)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 }; } - src/core/manage.ts:229-294 (helper)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 } : {}), }; } }