diff --git a/app/api/mcp/route.ts b/app/api/mcp/route.ts index 52a45a30..7722d46e 100644 --- a/app/api/mcp/route.ts +++ b/app/api/mcp/route.ts @@ -29,7 +29,12 @@ import { VIBN_GCS_LOCATION } from '@/lib/gcp/storage'; import { getApplicationRuntimeLogs } from '@/lib/coolify-logs'; import { execInCoolifyApp } from '@/lib/coolify-exec'; import { isCoolifySshConfigured, runOnCoolifyHost } from '@/lib/coolify-ssh'; -import { composeUp, composePs, type ResourceKind } from '@/lib/coolify-compose'; +import { + composeUp, + composePs, + attachToCoolifyProxyNetwork, + type ResourceKind, +} from '@/lib/coolify-compose'; import { listContainersForApp } from '@/lib/coolify-containers'; import { deployApplication, @@ -86,7 +91,7 @@ const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com'; export async function GET() { return NextResponse.json({ name: 'vibn-mcp', - version: '2.4.2', + version: '2.4.3', authentication: { scheme: 'Bearer', tokenPrefix: 'vibn_sk_', @@ -1262,6 +1267,12 @@ async function ensureServiceUp(uuid: string): Promise<{ { timeoutMs: 8_000 }, ); if (probe.stdout.trim().length > 0) { + // Coolify started the stack. Even on this happy path we still + // need to ensure the proxy-network attachment ran, since + // Coolify only attaches at the end of its full deploy + // pipeline (which can be skipped if a sidecar fails to come + // up). Idempotent — already-attached containers are no-ops. + await attachToCoolifyProxyNetwork(uuid).catch(() => { /* swallow */ }); return { started: true, startMethod: 'coolify-queue', diag: '' }; } } catch (e) { @@ -1303,7 +1314,10 @@ async function ensureServiceUp(uuid: string): Promise<{ // Something IS running — partial success. Surface the diag so // agents see WHY compose returned non-zero (usually a sidecar // depends_on timeout) but report started=true so happy-path - // workflows don't panic. + // workflows don't panic. composeUp already attached the proxy + // network, but call once more to cover any container that came + // up after the initial attach pass. + await attachToCoolifyProxyNetwork(uuid).catch(() => { /* swallow */ }); return { started: true, startMethod: 'compose-up', diag: composeDiag }; } } catch { /* fall through */ } diff --git a/lib/coolify-compose.ts b/lib/coolify-compose.ts index ced12c97..a4b9413f 100644 --- a/lib/coolify-compose.ts +++ b/lib/coolify-compose.ts @@ -82,13 +82,52 @@ async function composeRun( * Idempotent — Compose already-running containers are no-op'd. * Returns the raw SSH result so callers can surface diagnostics on * failure (most common: image-pull errors, port conflicts). + * + * After compose succeeds we also attach every stack container to the + * `coolify` proxy network. Coolify's UI-driven deploy does this as a + * post-step so Traefik can route public traffic to the container, but + * the rendered compose file only declares the service-private network. + * If we skip this step the stack runs fine on its own bridge but + * `crm.mark.vibnai.com` returns "no available server" from Traefik. */ export async function composeUp( kind: ResourceKind, uuid: string, opts: { timeoutMs?: number } = {}, ): Promise { - return composeRun(kind, uuid, ['up', '-d', '--remove-orphans'], opts); + const r = await composeRun(kind, uuid, ['up', '-d', '--remove-orphans'], opts); + // Best-effort: attach to the proxy network even if compose returned + // non-zero (sidecar `depends_on` timeouts still leave primary + // containers running, and we want them reachable). + await attachToCoolifyProxyNetwork(uuid).catch(() => { /* swallow */ }); + return r; +} + +/** + * Attach every container belonging to this Coolify resource to the + * `coolify` proxy network. Idempotent — `network connect` errors when + * the container is already attached, which we ignore. + */ +export async function attachToCoolifyProxyNetwork( + uuid: string, +): Promise { + // List containers on the resource's project network. Coolify names + // the bridge network after the resource UUID, so all stack members + // are reachable through it. + const ls = await runOnCoolifyHost( + `docker ps --filter network=${uuid} --format '{{.Names}}'`, + { timeoutMs: 10_000 }, + ); + const names = ls.stdout + .split('\n') + .map(s => s.trim()) + .filter(Boolean); + if (names.length === 0) return; + // Attach each one. `|| true` so already-connected returns 0. + const attaches = names.map(n => + `docker network connect coolify ${sq(n)} 2>/dev/null || true`, + ).join(' && '); + await runOnCoolifyHost(attaches, { timeoutMs: 30_000 }); } /** `docker compose down` — stops + removes containers; volumes preserved. */