server_maintain
Update, restart, or perform full maintenance on Kastell servers. Supports Coolify-managed servers for updates and maintenance, cloud provider API for restarts.
Instructions
Maintain Kastell servers. Actions: 'update' runs Coolify update via SSH (Coolify servers only — bare servers are blocked), 'restart' reboots server via cloud provider API (works for both Coolify and bare servers), 'maintain' runs full 5-step maintenance (Coolify servers only — bare servers are blocked). Snapshot not included — use server_backup tool. Requires SSH access for update, provider API tokens for restart/status. Manual servers: restart not available.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | Action: 'update' runs platform update via SSH (managed servers — Coolify or Dokploy), 'restart' reboots server via cloud provider API (both modes), 'maintain' runs full 5-step maintenance (status → update → health → reboot → final, managed servers only) | |
| server | No | Server name or IP. Auto-selected if only one server exists. | |
| skipReboot | No | Skip reboot and final check steps (only for 'maintain' action). Useful during business hours. |
Implementation Reference
- src/mcp/tools/serverMaintain.ts:32-235 (handler)The handleServerMaintain function acts as the central handler for the 'server_maintain' MCP tool. It switches between three actions: 'update', 'restart', and 'maintain', implementing necessary environment checks, token validation, and calling appropriate core services (updateServer, rebootAndWait, maintainServer).
export async function handleServerMaintain(params: { action: "update" | "restart" | "maintain"; server?: string; skipReboot?: boolean; }): Promise<McpResponse> { try { const servers = getServers(); if (servers.length === 0) { return mcpError("No servers found", undefined, [ { command: "kastell init", reason: "Deploy a server first" }, ]); } const server = resolveServerForMcp(params, servers); if (!server) { if (params.server) { return mcpError( `Server not found: ${params.server}`, `Available servers: ${servers.map((s) => s.name).join(", ")}`, ); } return mcpError( "Multiple servers found. Specify which server to use.", `Available: ${servers.map((s) => s.name).join(", ")}`, ); } switch (params.action) { case "update": { // Guard: update requires managed platform (Coolify/Dokploy) const modeError = requireManagedMode(server, "update"); if (modeError) { return mcpError(modeError, "Use SSH to manage bare servers directly"); } const platform = resolvePlatform(server); if (!platform) { return mcpError("No platform adapter available for this server"); } // Use core updateServer which validates via provider API before updating const apiToken = server.id.startsWith("manual-") ? "" : (getProviderToken(server.provider) ?? ""); const result = await updateServer(server, apiToken, platform); if (!result.success) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), suggested_actions: [ { command: `server_info { action: 'health', server: '${server.name}' }`, reason: "Check if server is reachable" }, ], }) }], isError: true, }; } const displayName = result.displayName ?? "Platform"; return mcpSuccess({ success: true, server: server.name, ip: server.ip, message: `${displayName} update completed successfully`, suggested_actions: [ { command: `server_info { action: 'health', server: '${server.name}' }`, reason: "Verify platform is running after update" }, { command: `server_logs { action: 'logs', server: '${server.name}' }`, reason: "Check logs after update" }, ], }); } case "restart": { if (isSafeMode()) { return mcpError( "Restart is disabled in SAFE_MODE", "Set KASTELL_SAFE_MODE=false to enable server reboot", [{ command: `server_maintain { action: 'update', server: '${server.name}' }`, reason: "Run platform update instead (non-destructive)" }], ); } // Restart requires API token for cloud provider — no mode guard (works on both) const isManual = server.id.startsWith("manual-"); if (isManual) { return mcpError( "Cannot reboot manually added server via API", `Use SSH: ssh root@${server.ip} reboot`, ); } const token = getProviderToken(server.provider); if (!token) { return mcpError( `No API token found for provider: ${server.provider}`, `Set environment variable: ${server.provider.toUpperCase()}_TOKEN`, ); } const result = await rebootAndWait(server, token); if (!result.success) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), suggested_actions: [ { command: `server_info { action: 'status', server: '${server.name}' }`, reason: "Check current server status" }, ], }) }], isError: true, }; } return mcpSuccess({ success: true, server: server.name, ip: server.ip, message: "Server restarted successfully", finalStatus: result.finalStatus, suggested_actions: [ { command: `server_info { action: 'health', server: '${server.name}' }`, reason: "Verify platform is accessible" }, { command: `server_logs { action: 'logs', server: '${server.name}' }`, reason: "Check logs after restart" }, ], }); } case "maintain": { // Guard: maintain requires Coolify const modeError = requireManagedMode(server, "maintain"); if (modeError) { return mcpError(modeError, "Use SSH to manage bare servers directly"); } if (isSafeMode()) { return mcpError( "Maintenance is disabled in SAFE_MODE", "Set KASTELL_SAFE_MODE=false to enable full maintenance (includes reboot)", [{ command: `server_maintain { action: 'update', server: '${server.name}' }`, reason: "Run platform update only (no token needed)" }], ); } // Maintain requires API token for non-manual servers const isManual = server.id.startsWith("manual-"); let token = ""; if (!isManual) { const envToken = getProviderToken(server.provider); if (!envToken) { return mcpError( `No API token found for provider: ${server.provider}`, `Set environment variable: ${server.provider.toUpperCase()}_TOKEN`, [{ command: `server_maintain { action: 'update', server: '${server.name}' }`, reason: "Run update only (no token needed)" }], ); } token = envToken; } const result = await maintainServer(server, token, { skipReboot: params.skipReboot ?? false, }); const suggestedActions = []; const hasFailure = result.steps.some((s) => s.status === "failure"); if (hasFailure) { suggestedActions.push( { command: `server_info { action: 'health', server: '${server.name}' }`, reason: "Check platform reachability" }, { command: `server_logs { action: 'logs', server: '${server.name}' }`, reason: "Check logs for errors" }, ); } else { suggestedActions.push( { command: `server_info { action: 'status', server: '${server.name}' }`, reason: "Verify full server status" }, { command: `server_logs { action: 'logs', server: '${server.name}' }`, reason: "Check platform startup logs" }, ); } return { content: [{ type: "text", text: JSON.stringify({ success: result.success, server: result.server, ip: result.ip, provider: result.provider, steps: result.steps, summary: { total: result.steps.length, success: result.steps.filter((s) => s.status === "success").length, failure: result.steps.filter((s) => s.status === "failure").length, skipped: result.steps.filter((s) => s.status === "skipped").length, }, suggested_actions: suggestedActions, }) }], ...(hasFailure ? { isError: true } : {}), }; } default: { return mcpError(`Unknown action: ${params.action as string}`); } } } catch (error: unknown) { return mcpError(getErrorMessage(error)); } } - The serverMaintainSchema object defines the input parameters for the 'server_maintain' tool using Zod, covering actions ('update', 'restart', 'maintain'), optional server selection, and a 'skipReboot' flag.
export const serverMaintainSchema = { action: z.enum(["update", "restart", "maintain"]).describe( "Action: 'update' runs platform update via SSH (managed servers — Coolify or Dokploy), 'restart' reboots server via cloud provider API (both modes), 'maintain' runs full 5-step maintenance (status → update → health → reboot → final, managed servers only)", ), server: z.string().optional().describe( "Server name or IP. Auto-selected if only one server exists.", ), skipReboot: z.boolean().default(false).describe( "Skip reboot and final check steps (only for 'maintain' action). Useful during business hours.", ), }; - src/mcp/server.ts:101-115 (registration)Registration of the 'server_maintain' tool in the MCP server instance, mapping it to the serverMaintainSchema and invoking the handleServerMaintain function.
server.registerTool("server_maintain", { description: "Maintain Kastell servers. Actions: 'update' runs Coolify update via SSH (Coolify servers only — bare servers are blocked), 'restart' reboots server via cloud provider API (works for both Coolify and bare servers), 'maintain' runs full 5-step maintenance (Coolify servers only — bare servers are blocked). Snapshot not included — use server_backup tool. Requires SSH access for update, provider API tokens for restart/status. Manual servers: restart not available.", inputSchema: serverMaintainSchema, annotations: { title: "Server Maintenance", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { return handleServerMaintain(params); });