@jurislm/coolify-plugin 4.0.20 → 4.0.22
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 +6 -2
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @jurislm/coolify-plugin
|
|
2
2
|
|
|
3
|
-
Portable Coolify MCP plugin. It exposes focused `coolify_*`
|
|
3
|
+
Portable Coolify MCP plugin. It exposes focused `coolify_*` tools generated from one pinned Coolify API contract, plus explicit composite tools for common workflows.
|
|
4
4
|
|
|
5
5
|
## Scope
|
|
6
6
|
|
|
@@ -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.
|
|
@@ -43,7 +47,7 @@ For local agents, a complete saved Configure URL/token pair takes precedence ove
|
|
|
43
47
|
|
|
44
48
|
## OpenAPI contract
|
|
45
49
|
|
|
46
|
-
`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
|
|
50
|
+
`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 server registers every generated operation with its generated schema and adds explicit composite capabilities for connection checks, inventory, diagnostics, and batch operations.
|
|
47
51
|
|
|
48
52
|
```sh
|
|
49
53
|
bun run api:fetch
|
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.22",
|
|
5
5
|
"description": "Portable Coolify MCP plugin with local stdio tools generated from the official OpenAPI contract.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "JurisLM",
|