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