feat(mcp): apps.create image/composeRaw pathways + apps.volumes.list/wipe

Third-party apps (Twenty, Directus, Cal.com, Plane…) should never need
a Gitea repo. This adds two new apps.create pathways:

  image: "twentyhq/twenty:1.23.0"   → Coolify /applications/dockerimage
  composeRaw: "services:\n..."       → Coolify /applications/dockercompose

No repo is created, no git clone, no PAT embedding. Agents can fetch
the official docker-compose.yml and pass it inline, or just give an
image name. Pathway 1 (repo) is unchanged.

Also adds volume management tools so agents can self-recover from the
most common compose failure (stale DB volume blocking fresh migrations):

  apps.volumes.list { uuid }                        → list volumes + sizes
  apps.volumes.wipe { uuid, volume, confirm }       → stop containers,
                                                       rm volume, done

Both volume tools go through the same vibn-logs SSH channel. The wipe
tool requires confirm == volume name to prevent accidents and verifies
the volume belongs to the target app (uuid in name).

lib/coolify.ts: createDockerImageApp + createDockerComposeApp helpers,
  dockerimage added to CoolifyBuildPack union.
app/api/mcp/route.ts: resolveFqdn + applyEnvsAndDeploy extracted as
  shared helpers; toolAppsCreate now dispatches on image/composeRaw/repo.
  toolAppsVolumesList + toolAppsVolumesWipe added.
  sq() moved to module scope (shared by exec + volumes tools).
  Version bumped to 2.3.0.

Made-with: Cursor
This commit is contained in:
2026-04-23 16:21:28 -07:00
parent 8c83f8c490
commit 6d71c63053
2 changed files with 342 additions and 54 deletions

View File

@@ -343,7 +343,7 @@ export async function restartDatabase(uuid: string): Promise<void> {
// Applications
// ──────────────────────────────────────────────────
export type CoolifyBuildPack = 'nixpacks' | 'static' | 'dockerfile' | 'dockercompose';
export type CoolifyBuildPack = 'nixpacks' | 'static' | 'dockerfile' | 'dockercompose' | 'dockerimage';
export interface CreatePrivateDeployKeyAppOpts {
projectUuid: string;
@@ -455,6 +455,92 @@ export async function createPublicApp(opts: CreatePublicAppOpts): Promise<{ uuid
});
}
// ──────────────────────────────────────────────────
// Repo-free app creation (Docker image / raw compose)
// ──────────────────────────────────────────────────
export interface CreateDockerImageAppOpts {
projectUuid: string;
image: string; // e.g. "twentyhq/twenty:1.23.0" or "nginx:alpine"
name?: string;
portsExposes?: string; // default "80"
domains?: string;
description?: string;
serverUuid?: string;
environmentName?: string;
destinationUuid?: string;
isForceHttpsEnabled?: boolean;
instantDeploy?: boolean;
}
export async function createDockerImageApp(
opts: CreateDockerImageAppOpts,
): Promise<{ uuid: string }> {
const body = stripUndefined({
project_uuid: opts.projectUuid,
server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID,
environment_name: opts.environmentName ?? 'production',
destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID,
docker_registry_image_name: opts.image,
name: opts.name,
description: opts.description,
ports_exposes: opts.portsExposes ?? '80',
domains: opts.domains,
is_force_https_enabled: opts.isForceHttpsEnabled ?? true,
instant_deploy: opts.instantDeploy ?? false,
});
return coolifyFetch('/applications/dockerimage', {
method: 'POST',
body: JSON.stringify(body),
});
}
export interface CreateDockerComposeAppOpts {
projectUuid: string;
composeRaw: string; // raw docker-compose YAML as a string
name?: string;
description?: string;
serverUuid?: string;
environmentName?: string;
destinationUuid?: string;
isForceHttpsEnabled?: boolean;
instantDeploy?: boolean;
/**
* Map compose service(s) to public domain(s) after creation.
* Array of { service, domain } pairs. The first entry becomes the
* primary public URL.
*/
composeDomains?: Array<{ service: string; domain: string }>;
}
export async function createDockerComposeApp(
opts: CreateDockerComposeAppOpts,
): Promise<{ uuid: string }> {
const body = stripUndefined({
project_uuid: opts.projectUuid,
server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID,
environment_name: opts.environmentName ?? 'production',
destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID,
build_pack: 'dockercompose',
name: opts.name,
description: opts.description,
docker_compose_raw: opts.composeRaw,
is_force_https_enabled: opts.isForceHttpsEnabled ?? true,
instant_deploy: opts.instantDeploy ?? false,
// domains for compose are set via docker_compose_domains after creation
docker_compose_domains: opts.composeDomains
? JSON.stringify(opts.composeDomains.map(({ service, domain }) => ({
name: service,
domain: `https://${domain.replace(/^https?:\/\//, '')}`,
})))
: undefined,
});
return coolifyFetch('/applications/dockercompose', {
method: 'POST',
body: JSON.stringify(body),
});
}
export async function updateApplication(
uuid: string,
patch: Record<string, unknown>