From 4c804d670bfc0ebf235cc11d6db7fcf6b011cddf Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Mon, 27 Apr 2026 18:09:43 -0700 Subject: [PATCH] Fix apps.list: filter compose services by environment_id, not non-existent project field Coolify's /api/v1/services response does not include a `project` field. Services belong to environments and environments belong to projects. The old filter checked s.project.uuid (always undefined) and silently dropped every service from the result, so compose-stack apps like Twenty CRM never showed up in apps.list. Now we resolve the project's environment IDs via getProject() and filter services where environment_id is in that set. Also surface the public service's fqdn in the response (extracted from s.applications) so the AI can immediately tell the user where the app lives. Made-with: Cursor --- app/api/mcp/route.ts | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/app/api/mcp/route.ts b/app/api/mcp/route.ts index 15c0c9ec..2b763356 100644 --- a/app/api/mcp/route.ts +++ b/app/api/mcp/route.ts @@ -43,6 +43,7 @@ import { listApplicationDeployments, listApplicationEnvs, listApplicationsInProject, + getProject, projectUuidOf, TenantError, upsertApplicationEnv, @@ -418,18 +419,28 @@ async function toolAppsList(principal: Principal) { // Fetch Applications and Services in parallel. // Services are compose stacks created via the composeRaw pathway; // they live at /services not /applications. - const [apps, allServices] = await Promise.allSettled([ + // Coolify's /services response does NOT include a `project` field — services + // belong to environments, and environments belong to projects. So we resolve + // the project's environment IDs first, then filter services by environment_id. + const [apps, allServices, project] = await Promise.allSettled([ listApplicationsInProject(projectUuid), listAllServices(), + getProject(projectUuid), ]); const appList = apps.status === 'fulfilled' ? apps.value : []; + const projectEnvIds = new Set( + project.status === 'fulfilled' + ? (project.value.environments ?? []).map((e) => e.id) + : [], + ); + const serviceList = (allServices.status === 'fulfilled' && Array.isArray(allServices.value) ? (allServices.value as Array>) : [] - ).filter(s => { - const proj = s.project as Record | undefined; - return proj?.uuid === projectUuid; + ).filter((s) => { + const envId = typeof s.environment_id === 'number' ? s.environment_id : Number(s.environment_id); + return projectEnvIds.has(envId); }); return NextResponse.json({ @@ -443,15 +454,21 @@ async function toolAppsList(principal: Principal) { gitBranch: a.git_branch ?? null, resourceType: 'application', })), - ...serviceList.map(s => ({ - uuid: String(s.uuid), - name: String(s.name ?? ''), - status: String(s.status ?? 'unknown'), - fqdn: null, - gitRepository: null, - gitBranch: null, - resourceType: 'service', - })), + ...serviceList.map((s) => { + // Try to extract a usable URL from the service's applications array + // (compose stacks expose their public service's fqdn there). + const apps = (s.applications as Array>) || []; + const publicApp = apps.find((a) => a.fqdn); + return { + uuid: String(s.uuid), + name: String(s.name ?? ''), + status: String(s.status ?? 'unknown'), + fqdn: publicApp?.fqdn ? String(publicApp.fqdn) : null, + gitRepository: null, + gitBranch: null, + resourceType: 'service', + }; + }), ], }); }