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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user