v2.16.0
What's Changed
Upgrade from 2.15.0 if you run Coolify 4.1 or older. 2.15.0 broke
system enable_api/disable_apiandvalidate_serveron every pre-4.2 instance; see the first entry under Fixed.
Added
-
tagstool: list, attach and detach tags on applications, databases and services (#298) — Coolify v4.2 exposed tag management over the API (coollabsio/coolify#9275).deployalready accepted a tag name and triggered a deployment for everything carrying it, so the server could deploy by a tag but not create or assign one — tags only worked if a human had set them up in the UI first. That loop is now closed: tag several resources, then deploy them together with a singledeploycall.One consolidated tool with an
actionparam, matchingenv_varsandcontrol.action=listwith no resource returns the current team's tags (tokens are team-scoped), which is how you discover a name; withresource+uuidit returns that resource's tags.attachcreates tags that do not exist yet. Upstream accepts eithertag_nameortag_names, and this client always sends the array form so there is one shape to reason about. Tool count 43 → 44. -
Integration suite for v4.2 method compatibility —
v42-compat.integration.test.tsasserts the pre-4.2 behaviour on an older instance and the v4.2 behaviour on a newer one, so the suite is meaningful either way. Every assertion is side-effect free: a rejected method executes nothing, and/deploywith a tag matching no resource proves the method was accepted without deploying anything./disableis never called, since it would cut off the API the client depends on. -
Version gating for the log integration tests — database and service log endpoints are v4.2-only, so on an older instance they now report as genuinely skipped rather than failed, with the version printed. Resolved at module scope so jest can skip the suites outright: an early
returninside a test still shows a green tick, which is the false confidence these tests exist to prevent.dotenvalso runs withoverride: true, because an emptyCOOLIFY_URLin the ambient environment otherwise wins over.envand silently skips everything. -
Tool annotations on all 43 tools, and migration off the deprecated
tool()API (#260) — every tool now declaresreadOnlyHint/destructiveHint/idempotentHint/openWorldHint. These are spec-stable since 2025-03-26 and already change client behaviour: Claude Code parallel-dispatches tools marked read-only, and VS Code Copilot stops raising confirmation dialogs for reads. 21 tools are read-only, 20 destructive, 2 neither (hetznerprovisions but only ever adds;validate_serveris idempotent).Registration moved from the SDK's deprecated
tool()overloads toregisterTool(), which is also the shape SDK v2 keeps, so this is prep for #259. All 43 call sites go through onedefineToolwrapper that looks annotations up from a single table — a tool missing from it throws at construction rather than silently shipping unannotated. The classification lives in one auditable place rather than scattered across 43 call sites, and tests assert the table and the registered tools stay exactly in step.Correction to the issue's premise: #260 said annotations were zero-cost because they ride the existing
tools/listresponse. They ride it, but they are not free — spelling out every hint measured at ~751 tokens against a ~6,600 token budget. Only non-default hints are emitted now (per spec:readOnlyHint=false,destructiveHint=true,idempotentHint=false,openWorldHint=true), which halves it to ~415 tokens, with a test that fails if the defaults creep back in. -
logstool: container logs for databases and services, not just applications (#300) —diagnose_appcould explain an unhealthy application by reading its logs, but the same question about a database or a service had no answer and sent you to the Coolify UI, which is exactly the moment you want logs.Added as one consolidated
logstool with aresourceparameter (application/database/service), matching theenv_varsandcontrolpattern, rather than three or four sibling tools — the token budget is the point of the v2.0.0 design.application_logsstill works and is marked superseded in its description; it will be removed in v3. Also addsshow_timestamps, which upstream has always supported and this client never sent.A service is a multi-container stack, so Coolify requires
sub_service_nameon/services/{uuid}/logsand "the service logs" has no single answer. Theservicetool gains alist_containersaction returning the applications and databases inside a service, andlogsrefuses a service request without acontainer, pointing at that action rather than silently picking one.
Fixed
-
The v4.2 GET-to-POST fallback never fired on pre-4.2 instances (#296 follow-up) — a regression shipped in 2.15.0 that broke
system enable_api/disable_apiandvalidate_serveron exactly the Coolify versions the fallback existed to support.postWithLegacyGetFallbackretried with GET only on a405. Coolify endsroutes/api.phpwith a catch-all —Route::any('/{any}', ...)returning404 "Not found."— which swallows an unmatched method+path before Laravel can raise a405. So a pre-4.2 instance answersPOST /enablewith404, the fallback never triggered, and the call failed outright where it had worked before #296.Found by running the new compatibility suite against a real Coolify 4.1.2, not by re-reading the spec: the route archaeology behind #296 was correct about which methods each version registers, and wrong about what a rejected method actually returns.
The fallback now treats a
405, or a 404 carrying the catch-all's signature, as a method rejection. Matching on the body shape rather than on status alone matters: the catch-all returns{message: "Not found.", docs: ...}and no controller 404 carries adocskey, so a controller's genuine "resource not found" is recognised as a real answer and never enters the retry path at all. Both cases mean no controller ran, so nothing executed. A500still propagates untouched, since it may mean the action partially ran.When the GET retry also fails, the error reported is the one from whichever request actually reached a controller — the POST is a routing miss by construction and says nothing about the request, so surfacing its bare "Not found." (complete with an irrelevant uuid hint) would bury the real
"Server not found.". -
Log endpoints returned an object behind a
stringtype (#300) —getApplicationLogswas declaredPromise<string>but handed back Coolify's{ logs: "..." }envelope unchanged, so any caller doing string work on it would have failed at runtime. Confirmed against a live Coolify instance; the unit tests had mocked a bare string, which is why it went unnoticed. Responses are now unwrapped to a plain string, with bare strings still accepted for older instances. -
Corrected the documented behaviour of
search_docs—site/concepts/how-it-works.mddescribed it as "a local MiniSearch index overdocs/openapi-chunks/". It actually fetches Coolify's publishedllms-full.txtat runtime and indexes that; it has never read the chunks.
Changed
-
Re-vendored
docs/coolify-openapi.yamlfrom upstream, now covering Coolify v4.2 (#302) — the bundled spec is ground truth for "does Coolify support X" and predated v4.2, so it was missing 42 paths (nothing was removed). The additions line up with the open v4.2 issues: tags on applications/databases/services (#298),/movebetween environments (#299), database, service and per-container logs (#300), service application and database management (#301), and destinations (#302). Also new and not previously tracked: volume backup schedules on storages, and DigitalOcean/Vultr server provisioning alongside Hetzner firewalls and networks. Five new schemas:VolumeBackupScheduleRequest,VolumeBackupScheduleResponse,ApplicationSetting,Destination,Tag. -
docs/openapi-chunks/is now generated, not hand-maintained (#302) — the chunks are a developer reference that CLAUDE.md and the contributing guide both point at, but they were curated by hand and silently went stale the moment the spec was re-vendored, which made the reference a trap.npm run build:chunksregenerates them from the bundled spec by first path segment, andnpm run check:chunk-driftfails CI if they diverge. Newdestinations-api,tags-apiandcloud-providers-apichunks; every path and schema in the spec is carried exactly once, asserted by tests. -
The vendored spec is byte-identical to upstream again — Prettier had reformatted it at some point, so
git diffagainst a fresh upstream download showed whitespace churn rather than real API changes.docs/coolify-openapi.yamland the generated chunks are now in.prettierignore, which also means re-vendoring is a clean overwrite.
🖱️ One-click Claude Desktop install: download coolify-mcp.mcpb below and drag it into Settings → Extensions.