@masonator/coolify-mcp
Version:
MCP server implementation for Coolify
906 lines (905 loc) • 48 kB
JavaScript
/**
* 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)));
}
}