/** * Coolify MCP Server * Consolidated tools for efficient token usage */ import { createRequire } from 'module'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { z } from 'zod'; import { CoolifyClient, type ServerSummary, type ProjectSummary, type ApplicationSummary, type DatabaseSummary, type ServiceSummary, type GitHubAppSummary, } from './coolify-client.js'; import type { CoolifyConfig, GitHubApp, BuildPack, ResponseAction, ResponsePagination, Deployment, } from '../types/coolify.js'; import { DocsSearchEngine } from './docs-search.js'; const _require = createRequire(import.meta.url); export const VERSION: string = _require('../../package.json').version; /** Wrap handler with error handling */ function wrap( fn: () => Promise, ): Promise<{ content: Array<{ type: 'text'; text: string }> }> { return fn() .then((result) => ({ content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], })) .catch((error) => ({ content: [ { type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], })); } const TRUNCATION_PREFIX = '...[truncated]...\n'; interface LogEntry { output?: string; timestamp?: string; type?: string; hidden?: boolean; command?: string | null; } export interface TruncatedLogsResult { logs: string; total: number; showing_start: number; showing_end: number; } /** * Truncate logs by entry count with pagination support. * Handles both JSON array format (Coolify deployment logs) and plain text. * Page 1 = most recent entries, page 2 = next older batch, etc. * Exported for testing. */ export function truncateLogs( logs: string, lineLimit: number = 200, charLimit: number = 50000, page: number = 1, ): TruncatedLogsResult { // Try parsing as JSON array (Coolify deployment log format) let lines: string[]; let total: number; try { const entries: LogEntry[] = JSON.parse(logs); if (Array.isArray(entries)) { const visible = entries.filter((e) => !e.hidden); total = visible.length; const end = total - (page - 1) * lineLimit; const start = Math.max(0, end - lineLimit); const slice = visible.slice(start, end); lines = slice.map((e) => `[${e.timestamp ?? ''}] ${e.output ?? ''}`); } else { const allLines = logs.split('\n'); total = allLines.length; const end = total - (page - 1) * lineLimit; const start = Math.max(0, end - lineLimit); lines = allLines.slice(start, end); } } catch { // Plain text logs — split by newlines const allLines = logs.split('\n'); total = allLines.length; const end = total - (page - 1) * lineLimit; const start = Math.max(0, end - lineLimit); lines = allLines.slice(start, end); } const end = total - (page - 1) * lineLimit; const start = Math.max(0, end - lineLimit); let result = lines.join('\n'); // Safety net: limit by characters if (result.length > charLimit) { const prefixLen = TRUNCATION_PREFIX.length; result = TRUNCATION_PREFIX + result.slice(-(charLimit - prefixLen)); } return { logs: result, total, showing_start: start + 1, showing_end: Math.min(end, total), }; } // ============================================================================= // Action Generators for HATEOAS-style responses // ============================================================================= /** Generate contextual actions for an application based on its status */ export function getApplicationActions(uuid: string, status?: string): ResponseAction[] { const actions: ResponseAction[] = [ { tool: 'application_logs', args: { uuid }, hint: 'View logs' }, ]; const s = (status || '').toLowerCase(); if (s.includes('running')) { actions.push({ tool: 'control', args: { resource: 'application', action: 'restart', uuid }, hint: 'Restart', }); actions.push({ tool: 'control', args: { resource: 'application', action: 'stop', uuid }, hint: 'Stop', }); } else { actions.push({ tool: 'control', args: { resource: 'application', action: 'start', uuid }, hint: 'Start', }); } return actions; } /** Generate contextual actions for a deployment */ export function getDeploymentActions( uuid: string, status: string, appUuid?: string, ): ResponseAction[] { const actions: ResponseAction[] = []; if (status === 'in_progress' || status === 'queued') { actions.push({ tool: 'deployment', args: { action: 'cancel', uuid }, hint: 'Cancel' }); } if (appUuid) { actions.push({ tool: 'get_application', args: { uuid: appUuid }, hint: 'View app' }); actions.push({ tool: 'application_logs', args: { uuid: appUuid }, hint: 'App logs' }); } return actions; } /** Generate pagination info for list endpoints */ export function getPagination( tool: string, page?: number, perPage?: number, count?: number, ): ResponsePagination | undefined { const p = page ?? 1; const pp = perPage ?? 50; if (!count || count < pp) { return p > 1 ? { prev: { tool, args: { page: p - 1, per_page: pp } } } : undefined; } return { ...(p > 1 && { prev: { tool, args: { page: p - 1, per_page: pp } } }), next: { tool, args: { page: p + 1, per_page: pp } }, }; } /** Wrap handler with error handling and HATEOAS actions */ function wrapWithActions( fn: () => Promise, getActions?: (result: T) => ResponseAction[], getPaginationFn?: (result: T) => ResponsePagination | undefined, ): Promise<{ content: Array<{ type: 'text'; text: string }> }> { return fn() .then((result) => { const actions = getActions?.(result) ?? []; const pagination = getPaginationFn?.(result); const response: Record = { data: result }; if (actions.length > 0) response._actions = actions; if (pagination) response._pagination = pagination; return { content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }] }; }) .catch((error) => ({ content: [ { type: 'text' as const, text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], })); } export class CoolifyMcpServer extends McpServer { private readonly client: CoolifyClient; private readonly docsSearch: DocsSearchEngine = new DocsSearchEngine(); constructor(config: CoolifyConfig) { super({ name: 'coolify', version: VERSION }); this.client = new CoolifyClient(config); this.registerTools(); } async connect(transport: Transport): Promise { await super.connect(transport); } private registerTools(): void { // ========================================================================= // Meta (2 tools) // ========================================================================= this.tool('get_version', 'Coolify API version', {}, async () => wrap(() => this.client.getVersion()), ); this.tool('get_mcp_version', 'MCP server version', {}, async () => ({ content: [ { type: 'text' as const, text: JSON.stringify({ version: VERSION, name: '@masonator/coolify-mcp' }), }, ], })); // ========================================================================= // Infrastructure Overview (1 tool) // ========================================================================= this.tool( 'get_infrastructure_overview', 'Overview of all resources with counts', {}, async () => wrap(async () => { const results = await Promise.allSettled([ this.client.listServers({ summary: true }), this.client.listProjects({ summary: true }), this.client.listApplications({ summary: true }), this.client.listDatabases({ summary: true }), this.client.listServices({ summary: true }), ]); const extract = (r: PromiseSettledResult): T | [] => r.status === 'fulfilled' ? r.value : []; const [servers, projects, applications, databases, services] = [ extract(results[0]) as ServerSummary[], extract(results[1]) as ProjectSummary[], extract(results[2]) as ApplicationSummary[], extract(results[3]) as DatabaseSummary[], extract(results[4]) as ServiceSummary[], ]; const errors = results .map((r, i) => r.status === 'rejected' ? `${['servers', 'projects', 'applications', 'databases', 'services'][i]}: ${r.reason}` : null, ) .filter(Boolean); return { summary: { servers: servers.length, projects: projects.length, applications: applications.length, databases: databases.length, services: services.length, }, servers, projects, applications, databases, services, ...(errors.length > 0 && { errors }), }; }), ); // ========================================================================= // Diagnostics (3 tools) // ========================================================================= this.tool( 'diagnose_app', 'App diagnostics by UUID/name/domain', { query: z.string() }, async ({ query }) => wrap(() => this.client.diagnoseApplication(query)), ); this.tool( 'diagnose_server', 'Server diagnostics by UUID/name/IP', { query: z.string() }, async ({ query }) => wrap(() => this.client.diagnoseServer(query)), ); this.tool('find_issues', 'Scan infrastructure for problems', {}, async () => wrap(() => this.client.findInfrastructureIssues()), ); // ========================================================================= // Servers (5 tools) // ========================================================================= this.tool( 'list_servers', 'List servers (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrap(() => this.client.listServers({ page, per_page, summary: true })), ); this.tool('get_server', 'Server details', { uuid: z.string() }, async ({ uuid }) => wrap(() => this.client.getServer(uuid)), ); this.tool('server_resources', 'Resources on server', { uuid: z.string() }, async ({ uuid }) => wrap(() => this.client.getServerResources(uuid)), ); this.tool('server_domains', 'Domains on server', { uuid: z.string() }, async ({ uuid }) => wrap(() => this.client.getServerDomains(uuid)), ); this.tool( 'validate_server', 'Validate server connection', { uuid: z.string() }, async ({ uuid }) => wrap(() => this.client.validateServer(uuid)), ); // ========================================================================= // Projects (1 tool - consolidated CRUD) // ========================================================================= this.tool( 'projects', 'Manage projects: list/get/create/update/delete', { action: z.enum(['list', 'get', 'create', 'update', 'delete']), uuid: z.string().optional(), name: z.string().optional(), description: z.string().optional(), page: z.number().optional(), per_page: z.number().optional(), }, async ({ action, uuid, name, description, page, per_page }) => { switch (action) { case 'list': return wrap(() => this.client.listProjects({ page, per_page, summary: true })); case 'get': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.getProject(uuid)); case 'create': if (!name) return { content: [{ type: 'text' as const, text: 'Error: name required' }] }; return wrap(() => this.client.createProject({ name, description })); case 'update': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.updateProject(uuid, { name, description })); case 'delete': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.deleteProject(uuid)); } }, ); // ========================================================================= // Environments (1 tool - consolidated CRUD) // ========================================================================= this.tool( 'environments', 'Manage environments: list/get/create/delete (get includes dragonfly/keydb/clickhouse DBs missing from API)', { action: z.enum(['list', 'get', 'create', 'delete']), project_uuid: z.string(), name: z.string().optional(), description: z.string().optional(), }, async ({ action, project_uuid, name, description }) => { switch (action) { case 'list': return wrap(() => this.client.listProjectEnvironments(project_uuid)); case 'get': if (!name) return { content: [{ type: 'text' as const, text: 'Error: name required' }] }; // Use enhanced method that includes missing DB types (#88) return wrap(() => this.client.getProjectEnvironmentWithDatabases(project_uuid, name)); case 'create': if (!name) return { content: [{ type: 'text' as const, text: 'Error: name required' }] }; return wrap(() => this.client.createProjectEnvironment(project_uuid, { name, description }), ); case 'delete': if (!name) return { content: [{ type: 'text' as const, text: 'Error: name required' }] }; return wrap(() => this.client.deleteProjectEnvironment(project_uuid, name)); } }, ); // ========================================================================= // Applications (4 tools) // ========================================================================= this.tool( 'list_applications', 'List apps (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrapWithActions( () => this.client.listApplications({ page, per_page, summary: true }), undefined, (result) => getPagination('list_applications', page, per_page, (result as unknown[]).length), ), ); this.tool('get_application', 'App details', { uuid: z.string() }, async ({ uuid }) => wrapWithActions( () => this.client.getApplication(uuid), (app) => getApplicationActions(app.uuid, app.status), ), ); this.tool( 'application', 'Manage app: create/update/delete/delete_preview', { action: z.enum([ 'create_public', 'create_github', 'create_key', 'create_dockerimage', 'update', 'delete', 'delete_preview', ]), uuid: z.string().optional(), // Create fields project_uuid: z.string().optional(), server_uuid: z.string().optional(), github_app_uuid: z.string().optional(), private_key_uuid: z.string().optional(), destination_uuid: z.string().optional(), git_repository: z.string().optional(), git_branch: z.string().optional(), environment_name: z.string().optional(), environment_uuid: z.string().optional(), build_pack: z.string().optional(), ports_exposes: z.string().optional(), // Docker image fields docker_registry_image_name: z.string().optional(), docker_registry_image_tag: z.string().optional(), // Update fields name: z.string().optional(), description: z.string().optional(), fqdn: z.string().optional(), domains: z.string().optional(), custom_docker_run_options: z.string().optional(), custom_labels: z.string().optional(), instant_deploy: z.boolean().optional(), // Health check fields health_check_enabled: z.boolean().optional(), health_check_path: z.string().optional(), health_check_port: z.number().optional(), health_check_host: z.string().optional(), health_check_method: z.string().optional(), health_check_return_code: z.number().optional(), health_check_scheme: z.string().optional(), health_check_response_text: z.string().optional(), health_check_interval: z.number().optional(), health_check_timeout: z.number().optional(), health_check_retries: z.number().optional(), health_check_start_period: z.number().optional(), // Build configuration fields (accepted on create_public/github/key + update; // create_dockerimage ignores these — pre-built image, no build step) base_directory: z.string().optional(), publish_directory: z.string().optional(), install_command: z.string().optional(), build_command: z.string().optional(), start_command: z.string().optional(), dockerfile_location: z.string().optional(), watch_paths: z.string().optional(), // Update-only: Coolify strips dockerfile_target_build on every create endpoint // (controller $allowedFields line 1014) but accepts on PATCH (line 2497). dockerfile_target_build: z.string().optional(), // Delete fields delete_volumes: z.boolean().optional(), // Preview fields pull_request_id: z.number().optional(), }, async (args) => { const { action, uuid, delete_volumes } = args; switch (action) { case 'create_public': if ( !args.project_uuid || !args.server_uuid || !args.git_repository || !args.git_branch || !args.build_pack || !args.ports_exposes ) { return { content: [ { type: 'text' as const, text: 'Error: project_uuid, server_uuid, git_repository, git_branch, build_pack, ports_exposes required', }, ], }; } return wrap(() => this.client.createApplicationPublic({ project_uuid: args.project_uuid!, server_uuid: args.server_uuid!, destination_uuid: args.destination_uuid, git_repository: args.git_repository!, git_branch: args.git_branch!, build_pack: args.build_pack! as BuildPack, ports_exposes: args.ports_exposes!, environment_name: args.environment_name, environment_uuid: args.environment_uuid, name: args.name, description: args.description, fqdn: args.fqdn, domains: args.domains, base_directory: args.base_directory, publish_directory: args.publish_directory, install_command: args.install_command, build_command: args.build_command, start_command: args.start_command, dockerfile_location: args.dockerfile_location, watch_paths: args.watch_paths, health_check_enabled: args.health_check_enabled, health_check_path: args.health_check_path, health_check_port: args.health_check_port, health_check_host: args.health_check_host, health_check_method: args.health_check_method, health_check_return_code: args.health_check_return_code, health_check_scheme: args.health_check_scheme, health_check_response_text: args.health_check_response_text, health_check_interval: args.health_check_interval, health_check_timeout: args.health_check_timeout, health_check_retries: args.health_check_retries, health_check_start_period: args.health_check_start_period, custom_docker_run_options: args.custom_docker_run_options, custom_labels: args.custom_labels, instant_deploy: args.instant_deploy, }), ); case 'create_github': if ( !args.project_uuid || !args.server_uuid || !args.github_app_uuid || !args.git_repository || !args.git_branch ) { return { content: [ { type: 'text' as const, text: 'Error: project_uuid, server_uuid, github_app_uuid, git_repository, git_branch required', }, ], }; } return wrap(() => this.client.createApplicationPrivateGH({ project_uuid: args.project_uuid!, server_uuid: args.server_uuid!, github_app_uuid: args.github_app_uuid!, destination_uuid: args.destination_uuid, git_repository: args.git_repository!, git_branch: args.git_branch!, build_pack: args.build_pack as BuildPack | undefined, ports_exposes: args.ports_exposes, environment_name: args.environment_name, environment_uuid: args.environment_uuid, name: args.name, description: args.description, fqdn: args.fqdn, domains: args.domains, base_directory: args.base_directory, publish_directory: args.publish_directory, install_command: args.install_command, build_command: args.build_command, start_command: args.start_command, dockerfile_location: args.dockerfile_location, watch_paths: args.watch_paths, health_check_enabled: args.health_check_enabled, health_check_path: args.health_check_path, health_check_port: args.health_check_port, health_check_host: args.health_check_host, health_check_method: args.health_check_method, health_check_return_code: args.health_check_return_code, health_check_scheme: args.health_check_scheme, health_check_response_text: args.health_check_response_text, health_check_interval: args.health_check_interval, health_check_timeout: args.health_check_timeout, health_check_retries: args.health_check_retries, health_check_start_period: args.health_check_start_period, custom_docker_run_options: args.custom_docker_run_options, custom_labels: args.custom_labels, instant_deploy: args.instant_deploy, }), ); case 'create_key': if ( !args.project_uuid || !args.server_uuid || !args.private_key_uuid || !args.git_repository || !args.git_branch ) { return { content: [ { type: 'text' as const, text: 'Error: project_uuid, server_uuid, private_key_uuid, git_repository, git_branch required', }, ], }; } return wrap(() => this.client.createApplicationPrivateKey({ project_uuid: args.project_uuid!, server_uuid: args.server_uuid!, private_key_uuid: args.private_key_uuid!, destination_uuid: args.destination_uuid, git_repository: args.git_repository!, git_branch: args.git_branch!, build_pack: args.build_pack as BuildPack | undefined, ports_exposes: args.ports_exposes, environment_name: args.environment_name, environment_uuid: args.environment_uuid, name: args.name, description: args.description, fqdn: args.fqdn, domains: args.domains, base_directory: args.base_directory, publish_directory: args.publish_directory, install_command: args.install_command, build_command: args.build_command, start_command: args.start_command, dockerfile_location: args.dockerfile_location, watch_paths: args.watch_paths, health_check_enabled: args.health_check_enabled, health_check_path: args.health_check_path, health_check_port: args.health_check_port, health_check_host: args.health_check_host, health_check_method: args.health_check_method, health_check_return_code: args.health_check_return_code, health_check_scheme: args.health_check_scheme, health_check_response_text: args.health_check_response_text, health_check_interval: args.health_check_interval, health_check_timeout: args.health_check_timeout, health_check_retries: args.health_check_retries, health_check_start_period: args.health_check_start_period, custom_docker_run_options: args.custom_docker_run_options, custom_labels: args.custom_labels, instant_deploy: args.instant_deploy, }), ); case 'create_dockerimage': if ( !args.project_uuid || !args.server_uuid || !args.docker_registry_image_name || !args.ports_exposes ) { return { content: [ { type: 'text' as const, text: 'Error: project_uuid, server_uuid, docker_registry_image_name, ports_exposes required', }, ], }; } return wrap(() => this.client.createApplicationDockerImage({ project_uuid: args.project_uuid!, server_uuid: args.server_uuid!, destination_uuid: args.destination_uuid, docker_registry_image_name: args.docker_registry_image_name!, ports_exposes: args.ports_exposes!, docker_registry_image_tag: args.docker_registry_image_tag, environment_name: args.environment_name, environment_uuid: args.environment_uuid, name: args.name, description: args.description, fqdn: args.fqdn, domains: args.domains, // Build-config fields (base_directory, install_command, etc.) // are intentionally NOT forwarded: /applications/dockerimage is // for pre-built registry images and has no build step. health_check_enabled: args.health_check_enabled, health_check_path: args.health_check_path, health_check_port: args.health_check_port, health_check_host: args.health_check_host, health_check_method: args.health_check_method, health_check_return_code: args.health_check_return_code, health_check_scheme: args.health_check_scheme, health_check_response_text: args.health_check_response_text, health_check_interval: args.health_check_interval, health_check_timeout: args.health_check_timeout, health_check_retries: args.health_check_retries, health_check_start_period: args.health_check_start_period, custom_docker_run_options: args.custom_docker_run_options, custom_labels: args.custom_labels, instant_deploy: args.instant_deploy, }), ); case 'update': { if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { action: _, uuid: __, delete_volumes: ___, ...updateData } = args; return wrap(() => this.client.updateApplication(uuid, updateData)); } case 'delete': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.deleteApplication(uuid, { deleteVolumes: delete_volumes }), ); case 'delete_preview': if (!uuid || !args.pull_request_id) return { content: [{ type: 'text' as const, text: 'Error: uuid, pull_request_id required' }], }; return wrap(() => this.client.deleteApplicationPreview(uuid, args.pull_request_id!)); } }, ); this.tool( 'application_logs', 'Get app logs', { uuid: z.string(), lines: z.number().optional() }, async ({ uuid, lines }) => wrap(() => this.client.getApplicationLogs(uuid, lines)), ); // ========================================================================= // Databases (3 tools) // ========================================================================= this.tool( 'list_databases', 'List databases (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrap(() => this.client.listDatabases({ page, per_page, summary: true })), ); this.tool('get_database', 'Database details', { uuid: z.string() }, async ({ uuid }) => wrap(() => this.client.getDatabase(uuid)), ); this.tool( 'database', 'Manage database: create/delete', { action: z.enum(['create', 'delete']), type: z .enum([ 'postgresql', 'mysql', 'mariadb', 'mongodb', 'redis', 'keydb', 'clickhouse', 'dragonfly', ]) .optional(), uuid: z.string().optional(), server_uuid: z.string().optional(), project_uuid: z.string().optional(), environment_name: z.string().optional(), name: z.string().optional(), description: z.string().optional(), image: z.string().optional(), is_public: z.boolean().optional(), public_port: z.number().optional(), instant_deploy: z.boolean().optional(), delete_volumes: z.boolean().optional(), // DB-specific optional fields postgres_user: z.string().optional(), postgres_password: z.string().optional(), postgres_db: z.string().optional(), mysql_root_password: z.string().optional(), mysql_user: z.string().optional(), mysql_password: z.string().optional(), mysql_database: z.string().optional(), mariadb_root_password: z.string().optional(), mariadb_user: z.string().optional(), mariadb_password: z.string().optional(), mariadb_database: z.string().optional(), mongo_initdb_root_username: z.string().optional(), mongo_initdb_root_password: z.string().optional(), mongo_initdb_database: z.string().optional(), redis_password: z.string().optional(), keydb_password: z.string().optional(), clickhouse_admin_user: z.string().optional(), clickhouse_admin_password: z.string().optional(), dragonfly_password: z.string().optional(), }, async (args) => { const { action, type, uuid, delete_volumes, ...dbData } = args; if (action === 'delete') { if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.deleteDatabase(uuid, { deleteVolumes: delete_volumes })); } // create if (!type || !args.server_uuid || !args.project_uuid) { return { content: [ { type: 'text' as const, text: 'Error: type, server_uuid, project_uuid required' }, ], }; } const dbMethods: Record Promise> = { postgresql: (d) => this.client.createPostgresql(d), mysql: (d) => this.client.createMysql(d), mariadb: (d) => this.client.createMariadb(d), mongodb: (d) => this.client.createMongodb(d), redis: (d) => this.client.createRedis(d), keydb: (d) => this.client.createKeydb(d), clickhouse: (d) => this.client.createClickhouse(d), dragonfly: (d) => this.client.createDragonfly(d), }; return wrap(() => dbMethods[type](dbData)); }, ); // ========================================================================= // Services (3 tools) // ========================================================================= this.tool( 'list_services', 'List services (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrap(() => this.client.listServices({ page, per_page, summary: true })), ); this.tool('get_service', 'Service details', { uuid: z.string() }, async ({ uuid }) => wrap(() => this.client.getService(uuid)), ); this.tool( 'service', 'Manage service: create/update/delete', { action: z.enum(['create', 'update', 'delete']), uuid: z.string().optional(), type: z.string().optional(), server_uuid: z.string().optional(), project_uuid: z.string().optional(), environment_name: z.string().optional(), name: z.string().optional(), description: z.string().optional(), instant_deploy: z.boolean().optional(), docker_compose_raw: z .string() .optional() .describe('Raw docker-compose YAML for custom services (auto base64-encoded)'), delete_volumes: z.boolean().optional(), }, async (args) => { const { action, uuid, delete_volumes } = args; switch (action) { case 'create': if (!args.server_uuid || !args.project_uuid) { return { content: [ { type: 'text' as const, text: 'Error: server_uuid, project_uuid required' }, ], }; } return wrap(() => this.client.createService({ project_uuid: args.project_uuid!, server_uuid: args.server_uuid!, type: args.type, name: args.name, description: args.description, environment_name: args.environment_name, instant_deploy: args.instant_deploy, docker_compose_raw: args.docker_compose_raw, }), ); case 'update': { if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { action: _, uuid: __, delete_volumes: ___, ...updateData } = args; return wrap(() => this.client.updateService(uuid, updateData)); } case 'delete': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.deleteService(uuid, { deleteVolumes: delete_volumes })); } }, ); // ========================================================================= // Resource Control (1 tool - start/stop/restart for all types) // ========================================================================= this.tool( 'control', 'Start/stop/restart app, database, or service', { resource: z.enum(['application', 'database', 'service']), action: z.enum(['start', 'stop', 'restart']), uuid: z.string(), }, async ({ resource, action, uuid }) => { const methods: Record Promise>> = { application: { start: (u) => this.client.startApplication(u), stop: (u) => this.client.stopApplication(u), restart: (u) => this.client.restartApplication(u), }, database: { start: (u) => this.client.startDatabase(u), stop: (u) => this.client.stopDatabase(u), restart: (u) => this.client.restartDatabase(u), }, service: { start: (u) => this.client.startService(u), stop: (u) => this.client.stopService(u), restart: (u) => this.client.restartService(u), }, }; // Generate contextual actions based on resource type and action taken const getControlActions = (): ResponseAction[] => { const actions: ResponseAction[] = []; if (resource === 'application') { actions.push({ tool: 'application_logs', args: { uuid }, hint: 'View logs' }); actions.push({ tool: 'get_application', args: { uuid }, hint: 'Check status' }); if (action === 'start' || action === 'restart') { actions.push({ tool: 'control', args: { resource: 'application', action: 'stop', uuid }, hint: 'Stop', }); } else { actions.push({ tool: 'control', args: { resource: 'application', action: 'start', uuid }, hint: 'Start', }); } } else if (resource === 'database') { actions.push({ tool: 'get_database', args: { uuid }, hint: 'Check status' }); } else if (resource === 'service') { actions.push({ tool: 'get_service', args: { uuid }, hint: 'Check status' }); } return actions; }; return wrapWithActions(() => methods[resource][action](uuid), getControlActions); }, ); // ========================================================================= // Environment Variables (1 tool - consolidated) // ========================================================================= this.tool( 'env_vars', "Manage env vars for app, service, or database. Values are masked by default (returned as '***') to avoid leaking secrets to MCP clients; pass reveal=true on the list action when the caller explicitly needs the plaintext (e.g. 'what is FOO set to?'). Set is_buildtime=false (and/or is_runtime=true) for runtime-only vars to avoid Dockerfile ARG issues with multiline values like PEM keys.", { resource: z.enum(['application', 'service', 'database']), action: z.enum(['list', 'create', 'update', 'delete', 'bulk_update']), uuid: z.string(), key: z.string().optional(), value: z.string().optional(), env_uuid: z.string().optional(), is_buildtime: z.boolean().optional(), is_runtime: z.boolean().optional(), reveal: z.boolean().optional(), data: z .array( z.object({ key: z.string(), value: z.string(), is_preview: z.boolean().optional(), is_buildtime: z.boolean().optional(), is_runtime: z.boolean().optional(), is_literal: z.boolean().optional(), is_multiline: z.boolean().optional(), is_shown_once: z.boolean().optional(), }), ) .optional(), }, async ({ resource, action, uuid, key, value, env_uuid, is_buildtime, is_runtime, reveal, data, }) => { if (resource === 'application') { switch (action) { case 'list': return wrap(() => this.client.listApplicationEnvVars(uuid, { summary: true, reveal }), ); case 'create': if (!key || !value) return { content: [{ type: 'text' as const, text: 'Error: key, value required' }] }; return wrap(() => this.client.createApplicationEnvVar(uuid, { key, value, is_buildtime, is_runtime, }), ); case 'update': if (!key || !value) return { content: [{ type: 'text' as const, text: 'Error: key, value required' }] }; return wrap(() => this.client.updateApplicationEnvVar(uuid, { key, value, is_buildtime, is_runtime, }), ); case 'delete': if (!env_uuid) return { content: [{ type: 'text' as const, text: 'Error: env_uuid required' }] }; return wrap(() => this.client.deleteApplicationEnvVar(uuid, env_uuid)); case 'bulk_update': if (!data) return { content: [{ type: 'text' as const, text: 'Error: data array required' }] }; return wrap(() => this.client.bulkUpdateApplicationEnvVars(uuid, { data })); } } else if (resource === 'service') { switch (action) { case 'list': return wrap(() => this.client.listServiceEnvVars(uuid, { reveal })); case 'create': if (!key || !value) return { content: [{ type: 'text' as const, text: 'Error: key, value required' }] }; return wrap(() => this.client.createServiceEnvVar(uuid, { key, value, is_buildtime, is_runtime }), ); case 'update': if (!key || !value) return { content: [{ type: 'text' as const, text: 'Error: key, value required' }] }; return wrap(() => this.client.updateServiceEnvVar(uuid, { key, value, is_buildtime, is_runtime }), ); case 'delete': if (!env_uuid) return { content: [{ type: 'text' as const, text: 'Error: env_uuid required' }] }; return wrap(() => this.client.deleteServiceEnvVar(uuid, env_uuid)); case 'bulk_update': if (!data) return { content: [{ type: 'text' as const, text: 'Error: data array required' }] }; return wrap(() => this.client.bulkUpdateServiceEnvVars(uuid, { data })); } } else { switch (action) { case 'list': return wrap(() => this.client.listDatabaseEnvVars(uuid)); case 'create': if (!key || !value) return { content: [{ type: 'text' as const, text: 'Error: key, value required' }] }; return wrap(() => this.client.createDatabaseEnvVar(uuid, { key, value, is_buildtime, is_runtime }), ); case 'update': if (!key || !value) return { content: [{ type: 'text' as const, text: 'Error: key, value required' }] }; return wrap(() => this.client.updateDatabaseEnvVar(uuid, { key, value, is_buildtime, is_runtime }), ); case 'delete': if (!env_uuid) return { content: [{ type: 'text' as const, text: 'Error: env_uuid required' }] }; return wrap(() => this.client.deleteDatabaseEnvVar(uuid, env_uuid)); case 'bulk_update': if (!data) return { content: [{ type: 'text' as const, text: 'Error: data array required' }] }; return wrap(() => this.client.bulkUpdateDatabaseEnvVars(uuid, { data })); } } }, ); // ========================================================================= // Deployments (3 tools) // ========================================================================= this.tool( 'list_deployments', 'List deployments (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrapWithActions( () => this.client.listDeployments({ page, per_page, summary: true }), undefined, (result) => getPagination('list_deployments', page, per_page, (result as unknown[]).length), ), ); this.tool( 'deploy', 'Deploy by tag/UUID', { tag_or_uuid: z.string(), force: z.boolean().optional() }, async ({ tag_or_uuid, force }) => wrapWithActions( () => this.client.deployByTagOrUuid(tag_or_uuid, force), () => [{ tool: 'list_deployments', args: {}, hint: 'Check deployment status' }], ), ); this.tool( 'deployment', 'Manage deployment: get/cancel/list_for_app. Logs excluded by default on all actions — for get use `lines` (paginated tail), for list_for_app use `include_logs: true` to include raw build-log blobs.', { action: z.enum(['get', 'cancel', 'list_for_app']), uuid: z.string(), lines: z.number().optional(), // Include logs truncated to last N entries (omit for no logs) page: z.number().optional(), // Log page (1=most recent, 2=older, etc.) max_chars: z.number().optional(), // Limit log output to last N chars (default: 50000) include_logs: z.boolean().optional(), // list_for_app only: include raw build logs (default false; upstream returns ~30KB per deployment) }, async ({ action, uuid, lines, page, max_chars, include_logs }) => { switch (action) { case 'get': // If lines param specified, include logs and truncate if (lines !== undefined) { const p = page ?? 1; const ll = lines; return wrapWithActions( async () => { const deployment = (await this.client.getDeployment(uuid, { includeLogs: true, })) as Deployment; if (deployment.logs) { const result = truncateLogs(deployment.logs, ll, max_chars ?? 50000, p); deployment.logs = result.logs; return { ...deployment, logs_meta: { total_entries: result.total, showing: `${result.showing_start}-${result.showing_end} of ${result.total}`, }, }; } return { ...deployment, logs_meta: undefined }; }, (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid), (dep) => { const total = dep.logs_meta?.total_entries ?? 0; const hasOlder = p * ll < total; const pagination: ResponsePagination = {}; if (hasOlder) pagination.next = { tool: 'deployment', args: { action: 'get', uuid, lines: ll, page: p + 1 }, }; if (p > 1) pagination.prev = { tool: 'deployment', args: { action: 'get', uuid, lines: ll, page: p - 1 }, }; return Object.keys(pagination).length > 0 ? pagination : undefined; }, ); } // Otherwise return essential info without logs return wrapWithActions( () => this.client.getDeployment(uuid), (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid), ); case 'cancel': return wrap(() => this.client.cancelDeployment(uuid)); case 'list_for_app': return wrap(() => this.client.listApplicationDeployments(uuid, { includeLogs: include_logs }), ); } }, ); // ========================================================================= // Private Keys (1 tool - consolidated) // ========================================================================= this.tool( 'private_keys', 'Manage SSH keys: list/get/create/update/delete', { action: z.enum(['list', 'get', 'create', 'update', 'delete']), uuid: z.string().optional(), name: z.string().optional(), description: z.string().optional(), private_key: z.string().optional(), }, async ({ action, uuid, name, description, private_key }) => { switch (action) { case 'list': return wrap(() => this.client.listPrivateKeys()); case 'get': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.getPrivateKey(uuid)); case 'create': if (!private_key) return { content: [{ type: 'text' as const, text: 'Error: private_key required' }] }; return wrap(() => this.client.createPrivateKey({ private_key, name: name || 'unnamed-key', description, }), ); case 'update': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.updatePrivateKey(uuid, { name, description, private_key }), ); case 'delete': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.deletePrivateKey(uuid)); } }, ); // ========================================================================= // GitHub Apps (1 tool - consolidated) // ========================================================================= this.tool( 'github_apps', 'Manage GitHub Apps: list/get/create/update/delete/list_repos/list_branches', { action: z.enum([ 'list', 'get', 'create', 'update', 'delete', 'list_repos', 'list_branches', ]), // GitHub apps use integer id, not uuid id: z.number().optional(), // Repo/branch browsing owner: z.string().optional(), repo: z.string().optional(), // Create/Update fields name: z.string().optional(), organization: z.string().optional(), api_url: z.string().optional(), html_url: z.string().optional(), custom_user: z.string().optional(), custom_port: z.number().optional(), app_id: z.number().optional(), installation_id: z.number().optional(), client_id: z.string().optional(), client_secret: z.string().optional(), webhook_secret: z.string().optional(), private_key_uuid: z.string().optional(), is_system_wide: z.boolean().optional(), }, async (args) => { const { action, id, ...apiData } = args; switch (action) { case 'list': return wrap(async () => { const apps = (await this.client.listGitHubApps({ summary: true, })) as GitHubAppSummary[]; return apps; }); case 'get': if (!id) return { content: [{ type: 'text' as const, text: 'Error: id required' }] }; return wrap(async () => { const apps = (await this.client.listGitHubApps()) as GitHubApp[]; const app = apps.find((a) => a.id === id); if (!app) throw new Error(`GitHub App with id ${id} not found`); return app; }); case 'create': if ( !apiData.name || !apiData.api_url || !apiData.html_url || !apiData.app_id || !apiData.installation_id || !apiData.client_id || !apiData.client_secret || !apiData.private_key_uuid ) { return { content: [ { type: 'text' as const, text: 'Error: name, api_url, html_url, app_id, installation_id, client_id, client_secret, private_key_uuid required', }, ], }; } return wrap(() => this.client.createGitHubApp({ name: apiData.name!, api_url: apiData.api_url!, html_url: apiData.html_url!, app_id: apiData.app_id!, installation_id: apiData.installation_id!, client_id: apiData.client_id!, client_secret: apiData.client_secret!, private_key_uuid: apiData.private_key_uuid!, organization: apiData.organization, custom_user: apiData.custom_user, custom_port: apiData.custom_port, webhook_secret: apiData.webhook_secret, is_system_wide: apiData.is_system_wide, }), ); case 'update': if (!id) return { content: [{ type: 'text' as const, text: 'Error: id required' }] }; return wrap(() => this.client.updateGitHubApp(id, apiData)); case 'delete': if (!id) return { content: [{ type: 'text' as const, text: 'Error: id required' }] }; return wrap(() => this.client.deleteGitHubApp(id)); case 'list_repos': if (!id) return { content: [{ type: 'text' as const, text: 'Error: id required' }] }; return wrap(() => this.client.listGitHubAppRepositories(id)); case 'list_branches': if (!id || !args.owner || !args.repo) return { content: [{ type: 'text' as const, text: 'Error: id, owner, repo required' }], }; return wrap(() => this.client.listGitHubAppBranches(id, args.owner!, args.repo!)); } }, ); // ========================================================================= // Database Backups (1 tool - consolidated) // ========================================================================= this.tool( 'database_backups', 'Manage backups: list_schedules/get_schedule/list_executions/get_execution/create/update/delete/delete_execution', { action: z.enum([ 'list_schedules', 'get_schedule', 'list_executions', 'get_execution', 'create', 'update', 'delete', 'delete_execution', ]), database_uuid: z.string(), backup_uuid: z.string().optional(), execution_uuid: z.string().optional(), // Backup configuration parameters frequency: z.string().optional(), enabled: z.boolean().optional(), save_s3: z.boolean().optional(), s3_storage_uuid: z.string().optional(), databases_to_backup: z.string().optional(), dump_all: z.boolean().optional(), database_backup_retention_days_locally: z.number().optional(), database_backup_retention_days_s3: z.number().optional(), database_backup_retention_amount_locally: z.number().optional(), database_backup_retention_amount_s3: z.number().optional(), }, async (args) => { const { action, database_uuid, backup_uuid, execution_uuid, ...backupData } = args; switch (action) { case 'list_schedules': return wrap(() => this.client.listDatabaseBackups(database_uuid)); case 'get_schedule': if (!backup_uuid) return { content: [{ type: 'text' as const, text: 'Error: backup_uuid required' }] }; return wrap(() => this.client.getDatabaseBackup(database_uuid, backup_uuid)); case 'list_executions': if (!backup_uuid) return { content: [{ type: 'text' as const, text: 'Error: backup_uuid required' }] }; return wrap(() => this.client.listBackupExecutions(database_uuid, backup_uuid)); case 'get_execution': if (!backup_uuid || !execution_uuid) return { content: [ { type: 'text' as const, text: 'Error: backup_uuid, execution_uuid required' }, ], }; return wrap(() => this.client.getBackupExecution(database_uuid, backup_uuid, execution_uuid), ); case 'create': if (!args.frequency) return { content: [{ type: 'text' as const, text: 'Error: frequency required' }] }; return wrap(() => this.client.createDatabaseBackup(database_uuid, { ...backupData, frequency: args.frequency!, }), ); case 'update': if (!backup_uuid) return { content: [{ type: 'text' as const, text: 'Error: backup_uuid required' }] }; return wrap(() => this.client.updateDatabaseBackup(database_uuid, backup_uuid, backupData), ); case 'delete': if (!backup_uuid) return { content: [{ type: 'text' as const, text: 'Error: backup_uuid required' }] }; return wrap(() => this.client.deleteDatabaseBackup(database_uuid, backup_uuid)); case 'delete_execution': if (!backup_uuid || !execution_uuid) return { content: [ { type: 'text' as const, text: 'Error: backup_uuid, execution_uuid required' }, ], }; return wrap(() => this.client.deleteBackupExecution(database_uuid, backup_uuid, execution_uuid), ); } }, ); // ========================================================================= // Teams (1 tool - consolidated) // ========================================================================= this.tool( 'teams', 'Manage teams: list/get/get_members/get_current/get_current_members', { action: z.enum(['list', 'get', 'get_members', 'get_current', 'get_current_members']), id: z.number().optional(), }, async ({ action, id }) => { switch (action) { case 'list': return wrap(() => this.client.listTeams()); case 'get': if (!id) return { content: [{ type: 'text' as const, text: 'Error: id required' }] }; return wrap(() => this.client.getTeam(id)); case 'get_members': if (!id) return { content: [{ type: 'text' as const, text: 'Error: id required' }] }; return wrap(() => this.client.getTeamMembers(id)); case 'get_current': return wrap(() => this.client.getCurrentTeam()); case 'get_current_members': return wrap(() => this.client.getCurrentTeamMembers()); } }, ); // ========================================================================= // Cloud Tokens (1 tool - consolidated) // ========================================================================= this.tool( 'cloud_tokens', 'Manage cloud provider tokens (Hetzner/DigitalOcean): list/get/create/update/delete/validate', { action: z.enum(['list', 'get', 'create', 'update', 'delete', 'validate']), uuid: z.string().optional(), provider: z.enum(['hetzner', 'digitalocean']).optional(), token: z.string().optional(), name: z.string().optional(), }, async ({ action, uuid, provider, token, name }) => { switch (action) { case 'list': return wrap(() => this.client.listCloudTokens()); case 'get': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.getCloudToken(uuid)); case 'create': if (!provider || !token || !name) return { content: [{ type: 'text' as const, text: 'Error: provider, token, name required' }], }; return wrap(() => this.client.createCloudToken({ provider, token, name })); case 'update': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.updateCloudToken(uuid, { name })); case 'delete': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.deleteCloudToken(uuid)); case 'validate': if (!uuid) return { content: [{ type: 'text' as const, text: 'Error: uuid required' }] }; return wrap(() => this.client.validateCloudToken(uuid)); } }, ); // ========================================================================= // Storages (1 tool - consolidated for app/db/service) // ========================================================================= this.tool( 'storages', 'Manage persistent/file storages for app, database, or service: list/create/update/delete', { resource: z.enum(['application', 'database', 'service']), action: z.enum(['list', 'create', 'update', 'delete']), uuid: z.string(), storage_uuid: z.string().optional(), type: z.enum(['persistent', 'file']).optional(), mount_path: z.string().optional(), name: z.string().optional(), host_path: z.string().optional(), content: z.string().optional(), is_directory: z.boolean().optional(), fs_path: z.string().optional(), is_preview_suffix_enabled: z.boolean().optional(), }, async (args) => { const { resource, action, uuid, storage_uuid } = args; if (action === 'create' && (!args.type || !args.mount_path)) return { content: [{ type: 'text' as const, text: 'Error: type, mount_path required' }] }; if (action === 'update' && (!args.type || !storage_uuid)) return { content: [{ type: 'text' as const, text: 'Error: type, storage_uuid required' }], }; if (action === 'delete' && !storage_uuid) return { content: [{ type: 'text' as const, text: 'Error: storage_uuid required' }] }; const methods: Record Promise>> = { application: { list: () => this.client.listApplicationStorages(uuid), create: () => this.client.createApplicationStorage(uuid, { type: args.type!, mount_path: args.mount_path!, name: args.name, host_path: args.host_path, content: args.content, is_directory: args.is_directory, fs_path: args.fs_path, is_preview_suffix_enabled: args.is_preview_suffix_enabled, }), update: () => this.client.updateApplicationStorage(uuid, { uuid: storage_uuid!, type: args.type!, mount_path: args.mount_path, name: args.name, host_path: args.host_path, content: args.content, is_directory: args.is_directory, is_preview_suffix_enabled: args.is_preview_suffix_enabled, }), delete: () => this.client.deleteApplicationStorage(uuid, storage_uuid!), }, database: { list: () => this.client.listDatabaseStorages(uuid), create: () => this.client.createDatabaseStorage(uuid, { type: args.type!, mount_path: args.mount_path!, name: args.name, host_path: args.host_path, content: args.content, is_directory: args.is_directory, fs_path: args.fs_path, is_preview_suffix_enabled: args.is_preview_suffix_enabled, }), update: () => this.client.updateDatabaseStorage(uuid, { uuid: storage_uuid!, type: args.type!, mount_path: args.mount_path, name: args.name, host_path: args.host_path, content: args.content, is_directory: args.is_directory, is_preview_suffix_enabled: args.is_preview_suffix_enabled, }), delete: () => this.client.deleteDatabaseStorage(uuid, storage_uuid!), }, service: { list: () => this.client.listServiceStorages(uuid), create: () => this.client.createServiceStorage(uuid, { type: args.type!, mount_path: args.mount_path!, name: args.name, host_path: args.host_path, content: args.content, is_directory: args.is_directory, fs_path: args.fs_path, is_preview_suffix_enabled: args.is_preview_suffix_enabled, }), update: () => this.client.updateServiceStorage(uuid, { uuid: storage_uuid!, type: args.type!, mount_path: args.mount_path, name: args.name, host_path: args.host_path, content: args.content, is_directory: args.is_directory, is_preview_suffix_enabled: args.is_preview_suffix_enabled, }), delete: () => this.client.deleteServiceStorage(uuid, storage_uuid!), }, }; return wrap(() => methods[resource][action]()); }, ); // ========================================================================= // Scheduled Tasks (1 tool - consolidated for app/service) // ========================================================================= this.tool( 'scheduled_tasks', 'Manage scheduled tasks for app or service: list/create/update/delete/list_executions', { resource: z.enum(['application', 'service']), action: z.enum(['list', 'create', 'update', 'delete', 'list_executions']), uuid: z.string(), task_uuid: z.string().optional(), name: z.string().optional(), command: z.string().optional(), frequency: z.string().optional(), container: z.string().optional(), timeout: z.number().optional(), enabled: z.boolean().optional(), }, async (args) => { const { resource, action, uuid, task_uuid } = args; const isApp = resource === 'application'; switch (action) { case 'list': return wrap(() => isApp ? this.client.listApplicationScheduledTasks(uuid) : this.client.listServiceScheduledTasks(uuid), ); case 'create': if (!args.name || !args.command || !args.frequency) return { content: [ { type: 'text' as const, text: 'Error: name, command, frequency required' }, ], }; return wrap(() => { const data = { name: args.name!, command: args.command!, frequency: args.frequency!, container: args.container, timeout: args.timeout, enabled: args.enabled, }; return isApp ? this.client.createApplicationScheduledTask(uuid, data) : this.client.createServiceScheduledTask(uuid, data); }); case 'update': if (!task_uuid) return { content: [{ type: 'text' as const, text: 'Error: task_uuid required' }] }; return wrap(() => { const data = { name: args.name, command: args.command, frequency: args.frequency, container: args.container, timeout: args.timeout, enabled: args.enabled, }; return isApp ? this.client.updateApplicationScheduledTask(uuid, task_uuid, data) : this.client.updateServiceScheduledTask(uuid, task_uuid, data); }); case 'delete': if (!task_uuid) return { content: [{ type: 'text' as const, text: 'Error: task_uuid required' }] }; return wrap(() => isApp ? this.client.deleteApplicationScheduledTask(uuid, task_uuid) : this.client.deleteServiceScheduledTask(uuid, task_uuid), ); case 'list_executions': if (!task_uuid) return { content: [{ type: 'text' as const, text: 'Error: task_uuid required' }] }; return wrap(() => isApp ? this.client.listApplicationScheduledTaskExecutions(uuid, task_uuid) : this.client.listServiceScheduledTaskExecutions(uuid, task_uuid), ); } }, ); // ========================================================================= // Hetzner Cloud (1 tool - consolidated) // ========================================================================= this.tool( 'hetzner', 'Hetzner cloud: list_locations/list_server_types/list_images/list_ssh_keys/create_server', { action: z.enum([ 'list_locations', 'list_server_types', 'list_images', 'list_ssh_keys', 'create_server', ]), cloud_provider_token_uuid: z.string().optional(), location: z.string().optional(), server_type: z.string().optional(), image: z.number().optional(), name: z.string().optional(), private_key_uuid: z.string().optional(), enable_ipv4: z.boolean().optional(), enable_ipv6: z.boolean().optional(), hetzner_ssh_key_ids: z.array(z.number()).optional(), cloud_init_script: z.string().optional(), instant_validate: z.boolean().optional(), }, async (args) => { const { action, cloud_provider_token_uuid: tokenUuid } = args; switch (action) { case 'list_locations': if (!tokenUuid) return { content: [ { type: 'text' as const, text: 'Error: cloud_provider_token_uuid required' }, ], }; return wrap(() => this.client.listHetznerLocations(tokenUuid)); case 'list_server_types': if (!tokenUuid) return { content: [ { type: 'text' as const, text: 'Error: cloud_provider_token_uuid required' }, ], }; return wrap(() => this.client.listHetznerServerTypes(tokenUuid)); case 'list_images': if (!tokenUuid) return { content: [ { type: 'text' as const, text: 'Error: cloud_provider_token_uuid required' }, ], }; return wrap(() => this.client.listHetznerImages(tokenUuid)); case 'list_ssh_keys': if (!tokenUuid) return { content: [ { type: 'text' as const, text: 'Error: cloud_provider_token_uuid required' }, ], }; return wrap(() => this.client.listHetznerSSHKeys(tokenUuid)); case 'create_server': if (!args.location || !args.server_type || !args.image || !args.private_key_uuid) return { content: [ { type: 'text' as const, text: 'Error: location, server_type, image, private_key_uuid required', }, ], }; return wrap(() => this.client.createHetznerServer({ cloud_provider_token_uuid: tokenUuid, location: args.location!, server_type: args.server_type!, image: args.image!, name: args.name, private_key_uuid: args.private_key_uuid!, enable_ipv4: args.enable_ipv4, enable_ipv6: args.enable_ipv6, hetzner_ssh_key_ids: args.hetzner_ssh_key_ids, cloud_init_script: args.cloud_init_script, instant_validate: args.instant_validate, }), ); } }, ); // ========================================================================= // System (1 tool - health/list_resources/api_control consolidated) // ========================================================================= this.tool( 'system', 'System operations: health/list_resources/enable_api/disable_api. `list_resources` defaults to an essential projection (uuid/name/type/status) to keep token budgets sane on instances with many resources; pass `include_full: true` for the raw Coolify payload. When `include_full: true`, webhook HMAC secrets and basic-auth password are masked unless `reveal: true` is also set (matches the `env_vars` `reveal` ergonomics).', { action: z.enum(['health', 'list_resources', 'enable_api', 'disable_api']), include_full: z.boolean().optional(), reveal: z.boolean().optional(), }, async ({ action, include_full, reveal }) => { switch (action) { case 'health': return wrap(() => this.client.getHealth()); case 'list_resources': return wrap(() => this.client.listResources({ include_full, reveal })); case 'enable_api': return wrap(() => this.client.enableApi()); case 'disable_api': return wrap(() => this.client.disableApi()); } }, ); // ========================================================================= // Documentation Search (1 tool) // ========================================================================= this.tool( 'search_docs', 'Search Coolify documentation for how-to guides, configuration, troubleshooting', { query: z.string().describe('Search query'), limit: z.number().optional().describe('Max results (default 5)'), }, async ({ query, limit }) => wrap(async () => { const results = await this.docsSearch.search(query, limit ?? 5); if (results.length === 0) { return { results: [], hint: 'No matches. Try broader or different keywords.' }; } return { results }; }), ); // ========================================================================= // Batch Operations (4 tools) // ========================================================================= this.tool( 'restart_project_apps', 'Restart all apps in project', { project_uuid: z.string() }, async ({ project_uuid }) => wrap(() => this.client.restartProjectApps(project_uuid)), ); this.tool( 'bulk_env_update', 'Update env var across multiple apps', { app_uuids: z.array(z.string()), key: z.string(), value: z.string(), is_buildtime: z.boolean().optional(), is_runtime: z.boolean().optional(), }, async ({ app_uuids, key, value, is_buildtime, is_runtime }) => wrap(() => this.client.bulkEnvUpdate(app_uuids, key, value, is_buildtime, is_runtime)), ); this.tool( 'stop_all_apps', 'EMERGENCY: Stop all running apps', { confirm: z.literal(true) }, async ({ confirm }) => { if (!confirm) return { content: [{ type: 'text' as const, text: 'Error: confirm=true required' }] }; return wrap(() => this.client.stopAllApps()); }, ); this.tool( 'redeploy_project', 'Redeploy all apps in project', { project_uuid: z.string(), force: z.boolean().optional() }, async ({ project_uuid, force }) => wrap(() => this.client.redeployProjectApps(project_uuid, force ?? true)), ); } }