@jurislm/coolify-plugin 4.0.29 → 4.2.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.
- package/.codex-plugin/plugin.json +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.js +21 -130
- package/dist/server.js +3 -3
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/coolify/SKILL.md +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -16,7 +16,8 @@ export interface BinaryEnvelope {
|
|
|
16
16
|
}
|
|
17
17
|
export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
18
18
|
type Operation = Pick<GeneratedOperation, "method" | "path" | "parameters">;
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function isEnvOperation(path: string): boolean;
|
|
20
|
+
export declare function redactSensitive<T>(value: T, tokens?: Array<string | undefined>, envResponse?: boolean): T;
|
|
20
21
|
export declare class CoolifyClient {
|
|
21
22
|
private readonly config;
|
|
22
23
|
private readonly fetchImpl;
|
package/dist/client.js
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
|
-
import { isAlias, isScalar, parseDocument, stringify, visit } from "yaml";
|
|
2
1
|
import { CoolifyApiError, redactErrorText } from "./errors.js";
|
|
3
2
|
const sensitiveKey = /(^value$|real_?value|private_?key|api_?key|user_?key|webhook_?url|token|secret|password|authorization|cookie)/iu;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
3
|
+
const envValueKey = /^(value|real_value)$/u;
|
|
4
|
+
const envPath = /\/envs(\/|$)/u;
|
|
5
|
+
const renderedComposeKey = "docker_compose";
|
|
7
6
|
const interpolation = /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/u;
|
|
8
7
|
const urlCredential = /([a-z][a-z0-9+.-]{0,31}:\/\/[^\s:/@]{0,256}):([^\s/?#]{1,256})@/giu;
|
|
9
8
|
const queryParameter = /([?&])([^=&#?\s"']{1,64})=([^&#\s"']+)/gu;
|
|
10
9
|
const querySecretSegments = new Set(["password", "passwd", "pass", "pwd", "secret", "token", "auth", "sig", "signature", "credential", "credentials", "apikey", "key"]);
|
|
11
10
|
const querySecretName = /(password|passwd|secret|token|api_?key|access_?key)/iu;
|
|
12
|
-
const flagValue = /^(true|false|yes|no|on|off|enabled|disabled|none)$/iu;
|
|
13
|
-
const assignment = /^([\w.-]+)=([\s\S]*)$/u;
|
|
14
|
-
const base64Text = /^[A-Za-z0-9+/]+={0,2}$/u;
|
|
15
|
-
function isComposeSecret(name, value) {
|
|
16
|
-
if (!composeSecretName.test(name) && !composeSecretSegment.test(name))
|
|
17
|
-
return false;
|
|
18
|
-
const text = typeof value === "string" ? value : typeof value === "number" ? String(value) : "";
|
|
19
|
-
return text.trim() !== "" && !interpolation.test(text) && !flagValue.test(text.trim());
|
|
20
|
-
}
|
|
21
11
|
function redactUrlCredentials(text) {
|
|
22
12
|
return text
|
|
23
13
|
.replace(urlCredential, (match, prefix, password) => interpolation.test(password) ? match : `${prefix}:[REDACTED]@`)
|
|
@@ -26,129 +16,30 @@ function redactUrlCredentials(text) {
|
|
|
26
16
|
function isQuerySecret(name) {
|
|
27
17
|
return querySecretName.test(name) || name.toLowerCase().split(/[_.-]/u).some((segment) => querySecretSegments.has(segment));
|
|
28
18
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const portValue = /^[\d.:[\]]+(\/(tcp|udp|sctp))?$/iu;
|
|
32
|
-
const envOption = /(^|\s)(-e|--env)(=|\s+)(?:"([A-Za-z_][\w.]{0,127})=([^"\n]{0,4096})"|'([A-Za-z_][\w.]{0,127})=([^'\n]{0,4096})'|([A-Za-z_][\w.]{0,127})=("[^"\n]{0,4096}"|'[^'\n]{0,4096}'|[^\s"']*))/gu;
|
|
33
|
-
const flagEquals = /(^|\s)(--?[A-Za-z][\w-]{0,63})=("[^"\n]{0,4096}"|'[^'\n]{0,4096}'|[^\s"']+)/gu;
|
|
34
|
-
const flagSpace = /(^|\s)(--?[A-Za-z][\w-]{0,63})(\s+)("[^"\n]{0,4096}"|'[^'\n]{0,4096}'|[^\s"'-][^\s"']*)/gu;
|
|
35
|
-
function unquote(value) {
|
|
36
|
-
return value.replace(/^(["'])([\s\S]*)\1$/u, "$2");
|
|
37
|
-
}
|
|
38
|
-
function quoteOf(value) {
|
|
39
|
-
return value.startsWith("\"") || value.startsWith("'") ? value[0] : "";
|
|
40
|
-
}
|
|
41
|
-
function isSecretFlag(flag) {
|
|
42
|
-
if (flag === "-p")
|
|
43
|
-
return true;
|
|
44
|
-
if (!flag.startsWith("--"))
|
|
45
|
-
return false;
|
|
46
|
-
const name = flag.slice(2).replace(/-/gu, "_");
|
|
47
|
-
return composeSecretName.test(name) || composeSecretSegment.test(name) || secretFlagName.test(name);
|
|
48
|
-
}
|
|
49
|
-
function isSecretFlagValue(flag, value) {
|
|
50
|
-
const bare = unquote(value);
|
|
51
|
-
if (bare === "" || bare === "[REDACTED]" || interpolation.test(bare) || flagValue.test(bare))
|
|
52
|
-
return false;
|
|
53
|
-
return flag !== "-p" || !portValue.test(bare);
|
|
54
|
-
}
|
|
55
|
-
function redactCommandLine(text) {
|
|
56
|
-
return text
|
|
57
|
-
.replace(envOption, (match, lead, flag, separator, doubleKey, _doubleValue, singleKey, _singleValue, key, value) => {
|
|
58
|
-
if (doubleKey !== undefined)
|
|
59
|
-
return isComposeSecret(doubleKey, _doubleValue) ? `${lead}${flag}${separator}"${doubleKey}=[REDACTED]"` : match;
|
|
60
|
-
if (singleKey !== undefined)
|
|
61
|
-
return isComposeSecret(singleKey, _singleValue) ? `${lead}${flag}${separator}'${singleKey}=[REDACTED]'` : match;
|
|
62
|
-
if (key === undefined || value === undefined)
|
|
63
|
-
return match;
|
|
64
|
-
const quote = quoteOf(value);
|
|
65
|
-
return isComposeSecret(key, unquote(value)) ? `${lead}${flag}${separator}${key}=${quote}[REDACTED]${quote}` : match;
|
|
66
|
-
})
|
|
67
|
-
.replace(flagEquals, (match, lead, flag, value) => isSecretFlag(flag) && isSecretFlagValue(flag, value) ? `${lead}${flag}=${quoteOf(value)}[REDACTED]${quoteOf(value)}` : match)
|
|
68
|
-
.replace(flagSpace, (match, lead, flag, space, value) => isSecretFlag(flag) && isSecretFlagValue(flag, value) ? `${lead}${flag}${space}${quoteOf(value)}[REDACTED]${quoteOf(value)}` : match);
|
|
69
|
-
}
|
|
70
|
-
function redactCommand(value) {
|
|
71
|
-
if (typeof value === "string")
|
|
72
|
-
return redactCommandLine(value);
|
|
73
|
-
if (!Array.isArray(value))
|
|
74
|
-
return value;
|
|
75
|
-
return value.map((token, index) => {
|
|
76
|
-
if (typeof token !== "string")
|
|
77
|
-
return token;
|
|
78
|
-
const flag = value[index - 1];
|
|
79
|
-
return typeof flag === "string" && !flag.includes("=") && isSecretFlag(flag) && isSecretFlagValue(flag, token) ? "[REDACTED]" : redactCommandLine(token);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
const composeAliasLimit = 100;
|
|
83
|
-
function redactComposeValue(value) {
|
|
84
|
-
if (Array.isArray(value))
|
|
85
|
-
return value.map(redactComposeValue);
|
|
86
|
-
if (value && typeof value === "object") {
|
|
87
|
-
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, isComposeSecret(key, child) ? "[REDACTED]" : redactComposeValue(commandKey.test(key) ? redactCommand(child) : child)]));
|
|
88
|
-
}
|
|
89
|
-
if (typeof value !== "string")
|
|
90
|
-
return value;
|
|
91
|
-
const match = assignment.exec(value);
|
|
92
|
-
return match && isComposeSecret(match[1], match[2]) ? `${match[1]}=[REDACTED]` : redactUrlCredentials(value);
|
|
93
|
-
}
|
|
94
|
-
function redactComposeYaml(text) {
|
|
95
|
-
if (text.trim() === "" || text === "[REDACTED]")
|
|
96
|
-
return text;
|
|
97
|
-
const document = parseDocument(text, { schema: "failsafe", merge: true });
|
|
98
|
-
if (document.errors.length > 0)
|
|
99
|
-
return "[REDACTED]";
|
|
100
|
-
let parsed;
|
|
101
|
-
let rendered;
|
|
102
|
-
try {
|
|
103
|
-
const secrets = [];
|
|
104
|
-
visit(document, {
|
|
105
|
-
Pair(_key, pair) {
|
|
106
|
-
const key = isAlias(pair.key) ? pair.key.resolve(document) : pair.key;
|
|
107
|
-
const value = isAlias(pair.value) ? pair.value.resolve(document) : pair.value;
|
|
108
|
-
if (isScalar(key) && isScalar(value) && isComposeSecret(String(key.value), value.value))
|
|
109
|
-
secrets.push(value);
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
for (const node of secrets)
|
|
113
|
-
node.value = "[REDACTED]";
|
|
114
|
-
parsed = document.toJS({ maxAliasCount: composeAliasLimit });
|
|
115
|
-
rendered = stringify(redactComposeValue(parsed), { lineWidth: 0 });
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return "[REDACTED]";
|
|
119
|
-
}
|
|
120
|
-
return text.endsWith("\n") ? rendered : rendered.replace(/\n$/u, "");
|
|
19
|
+
function withoutTokens(text, tokens) {
|
|
20
|
+
return tokens.reduce((redacted, token) => token ? redacted.replaceAll(token, "[REDACTED]") : redacted, text);
|
|
121
21
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
return undefined;
|
|
125
|
-
const bytes = Buffer.from(text, "base64");
|
|
126
|
-
if (bytes.toString("base64") !== text)
|
|
127
|
-
return undefined;
|
|
128
|
-
try {
|
|
129
|
-
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
130
|
-
return /[\n:]/u.test(decoded) ? decoded : undefined;
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
22
|
+
export function isEnvOperation(path) {
|
|
23
|
+
return envPath.test(path);
|
|
135
24
|
}
|
|
136
|
-
function
|
|
137
|
-
const decoded = decodeBase64Text(text);
|
|
138
|
-
return decoded === undefined ? redactComposeYaml(text) : Buffer.from(redactComposeYaml(decoded), "utf8").toString("base64");
|
|
139
|
-
}
|
|
140
|
-
export function redactSensitive(value) {
|
|
25
|
+
export function redactSensitive(value, tokens = [], envResponse = false) {
|
|
141
26
|
if (typeof value === "string")
|
|
142
|
-
return redactUrlCredentials(value);
|
|
27
|
+
return withoutTokens(redactUrlCredentials(value), tokens);
|
|
143
28
|
if (Array.isArray(value))
|
|
144
|
-
return value.map(redactSensitive);
|
|
29
|
+
return value.map((item) => redactSensitive(item, tokens, envResponse));
|
|
145
30
|
if (!value || typeof value !== "object")
|
|
146
31
|
return value;
|
|
147
32
|
const binary = value.encoding === "base64" && typeof value.contentType === "string" && typeof value.value === "string";
|
|
148
|
-
return Object.fromEntries(Object.entries(value).map(([key, child]) => [
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
33
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => key !== renderedComposeKey).map(([key, child]) => [key, redactField(key, child, tokens, envResponse, binary)]));
|
|
34
|
+
}
|
|
35
|
+
function redactField(key, child, tokens, envResponse, binary) {
|
|
36
|
+
if (binary && key === "value")
|
|
37
|
+
return child;
|
|
38
|
+
if (envResponse && envValueKey.test(key))
|
|
39
|
+
return typeof child === "string" ? withoutTokens(child, tokens) : child;
|
|
40
|
+
if (sensitiveKey.test(key))
|
|
41
|
+
return "[REDACTED]";
|
|
42
|
+
return redactSensitive(child, tokens, envResponse);
|
|
152
43
|
}
|
|
153
44
|
function pathValue(value, name) {
|
|
154
45
|
if (value === undefined || value === null)
|
|
@@ -246,7 +137,7 @@ export class CoolifyClient {
|
|
|
246
137
|
catch {
|
|
247
138
|
throw new Error("Coolify API returned an unreadable response body");
|
|
248
139
|
}
|
|
249
|
-
return redactSensitive({ data, status: response.status, request: { method: operation.method, path } });
|
|
140
|
+
return redactSensitive({ data, status: response.status, request: { method: operation.method, path } }, [token], isEnvOperation(operation.path));
|
|
250
141
|
}
|
|
251
142
|
}
|
|
252
143
|
export { CoolifyApiError };
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { CoolifyClient, redactSensitive } from "./client.js";
|
|
3
|
+
import { CoolifyClient, isEnvOperation, redactSensitive } from "./client.js";
|
|
4
4
|
import { formatToolError } from "./errors.js";
|
|
5
5
|
import { registerCapabilities } from "./capabilities.js";
|
|
6
6
|
import { operations } from "./generated/operations.js";
|
|
@@ -8,7 +8,7 @@ import { pluginVersion } from "./version.js";
|
|
|
8
8
|
const outputRequestSchema = z.object({ method: z.string(), path: z.string() });
|
|
9
9
|
export function createServer(config, fetchImpl) {
|
|
10
10
|
const client = new CoolifyClient(config, fetchImpl);
|
|
11
|
-
const server = new McpServer({ name: "coolify-plugin", version: pluginVersion }, { instructions: "Use read tools to identify exact Coolify resources before mutations. Never expose
|
|
11
|
+
const server = new McpServer({ name: "coolify-plugin", version: pluginVersion }, { instructions: "Use read tools to identify exact Coolify resources before mutations. Never expose the Coolify access token. Env tools return actual environment variable values; report them only when the user asks." });
|
|
12
12
|
for (const operation of operations) {
|
|
13
13
|
server.registerTool(operation.name, {
|
|
14
14
|
title: operation.title,
|
|
@@ -20,7 +20,7 @@ export function createServer(config, fetchImpl) {
|
|
|
20
20
|
try {
|
|
21
21
|
const envelope = await client.request(operation, input);
|
|
22
22
|
const data = operation.responseSchema.parse(envelope.data);
|
|
23
|
-
const structuredContent = redactSensitive({ data, status: envelope.status, request: envelope.request });
|
|
23
|
+
const structuredContent = redactSensitive({ data, status: envelope.status, request: envelope.request }, [config.token], isEnvOperation(operation.path));
|
|
24
24
|
return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
25
25
|
}
|
|
26
26
|
catch (error) {
|
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.2.0",
|
|
5
5
|
"description": "Portable Coolify MCP plugin with local stdio tools generated from the official OpenAPI contract.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "JurisLM",
|
package/skills/coolify/SKILL.md
CHANGED
|
@@ -5,4 +5,4 @@ description: Manage a configured Coolify instance through generated local MCP to
|
|
|
5
5
|
|
|
6
6
|
# Coolify
|
|
7
7
|
|
|
8
|
-
Use `coolify_*` tools only after resolving the target resource. Read before mutations and do not expose `COOLIFY_ACCESS_TOKEN`
|
|
8
|
+
Use `coolify_*` tools only after resolving the target resource. Read before mutations and do not expose `COOLIFY_ACCESS_TOKEN`. Env tools return actual environment variable values, including `is_shown_once` ones; report them only when the user asks.
|