@jurislm/coolify-plugin 4.0.20 → 4.0.21

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": "4.0.20",
3
+ "version": "4.0.21",
4
4
  "description": "Fallback Codex metadata for local stdio Coolify tools.",
5
5
  "author": {
6
6
  "name": "JurisLM",
package/README.md CHANGED
@@ -33,6 +33,10 @@ codex plugin marketplace add https://github.com/jurislm/coolify-plugin
33
33
  codex plugin add coolify-plugin@coolify-marketplace
34
34
  ```
35
35
 
36
+ To check a connection, call `coolify_get_mcp_version`, then `coolify_check_connection`. The second tool reports the selected variable pair, whether a URL and credential are present, and the HTTP status of `/health` and `/version`. It never returns credential values. With a URL but no credential, it still checks public `/health` and leaves `/version` unattempted. A healthy `/health` with a 401 from `/version` means the instance is reachable but rejected the authenticated request; the response alone does not establish whether the credential expired, was revoked, or came from the wrong launcher.
37
+
38
+ Check the active MCP registration separately. A standalone server named `coolify` can run the same package while the marketplace plugin's MCP is disabled. `coolify_get_mcp_version` identifies the package version, not which registration launched it. `coolify_get_infrastructure_overview` reports authentication failures and all-list failures as tool errors. When only some lists fail for other reasons, their counts are `null` and `complete` is `false`.
39
+
36
40
  ## Install in Cursor
37
41
 
38
42
  In Cursor, open **Customize → Plugins → Add Marketplace → Import from GitHub**, enter `https://github.com/jurislm/coolify-plugin`, then install **Coolify Plugin**. The marketplace selects `plugins/cursor`, keeping the Codex-only root `.mcp.json` out of Cursor's plugin root. For Cloud Agents, set both `COOLIFY_BASE_URL` and `COOLIFY_ACCESS_TOKEN` in the Cloud environment's secrets and start a new agent from that environment. Local agents can use the plugin's **Configure** panel. After updating from v4.0.16 or earlier, re-enter both Local Configure fields because their variable names changed. If either Cloud environment variable is set, the plugin uses only the Cloud pair and does not mix it with Configure values. Run a read-only Coolify tool to verify provider access.
@@ -1,3 +1,4 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { CoolifyClient } from "./client.js";
3
- export declare function registerCapabilities(server: McpServer, client: CoolifyClient): void;
3
+ import type { CoolifyConfig } from "./config.js";
4
+ export declare function registerCapabilities(server: McpServer, client: CoolifyClient, config: CoolifyConfig): void;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { redactSensitive } from "./client.js";
2
+ import { CoolifyApiError, redactSensitive } from "./client.js";
3
3
  import { operations } from "./generated/operations.js";
4
4
  import { pluginVersion } from "./version.js";
5
5
  const outputSchema = z.object({ data: z.unknown(), status: z.number(), request: z.object({ method: z.string(), path: z.string() }) });
@@ -27,7 +27,7 @@ function truncateLogs(logs, lineLimit = 200, charLimit = 50_000) {
27
27
  function environmentSummary(value) {
28
28
  return rows(value).map((item) => ({ key: item.key, is_build_time: item.is_buildtime === true }));
29
29
  }
30
- export function registerCapabilities(server, client) {
30
+ export function registerCapabilities(server, client, config) {
31
31
  const call = async (name, input = {}) => {
32
32
  const operation = operations.find((item) => item.name === name);
33
33
  if (!operation)
@@ -44,23 +44,31 @@ export function registerCapabilities(server, client) {
44
44
  };
45
45
  const settledRows = async (names) => {
46
46
  const settled = await Promise.allSettled(names.map((name) => call(`coolify_list_${name}`)));
47
+ const auth = settled.find((entry) => entry.status === "rejected" && entry.reason instanceof CoolifyApiError && [401, 403].includes(entry.reason.status));
48
+ if (auth?.status === "rejected")
49
+ throw auth.reason;
50
+ const first = settled[0];
51
+ if (first?.status === "rejected" && settled.every((entry) => entry.status === "rejected"))
52
+ throw first.reason;
47
53
  const values = [];
48
54
  const errors = [];
55
+ const failed = new Set();
49
56
  settled.forEach((entry, index) => {
50
57
  if (entry.status === "fulfilled")
51
58
  values[index] = rows(entry.value);
52
59
  else {
53
60
  values[index] = [];
61
+ failed.add(index);
54
62
  errors.push(`${names[index]}: ${errorMessage(entry.reason)}`);
55
63
  }
56
64
  });
57
- return { values, errors };
65
+ return { values, errors, failed };
58
66
  };
59
67
  const overview = async () => {
60
68
  const names = ["servers", "projects", "applications", "databases", "services"];
61
- const { values, errors } = await settledRows(names);
69
+ const { values, errors, failed } = await settledRows(names);
62
70
  const data = Object.fromEntries(names.map((name, index) => [name, values[index]]));
63
- return { summary: Object.fromEntries(names.map((name, index) => [name, values[index].length])), ...data, ...(errors.length ? { errors } : {}) };
71
+ return { complete: errors.length === 0, summary: Object.fromEntries(names.map((name, index) => [name, failed.has(index) ? null : values[index].length])), ...data, ...(errors.length ? { errors } : {}) };
64
72
  };
65
73
  const batch = async (resources, action) => {
66
74
  const settled = await Promise.allSettled(resources.map(action));
@@ -93,6 +101,21 @@ export function registerCapabilities(server, client) {
93
101
  return rows(apps).filter((app) => environmentIds.has(app.environment_id));
94
102
  };
95
103
  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 }));
104
+ register("coolify_check_connection", "Check selected configuration and Coolify API status without exposing credentials.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async () => {
105
+ const probe = async (name) => {
106
+ const operation = operations.find((item) => item.name === name);
107
+ if (!operation || !config.baseUrl || (!config.token && operation.path !== "/health"))
108
+ return null;
109
+ try {
110
+ return (await client.request(operation, {})).status;
111
+ }
112
+ catch (error) {
113
+ return error instanceof CoolifyApiError && error.status > 0 ? error.status : null;
114
+ }
115
+ };
116
+ const [healthStatus, versionStatus] = await Promise.all([probe("coolify_healthcheck"), probe("coolify_version")]);
117
+ return { plugin_version: pluginVersion, configuration: { selected_variables: config.selectedVariables ?? "unknown", base_url_configured: Boolean(config.baseUrl), credential_present: Boolean(config.token) }, provider: { health_status: healthStatus, version_status: versionStatus } };
118
+ });
96
119
  register("coolify_get_infrastructure_overview", "Summarize Coolify infrastructure.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, overview);
97
120
  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 }) => {
98
121
  let app;
package/dist/client.js CHANGED
@@ -42,7 +42,7 @@ export class CoolifyClient {
42
42
  if (!baseUrl)
43
43
  throw new CoolifyApiError(0, operation.method, operation.path, "COOLIFY_BASE_URL is required");
44
44
  const token = this.config.token;
45
- if (!token)
45
+ if (!token && !(operation.method === "GET" && operation.path === "/health"))
46
46
  throw new CoolifyApiError(0, operation.method, operation.path, "COOLIFY_ACCESS_TOKEN is required");
47
47
  let path = operation.path;
48
48
  for (const parameter of operation.parameters)
@@ -55,7 +55,9 @@ export class CoolifyClient {
55
55
  for (const parameter of operation.parameters)
56
56
  if (parameter.location === "query")
57
57
  appendQuery(url, parameter.name, input[parameter.name]);
58
- const headers = new Headers({ accept: "application/json, text/plain, */*", authorization: `Bearer ${token}` });
58
+ const headers = new Headers({ accept: "application/json, text/plain, */*" });
59
+ if (token)
60
+ headers.set("authorization", `Bearer ${token}`);
59
61
  const init = { method: operation.method, headers, signal: AbortSignal.timeout(this.config.timeoutMs) };
60
62
  if (input.body !== undefined && !["GET", "HEAD"].includes(operation.method)) {
61
63
  headers.set("content-type", "application/json");
@@ -69,7 +71,8 @@ export class CoolifyClient {
69
71
  response = await this.fetchImpl(url, init);
70
72
  }
71
73
  catch (error) {
72
- throw new CoolifyApiError(0, operation.method, path, `Coolify request failed for ${operation.method} ${path}: ${error instanceof Error ? error.message.replaceAll(token, "[REDACTED]") : "request error"}`);
74
+ const cause = error instanceof Error ? error.message : "request error";
75
+ throw new CoolifyApiError(0, operation.method, path, `Coolify request failed for ${operation.method} ${path}: ${token ? cause.replaceAll(token, "[REDACTED]") : cause}`);
73
76
  }
74
77
  if (!response.ok) {
75
78
  let fields = "";
@@ -78,7 +81,7 @@ export class CoolifyClient {
78
81
  const body = await response.json();
79
82
  const errors = body?.errors;
80
83
  if (errors && typeof errors === "object" && !Array.isArray(errors)) {
81
- fields = Object.keys(errors).filter((key) => /^[a-z][a-z0-9_.-]{0,63}$/iu.test(key)).slice(0, 8).map((key) => key.replaceAll(token, "[REDACTED]")).join(", ");
84
+ fields = Object.keys(errors).filter((key) => /^[a-z][a-z0-9_.-]{0,63}$/iu.test(key)).slice(0, 8).map((key) => token ? key.replaceAll(token, "[REDACTED]") : key).join(", ");
82
85
  }
83
86
  }
84
87
  catch { }
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export interface CoolifyConfig {
2
2
  baseUrl?: string;
3
3
  token?: string;
4
+ selectedVariables?: "COOLIFY_CLOUD_*" | "CURSOR_COOLIFY_*" | "COOLIFY_*" | "none";
4
5
  timeoutMs: number;
5
6
  }
6
7
  export declare class ConfigError extends Error {
package/dist/config.js CHANGED
@@ -21,8 +21,9 @@ export function loadConfig(env = process.env) {
21
21
  const useConfigure = configureComplete || (!canonicalComplete && Boolean(configureUrl || configureToken));
22
22
  const rawUrl = useCloud ? cloudUrl : useConfigure ? configureUrl : canonicalUrl;
23
23
  const token = useCloud ? cloudToken : useConfigure ? configureToken : canonicalToken;
24
+ const selectedVariables = useCloud ? "COOLIFY_CLOUD_*" : useConfigure ? "CURSOR_COOLIFY_*" : canonicalUrl || canonicalToken ? "COOLIFY_*" : "none";
24
25
  if (!rawUrl)
25
- return { ...(token ? { token } : {}), timeoutMs: 30_000 };
26
+ return { ...(token ? { token } : {}), selectedVariables, timeoutMs: 30_000 };
26
27
  let url;
27
28
  try {
28
29
  url = new URL(rawUrl);
@@ -38,5 +39,5 @@ export function loadConfig(env = process.env) {
38
39
  url.pathname = url.pathname.replace(/\/+$/u, "");
39
40
  if (!url.pathname.endsWith("/api/v1"))
40
41
  url.pathname = `${url.pathname}/api/v1`.replace(/^\/\//u, "/");
41
- return { baseUrl: url.toString().replace(/\/$/u, ""), ...(token ? { token } : {}), timeoutMs: 30_000 };
42
+ return { baseUrl: url.toString().replace(/\/$/u, ""), ...(token ? { token } : {}), selectedVariables, timeoutMs: 30_000 };
42
43
  }
package/dist/server.js CHANGED
@@ -30,6 +30,6 @@ export function createServer(config, fetchImpl) {
30
30
  }
31
31
  });
32
32
  }
33
- registerCapabilities(server, client);
33
+ registerCapabilities(server, client, config);
34
34
  return server;
35
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jurislm/coolify-plugin",
3
- "version": "4.0.20",
3
+ "version": "4.0.21",
4
4
  "description": "Portable local stdio MCP plugin for Coolify",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "coolify-plugin",
4
- "version": "4.0.20",
4
+ "version": "4.0.21",
5
5
  "description": "Portable Coolify MCP plugin with local stdio tools generated from the official OpenAPI contract.",
6
6
  "author": {
7
7
  "name": "JurisLM",