@jurislm/coolify-plugin 4.0.24 → 4.0.26
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/.mcp.json +1 -1
- package/README.md +23 -1
- package/dist/capabilities.js +16 -8
- package/dist/client.js +24 -15
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +15 -0
- package/dist/index.js +5 -2
- package/dist/server.js +3 -4
- package/launchers/coolify-desktop.zsh +23 -0
- package/package.json +8 -2
- package/plugin.json +1 -1
package/.mcp.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "stdio",
|
|
5
5
|
"command": "bunx",
|
|
6
6
|
"args": ["-y", "@jurislm/coolify-plugin@latest"],
|
|
7
|
-
"env_vars": ["COOLIFY_BASE_URL", "COOLIFY_ACCESS_TOKEN"]
|
|
7
|
+
"env_vars": ["COOLIFY_CLOUD_BASE_URL", "COOLIFY_CLOUD_ACCESS_TOKEN", "CURSOR_COOLIFY_BASE_URL", "CURSOR_COOLIFY_ACCESS_TOKEN", "COOLIFY_BASE_URL", "COOLIFY_ACCESS_TOKEN"]
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
}
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Portable Coolify MCP plugin. It exposes focused `coolify_*` tools generated from
|
|
|
4
4
|
|
|
5
5
|
Official website: [JurisLM Coolify Plugin](https://jurislm.github.io/coolify-plugin/).
|
|
6
6
|
|
|
7
|
+
Follow the shared [JurisLM Plugin Architecture v1](https://github.com/jurislm/woodpecker-ci-plugin/blob/main/docs/plugin-architecture.md) for package, host configuration and acceptance boundaries.
|
|
8
|
+
|
|
7
9
|
## Scope
|
|
8
10
|
|
|
9
11
|
This repository provides a local Codex and Cursor plugin. It runs a stdio MCP server against a user-configured Coolify instance. OpenAI public Plugin Directory submission is outside this scope; public HTTPS, OAuth, and listing requirements are not acceptance criteria for this local plugin.
|
|
@@ -28,13 +30,31 @@ bun dist/index.js
|
|
|
28
30
|
`mcp.json` and `.mcp.json` use the same published-package `bunx` stdio registration as the Woodpecker CI plugin. `.mcp.json.example` contains placeholder environment values only. NPM package release is allowed.
|
|
29
31
|
|
|
30
32
|
For Codex repository marketplace installation, use the repository root and leave the sparse path empty. The supported marketplace manifest is `.agents/plugins/marketplace.json`; do not enter `plugins/codex`.
|
|
31
|
-
The Codex
|
|
33
|
+
The native Codex registration forwards `COOLIFY_CLOUD_BASE_URL`, `COOLIFY_CLOUD_ACCESS_TOKEN`, `CURSOR_COOLIFY_BASE_URL`, `CURSOR_COOLIFY_ACCESS_TOKEN`, `COOLIFY_BASE_URL` and `COOLIFY_ACCESS_TOKEN` from its owning environment. Portable `mcp.json` contains no host-specific fields or credential defaults.
|
|
32
34
|
|
|
33
35
|
```sh
|
|
34
36
|
codex plugin marketplace add https://github.com/jurislm/coolify-plugin
|
|
35
37
|
codex plugin add coolify-plugin@coolify-marketplace
|
|
36
38
|
```
|
|
37
39
|
|
|
40
|
+
### Codex desktop on macOS
|
|
41
|
+
|
|
42
|
+
A Codex process launched from the Dock or Finder does not read zsh startup files. If your connection settings are exported by `.zshenv`, copy [`launchers/coolify-desktop.zsh`](launchers/coolify-desktop.zsh) to `~/.codex/bin/coolify-mcp.zsh`, then add this opt-in override to `~/.codex/config.toml`:
|
|
43
|
+
|
|
44
|
+
```toml
|
|
45
|
+
[plugins."coolify-plugin@coolify-marketplace".mcp_servers.coolify]
|
|
46
|
+
enabled = false
|
|
47
|
+
|
|
48
|
+
[mcp_servers.coolify]
|
|
49
|
+
command = "/bin/zsh"
|
|
50
|
+
args = ["-f", "-c", 'source "$HOME/.codex/bin/coolify-mcp.zsh"']
|
|
51
|
+
startup_timeout_sec = 30
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The launcher checks startup-file syntax before explicitly sourcing `${ZDOTDIR:-$HOME}/.zshenv`, suppresses stdout and preserves stderr. Source status 0 or 1 is accepted because a final optional guard can return 1; higher statuses stop startup. It retains only the six supported connection variables, `HOME`, `PATH`, `TMPDIR` and `LANG`, fixes PATH to `$HOME/.bun/bin:/usr/bin:/bin` and executes the absolute `$HOME/.bun/bin/bunx` path. The `--require-config` option reuses `loadConfig` to require the selected complete URL/token pair before starting stdio; trim, placeholder handling and pair precedence remain the same. Ordinary portable initialization and tool discovery can still start without credentials.
|
|
55
|
+
|
|
56
|
+
Check `codex mcp get coolify`, restart Codex and open a fresh chat. Read `coolify_get_mcp_version` before connection checks and authenticated reads. Record the resolved package version, actual registration source, HTTP status and returned counts. Standalone stdio checks establish process behavior; fresh chat registration and provider reads require their own evidence.
|
|
57
|
+
|
|
38
58
|
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.
|
|
39
59
|
|
|
40
60
|
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`.
|
|
@@ -60,7 +80,9 @@ bun run api:check
|
|
|
60
80
|
## Checks
|
|
61
81
|
|
|
62
82
|
```sh
|
|
83
|
+
bun run check
|
|
63
84
|
bun run api:check
|
|
85
|
+
bun run lint
|
|
64
86
|
bun run typecheck
|
|
65
87
|
bun test
|
|
66
88
|
bun run build
|
package/dist/capabilities.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { CoolifyApiError, redactSensitive } from "./client.js";
|
|
3
|
+
import { errorMessage as safeErrorMessage } from "./errors.js";
|
|
3
4
|
import { operations } from "./generated/operations.js";
|
|
4
5
|
import { pluginVersion } from "./version.js";
|
|
5
6
|
const outputSchema = z.object({ data: z.unknown(), status: z.number(), request: z.object({ method: z.string(), path: z.string() }) });
|
|
@@ -13,7 +14,6 @@ function rows(value) {
|
|
|
13
14
|
function status(value) { return typeof value.status === "string" ? value.status : ""; }
|
|
14
15
|
function running(value) { return /(^|:)running($|:)/iu.test(status(value)); }
|
|
15
16
|
function unhealthy(value) { return /exited|unhealthy|error|stopped/iu.test(status(value)); }
|
|
16
|
-
function errorMessage(value) { return value instanceof Error ? value.message : String(value); }
|
|
17
17
|
function truncateLogs(logs, lineLimit = 200, charLimit = 50_000) {
|
|
18
18
|
if (logs.split("\n").length <= lineLimit && logs.length <= charLimit)
|
|
19
19
|
return logs;
|
|
@@ -28,6 +28,7 @@ function environmentSummary(value) {
|
|
|
28
28
|
return rows(value).map((item) => ({ key: item.key, is_build_time: item.is_buildtime === true }));
|
|
29
29
|
}
|
|
30
30
|
export function registerCapabilities(server, client, config) {
|
|
31
|
+
const errorMessage = (error) => safeErrorMessage(error, [config.token]);
|
|
31
32
|
const call = async (name, input = {}) => {
|
|
32
33
|
const operation = operations.find((item) => item.name === name);
|
|
33
34
|
if (!operation)
|
|
@@ -40,7 +41,14 @@ export function registerCapabilities(server, client, config) {
|
|
|
40
41
|
return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
41
42
|
};
|
|
42
43
|
const register = (name, description, inputSchema, annotations, handler) => {
|
|
43
|
-
server.registerTool(name, { title: description, description, inputSchema, outputSchema, annotations }, async (input) =>
|
|
44
|
+
server.registerTool(name, { title: description, description, inputSchema, outputSchema, annotations }, async (input) => {
|
|
45
|
+
try {
|
|
46
|
+
return result(name, await handler(input));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return { isError: true, content: [{ type: "text", text: errorMessage(error) }] };
|
|
50
|
+
}
|
|
51
|
+
});
|
|
44
52
|
};
|
|
45
53
|
const settledRows = async (names) => {
|
|
46
54
|
const settled = await Promise.allSettled(names.map((name) => call(`coolify_list_${name}`)));
|
|
@@ -100,7 +108,7 @@ export function registerCapabilities(server, client, config) {
|
|
|
100
108
|
const environmentIds = new Set(rows(environments).map((environment) => environment.id));
|
|
101
109
|
return rows(apps).filter((app) => environmentIds.has(app.environment_id));
|
|
102
110
|
};
|
|
103
|
-
register("coolify_get_mcp_version", "Get the local Coolify plugin version.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
111
|
+
register("coolify_get_mcp_version", "Get the local Coolify plugin version.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, () => Promise.resolve({ name: "@jurislm/coolify-plugin", version: pluginVersion }));
|
|
104
112
|
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
113
|
const probe = async (name) => {
|
|
106
114
|
const operation = operations.find((item) => item.name === name);
|
|
@@ -126,7 +134,7 @@ export function registerCapabilities(server, client, config) {
|
|
|
126
134
|
return { application: null, health: { status: "unknown", issues: [] }, logs: null, environment_variables: { count: 0, variables: [] }, recent_deployments: [], errors: [errorMessage(error)] };
|
|
127
135
|
}
|
|
128
136
|
if (!app?.uuid)
|
|
129
|
-
return { application: null, health: { status: "unknown", issues: [] }, logs: null, environment_variables: { count: 0, variables: [] }, recent_deployments: [], errors: [`No application found matching ${query}`] };
|
|
137
|
+
return { application: null, health: { status: "unknown", issues: [] }, logs: null, environment_variables: { count: 0, variables: [] }, recent_deployments: [], errors: [`No application found matching ${String(query)}`] };
|
|
130
138
|
const names = ["application", "logs", "environment_variables", "deployments"];
|
|
131
139
|
const settled = await Promise.allSettled([
|
|
132
140
|
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 }),
|
|
@@ -166,7 +174,7 @@ export function registerCapabilities(server, client, config) {
|
|
|
166
174
|
return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], errors: [errorMessage(error)] };
|
|
167
175
|
}
|
|
168
176
|
if (!found?.uuid)
|
|
169
|
-
return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], errors: [`No server found matching ${query}`] };
|
|
177
|
+
return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], errors: [`No server found matching ${String(query)}`] };
|
|
170
178
|
const names = ["server", "resources", "domains"];
|
|
171
179
|
const settled = await Promise.allSettled([
|
|
172
180
|
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 }),
|
|
@@ -198,7 +206,7 @@ export function registerCapabilities(server, client, config) {
|
|
|
198
206
|
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 })));
|
|
199
207
|
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 }) => {
|
|
200
208
|
if (!app_uuids.length)
|
|
201
|
-
return batch([],
|
|
209
|
+
return batch([], () => Promise.resolve(undefined));
|
|
202
210
|
const names = rows(await call("coolify_list_applications"));
|
|
203
211
|
const labels = new Map(names.map((item) => [item.uuid, item.name ?? item.uuid]));
|
|
204
212
|
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 } }));
|
|
@@ -214,7 +222,7 @@ export function registerCapabilities(server, client, config) {
|
|
|
214
222
|
return { environment: null, missing_database_types: [...databaseTypes], ...(errors.length ? { errors } : {}) };
|
|
215
223
|
const matching = databases.filter((database) => environment.id !== undefined && database.environment_id === environment.id);
|
|
216
224
|
const byType = new Map();
|
|
217
|
-
matching.forEach((database) => { const type =
|
|
225
|
+
matching.forEach((database) => { const type = typeof database.database_type === "string" ? database.database_type.toLowerCase() : ""; for (const expected of databaseTypes)
|
|
218
226
|
if (type.includes(expected))
|
|
219
227
|
byType.set(expected, [...(byType.get(expected) ?? []), database]); });
|
|
220
228
|
const missing = databaseTypes.filter((type) => !byType.has(type));
|
|
@@ -223,6 +231,6 @@ export function registerCapabilities(server, client, config) {
|
|
|
223
231
|
register("coolify_docker_network_alias", "Generate Docker network alias remediation commands.", z.object({ server_uuid: z.string().regex(/^[a-zA-Z0-9-]{1,64}$/u), db_uuid: z.string().regex(/^[a-zA-Z0-9-]{1,64}$/u), name: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/u), network: z.string().optional() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ server_uuid, db_uuid, name, network = "coolify" }) => {
|
|
224
232
|
const server = await call("coolify_get_server_by_uuid", { uuid: server_uuid }).catch(() => null);
|
|
225
233
|
const quote = (value) => `'${value.replace(/'/gu, "'\\''")}'`;
|
|
226
|
-
return { bug: "Coolify database containers may require a friendly Docker network alias.", warning: "The alias is lost after a rebuild or redeploy.", ...(server ? { server: { ip: server.ip, user: server.user, port: server.port } } : { server_lookup_failed: "Could not retrieve server details" }), commands: { ssh_connect: server ? `ssh -p ${server.port} ${server.user}@${server.ip}` : "ssh -p <port> <user>@<server-ip>", add_alias: [`docker network disconnect ${quote(String(network))} ${quote(String(db_uuid))}`, `docker network connect ${quote(String(network))} ${quote(String(db_uuid))} --alias ${quote(String(name))} --alias ${quote(String(db_uuid))}`], verify: `docker exec <any-app-container> getent hosts ${quote(String(name))}` }, next_actions: [{ tool: "coolify_get_database_by_uuid", args: { uuid: db_uuid }, hint: "Check database status" }] };
|
|
234
|
+
return { bug: "Coolify database containers may require a friendly Docker network alias.", warning: "The alias is lost after a rebuild or redeploy.", ...(server ? { server: { ip: server.ip, user: server.user, port: server.port } } : { server_lookup_failed: "Could not retrieve server details" }), commands: { ssh_connect: server ? `ssh -p ${String(server.port)} ${String(server.user)}@${String(server.ip)}` : "ssh -p <port> <user>@<server-ip>", add_alias: [`docker network disconnect ${quote(String(network))} ${quote(String(db_uuid))}`, `docker network connect ${quote(String(network))} ${quote(String(db_uuid))} --alias ${quote(String(name))} --alias ${quote(String(db_uuid))}`], verify: `docker exec <any-app-container> getent hosts ${quote(String(name))}` }, next_actions: [{ tool: "coolify_get_database_by_uuid", args: { uuid: db_uuid }, hint: "Check database status" }] };
|
|
227
235
|
});
|
|
228
236
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
import { CoolifyApiError } from "./errors.js";
|
|
1
|
+
import { CoolifyApiError, redactErrorText } from "./errors.js";
|
|
2
2
|
const sensitiveKey = /(^value$|real_?value|private_?key|api_?key|user_?key|webhook_?url|token|secret|password|authorization|cookie)/iu;
|
|
3
3
|
export function redactSensitive(value) {
|
|
4
4
|
if (Array.isArray(value))
|
|
5
5
|
return value.map(redactSensitive);
|
|
6
6
|
if (!value || typeof value !== "object")
|
|
7
7
|
return value;
|
|
8
|
+
const binary = value.encoding === "base64" && typeof value.contentType === "string" && typeof value.value === "string";
|
|
8
9
|
return Object.fromEntries(Object.entries(value).map(([key, child]) => [
|
|
9
10
|
key,
|
|
10
|
-
sensitiveKey.test(key) ? "[REDACTED]" : redactSensitive(child),
|
|
11
|
+
sensitiveKey.test(key) && !(binary && key === "value") ? "[REDACTED]" : redactSensitive(child),
|
|
11
12
|
]));
|
|
12
13
|
}
|
|
13
14
|
function pathValue(value, name) {
|
|
14
15
|
if (value === undefined || value === null)
|
|
15
16
|
throw new Error(`Missing required path parameter: ${name}`);
|
|
16
|
-
return encodeURIComponent(
|
|
17
|
+
return encodeURIComponent(stringifyScalar(value));
|
|
17
18
|
}
|
|
19
|
+
function stringifyScalar(value) { return String(value); }
|
|
18
20
|
function appendQuery(url, name, value) {
|
|
19
21
|
if (value === undefined || value === null)
|
|
20
22
|
return;
|
|
21
23
|
if (Array.isArray(value))
|
|
22
24
|
for (const item of value)
|
|
23
|
-
url.searchParams.append(name,
|
|
25
|
+
url.searchParams.append(name, stringifyScalar(item));
|
|
24
26
|
else
|
|
25
|
-
url.searchParams.set(name, typeof value === "object" ? JSON.stringify(value) :
|
|
27
|
+
url.searchParams.set(name, typeof value === "object" ? JSON.stringify(value) : stringifyScalar(value));
|
|
26
28
|
}
|
|
27
29
|
function base64(bytes) {
|
|
28
30
|
let text = "";
|
|
@@ -72,7 +74,7 @@ export class CoolifyClient {
|
|
|
72
74
|
}
|
|
73
75
|
catch (error) {
|
|
74
76
|
const cause = error instanceof Error ? error.message : "request error";
|
|
75
|
-
throw new CoolifyApiError(0, operation.method, path, `Coolify request failed for ${operation.method} ${path}: ${
|
|
77
|
+
throw new CoolifyApiError(0, operation.method, path, `Coolify request failed for ${operation.method} ${path}: ${redactErrorText(cause, [token])}`);
|
|
76
78
|
}
|
|
77
79
|
if (!response.ok) {
|
|
78
80
|
let fields = "";
|
|
@@ -84,19 +86,26 @@ export class CoolifyClient {
|
|
|
84
86
|
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(", ");
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
|
-
catch {
|
|
89
|
+
catch {
|
|
90
|
+
fields = "";
|
|
91
|
+
}
|
|
88
92
|
}
|
|
89
93
|
throw new CoolifyApiError(response.status, operation.method, path, `Coolify API returned ${response.status} for ${operation.method} ${path}${fields ? `; fields: ${fields}` : ""}`);
|
|
90
94
|
}
|
|
91
95
|
let data = null;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
try {
|
|
97
|
+
if (response.status !== 204) {
|
|
98
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
99
|
+
if (contentType.includes("json"))
|
|
100
|
+
data = await response.json();
|
|
101
|
+
else if (contentType.startsWith("text/") || contentType.includes("xml"))
|
|
102
|
+
data = await response.text();
|
|
103
|
+
else
|
|
104
|
+
data = { encoding: "base64", contentType: contentType || "application/octet-stream", value: base64(await response.arrayBuffer()) };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new Error("Coolify API returned an unreadable response body");
|
|
100
109
|
}
|
|
101
110
|
return redactSensitive({ data, status: response.status, request: { method: operation.method, path } });
|
|
102
111
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -4,3 +4,18 @@ export declare class CoolifyApiError extends Error {
|
|
|
4
4
|
readonly path: string;
|
|
5
5
|
constructor(status: number, method: string, path: string, message: string);
|
|
6
6
|
}
|
|
7
|
+
export declare function redactErrorText(message: string, tokens?: Array<string | undefined>): string;
|
|
8
|
+
export declare function errorMessage(error: unknown, tokens?: Array<string | undefined>): string;
|
|
9
|
+
export declare function formatToolError(error: unknown, tokens: Array<string | undefined>): {
|
|
10
|
+
code: string;
|
|
11
|
+
status: number;
|
|
12
|
+
method: string;
|
|
13
|
+
path: string;
|
|
14
|
+
message: string;
|
|
15
|
+
} | {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
status?: undefined;
|
|
19
|
+
method?: undefined;
|
|
20
|
+
path?: undefined;
|
|
21
|
+
};
|
package/dist/errors.js
CHANGED
|
@@ -10,3 +10,18 @@ export class CoolifyApiError extends Error {
|
|
|
10
10
|
this.name = "CoolifyApiError";
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
export function redactErrorText(message, tokens = []) {
|
|
14
|
+
let redacted = message;
|
|
15
|
+
for (const token of tokens)
|
|
16
|
+
if (token)
|
|
17
|
+
redacted = redacted.replaceAll(token, "[REDACTED]");
|
|
18
|
+
return redacted.replace(/Bearer\s+[^\s,;]+/giu, "Bearer [REDACTED]");
|
|
19
|
+
}
|
|
20
|
+
export function errorMessage(error, tokens = []) {
|
|
21
|
+
return redactErrorText(error instanceof Error ? error.message : String(error), tokens);
|
|
22
|
+
}
|
|
23
|
+
export function formatToolError(error, tokens) {
|
|
24
|
+
return error instanceof CoolifyApiError
|
|
25
|
+
? { code: "COOLIFY_API_ERROR", status: error.status, method: error.method, path: error.path, message: errorMessage(error, tokens) }
|
|
26
|
+
: { code: "COOLIFY_TOOL_ERROR", message: errorMessage(error, tokens) };
|
|
27
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { loadConfig } from "./config.js";
|
|
2
|
+
import { ConfigError, loadConfig } from "./config.js";
|
|
3
3
|
import { createServer } from "./server.js";
|
|
4
4
|
import { createStdioTransport } from "./transports/stdio.js";
|
|
5
5
|
try {
|
|
6
|
-
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
if (process.argv.includes("--require-config") && (!config.baseUrl || !config.token))
|
|
8
|
+
throw new ConfigError("A complete URL/token pair is required for the selected Coolify configuration");
|
|
9
|
+
await createServer(config).connect(createStdioTransport());
|
|
7
10
|
}
|
|
8
11
|
catch (error) {
|
|
9
12
|
console.error(`Coolify MCP server failed: ${error instanceof Error ? error.message : String(error)}`);
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { CoolifyClient, redactSensitive } from "./client.js";
|
|
4
|
+
import { formatToolError } from "./errors.js";
|
|
4
5
|
import { registerCapabilities } from "./capabilities.js";
|
|
5
6
|
import { operations } from "./generated/operations.js";
|
|
6
7
|
import { pluginVersion } from "./version.js";
|
|
@@ -23,9 +24,7 @@ export function createServer(config, fetchImpl) {
|
|
|
23
24
|
return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
24
25
|
}
|
|
25
26
|
catch (error) {
|
|
26
|
-
const details = error
|
|
27
|
-
? { code: "COOLIFY_API_ERROR", status: error.status, method: error.method, path: error.path, message: error.message }
|
|
28
|
-
: { code: "COOLIFY_TOOL_ERROR", message: error instanceof Error ? error.message : String(error) };
|
|
27
|
+
const details = formatToolError(error, [config.token]);
|
|
29
28
|
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: details }) }] };
|
|
30
29
|
}
|
|
31
30
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/zsh -f
|
|
2
|
+
startup_file="${ZDOTDIR:-$HOME}/.zshenv"
|
|
3
|
+
if [[ -r $startup_file ]]; then
|
|
4
|
+
/bin/zsh -f -n "$startup_file" >/dev/null || exit $?
|
|
5
|
+
startup_status=0
|
|
6
|
+
source "$startup_file" >/dev/null || startup_status=$?
|
|
7
|
+
if (( startup_status > 1 )); then
|
|
8
|
+
exit "$startup_status"
|
|
9
|
+
fi
|
|
10
|
+
fi
|
|
11
|
+
zmodload zsh/parameter
|
|
12
|
+
export PATH="$HOME/.bun/bin:/usr/bin:/bin"
|
|
13
|
+
for name in ${(k)parameters}; do
|
|
14
|
+
case "$name" in
|
|
15
|
+
HOME|PATH|TMPDIR|LANG|COOLIFY_CLOUD_BASE_URL|COOLIFY_CLOUD_ACCESS_TOKEN|CURSOR_COOLIFY_BASE_URL|CURSOR_COOLIFY_ACCESS_TOKEN|COOLIFY_BASE_URL|COOLIFY_ACCESS_TOKEN) ;;
|
|
16
|
+
*)
|
|
17
|
+
if [[ ${parameters[$name]} == *export* ]]; then
|
|
18
|
+
unset "$name"
|
|
19
|
+
fi
|
|
20
|
+
;;
|
|
21
|
+
esac
|
|
22
|
+
done
|
|
23
|
+
exec "$HOME/.bun/bin/bunx" -y @jurislm/coolify-plugin@latest --require-config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jurislm/coolify-plugin",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.26",
|
|
4
4
|
"description": "Portable local stdio MCP plugin for Coolify",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
|
+
"launchers",
|
|
14
15
|
"api",
|
|
15
16
|
"openapi",
|
|
16
17
|
"assets",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"clean": "rm -rf dist",
|
|
32
33
|
"build": "bun run clean && tsc",
|
|
33
34
|
"start": "bun dist/index.js",
|
|
35
|
+
"lint": "bun ./node_modules/eslint/bin/eslint.js src scripts --max-warnings=0",
|
|
34
36
|
"typecheck": "tsc --noEmit",
|
|
35
37
|
"typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
|
|
36
38
|
"test": "bun test --path-ignore-patterns dist",
|
|
@@ -40,7 +42,7 @@
|
|
|
40
42
|
"manifest:check": "bun scripts/validate-plugin-manifests.ts",
|
|
41
43
|
"package:check": "bun pm pack --dry-run 2>&1 | bun scripts/package-contents-check.ts",
|
|
42
44
|
"release:test": "bun scripts/ci/test-release-pr-auto-merge.ts && bun scripts/ci/test-release-workflows.ts",
|
|
43
|
-
"check": "bun run manifest:check && bun run api:check && bun run typecheck && bun run typecheck:tests && bun run build && bun test && bun run release:test && bun pm pack --dry-run 2>&1 | bun scripts/package-contents-check.ts",
|
|
45
|
+
"check": "bun run manifest:check && bun run api:check && bun run lint && bun run typecheck && bun run typecheck:tests && bun run build && bun test && bun run release:test && bun pm pack --dry-run 2>&1 | bun scripts/package-contents-check.ts",
|
|
44
46
|
"prepublishOnly": "bun run check"
|
|
45
47
|
},
|
|
46
48
|
"dependencies": {
|
|
@@ -48,10 +50,14 @@
|
|
|
48
50
|
"zod": "4.6.5"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
53
|
+
"@eslint/js": "10.0.1",
|
|
51
54
|
"@types/bun": "1.4.2",
|
|
55
|
+
"ajv": "8.20.0",
|
|
56
|
+
"eslint": "10.11.0",
|
|
52
57
|
"json-schema-to-zod": "2.8.1",
|
|
53
58
|
"openapi-typescript": "7.13.0",
|
|
54
59
|
"typescript": "5.9.3",
|
|
60
|
+
"typescript-eslint": "8.70.1",
|
|
55
61
|
"yaml": "2.9.1"
|
|
56
62
|
},
|
|
57
63
|
"packageManager": "bun@1.3.14"
|
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.26",
|
|
5
5
|
"description": "Portable Coolify MCP plugin with local stdio tools generated from the official OpenAPI contract.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "JurisLM",
|