Stu Mason · Folkestone, Kent
One entryMerged PR· StuMason/coolify-mcp· TypeScript

PR #242 merged: fix: project deployment get through essential projection when logs requested (#232)

additions
+200
deletions
-16
files changed
5

Problem

deployment {action: get, uuid, lines: N} bypassed the toDeploymentEssential() projection: the handler called getDeployment(uuid, { includeLogs: true }), which returned the raw upstream Coolify payload and spread it directly into the tool response.

That raw payload embeds the entire application graph (rendered docker_compose, custom_labels, webhook secrets) plus the destination server object, including server.settings.logdrain_custom_config (a live bearer token for the log drain) and sentinel_token.

Observed in production: deployment {action: get, uuid, lines: 5} returned a ~78KB payload for 5 log lines, with the secrets above embedded in the body — undoing the webhook-secret masking added in v2.12.0 for list_resources, and blowing typical MCP response token budgets.

The same bypass existed in deployment {action: list_for_app, include_logs: true} via listApplicationDeployments(uuid, { includeLogs: true }).

Fix

  • CoolifyClient.getDeployment() and CoolifyClient.listApplicationDeployments() now always project through toDeploymentEssential(). When logs are requested, the raw log string is attached as a logs field on the projection — the raw upstream object (application graph, server/destination graph, secrets) is never returned.
  • Added logs?: string as an optional field on DeploymentEssential (src/types/coolify.ts).
  • src/lib/mcp-server.ts's deployment tool handler needed no structural changes beyond dropping a now-unneeded as Deployment cast — it already just spreads whatever the client returns, and the client now only ever returns the essential shape. Pagination (logs_meta, next/prev) and max_chars handling via truncateLogs() are unchanged.
  • Did not route the output through maskResourceItemFull/SENSITIVE_RESOURCE_FIELDS (the v2.12.0 list_resources masking helper) — that helper targets the flat ResourceListItemFull shape from /resources and doesn't apply cleanly here. toDeploymentEssential() is already an allowlist projection (it only ever copies named-safe fields across), so there's no raw-field surface left for that denylist to guard. Forcing it in would be contorting the code for no real protection.

Tests

  • src/__tests__/coolify-client.test.ts: updated the two tests that previously asserted includeLogs: true returns the raw deployment/list — they now assert the essential projection with logs attached, and that raw fields (application, destination, id) never appear.
  • src/__tests__/mcp-server.test.ts: new deployment tool handler (#232 essential projection) suite. Mocks the raw HTTP layer (not the client) with a realistic bloated upstream payload embedding logdrain_custom_config, sentinel_token, manual_webhook_secret_*, and docker_compose, then drives the real deployment tool end-to-end:
    • asserts the serialized response contains none of those leaked keys and no nested application/server/destination objects,
    • asserts the response stays under 20KB even with a bloated upstream payload.

Verification

  • npm test — 365 passed
  • npm run lint — 0 errors (4 pre-existing no-explicit-any warnings, unrelated to this change)
  • npx prettier --check . — clean
  • npm run build — clean

Closes #232

🤖 Generated with Claude Code