/** * MCP Tool handlers for Coolify. * * All handlers route through the Coolify SDK facade. * Uses a generic mcpCall wrapper to eliminate boilerplate. * * @module */ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { Coolify } from "../sdk.js"; /** Singleton SDK for MCP usage (reads env vars / config file). */ let _sdk: Coolify | null = null; function getSdk(): Coolify { if (!_sdk) _sdk = Coolify.fromEnv(); return _sdk; } /** * Wraps an SDK call into an MCP tool result. * On success → JSON response. On error → isError response. * * @param fn - Async function that calls the SDK (throws on error) * @param label - Error context label * @returns MCP CallToolResult */ async function mcpCall( fn: (sdk: Coolify) => Promise, label: string, ): Promise { try { const data = await fn(getSdk()); return { content: [ { type: "text", text: JSON.stringify( typeof data === "object" && data !== null ? { success: true, ...data } : { success: true, result: data }, null, 2, ), }, ], }; } catch (error) { return { content: [ { type: "text", text: `${label}: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } /** Wraps SDK call returning an array into { count, items } format. */ async function mcpList( fn: (sdk: Coolify) => Promise, itemKey: string, label: string, ): Promise { return mcpCall(async (sdk) => { const items = await fn(sdk); return { count: items.length, [itemKey]: items }; }, label); } /** * Main handler — routes tool calls to SDK methods. * * @param name - Tool name * @param args - Tool arguments * @returns MCP tool result */ export async function handleToolCall( name: string, args: Record, ): Promise { const a = args as any; switch (name) { // ─── Applications ────────────────────────────────────────────────── case "deploy": return mcpCall( (s) => s.applications .deploy(a.uuid, { force: a.force, tag: a.tag }) .then((r) => ({ deploymentUuid: r.deploymentUuid, resourceUuid: r.resourceUuid, message: "Deployment started", })), "Deployment failed", ); case "list_applications": return mcpList( (s) => s.applications.list(), "applications", "Failed to list applications", ); case "get_application_details": return mcpCall( (s) => s.applications.resolve(a.uuid).then((app) => ({ application: app })), "Failed to get application", ); case "start_application": return mcpCall( (s) => s.applications .start(a.uuid, { force: a.force, instantDeploy: a.instantDeploy }) .then(() => ({ message: `Application ${a.uuid} started`, })), "Failed to start application", ); case "stop_application": return mcpCall( (s) => s.applications.stop(a.uuid).then(() => ({ message: `Application ${a.uuid} stopped`, })), "Failed to stop application", ); case "restart_application": return mcpCall( (s) => s.applications.restart(a.uuid).then(() => ({ message: `Application ${a.uuid} restarted`, })), "Failed to restart application", ); case "delete_application": return mcpCall( (s) => s.applications.delete(a.uuid).then(() => ({ message: `Application ${a.uuid} deleted`, })), "Failed to delete application", ); case "update_application": return mcpCall( (s) => s.applications .update(a.uuid, { name: a.name, description: a.description, buildPack: a.buildPack, gitBranch: a.gitBranch, portsExposes: a.portsExposes, installCommand: a.installCommand, buildCommand: a.buildCommand, startCommand: a.startCommand, domains: a.domains, isForceHttpsEnabled: a.isForceHttpsEnabled, isAutoDeployEnabled: a.isAutoDeployEnabled, watchPaths: a.watchPaths, }) .then((app) => ({ message: `Application ${a.uuid} updated`, application: app, })), "Failed to update application", ); case "get_application": return mcpCall( (s) => s.applications.get(a.uuid).then((app) => ({ uuid: app.uuid, name: app.name, status: app.status, fqdn: app.fqdn, git_repository: app.git_repository, git_branch: app.git_branch, build_pack: app.build_pack, dockerfile_location: app.dockerfile_location, base_directory: app.base_directory, watch_paths: app.watch_paths, settings: app.settings, })), "Failed to get application", ); case "get_application_logs": return mcpCall( (s) => s.applications .logs(a.uuid, { tail: a.tail, serviceName: a.serviceName }) .then((logs) => ({ timestamp: logs.timestamp, logCount: logs.logs.length, logs: logs.logs, })), "Failed to get logs", ); case "get_deployment_history": return mcpList( (s) => s.applications.deployments(a.uuid), "deployments", "Failed to get deployment history", ); case "get_application_deployments": return mcpList( (s) => s.applications.deployments(a.uuid), "deployments", "Failed to get deployments", ); case "execute_command": return mcpCall( (s) => s.applications.exec(a.uuid, a.command), "Failed to execute command", ); // ─── Environment Variables ───────────────────────────────────────── case "get_env_vars": return mcpCall( (s) => s.applications.envVars(a.uuid).then((vars) => ({ total: vars.length, runtime: vars.filter((v) => v.is_runtime), buildtime: vars.filter((v) => v.is_buildtime), })), "Failed to get env vars", ); case "set_env_vars": return mcpCall((s) => { const entries = Object.entries(a.envVars as Record); return s.applications .bulkSetEnv( a.uuid, entries.map(([key, value]) => ({ key, value })), ) .then(() => ({ message: `Set ${entries.length} environment variable(s)`, })); }, "Failed to set env vars"); case "bulk_update_env_vars": return mcpCall( (s) => s.applications.bulkSetEnv(a.uuid, a.envVars).then(() => ({ message: `Bulk updated ${a.envVars.length} variable(s)`, })), "Failed to bulk update env vars", ); // ─── Domains ─────────────────────────────────────────────────────── case "set_domains": return mcpCall( (s) => s.applications .update(a.uuid, { domains: a.domains, isForceHttpsEnabled: a.forceHttps ?? true, }) .then(() => ({ message: `Domains set for ${a.uuid}`, domains: a.domains.split(",").map((d: string) => d.trim()), })), "Failed to set domains", ); // ─── Deployment Status ───────────────────────────────────────────── case "get_deployment_status": return mcpCall( (s) => s.applications .resolve(a.uuid) .then((app) => ({ status: app.status })), "Failed to get status", ); // ─── Deployments ─────────────────────────────────────────────────── case "list_deployments": return mcpList( (s) => s.deployments.active(), "deployments", "Failed to list deployments", ); case "get_deployment": return mcpCall( (s) => s.deployments.get(a.deploymentUuid).then((d) => ({ deployment: d })), "Failed to get deployment", ); case "cancel_deployment": return mcpCall( (s) => s.deployments .cancel(a.deploymentUuid) .then(() => ({ message: "Deployment cancelled" })), "Failed to cancel deployment", ); // ─── Servers ─────────────────────────────────────────────────────── case "list_servers": return mcpList( (s) => s.servers.list(), "servers", "Failed to list servers", ); case "get_server": return mcpCall( (s) => s.servers.get(a.serverUuid).then((srv) => ({ server: srv })), "Failed to get server", ); case "get_server_destinations": return mcpList( (s) => s.servers.destinations(a.serverUuid), "destinations", "Failed to get destinations", ); case "get_server_resources": return mcpList( (s) => s.servers.resources(a.serverUuid), "resources", "Failed to get server resources", ); case "get_server_domains": return mcpList( (s) => s.servers.domains(a.serverUuid), "domains", "Failed to get server domains", ); case "validate_server": return mcpCall( (s) => s.servers .validate(a.serverUuid) .then(() => ({ message: "Server validated" })), "Failed to validate server", ); // ─── Projects ────────────────────────────────────────────────────── case "list_projects": return mcpList( (s) => s.projects.list(), "projects", "Failed to list projects", ); case "create_project": return mcpCall( (s) => s.projects.create(a.name, a.description).then((p) => ({ message: `Project "${a.name}" created`, uuid: p.uuid, })), "Failed to create project", ); case "create_application": return mcpCall( (s) => s.applications .create({ name: a.name, description: a.description, projectUuid: a.projectUuid, environmentUuid: a.environmentUuid, serverUuid: a.serverUuid, type: a.type || "public", githubAppUuid: a.githubAppUuid, githubRepoUrl: a.githubRepoUrl, branch: a.branch, buildPack: a.buildPack, portsExposes: a.portsExposes, dockerComposeLocation: a.dockerComposeLocation, dockerfileLocation: a.dockerfileLocation, baseDirectory: a.baseDirectory, }) .then((r) => ({ message: `Application "${a.name}" created`, uuid: r.uuid, })), "Failed to create application", ); // ─── Teams ───────────────────────────────────────────────────────── case "list_teams": return mcpList((s) => s.teams.list(), "teams", "Failed to list teams"); case "get_current_team": return mcpCall( (s) => s.teams.current().then((t) => ({ team: t })), "Failed to get current team", ); case "get_team_members": return mcpList( (s) => s.teams.members(a.teamId), "members", "Failed to get team members", ); // ─── Databases ───────────────────────────────────────────────────── case "list_databases": return mcpList( (s) => s.databases.list(), "databases", "Failed to list databases", ); case "get_database": return mcpCall( (s) => s.databases.get(a.uuid).then((db) => ({ database: db })), "Failed to get database", ); case "create_database": return mcpCall( (s) => s.databases.create(a.dbType, a.data).then((r) => ({ message: `${a.dbType} database created`, uuid: r.uuid, })), "Failed to create database", ); case "delete_database": return mcpCall( (s) => s.databases .delete(a.uuid) .then(() => ({ message: "Database deleted" })), "Failed to delete database", ); case "start_database": return mcpCall( (s) => s.databases .start(a.uuid) .then(() => ({ message: "Database started" })), "Failed to start database", ); case "stop_database": return mcpCall( (s) => s.databases .stop(a.uuid) .then(() => ({ message: "Database stopped" })), "Failed to stop database", ); case "restart_database": return mcpCall( (s) => s.databases .restart(a.uuid) .then(() => ({ message: "Database restarted" })), "Failed to restart database", ); // ─── Services ────────────────────────────────────────────────────── case "list_services": return mcpList( (s) => s.services.list(), "services", "Failed to list services", ); case "get_service": return mcpCall( (s) => s.services.get(a.uuid).then((svc) => ({ service: svc })), "Failed to get service", ); case "start_service": return mcpCall( (s) => s.services.start(a.uuid).then(() => ({ message: "Service started" })), "Failed to start service", ); case "stop_service": return mcpCall( (s) => s.services.stop(a.uuid).then(() => ({ message: "Service stopped" })), "Failed to stop service", ); case "restart_service": return mcpCall( (s) => s.services .restart(a.uuid) .then(() => ({ message: "Service restarted" })), "Failed to restart service", ); case "delete_service": return mcpCall( (s) => s.services .delete(a.uuid) .then(() => ({ message: "Service deleted" })), "Failed to delete service", ); // ─── Private Keys ────────────────────────────────────────────────── case "list_private_keys": return mcpList( (s) => s.keys.list(), "keys", "Failed to list private keys", ); case "get_private_key": return mcpCall( (s) => s.keys.get(a.uuid).then((k) => ({ privateKey: k })), "Failed to get private key", ); // ─── Smart Resolution ────────────────────────────────────────────── case "resolve_application": return mcpCall( (s) => s.applications.resolve(a.query).then((app) => ({ application: { uuid: app.uuid, name: app.name, status: app.status, fqdn: app.fqdn, }, })), "Failed to resolve application", ); case "resolve_server": return mcpCall( (s) => s.servers.resolve(a.query).then((srv) => ({ server: { uuid: srv.uuid, name: srv.name, ip: srv.ip, is_reachable: srv.is_reachable, }, })), "Failed to resolve server", ); // ─── Diagnostics ─────────────────────────────────────────────────── case "diagnose_application": return mcpCall((s) => s.diagnose.app(a.query), "Diagnosis failed"); case "diagnose_server": return mcpCall((s) => s.diagnose.server(a.query), "Diagnosis failed"); case "find_infrastructure_issues": return mcpCall( (s) => s.diagnose.infrastructure(), "Infrastructure scan failed", ); // ─── Batch Operations ────────────────────────────────────────────── case "restart_project_apps": return mcpCall( (s) => s.batch.restartProject(a.projectUuid), "Batch restart failed", ); case "redeploy_project_apps": return mcpCall( (s) => s.batch.redeployProject(a.projectUuid, a.force), "Batch redeploy failed", ); case "stop_all_apps": return mcpCall((s) => s.batch.stopAll(), "Stop all failed"); // ─── Network Diagnostics ───────────────────────────────────────── case "inspect_network": return mcpCall( (s) => s.network.inspect(a.uuid, a.servicesToTest), "Network inspection failed", ); case "analyze_deploy_failure": return mcpCall( (s) => s.diagnose.deployFailure(a.deploymentUuid), "Deploy analysis failed", ); // ─── Version / Health ────────────────────────────────────────────── case "get_version": return mcpCall( (s) => s.version().then((v) => ({ version: v.version })), "Failed to get version", ); case "health_check": return mcpCall( (s) => s.servers.list().then(() => ({ status: "healthy", message: "Coolify API is accessible", })), "Health check failed", ); case "get_resource_usage": return mcpCall( (s) => s.applications .resolve(a.uuid) .then((app) => ({ status: app.status })), "Failed to get resource usage", ); default: return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } }