@jurislm/coolify-plugin 3.7.11 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coolify-plugin",
3
- "version": "3.7.11",
3
+ "version": "4.0.0",
4
4
  "description": "Fallback Codex metadata for local stdio Coolify tools.",
5
5
  "author": {
6
6
  "name": "JurisLM",
package/.mcp.json.example CHANGED
@@ -4,8 +4,8 @@
4
4
  "command": "bunx",
5
5
  "args": ["-y", "@jurislm/coolify-plugin@latest"],
6
6
  "env": {
7
- "COOLIFY_URL": "https://coolify.example",
8
- "COOLIFY_TOKEN": "<set-in-host-environment>"
7
+ "COOLIFY_BASE_URL": "https://coolify.example",
8
+ "COOLIFY_ACCESS_TOKEN": "<set-in-host-environment>"
9
9
  }
10
10
  }
11
11
  }
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # @jurislm/coolify-plugin
2
2
 
3
- Portable Coolify MCP plugin. It exposes focused `coolify_*` stdio tools generated from the committed official Coolify OpenAPI snapshot.
3
+ Portable Coolify MCP plugin. It exposes focused `coolify_*` stdio tools generated from one pinned Coolify API contract.
4
4
 
5
5
  ## Configure
6
6
 
7
- Only these environment variables are accepted:
7
+ The plugin reads the same global environment variables used by the Coolify setup:
8
8
 
9
9
  ```sh
10
- export COOLIFY_URL=https://coolify.example
11
- export COOLIFY_TOKEN=your-api-token
10
+ export COOLIFY_BASE_URL=https://coolify.example
11
+ export COOLIFY_ACCESS_TOKEN=your-api-token
12
12
  ```
13
13
 
14
14
  Build and run locally:
@@ -30,7 +30,7 @@ codex plugin add coolify-plugin@coolify-marketplace
30
30
 
31
31
  ## OpenAPI contract
32
32
 
33
- `openapi/coolify-openapi.json` is fetched from the official Coolify repository. `api/manifest.json` records its source, fetch time, persisted-snapshot SHA-256, document/info versions, path count, and operation count. `bun run api:check` verifies the snapshot offline before checking generated parity.
33
+ `openapi/coolify-openapi.json` is built from the official Coolify v4.3.23 release. `api/manifest.json` records the upstream SHA-256, the current API contract corrections, and the persisted SHA-256. `bun run api:check` verifies the contract offline before checking generated parity. The runtime uses the generated schemas directly.
34
34
 
35
35
  ```sh
36
36
  bun run api:fetch
package/api/manifest.json CHANGED
@@ -1,9 +1,20 @@
1
1
  {
2
- "sourceUrl": "https://raw.githubusercontent.com/coollabsio/coolify/main/openapi.json",
3
- "fetchedAt": "2026-09-16T10:31:42.036Z",
4
- "sha256": "4dbb392aac5e0a46186c4e6e5237f86d9534bb99bf6b8b82351a513598af3900",
2
+ "sourceUrl": "https://raw.githubusercontent.com/coollabsio/coolify/v4.3.23/openapi.json",
3
+ "fetchedAt": "2026-09-23T12:23:33.000Z",
4
+ "sha256": "e80f5efe8df44d0887285a8b923bca84bfc3af6a62b1e63405164e71939d6595",
5
+ "upstreamSha256": "964360ffb3d4c026252aa4657d6561190f59ac4256395bf0645bf6a4f171ec88",
5
6
  "openapiVersion": "3.1.0",
6
7
  "infoVersion": "0.1",
7
8
  "pathCount": 192,
8
- "operationCount": 275
9
+ "operationCount": 275,
10
+ "contractCorrections": [
11
+ "List database and resource responses are arrays",
12
+ "Application deployment history is a count plus deployments collection",
13
+ "Resource field nullability and identifiers match the active API",
14
+ "Database updates expose custom Docker run options",
15
+ "Unsupported server validation operation removed",
16
+ "Database PATCH omits unspecified health-check defaults",
17
+ "Server reachability and service status are response fields",
18
+ "Server metadata, deployment git type, and environment descriptions can be null"
19
+ ]
9
20
  }
@@ -1,3 +1,3 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { CoolifyClient } from "./client.js";
3
- export declare function registerV36Capabilities(server: McpServer, client: CoolifyClient): void;
3
+ export declare function registerCapabilities(server: McpServer, client: CoolifyClient): void;
@@ -8,16 +8,10 @@ const truncationPrefix = "...[truncated]...\n";
8
8
  function rows(value) {
9
9
  if (Array.isArray(value))
10
10
  return value.filter((item) => !!item && typeof item === "object");
11
- if (!value || typeof value !== "object")
12
- return [];
13
- for (const key of ["data", "items", "results", "deployments", "applications", "databases", "resources", "services"]) {
14
- const nested = value[key];
15
- if (Array.isArray(nested))
16
- return rows(nested);
17
- }
18
11
  return [];
19
12
  }
20
13
  function status(value) { return typeof value.status === "string" ? value.status : ""; }
14
+ function running(value) { return /(^|:)running($|:)/iu.test(status(value)); }
21
15
  function unhealthy(value) { return /exited|unhealthy|error|stopped/iu.test(status(value)); }
22
16
  function errorMessage(value) { return value instanceof Error ? value.message : String(value); }
23
17
  function truncateLogs(logs, lineLimit = 200, charLimit = 50_000) {
@@ -31,14 +25,15 @@ function truncateLogs(logs, lineLimit = 200, charLimit = 50_000) {
31
25
  return result;
32
26
  }
33
27
  function environmentSummary(value) {
34
- return rows(value).map((item) => ({ key: item.key, is_build_time: item.is_build_time ?? item.is_buildtime ?? false }));
28
+ return rows(value).map((item) => ({ key: item.key, is_build_time: item.is_buildtime === true }));
35
29
  }
36
- export function registerV36Capabilities(server, client) {
30
+ export function registerCapabilities(server, client) {
37
31
  const call = async (name, input = {}) => {
38
32
  const operation = operations.find((item) => item.name === name);
39
33
  if (!operation)
40
34
  throw new Error(`Missing generated operation: ${name}`);
41
- return (await client.request(operation, operation.inputSchema.parse(input))).data;
35
+ const data = (await client.request(operation, operation.inputSchema.parse(input))).data;
36
+ return operation.responseSchema.parse(data);
42
37
  };
43
38
  const result = (name, data) => {
44
39
  const structuredContent = redactSensitive({ data, status: 200, request: { method: "COMPOSITE", path: `/capabilities/${name}` } });
@@ -89,6 +84,14 @@ export function registerV36Capabilities(server, client) {
89
84
  const servers = rows(await call("coolify_list_servers"));
90
85
  return servers.find((item) => [item.uuid, item.name, item.ip].some((value) => typeof value === "string" && value.toLowerCase().includes(query.toLowerCase())));
91
86
  };
87
+ const projectApplications = async (projectUuid) => {
88
+ const [apps, environments] = await Promise.all([
89
+ call("coolify_list_applications"),
90
+ call("coolify_get_environments", { uuid: projectUuid }),
91
+ ]);
92
+ const environmentIds = new Set(rows(environments).map((environment) => environment.id));
93
+ return rows(apps).filter((app) => environmentIds.has(app.environment_id));
94
+ };
92
95
  register("coolify_get_mcp_version", "Get the local Coolify plugin version.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async () => ({ name: "@jurislm/coolify-plugin", version: pluginVersion }));
93
96
  register("coolify_get_infrastructure_overview", "Summarize Coolify infrastructure.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, overview);
94
97
  register("coolify_diagnose_application", "Diagnose an application by UUID, name, or domain.", z.object({ query: z.string() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ query }) => {
@@ -103,7 +106,7 @@ export function registerV36Capabilities(server, client) {
103
106
  return { application: null, health: { status: "unknown", issues: [] }, logs: null, environment_variables: { count: 0, variables: [] }, recent_deployments: [], errors: [`No application found matching ${query}`] };
104
107
  const names = ["application", "logs", "environment_variables", "deployments"];
105
108
  const settled = await Promise.allSettled([
106
- call("coolify_get_application_by_uuid", { uuid: app.uuid }), call("coolify_get_application_logs_by_uuid", { uuid: app.uuid, lines: 50 }), call("coolify_list_envs_by_application_uuid", { uuid: app.uuid }), call("coolify_list_deployments_by_app_uuid", { uuid: app.uuid }),
109
+ call("coolify_get_application_by_uuid", { uuid: app.uuid }), running(app) ? call("coolify_get_application_logs_by_uuid", { uuid: app.uuid, lines: 50 }) : Promise.resolve(null), call("coolify_list_envs_by_application_uuid", { uuid: app.uuid }), call("coolify_list_deployments_by_app_uuid", { uuid: app.uuid }),
107
110
  ]);
108
111
  const errors = [];
109
112
  const value = (index) => { const entry = settled[index]; if (entry.status === "fulfilled")
@@ -111,7 +114,8 @@ export function registerV36Capabilities(server, client) {
111
114
  const details = value(0);
112
115
  const rawLogs = value(1);
113
116
  const envVars = environmentSummary(value(2));
114
- const deployments = rows(value(3));
117
+ const deploymentCollection = value(3);
118
+ const deployments = rows(deploymentCollection?.deployments);
115
119
  const appInfo = details ?? app;
116
120
  const appStatus = status(appInfo);
117
121
  const issues = [];
@@ -127,8 +131,8 @@ export function registerV36Capabilities(server, client) {
127
131
  health = "unhealthy";
128
132
  issues.push(`${failed} failed deployment(s) in last 5`);
129
133
  }
130
- const logs = typeof rawLogs === "string" ? rawLogs : rawLogs && typeof rawLogs === "object" && typeof rawLogs.logs === "string" ? rawLogs.logs : null;
131
- return { application: { uuid: appInfo.uuid, name: appInfo.name, status: appStatus || "unknown", fqdn: appInfo.fqdn ?? null, git_repository: appInfo.git_repository ?? null, git_branch: appInfo.git_branch ?? null }, health: { status: health, issues }, logs: logs === null ? null : truncateLogs(logs), environment_variables: { count: envVars.length, variables: envVars }, recent_deployments: deployments.slice(0, 5).map((item) => ({ uuid: item.uuid, status: item.status, created_at: item.created_at })), ...(errors.length ? { errors } : {}) };
134
+ const logs = rawLogs && typeof rawLogs === "object" && typeof rawLogs.logs === "string" ? rawLogs.logs : null;
135
+ return { application: { uuid: appInfo.uuid, name: appInfo.name, status: appStatus || "unknown", fqdn: appInfo.fqdn ?? null, git_repository: appInfo.git_repository ?? null, git_branch: appInfo.git_branch ?? null }, health: { status: health, issues }, logs: logs === null ? null : truncateLogs(logs), environment_variables: { count: envVars.length, variables: envVars }, recent_deployments: deployments.slice(0, 5).map((item) => ({ uuid: item.deployment_uuid, status: item.status, created_at: item.created_at })), ...(errors.length ? { errors } : {}) };
132
136
  });
133
137
  register("coolify_diagnose_server", "Diagnose a server by UUID, name, or IP.", z.object({ query: z.string() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ query }) => {
134
138
  let found;
@@ -136,13 +140,13 @@ export function registerV36Capabilities(server, client) {
136
140
  found = await serverByQuery(String(query));
137
141
  }
138
142
  catch (error) {
139
- return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], validation: null, errors: [errorMessage(error)] };
143
+ return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], errors: [errorMessage(error)] };
140
144
  }
141
145
  if (!found?.uuid)
142
- return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], validation: null, errors: [`No server found matching ${query}`] };
143
- const names = ["server", "resources", "domains", "validation"];
146
+ return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], errors: [`No server found matching ${query}`] };
147
+ const names = ["server", "resources", "domains"];
144
148
  const settled = await Promise.allSettled([
145
- call("coolify_get_server_by_uuid", { uuid: found.uuid }), call("coolify_get_resources_by_server_uuid", { uuid: found.uuid }), call("coolify_get_domains_by_server_uuid", { uuid: found.uuid }), call("coolify_validate_server_by_uuid", { uuid: found.uuid }),
149
+ call("coolify_get_server_by_uuid", { uuid: found.uuid }), call("coolify_get_resources_by_server_uuid", { uuid: found.uuid }), call("coolify_get_domains_by_server_uuid", { uuid: found.uuid }),
146
150
  ]);
147
151
  const errors = [];
148
152
  const value = (index) => { const entry = settled[index]; if (entry.status === "fulfilled")
@@ -157,7 +161,7 @@ export function registerV36Capabilities(server, client) {
157
161
  const unhealthyResources = resources.filter(unhealthy).length;
158
162
  if (unhealthyResources)
159
163
  issues.push(`${unhealthyResources} unhealthy resource(s)`);
160
- return { server: { uuid: details.uuid, name: details.name, ip: details.ip, status: details.status ?? null, is_reachable: details.is_reachable ?? null }, health: { status: issues.length ? "unhealthy" : details.is_reachable === true ? "healthy" : "unknown", issues }, resources: resources.map((item) => ({ uuid: item.uuid, name: item.name, type: item.type, status: item.status })), domains: rows(value(2)), validation: value(3), ...(errors.length ? { errors } : {}) };
164
+ return { server: { uuid: details.uuid, name: details.name, ip: details.ip, status: details.status ?? null, is_reachable: details.is_reachable ?? null }, health: { status: issues.length ? "unhealthy" : details.is_reachable === true ? "healthy" : "unknown", issues }, resources: resources.map((item) => ({ uuid: item.uuid, name: item.name, type: item.type, status: item.status })), domains: rows(value(2)), ...(errors.length ? { errors } : {}) };
161
165
  });
162
166
  register("coolify_find_issues", "Find unhealthy Coolify infrastructure.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async () => {
163
167
  const names = ["servers", "applications", "databases", "services"];
@@ -168,7 +172,7 @@ export function registerV36Capabilities(server, client) {
168
172
  values.slice(1).forEach((items, index) => items.filter(unhealthy).forEach((item) => issues.push({ type: names[index + 1].slice(0, -1), uuid: item.uuid, name: item.name, issue: `${names[index + 1].slice(0, -1)} status: ${status(item)}`, status: status(item) })));
169
173
  return { summary: { total_issues: issues.length, unhealthy_applications: issues.filter((item) => item.type === "application").length, unhealthy_databases: issues.filter((item) => item.type === "database").length, unhealthy_services: issues.filter((item) => item.type === "service").length, unreachable_servers: issues.filter((item) => item.type === "server").length }, issues, ...(errors.length ? { errors } : {}) };
170
174
  });
171
- register("coolify_restart_project_applications", "Restart every application in a project.", z.object({ project_uuid: z.string() }), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ project_uuid }) => batch(rows(await call("coolify_list_applications")).filter((app) => app.project_uuid === project_uuid), (app) => call("coolify_restart_application_by_uuid", { uuid: app.uuid })));
175
+ register("coolify_restart_project_applications", "Restart every application in a project.", z.object({ project_uuid: z.string() }), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ project_uuid }) => batch(await projectApplications(String(project_uuid)), (app) => call("coolify_restart_application_by_uuid", { uuid: app.uuid })));
172
176
  register("coolify_bulk_update_application_env", "Update an environment variable across applications.", z.object({ app_uuids: z.array(z.string()), key: z.string(), value: z.string() }).strict(), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ app_uuids, key, value }) => {
173
177
  if (!app_uuids.length)
174
178
  return batch([], async () => undefined);
@@ -176,8 +180,8 @@ export function registerV36Capabilities(server, client) {
176
180
  const labels = new Map(names.map((item) => [item.uuid, item.name ?? item.uuid]));
177
181
  return batch(app_uuids.map((uuid) => ({ uuid, name: labels.get(uuid) ?? uuid })), (app) => call("coolify_update_env_by_application_uuid", { uuid: app.uuid, body: { key, value } }));
178
182
  });
179
- register("coolify_stop_all_applications", "Emergency stop every running application.", z.object({ confirm_stop_all_applications: z.literal(true) }), { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, async () => batch(rows(await call("coolify_list_applications")).filter((app) => /running|healthy/iu.test(status(app))), (app) => call("coolify_stop_application_by_uuid", { uuid: app.uuid })));
180
- register("coolify_redeploy_project_applications", "Redeploy every application in a project.", z.object({ project_uuid: z.string(), force: z.boolean().optional() }), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ project_uuid, force }) => batch(rows(await call("coolify_list_applications")).filter((app) => app.project_uuid === project_uuid), (app) => call("coolify_deploy_by_tag_or_uuid", { uuid: app.uuid, force: force ?? true })));
183
+ register("coolify_stop_all_applications", "Emergency stop every running application.", z.object({ confirm_stop_all_applications: z.literal(true) }), { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, async () => batch(rows(await call("coolify_list_applications")).filter(running), (app) => call("coolify_stop_application_by_uuid", { uuid: app.uuid })));
184
+ register("coolify_redeploy_project_applications", "Redeploy every application in a project.", z.object({ project_uuid: z.string(), force: z.boolean().optional() }), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ project_uuid, force }) => batch(await projectApplications(String(project_uuid)), (app) => call("coolify_deploy_by_tag_or_uuid", { uuid: app.uuid, force: force ?? true })));
181
185
  register("coolify_get_environment", "Get an environment and its relevant database types.", z.object({ project_uuid: z.string(), environment_name_or_uuid: z.string() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ project_uuid, environment_name_or_uuid }) => {
182
186
  const settled = await Promise.allSettled([call("coolify_get_environment_by_name_or_uuid", { uuid: project_uuid, environment_name_or_uuid }), call("coolify_list_databases")]);
183
187
  const errors = [];
@@ -185,9 +189,9 @@ export function registerV36Capabilities(server, client) {
185
189
  const databases = settled[1].status === "fulfilled" ? rows(settled[1].value) : (errors.push(`databases: ${errorMessage(settled[1].reason)}`), []);
186
190
  if (!environment)
187
191
  return { environment: null, missing_database_types: [...databaseTypes], ...(errors.length ? { errors } : {}) };
188
- const matching = databases.filter((database) => (environment.id !== undefined && database.environment_id === environment.id) || (environment.uuid !== undefined && database.environment_uuid === environment.uuid) || (environment.name !== undefined && database.environment_name === environment.name));
192
+ const matching = databases.filter((database) => environment.id !== undefined && database.environment_id === environment.id);
189
193
  const byType = new Map();
190
- matching.forEach((database) => { const type = String(database.database_type ?? database.type ?? "").toLowerCase(); for (const expected of databaseTypes)
194
+ matching.forEach((database) => { const type = String(database.database_type ?? "").toLowerCase(); for (const expected of databaseTypes)
191
195
  if (type.includes(expected))
192
196
  byType.set(expected, [...(byType.get(expected) ?? []), database]); });
193
197
  const missing = databaseTypes.filter((type) => !byType.has(type));
package/dist/client.js CHANGED
@@ -30,21 +30,6 @@ function base64(bytes) {
30
30
  text += String.fromCharCode(byte);
31
31
  return btoa(text);
32
32
  }
33
- function normalizeKnownCollection(path, value) {
34
- const collectionPaths = new Set(["/databases", "/resources", "/deployments", "/deployments/applications/{uuid}"]);
35
- if (!collectionPaths.has(path))
36
- return value;
37
- if (Array.isArray(value))
38
- return value;
39
- if (!value || typeof value !== "object")
40
- throw new Error(`Coolify collection response for ${path} must be an array or named array wrapper`);
41
- for (const key of ["data", "items", "results", "databases", "resources", "deployments"]) {
42
- const nested = value[key];
43
- if (Array.isArray(nested))
44
- return nested;
45
- }
46
- throw new Error(`Coolify collection response for ${path} must be an array or named array wrapper`);
47
- }
48
33
  export class CoolifyClient {
49
34
  config;
50
35
  fetchImpl;
@@ -55,10 +40,10 @@ export class CoolifyClient {
55
40
  async request(operation, input) {
56
41
  const baseUrl = this.config.baseUrl;
57
42
  if (!baseUrl)
58
- throw new CoolifyApiError(0, operation.method, operation.path, "COOLIFY_URL is required");
43
+ throw new CoolifyApiError(0, operation.method, operation.path, "COOLIFY_BASE_URL is required");
59
44
  const token = this.config.token;
60
45
  if (!token)
61
- throw new CoolifyApiError(0, operation.method, operation.path, "COOLIFY_TOKEN is required");
46
+ throw new CoolifyApiError(0, operation.method, operation.path, "COOLIFY_ACCESS_TOKEN is required");
62
47
  let path = operation.path;
63
48
  for (const parameter of operation.parameters)
64
49
  if (parameter.location === "path") {
@@ -89,7 +74,7 @@ export class CoolifyClient {
89
74
  if (response.status !== 204) {
90
75
  const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
91
76
  if (contentType.includes("json"))
92
- data = normalizeKnownCollection(operation.path, await response.json());
77
+ data = await response.json();
93
78
  else if (contentType.startsWith("text/") || contentType.includes("xml"))
94
79
  data = await response.text();
95
80
  else
package/dist/config.js CHANGED
@@ -5,8 +5,8 @@ export class ConfigError extends Error {
5
5
  }
6
6
  }
7
7
  export function loadConfig(env = process.env) {
8
- const rawUrl = env.COOLIFY_URL?.trim();
9
- const token = env.COOLIFY_TOKEN?.trim();
8
+ const rawUrl = env.COOLIFY_BASE_URL?.trim();
9
+ const token = env.COOLIFY_ACCESS_TOKEN?.trim();
10
10
  if (!rawUrl)
11
11
  return { ...(token ? { token } : {}), timeoutMs: 30_000 };
12
12
  let url;
@@ -14,13 +14,13 @@ export function loadConfig(env = process.env) {
14
14
  url = new URL(rawUrl);
15
15
  }
16
16
  catch {
17
- throw new ConfigError("COOLIFY_URL must be a valid HTTP(S) URL");
17
+ throw new ConfigError("COOLIFY_BASE_URL must be a valid HTTP(S) URL");
18
18
  }
19
19
  if (url.protocol !== "http:" && url.protocol !== "https:") {
20
- throw new ConfigError("COOLIFY_URL must use http:// or https://");
20
+ throw new ConfigError("COOLIFY_BASE_URL must use http:// or https://");
21
21
  }
22
22
  if (url.search || url.hash)
23
- throw new ConfigError("COOLIFY_URL must not include a query or fragment");
23
+ throw new ConfigError("COOLIFY_BASE_URL must not include a query or fragment");
24
24
  url.pathname = url.pathname.replace(/\/+$/u, "");
25
25
  if (!url.pathname.endsWith("/api/v1"))
26
26
  url.pathname = `${url.pathname}/api/v1`.replace(/^\/\//u, "/");