@jurislm/coolify-plugin 4.0.19 → 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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +4 -0
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +28 -5
- package/dist/client.js +7 -4
- package/dist/config.d.ts +1 -0
- package/dist/config.js +3 -2
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
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.
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { CoolifyClient } from "./client.js";
|
|
3
|
-
|
|
3
|
+
import type { CoolifyConfig } from "./config.js";
|
|
4
|
+
export declare function registerCapabilities(server: McpServer, client: CoolifyClient, config: CoolifyConfig): void;
|
package/dist/capabilities.js
CHANGED
|
@@ -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, */*"
|
|
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
|
-
|
|
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
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
package/package.json
CHANGED
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.
|
|
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",
|