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

PR #245 merged: feat: add run_once action to scheduled_tasks (#233)

additions
+369
deletions
-3
files changed
3

Summary

Adds action: "run_once" to the scheduled_tasks tool — a composite that runs a one-off command in an app/service container and returns its output. There is no upstream trigger/run-now endpoint for scheduled tasks (verified: no such route in docs/coolify-openapi.yaml, no such client method), so this composes the existing create / list_executions / delete client methods rather than adding a new endpoint.

Params: resource ('application'|'service'), uuid, command, container, optional timeout, optional wait_seconds (poll budget, default 90).

Behaviour:

  1. Creates a task with a generated oneoff-<random suffix> name, frequency: "* * * * *", enabled.
  2. Polls list_executions every ~5s (attempt-counted, not wall-clock-gated, so it stays testable) for up to wait_seconds until the first execution reaches a terminal (non-running) status.
  3. Always deletes the task afterwards — success, timeout, or a polling error all fall through to the same cleanup step (Rust-finally-equivalent structure). If the delete itself fails, the response says so loudly, includes the task UUID, and warns that the cron will keep firing every minute until it's removed by hand.
  4. Returns the execution's status + message (the API returns the command's stdout in message).

Idempotency caveat (carried in the tool description): the underlying cron can fire more than once before cleanup lands — deleting right after the first observed execution shrinks the window but doesn't close it. Commands must be idempotent or tolerant of re-execution.

Command length: noted in the tool description that Coolify silently 500s on scheduled-task commands over ~255 chars (#234, still open/unfixed on this branch) — not implemented as a hard .max() validation here since that's #234's scope, just documented so run_once callers aren't surprised.

Type modelling (#237 item 2): turns out ScheduledTaskExecution.message already existed in src/types/coolify.ts (added back in #172) — the issue's premise was slightly stale. What was actually missing was the list_executions tool description not mentioning that stdout lives in message; fixed that instead.

Docs: added the every-minute/idempotency gotcha (plus the "no run-now endpoint" context and the message-is-stdout note) to site/concepts/coolify-api-gotchas.md.

Closes #233 Closes #208

Test plan

  • npm test — 369 tests passing, coverage 98.1%/86.36%/99.53%/98.91% (all above threshold)
  • New scheduled_tasks tool handler - run_once suite covers: success path (create→poll→return output→delete called), timeout path (no execution appears, delete still called, message includes task UUID), poll-error path (list_executions throws, delete still called, error surfaced), delete-failure path (message warns loudly with UUID + underlying error), input validation, and the service-resource variant
  • npm run lint — 0 errors (4 pre-existing warnings, none introduced by this change)
  • npx prettier --check . — clean
  • npm run build — clean

Notes / uncertainties

  • The poll loop counts attempts (wait_seconds / 5) rather than gating on wall-clock time, specifically so the "instant sleep" test stub doesn't turn timeout tests into real multi-second waits. This is a slight approximation of "poll every ~5s for wait_seconds" but keeps behaviour deterministic and tests fast.
  • sleep is a private class method overridden directly on the instance in tests (server.sleep = () => Promise.resolve()) rather than via jest.spyOn, because jest.spyOn's generic inference reduces to never when intersecting an object type with a class that already declares the member as private. Flagging in case there's a preferred pattern already in use elsewhere I missed.
  • Did not touch package.json/CHANGELOG.md per the task constraints — happy to add a changelog entry if that's expected before merge.

🤖 Generated with Claude Code