feat(mcp): proper Coolify Services support for composeRaw pathway

Coolify's /applications/dockercompose creates a Service (not Application)
with its own API surface. Wire it up correctly:

lib/coolify.ts
  - createDockerComposeApp returns { uuid, resourceType: 'service' }
  - Add startService, stopService, getService, listAllServices helpers
  - Add listServiceEnvs, upsertServiceEnv, bulkUpsertServiceEnvs for
    the /services/{uuid}/envs endpoint

app/api/mcp/route.ts
  - toolAppsList: includes Services (compose stacks) alongside Applications
  - toolAppsDeploy: falls back to /services/{uuid}/start for service UUIDs
  - toolAppsCreate composeRaw path: uses upsertServiceEnv + startService
    instead of Application deploy; notes that domain routing must be
    configured post-startup via SERVER_URL env

Made-with: Cursor
This commit is contained in:
2026-04-23 17:02:21 -07:00
parent f27e572fdb
commit 040f0c6256
2 changed files with 164 additions and 47 deletions

View File

@@ -513,13 +513,22 @@ export interface CreateDockerComposeAppOpts {
composeDomains?: Array<{ service: string; domain: string }>;
}
/**
* Create an inline Docker Compose stack.
*
* Coolify's /applications/dockercompose endpoint creates a **Service**
* resource (not an Application). Services have their own API:
* /services/{uuid} GET/PATCH
* /services/{uuid}/envs GET/POST
* /services/{uuid}/start POST
* /services/{uuid}/stop POST
* /services/{uuid} DELETE
* The UUID returned here is a service UUID; callers should treat it as
* such and use the service helpers below for lifecycle operations.
*/
export async function createDockerComposeApp(
opts: CreateDockerComposeAppOpts,
): Promise<{ uuid: string }> {
// NOTE: the /applications/dockercompose endpoint has a restricted
// field allowlist vs the PATCH endpoint. It rejects `build_pack`
// (hardcoded to "dockercompose"), `is_force_https_enabled`, and
// `docker_compose_domains` — those must be set via PATCH afterward.
): Promise<{ uuid: string; resourceType: 'service' }> {
const body = stripUndefined({
project_uuid: opts.projectUuid,
server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID,
@@ -527,7 +536,6 @@ export async function createDockerComposeApp(
destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID,
name: opts.name,
description: opts.description,
// Coolify requires docker_compose_raw to be base64-encoded
docker_compose_raw: Buffer.from(opts.composeRaw, 'utf8').toString('base64'),
instant_deploy: opts.instantDeploy ?? false,
});
@@ -535,26 +543,51 @@ export async function createDockerComposeApp(
method: 'POST',
body: JSON.stringify(body),
}) as { uuid: string };
return { uuid: created.uuid, resourceType: 'service' };
}
// Coolify creates apps asynchronously. Wait briefly before PATCHing
// so the record is committed to the DB.
if (opts.composeDomains && opts.composeDomains.length > 0) {
await new Promise(r => setTimeout(r, 2500));
await coolifyFetch(`/applications/${created.uuid}`, {
method: 'PATCH',
body: JSON.stringify(stripUndefined({
is_force_https_enabled: opts.isForceHttpsEnabled ?? true,
docker_compose_domains: JSON.stringify(
opts.composeDomains.map(({ service, domain }) => ({
name: service,
domain: `https://${domain.replace(/^https?:\/\//, '')}`,
})),
),
})),
});
}
// ── Coolify Services API ─────────────────────────────────────────────
return created;
export async function startService(uuid: string): Promise<void> {
await coolifyFetch(`/services/${uuid}/start`, { method: 'POST' });
}
export async function stopService(uuid: string): Promise<void> {
await coolifyFetch(`/services/${uuid}/stop`, { method: 'POST' });
}
export async function getService(uuid: string): Promise<Record<string, unknown>> {
return coolifyFetch(`/services/${uuid}`);
}
export interface ServiceEnvVar { key: string; value: string; is_preview?: boolean; is_literal?: boolean; }
export async function listServiceEnvs(uuid: string): Promise<ServiceEnvVar[]> {
return coolifyFetch(`/services/${uuid}/envs`);
}
export async function bulkUpsertServiceEnvs(
uuid: string,
envs: ServiceEnvVar[],
): Promise<void> {
await coolifyFetch(`/services/${uuid}/envs/bulk`, {
method: 'PUT',
body: JSON.stringify({ data: envs }),
});
}
export async function upsertServiceEnv(
uuid: string,
env: ServiceEnvVar,
): Promise<void> {
await coolifyFetch(`/services/${uuid}/envs`, {
method: 'POST',
body: JSON.stringify(env),
});
}
export async function listAllServices(): Promise<Array<Record<string, unknown>>> {
return coolifyFetch('/services') as Promise<Array<Record<string, unknown>>>;
}
export async function updateApplication(