diff --git a/vibn-frontend/app/api/mcp/route.ts b/vibn-frontend/app/api/mcp/route.ts index cd0e7584..76ad141b 100644 --- a/vibn-frontend/app/api/mcp/route.ts +++ b/vibn-frontend/app/api/mcp/route.ts @@ -97,6 +97,7 @@ import { deleteApplicationEnv, // Phase 4 ── create/update/delete + domains + databases + services createPublicApp, + createPrivateDeployKeyApp, createDockerImageApp, createDockerComposeApp, startService, @@ -2100,30 +2101,61 @@ async function toolAppsCreate( } } - const created = await createPublicApp({ - ...commonOpts, - gitRepository: giteaHttpsUrl( - repoOrg, - repoName, - botCreds.username, - botCreds.token, - ), - gitBranch: String(params.branch ?? repo.default_branch ?? "main"), - portsExposes: String(params.ports ?? "3000"), - buildPack: (params.buildPack as any) ?? "nixpacks", - name: appName, - domains: toDomainsString([fqdn]), - isAutoDeployEnabled: true, - dockerComposeLocation: params.dockerComposeLocation - ? String(params.dockerComposeLocation) - : undefined, - dockerfileLocation: params.dockerfileLocation - ? String(params.dockerfileLocation) - : undefined, - baseDirectory: params.baseDirectory - ? String(params.baseDirectory) - : undefined, - }); + // Fall back to creating a private app with a deploy key if available + const privateKeyUuid = ws.coolify_private_key_uuid; + + let created; + + if (privateKeyUuid) { + created = await createPrivateDeployKeyApp({ + ...commonOpts, + privateKeyUuid, + gitRepository: `git@git.vibnai.com:${repoOrg}/${repoName}.git`, + gitBranch: String(params.branch ?? repo.default_branch ?? "main"), + portsExposes: String(params.ports ?? "3000"), + buildPack: (params.buildPack as any) ?? "nixpacks", + name: appName, + domains: toDomainsString([fqdn]), + isAutoDeployEnabled: true, + dockerComposeLocation: params.dockerComposeLocation + ? String(params.dockerComposeLocation) + : undefined, + dockerfileLocation: params.dockerfileLocation + ? String(params.dockerfileLocation) + : undefined, + baseDirectory: params.baseDirectory + ? String(params.baseDirectory) + : undefined, + }); + } else { + // If no deploy key is configured for this workspace, fall back to public app + // with embedded basic-auth credentials (often fails on newer Coolify versions due to strict cloning) + created = await createPublicApp({ + ...commonOpts, + gitRepository: giteaHttpsUrl( + repoOrg, + repoName, + botCreds.username, + botCreds.token, + ), + gitBranch: String(params.branch ?? repo.default_branch ?? "main"), + portsExposes: String(params.ports ?? "3000"), + buildPack: (params.buildPack as any) ?? "nixpacks", + name: appName, + domains: toDomainsString([fqdn]), + isAutoDeployEnabled: true, + dockerComposeLocation: params.dockerComposeLocation + ? String(params.dockerComposeLocation) + : undefined, + dockerfileLocation: params.dockerfileLocation + ? String(params.dockerfileLocation) + : undefined, + baseDirectory: params.baseDirectory + ? String(params.baseDirectory) + : undefined, + }); + } + await linkIfRequested(created.uuid, "application"); const dep = await applyEnvsAndDeploy(created.uuid, params); @@ -5082,17 +5114,48 @@ async function toolDevServerStart( name: typeof params.name === "string" ? params.name : undefined, workspace: principal.workspace, }); - void probeDevServerReadiness(project.id, row.id, row.port).catch((err) => - console.error("[dev_server.start] probeDevServerReadiness failed:", err), - ); + + // Instead of firing-and-forgetting, we now wait for the server to ACTUALLY + // spin up and serve HTTP traffic before we return success to the AI. + // This allows the AI to see the exact health check failure synchronously. + let isHealthy = false; + let failureOutput = ""; + + try { + await probeDevServerReadiness(project.id, row.id, row.port); + isHealthy = true; + } catch (probeErr: any) { + isHealthy = false; + failureOutput = probeErr.message || String(probeErr); + console.error("[dev_server.start] Synchronous probe failed:", probeErr); + } + + if (!isHealthy) { + return NextResponse.json({ + result: { + ok: false, + error: + "Server failed to start or bind to port within the timeout window.", + healthCheck: { + status: 500, + output: failureOutput, + }, + }, + }); + } + return NextResponse.json({ result: { + ok: true, id: row.id, name: row.name, port: row.port, pid: row.pid, previewUrl: row.preview_url, state: row.state, + healthCheck: { + status: 200, + }, note: "Preview URL is auto-published via Traefik labels baked into the dev-container compose. " + "It will respond once (a) DNS *.preview.vibnai.com resolves to the Coolify host and " +