UNPKG

@masonator/coolify-mcp

Version:

MCP server implementation for Coolify

906 lines (905 loc) 48 kB
/** * Coolify MCP Server * Consolidated tools for efficient token usage */ import { createRequire } from 'module'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { CoolifyClient, } from './coolify-client.js'; const _require = createRequire(import.meta.url); export const VERSION = _require('../../package.json').version; /** Wrap handler with error handling */ function wrap(fn) { return fn() .then((result) => ({ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], })) .catch((error) => ({ content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], })); } const TRUNCATION_PREFIX = '...[truncated]...\n'; /** * Truncate logs by line count and character count. * Exported for testing. */ export function truncateLogs(logs, lineLimit = 200, charLimit = 50000) { // First: limit by lines const logLines = logs.split('\n'); const limitedLines = logLines.slice(-lineLimit); let truncatedLogs = limitedLines.join('\n'); // Second: limit by characters (safety net for huge lines) if (truncatedLogs.length > charLimit) { // Account for prefix length to stay within limit const prefixLen = TRUNCATION_PREFIX.length; truncatedLogs = TRUNCATION_PREFIX + truncatedLogs.slice(-(charLimit - prefixLen)); } return truncatedLogs; } // ============================================================================= // Action Generators for HATEOAS-style responses // ============================================================================= /** Generate contextual actions for an application based on its status */ export function getApplicationActions(uuid, status) { const actions = [ { 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, status, appUuid) { const actions = []; 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, page, perPage, count) { 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, getActions, getPaginationFn) { return fn() .then((result) => { const actions = getActions?.(result) ?? []; const pagination = getPaginationFn?.(result); const response = { data: result }; if (actions.length > 0) response._actions = actions; if (pagination) response._pagination = pagination; return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] }; }) .catch((error) => ({ content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], })); } export class CoolifyMcpServer extends McpServer { constructor(config) { super({ name: 'coolify', version: VERSION }); this.client = new CoolifyClient(config); this.registerTools(); } async connect(transport) { await super.connect(transport); } registerTools() { // ========================================================================= // 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', 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) => r.status === 'fulfilled' ? r.value : []; const [servers, projects, applications, databases, services] = [ extract(results[0]), extract(results[1]), extract(results[2]), extract(results[3]), extract(results[4]), ]; 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', text: 'Error: uuid required' }] }; return wrap(() => this.client.getProject(uuid)); case 'create': if (!name) return { content: [{ type: 'text', text: 'Error: name required' }] }; return wrap(() => this.client.createProject({ name, description })); case 'update': if (!uuid) return { content: [{ type: 'text', text: 'Error: uuid required' }] }; return wrap(() => this.client.updateProject(uuid, { name, description })); case 'delete': if (!uuid) return { content: [{ type: 'text', 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', 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', text: 'Error: name required' }] }; return wrap(() => this.client.createProjectEnvironment(project_uuid, { name, description })); case 'delete': if (!name) return { content: [{ type: 'text', 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.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', { action: z.enum([ 'create_public', 'create_github', 'create_key', 'create_dockerimage', 'update', 'delete', ]), 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(), 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(), // 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(), // Delete fields delete_volumes: z.boolean().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', 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, git_repository: args.git_repository, git_branch: args.git_branch, build_pack: args.build_pack, ports_exposes: args.ports_exposes, environment_name: args.environment_name, environment_uuid: args.environment_uuid, name: args.name, description: args.description, fqdn: args.fqdn, })); case 'create_github': if (!args.project_uuid || !args.server_uuid || !args.github_app_uuid || !args.git_repository || !args.git_branch) { return { content: [ { type: 'text', 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, git_repository: args.git_repository, git_branch: args.git_branch, build_pack: args.build_pack, ports_exposes: args.ports_exposes, environment_name: args.environment_name, environment_uuid: args.environment_uuid, name: args.name, description: args.description, fqdn: args.fqdn, })); case 'create_key': if (!args.project_uuid || !args.server_uuid || !args.private_key_uuid || !args.git_repository || !args.git_branch) { return { content: [ { type: 'text', 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, git_repository: args.git_repository, git_branch: args.git_branch, build_pack: args.build_pack, ports_exposes: args.ports_exposes, environment_name: args.environment_name, environment_uuid: args.environment_uuid, name: args.name, description: args.description, fqdn: args.fqdn, })); case 'create_dockerimage': if (!args.project_uuid || !args.server_uuid || !args.docker_registry_image_name || !args.ports_exposes) { return { content: [ { type: 'text', 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, 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, })); case 'update': { if (!uuid) return { content: [{ type: 'text', 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', text: 'Error: uuid required' }] }; return wrap(() => this.client.deleteApplication(uuid, { deleteVolumes: delete_volumes })); } }); 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', 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', text: 'Error: type, server_uuid, project_uuid required' }, ], }; } const dbMethods = { 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', 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', 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', 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 = { 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 = () => { const actions = []; 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 or service', { resource: z.enum(['application', 'service']), action: z.enum(['list', 'create', 'update', 'delete']), uuid: z.string(), key: z.string().optional(), value: z.string().optional(), env_uuid: z.string().optional(), }, async ({ resource, action, uuid, key, value, env_uuid }) => { if (resource === 'application') { switch (action) { case 'list': return wrap(() => this.client.listApplicationEnvVars(uuid, { summary: true })); case 'create': if (!key || !value) return { content: [{ type: 'text', text: 'Error: key, value required' }] }; // Note: is_build_time is not passed - Coolify API rejects it for create action return wrap(() => this.client.createApplicationEnvVar(uuid, { key, value })); case 'update': if (!key || !value) return { content: [{ type: 'text', text: 'Error: key, value required' }] }; return wrap(() => this.client.updateApplicationEnvVar(uuid, { key, value })); case 'delete': if (!env_uuid) return { content: [{ type: 'text', text: 'Error: env_uuid required' }] }; return wrap(() => this.client.deleteApplicationEnvVar(uuid, env_uuid)); } } else { switch (action) { case 'list': return wrap(() => this.client.listServiceEnvVars(uuid)); case 'create': if (!key || !value) return { content: [{ type: 'text', text: 'Error: key, value required' }] }; return wrap(() => this.client.createServiceEnvVar(uuid, { key, value })); case 'update': return { content: [ { type: 'text', text: 'Error: service env update not supported' }, ], }; case 'delete': if (!env_uuid) return { content: [{ type: 'text', text: 'Error: env_uuid required' }] }; return wrap(() => this.client.deleteServiceEnvVar(uuid, env_uuid)); } } }); // ========================================================================= // 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.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, use lines param to include)', { action: z.enum(['get', 'cancel', 'list_for_app']), uuid: z.string(), lines: z.number().optional(), // Include logs truncated to last N lines (omit for no logs) max_chars: z.number().optional(), // Limit log output to last N chars (default: 50000) }, async ({ action, uuid, lines, max_chars }) => { switch (action) { case 'get': // If lines param specified, include logs and truncate if (lines !== undefined) { return wrapWithActions(async () => { const deployment = (await this.client.getDeployment(uuid, { includeLogs: true, })); if (deployment.logs) { deployment.logs = truncateLogs(deployment.logs, lines, max_chars ?? 50000); } return deployment; }, (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid)); } // 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)); } }); // ========================================================================= // 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', text: 'Error: uuid required' }] }; return wrap(() => this.client.getPrivateKey(uuid)); case 'create': if (!private_key) return { content: [{ type: 'text', 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', text: 'Error: uuid required' }] }; return wrap(() => this.client.updatePrivateKey(uuid, { name, description, private_key })); case 'delete': if (!uuid) return { content: [{ type: 'text', 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', { action: z.enum(['list', 'get', 'create', 'update', 'delete']), // GitHub apps use integer id, not uuid id: z.number().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, })); return apps; }); case 'get': if (!id) return { content: [{ type: 'text', text: 'Error: id required' }] }; return wrap(async () => { const apps = (await this.client.listGitHubApps()); 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', 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', text: 'Error: id required' }] }; return wrap(() => this.client.updateGitHubApp(id, apiData)); case 'delete': if (!id) return { content: [{ type: 'text', text: 'Error: id required' }] }; return wrap(() => this.client.deleteGitHubApp(id)); } }); // ========================================================================= // Database Backups (1 tool - consolidated) // ========================================================================= this.tool('database_backups', 'Manage backups: list_schedules/get_schedule/list_executions/get_execution/create/update/delete', { action: z.enum([ 'list_schedules', 'get_schedule', 'list_executions', 'get_execution', 'create', 'update', 'delete', ]), 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', text: 'Error: backup_uuid required' }] }; return wrap(() => this.client.getDatabaseBackup(database_uuid, backup_uuid)); case 'list_executions': if (!backup_uuid) return { content: [{ type: 'text', 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', 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', text: 'Error: frequency required' }] }; return wrap(() => this.client.createDatabaseBackup(database_uuid, { ...backupData, frequency: args.frequency, })); case 'update': if (!backup_uuid) return { content: [{ type: 'text', text: 'Error: backup_uuid required' }] }; return wrap(() => this.client.updateDatabaseBackup(database_uuid, backup_uuid, backupData)); case 'delete': if (!backup_uuid) return { content: [{ type: 'text', text: 'Error: backup_uuid required' }] }; return wrap(() => this.client.deleteDatabaseBackup(database_uuid, backup_uuid)); } }); // ========================================================================= // 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_build_time: z.boolean().optional(), }, async ({ app_uuids, key, value, is_build_time }) => wrap(() => this.client.bulkEnvUpdate(app_uuids, key, value, is_build_time))); this.tool('stop_all_apps', 'EMERGENCY: Stop all running apps', { confirm: z.literal(true) }, async ({ confirm }) => { if (!confirm) return { content: [{ type: 'text', 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))); } }