feat(mcp): apps.logs — compose-aware runtime logs
Adds apps.logs MCP tool + session REST endpoint for tailing runtime
container logs. Unblocks cold-start debugging for agent-deployed
compose apps (Twenty, Cal.com, Plane, etc.) where Coolify's own
/applications/{uuid}/logs endpoint returns empty.
Architecture:
- dockerfile / nixpacks / static apps → Coolify's REST logs API
- dockercompose apps → SSH into Coolify host,
`docker logs` per service
New SSH path uses a dedicated `vibn-logs` user (docker group, no
sudo, no pty, no port-forwarding, single ed25519 key). Private key
lives in COOLIFY_SSH_PRIVATE_KEY_B64 on the vibn-frontend Coolify
app; authorized_key is installed by scripts/setup-vibn-logs-user.sh
on the Coolify host.
Tool shape:
params: { uuid, service?, lines? (default 200, max 5000) }
returns: { uuid, buildPack, source: 'coolify_api'|'ssh_docker'|'empty',
services: { [name]: { container, lines, bytes, logs, status? } },
warnings: string[], truncated: boolean }
Made-with: Cursor
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
getWorkspaceGcsHmacCredentials,
|
||||
} from '@/lib/workspace-gcs';
|
||||
import { VIBN_GCS_LOCATION } from '@/lib/gcp/storage';
|
||||
import { getApplicationRuntimeLogs } from '@/lib/coolify-logs';
|
||||
import {
|
||||
deployApplication,
|
||||
getApplicationInProject,
|
||||
@@ -97,6 +98,7 @@ export async function GET() {
|
||||
'apps.deployments',
|
||||
'apps.domains.list',
|
||||
'apps.domains.set',
|
||||
'apps.logs',
|
||||
'apps.envs.list',
|
||||
'apps.envs.upsert',
|
||||
'apps.envs.delete',
|
||||
@@ -189,6 +191,8 @@ export async function POST(request: Request) {
|
||||
return await toolAppsDomainsList(principal, params);
|
||||
case 'apps.domains.set':
|
||||
return await toolAppsDomainsSet(principal, params);
|
||||
case 'apps.logs':
|
||||
return await toolAppsLogs(principal, params);
|
||||
|
||||
case 'databases.list':
|
||||
return await toolDatabasesList(principal);
|
||||
@@ -400,6 +404,34 @@ async function toolAppsDeployments(principal: Principal, params: Record<string,
|
||||
return NextResponse.json({ result: deployments });
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime logs for a Coolify app. Compose-aware:
|
||||
* - Dockerfile/nixpacks apps → Coolify's `/applications/{uuid}/logs`
|
||||
* - Compose apps → SSH into Coolify host, `docker logs` per service
|
||||
*
|
||||
* Params:
|
||||
* uuid – app uuid (required)
|
||||
* service – compose service filter (optional)
|
||||
* lines – tail lines per container, default 200, max 5000
|
||||
*/
|
||||
async function toolAppsLogs(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const appUuid = String(params.uuid ?? params.appUuid ?? '').trim();
|
||||
if (!appUuid) {
|
||||
return NextResponse.json({ error: 'Param "uuid" is required' }, { status: 400 });
|
||||
}
|
||||
await getApplicationInProject(appUuid, projectUuid);
|
||||
|
||||
const linesRaw = Number(params.lines ?? 200);
|
||||
const lines = Number.isFinite(linesRaw) ? linesRaw : 200;
|
||||
const serviceRaw = params.service;
|
||||
const service = typeof serviceRaw === 'string' && serviceRaw.trim() ? serviceRaw.trim() : undefined;
|
||||
|
||||
const result = await getApplicationRuntimeLogs(appUuid, { lines, service });
|
||||
return NextResponse.json({ result });
|
||||
}
|
||||
|
||||
async function toolAppsEnvsList(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
|
||||
49
app/api/workspaces/[slug]/apps/[uuid]/logs/route.ts
Normal file
49
app/api/workspaces/[slug]/apps/[uuid]/logs/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* GET /api/workspaces/[slug]/apps/[uuid]/logs
|
||||
*
|
||||
* Runtime logs for a Coolify app. Compose-aware: Coolify's REST API
|
||||
* for non-compose build packs, SSH into the Coolify host for per-
|
||||
* service `docker logs` on compose apps.
|
||||
*
|
||||
* Query params:
|
||||
* lines – tail lines per container (default 200, max 5000)
|
||||
* service – limit to one compose service (optional)
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth';
|
||||
import { getApplicationInProject, TenantError } from '@/lib/coolify';
|
||||
import { getApplicationRuntimeLogs } from '@/lib/coolify-logs';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ slug: string; uuid: string }> }
|
||||
) {
|
||||
const { slug, uuid } = await params;
|
||||
const principal = await requireWorkspacePrincipal(request, { targetSlug: slug });
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
const ws = principal.workspace;
|
||||
if (!ws.coolify_project_uuid) {
|
||||
return NextResponse.json({ error: 'Workspace has no Coolify project yet' }, { status: 503 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const linesRaw = Number(url.searchParams.get('lines') ?? '200');
|
||||
const lines = Number.isFinite(linesRaw) ? linesRaw : 200;
|
||||
const service = url.searchParams.get('service') ?? undefined;
|
||||
|
||||
try {
|
||||
await getApplicationInProject(uuid, ws.coolify_project_uuid);
|
||||
const result = await getApplicationRuntimeLogs(uuid, { lines, service });
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
if (err instanceof TenantError) {
|
||||
return NextResponse.json({ error: err.message }, { status: 403 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch runtime logs', details: err instanceof Error ? err.message : String(err) },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user