server_info
Retrieve and monitor Kastell-managed server information including status, health checks, available types, and pricing across supported cloud providers.
Instructions
Get information about Kastell-managed servers. Actions: 'list' all servers, 'status' check cloud provider + Coolify/bare status, 'health' check Coolify reachability or SSH access for bare servers, 'sizes' list available server types with prices for a provider+region. Requires provider API tokens as environment variables (HETZNER_TOKEN, DIGITALOCEAN_TOKEN, VULTR_TOKEN, LINODE_TOKEN) for status/sizes checks. Avoid calling repeatedly in short intervals to prevent provider API rate limiting. For fleet-wide health and audit scores across all servers, use server_fleet instead.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | Action to perform: 'list' all servers, 'status' check server/cloud status, 'health' check Coolify reachability (or SSH reachability for bare servers), 'sizes' list available server types with prices for a provider+region | |
| server | No | Server name or IP. Required for single-server status/health. Omit for all servers. | |
| provider | No | Cloud provider (required for 'sizes' action) | |
| region | No | Region/location ID (required for 'sizes' action, e.g. 'nbg1' for Hetzner, 'fra1' for DigitalOcean) | |
| mode | No | Server mode filter for 'sizes' action. Coolify requires min 2GB RAM. Default: coolify |
Implementation Reference
- src/mcp/tools/serverInfo.ts:144-400 (handler)The handleServerInfo function processes actions (list, status, health, sizes) for server info management and interacts with various utilities to fetch server details or health status.
export async function handleServerInfo(params: { action: "list" | "status" | "health" | "sizes"; server?: string; provider?: SupportedProvider; region?: string; mode?: ServerMode; }): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> { try { switch (params.action) { case "list": { const servers = getServers(); return mcpSuccess(formatServerList(servers)); } case "status": { const servers = getServers(); if (servers.length === 0) { return mcpError( "No servers found", undefined, [{ command: "kastell init", reason: "Deploy a server first" }], ); } if (params.server) { const server = findServer(params.server); if (!server) { return { content: [{ type: "text", text: JSON.stringify({ error: `Server not found: ${params.server}`, available_servers: servers.map((s) => s.name), }) }], isError: true, }; } const token = server.id.startsWith("manual-") ? "" : (getProviderToken(server.provider) ?? ""); if (!token && !server.id.startsWith("manual-")) { return { content: [{ type: "text", text: JSON.stringify({ error: `No API token found for provider: ${server.provider}`, hint: `Set environment variable: ${server.provider.toUpperCase()}_TOKEN`, server: { name: server.name, ip: server.ip, provider: server.provider }, }) }], isError: true, }; } const result = await checkServerStatus(server, token); return mcpSuccess(formatStatusResults([result])); } // All servers const tokenMap = collectProviderTokensFromEnv(servers); const missingTokenProviders = [ ...new Set(servers.filter((s) => !s.id.startsWith("manual-")).map((s) => s.provider)), ].filter((p) => !tokenMap.has(p)); if (missingTokenProviders.length > 0) { return { content: [{ type: "text", text: JSON.stringify({ error: "Missing API tokens for providers", missing: missingTokenProviders.map((p) => ({ provider: p, envVar: `${p.toUpperCase()}_TOKEN`, })), hint: "Set environment variables for each provider", }) }], isError: true, }; } const results = await checkAllServersStatus(servers, tokenMap); return mcpSuccess(formatStatusResults(results)); } case "health": { const servers = getServers(); if (servers.length === 0) { return mcpError( "No servers found", undefined, [{ command: "kastell init", reason: "Deploy a server first" }], ); } if (params.server) { const server = findServer(params.server); if (!server) { return { content: [{ type: "text", text: JSON.stringify({ error: `Server not found: ${params.server}`, available_servers: servers.map((s) => s.name), }) }], isError: true, }; } // Bare server: check SSH reachability if (isBareServer(server)) { const sshResult = await checkBareServerSsh(server); const suggestedActions: SuggestedAction[] = []; if (sshResult.hostKeyMismatch) { suggestedActions.push({ command: `ssh-keygen -R ${server.ip}`, reason: "Remove stale host key to fix SSH connection", }); } else if (sshResult.reachable) { suggestedActions.push({ command: `ssh root@${server.ip}`, reason: "Connect to your bare server" }); } else { suggestedActions.push({ command: `kastell status ${server.name}`, reason: "Check server cloud status" }); } return mcpSuccess({ server: server.name, ip: server.ip, mode: "bare", sshReachable: sshResult.reachable, ...(sshResult.hostKeyMismatch ? { hostKeyMismatch: true } : {}), suggested_actions: suggestedActions, }); } // Platform server: use adapter health check const platform = resolvePlatform(server); if (!platform) { return mcpSuccess({ server: server.name, ip: server.ip, platformStatus: "unknown" }); } const adapter = getAdapter(platform); const healthResult = await adapter.healthCheck(server.ip, server.domain); const port = platform === "dokploy" ? DOKPLOY_PORT : COOLIFY_PORT; const suggestedActions: SuggestedAction[] = healthResult.status === "not reachable" ? [{ command: `kastell status ${server.name} --autostart`, reason: `Try auto-restart ${platform}` }] : [{ command: `http://${server.ip}:${port}`, reason: `Access ${platform} dashboard` }]; return mcpSuccess({ server: server.name, ip: server.ip, platformStatus: healthResult.status, [`${platform}Url`]: healthResult.status === "running" ? `http://${server.ip}:${port}` : null, suggested_actions: suggestedActions, }); } // All servers health — route based on mode const healthResults = await Promise.all( servers.map(async (s) => { if (isBareServer(s)) { const sshResult = await checkBareServerSsh(s); return { name: s.name, ip: s.ip, mode: "bare" as const, sshReachable: sshResult.reachable, ...(sshResult.hostKeyMismatch ? { hostKeyMismatch: true } : {}), }; } const plat = resolvePlatform(s); const platStatus = plat ? (await getAdapter(plat).healthCheck(s.ip, s.domain)).status : "unknown"; return { name: s.name, ip: s.ip, mode: plat ?? s.mode ?? "coolify", platform: plat ?? "coolify", platformStatus: platStatus, }; }), ); const coolifyResults = healthResults.filter((r) => r.mode === "coolify"); const bareResults = healthResults.filter((r) => r.mode === "bare"); const notReachableCoolify = coolifyResults.filter( (r) => "platformStatus" in r && r.platformStatus === "not reachable", ); const suggestedActions: SuggestedAction[] = notReachableCoolify.length > 0 ? notReachableCoolify.map((r) => ({ command: `kastell status ${r.name} --autostart`, reason: `Coolify not reachable on ${r.name}`, })) : [{ command: "server_info { action: 'status' }", reason: "All healthy, check full status" }]; return mcpSuccess({ results: healthResults, summary: { total: healthResults.length, running: coolifyResults.filter( (r) => "platformStatus" in r && r.platformStatus === "running", ).length, notReachable: notReachableCoolify.length, bare: bareResults.length, }, suggested_actions: suggestedActions, }); } case "sizes": { if (!params.provider) { return mcpError( "Provider is required for 'sizes' action", "Specify provider: 'hetzner', 'digitalocean', 'vultr', or 'linode'", ); } if (!params.region) { return mcpError( "Region is required for 'sizes' action", "Specify region (e.g. 'nbg1' for Hetzner, 'fra1' for DigitalOcean, 'ewr' for Vultr, 'us-east' for Linode)", ); } const token = getProviderToken(params.provider); if (!token) { return mcpError( `No API token found for provider: ${params.provider}`, `Set environment variable: ${params.provider.toUpperCase()}_TOKEN`, ); } const provider = createProviderWithToken(params.provider, token); const mode: ServerMode = params.mode ?? "coolify"; const sizes = await provider.getAvailableServerTypes(params.region, mode); return mcpSuccess({ provider: params.provider, region: params.region, mode, sizes: sizes.map((s) => ({ id: s.id, name: s.name, vcpu: s.vcpu, ram: `${s.ram}GB`, disk: `${s.disk}GB`, price: s.price, })), total: sizes.length, suggested_actions: [ { command: `server_provision { provider: '${params.provider}', name: 'my-server', region: '${params.region}', size: '${sizes[0]?.id ?? "..."}', mode: '${mode}' }`, reason: "Provision a server with one of these sizes", }, ], }); } default: return mcpError( `Invalid action: ${String(params.action)}`, "Valid actions: list, status, health, sizes", ); } } catch (error: unknown) { return mcpError(getErrorMessage(error)); } } - src/mcp/tools/serverInfo.ts:16-32 (schema)The serverInfoSchema defines the input parameters for the server_info tool, including action, server name, provider, region, and server mode.
export const serverInfoSchema = { action: z.enum(["list", "status", "health", "sizes"]).describe( "Action to perform: 'list' all servers, 'status' check server/cloud status, 'health' check Coolify reachability (or SSH reachability for bare servers), 'sizes' list available server types with prices for a provider+region", ), server: z.string().optional().describe( "Server name or IP. Required for single-server status/health. Omit for all servers.", ), provider: z.enum(SUPPORTED_PROVIDERS).optional().describe( "Cloud provider (required for 'sizes' action)", ), region: z.string().optional().describe( "Region/location ID (required for 'sizes' action, e.g. 'nbg1' for Hetzner, 'fra1' for DigitalOcean)", ), mode: z.enum(["coolify", "bare"]).optional().describe( "Server mode filter for 'sizes' action. Coolify requires min 2GB RAM. Default: coolify", ), }; - src/mcp/server.ts:56-68 (registration)The server_info tool is registered here in the MCP server, binding it to the handleServerInfo function and the serverInfoSchema.
server.registerTool("server_info", { description: "Get information about Kastell-managed servers. Actions: 'list' all servers, 'status' check cloud provider + Coolify/bare status, 'health' check Coolify reachability or SSH access for bare servers, 'sizes' list available server types with prices for a provider+region. Requires provider API tokens as environment variables (HETZNER_TOKEN, DIGITALOCEAN_TOKEN, VULTR_TOKEN, LINODE_TOKEN) for status/sizes checks. Avoid calling repeatedly in short intervals to prevent provider API rate limiting. For fleet-wide health and audit scores across all servers, use server_fleet instead.", inputSchema: serverInfoSchema, annotations: { title: "Server Information", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { return handleServerInfo(params);