@jurislm/coolify-plugin 3.7.1
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/.app.json.example +4 -0
- package/.codex-plugin/plugin.json +25 -0
- package/.mcp.json +5 -0
- package/.mcp.json.example +9 -0
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/api/manifest.json +9 -0
- package/dist/capabilities.d.ts +3 -0
- package/dist/capabilities.js +201 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +95 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +30 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +12 -0
- package/dist/generated/coolify-api.d.ts +16445 -0
- package/dist/generated/coolify-api.js +5 -0
- package/dist/generated/coolify-zod.d.ts +490 -0
- package/dist/generated/coolify-zod.js +22 -0
- package/dist/generated/operations.d.ts +21 -0
- package/dist/generated/operations.js +279 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +35 -0
- package/dist/stream.d.ts +1 -0
- package/dist/stream.js +13 -0
- package/dist/transports/stdio.d.ts +2 -0
- package/dist/transports/stdio.js +4 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/mcp.json +6 -0
- package/openapi/coolify-openapi.json +23077 -0
- package/package.json +57 -0
- package/plugin.json +28 -0
- package/skills/coolify/SKILL.md +8 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coolify-plugin",
|
|
3
|
+
"version": "3.7.1",
|
|
4
|
+
"description": "Fallback Codex metadata for local stdio Coolify tools.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "JurisLM"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"skills": "./skills/",
|
|
10
|
+
"mcpServers": "./.mcp.json",
|
|
11
|
+
"interface": {
|
|
12
|
+
"displayName": "Coolify",
|
|
13
|
+
"shortDescription": "Local Coolify API tools",
|
|
14
|
+
"longDescription": "Generated local stdio tools for a configured Coolify instance.",
|
|
15
|
+
"developerName": "JurisLM",
|
|
16
|
+
"category": "Developer Tools",
|
|
17
|
+
"capabilities": [
|
|
18
|
+
"MCP",
|
|
19
|
+
"Local"
|
|
20
|
+
],
|
|
21
|
+
"defaultPrompt": [
|
|
22
|
+
"List my Coolify resources."
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
package/.mcp.json
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 terry90918
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @jurislm/coolify-plugin
|
|
2
|
+
|
|
3
|
+
Portable Coolify MCP plugin. It exposes focused `coolify_*` stdio tools generated from the committed official Coolify OpenAPI snapshot.
|
|
4
|
+
|
|
5
|
+
## Configure
|
|
6
|
+
|
|
7
|
+
Only these environment variables are accepted:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
export COOLIFY_URL=https://coolify.example
|
|
11
|
+
export COOLIFY_TOKEN=your-api-token
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Build and run locally:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
bun install --frozen-lockfile
|
|
18
|
+
bun run check
|
|
19
|
+
bun dist/index.js
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`mcp.json` is a portable stdio registration; `.mcp.json.example` is the secret-free absolute-path example. NPM package release is allowed. Remote MCP transport, OAuth, vault, hosting, and OpenAI Plugin Directory submission are out of scope.
|
|
23
|
+
|
|
24
|
+
## OpenAPI contract
|
|
25
|
+
|
|
26
|
+
`openapi/coolify-openapi.json` is fetched from the official Coolify repository. `api/manifest.json` records its source, fetch time, persisted-snapshot SHA-256, document/info versions, path count, and operation count. `bun run api:check` verifies the snapshot offline before checking generated parity.
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
bun run api:fetch
|
|
30
|
+
bun run api:generate
|
|
31
|
+
bun run api:check
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Checks
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
bun run api:check
|
|
38
|
+
bun run typecheck
|
|
39
|
+
bun test
|
|
40
|
+
bun run build
|
|
41
|
+
bun run package:check
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceUrl": "https://raw.githubusercontent.com/coollabsio/coolify/main/openapi.json",
|
|
3
|
+
"fetchedAt": "2026-09-16T10:31:42.036Z",
|
|
4
|
+
"sha256": "4dbb392aac5e0a46186c4e6e5237f86d9534bb99bf6b8b82351a513598af3900",
|
|
5
|
+
"openapiVersion": "3.1.0",
|
|
6
|
+
"infoVersion": "0.1",
|
|
7
|
+
"pathCount": 192,
|
|
8
|
+
"operationCount": 275
|
|
9
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { redactSensitive } from "./client.js";
|
|
3
|
+
import { operations } from "./generated/operations.js";
|
|
4
|
+
import { pluginVersion } from "./version.js";
|
|
5
|
+
const outputSchema = z.object({ data: z.unknown(), status: z.number(), request: z.object({ method: z.string(), path: z.string() }) });
|
|
6
|
+
const databaseTypes = ["dragonfly", "keydb", "clickhouse"];
|
|
7
|
+
const truncationPrefix = "...[truncated]...\n";
|
|
8
|
+
function rows(value) {
|
|
9
|
+
if (Array.isArray(value))
|
|
10
|
+
return value.filter((item) => !!item && typeof item === "object");
|
|
11
|
+
if (!value || typeof value !== "object")
|
|
12
|
+
return [];
|
|
13
|
+
for (const key of ["data", "items", "results", "deployments", "applications", "databases", "resources", "services"]) {
|
|
14
|
+
const nested = value[key];
|
|
15
|
+
if (Array.isArray(nested))
|
|
16
|
+
return rows(nested);
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
function status(value) { return typeof value.status === "string" ? value.status : ""; }
|
|
21
|
+
function unhealthy(value) { return /exited|unhealthy|error|stopped/iu.test(status(value)); }
|
|
22
|
+
function errorMessage(value) { return value instanceof Error ? value.message : String(value); }
|
|
23
|
+
function truncateLogs(logs, lineLimit = 200, charLimit = 50_000) {
|
|
24
|
+
if (logs.split("\n").length <= lineLimit && logs.length <= charLimit)
|
|
25
|
+
return logs;
|
|
26
|
+
let result = logs.split("\n").slice(-lineLimit).join("\n");
|
|
27
|
+
if (result.length > charLimit)
|
|
28
|
+
result = truncationPrefix + result.slice(-(charLimit - truncationPrefix.length));
|
|
29
|
+
else
|
|
30
|
+
result = truncationPrefix + result;
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function environmentSummary(value) {
|
|
34
|
+
return rows(value).map((item) => ({ key: item.key, is_build_time: item.is_build_time ?? item.is_buildtime ?? false }));
|
|
35
|
+
}
|
|
36
|
+
export function registerV36Capabilities(server, client) {
|
|
37
|
+
const call = async (name, input = {}) => {
|
|
38
|
+
const operation = operations.find((item) => item.name === name);
|
|
39
|
+
if (!operation)
|
|
40
|
+
throw new Error(`Missing generated operation: ${name}`);
|
|
41
|
+
return (await client.request(operation, operation.inputSchema.parse(input))).data;
|
|
42
|
+
};
|
|
43
|
+
const result = (name, data) => {
|
|
44
|
+
const structuredContent = redactSensitive({ data, status: 200, request: { method: "COMPOSITE", path: `/capabilities/${name}` } });
|
|
45
|
+
return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
46
|
+
};
|
|
47
|
+
const register = (name, description, inputSchema, annotations, handler) => {
|
|
48
|
+
server.registerTool(name, { title: name, description, inputSchema, outputSchema, annotations }, async (input) => result(name, await handler(input)));
|
|
49
|
+
};
|
|
50
|
+
const settledRows = async (names) => {
|
|
51
|
+
const settled = await Promise.allSettled(names.map((name) => call(`coolify_list_${name}`)));
|
|
52
|
+
const values = [];
|
|
53
|
+
const errors = [];
|
|
54
|
+
settled.forEach((entry, index) => {
|
|
55
|
+
if (entry.status === "fulfilled")
|
|
56
|
+
values[index] = rows(entry.value);
|
|
57
|
+
else {
|
|
58
|
+
values[index] = [];
|
|
59
|
+
errors.push(`${names[index]}: ${errorMessage(entry.reason)}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return { values, errors };
|
|
63
|
+
};
|
|
64
|
+
const overview = async () => {
|
|
65
|
+
const names = ["servers", "projects", "applications", "databases", "services"];
|
|
66
|
+
const { values, errors } = await settledRows(names);
|
|
67
|
+
const data = Object.fromEntries(names.map((name, index) => [name, values[index]]));
|
|
68
|
+
return { summary: Object.fromEntries(names.map((name, index) => [name, values[index].length])), ...data, ...(errors.length ? { errors } : {}) };
|
|
69
|
+
};
|
|
70
|
+
const batch = async (resources, action) => {
|
|
71
|
+
const settled = await Promise.allSettled(resources.map(action));
|
|
72
|
+
const succeeded = [];
|
|
73
|
+
const failed = [];
|
|
74
|
+
settled.forEach((entry, index) => {
|
|
75
|
+
const resource = resources[index];
|
|
76
|
+
const item = { uuid: resource.uuid, name: resource.name ?? resource.uuid };
|
|
77
|
+
if (entry.status === "fulfilled")
|
|
78
|
+
succeeded.push(item);
|
|
79
|
+
else
|
|
80
|
+
failed.push({ ...item, error: errorMessage(entry.reason) });
|
|
81
|
+
});
|
|
82
|
+
return { summary: { total: resources.length, succeeded: succeeded.length, failed: failed.length }, succeeded, failed };
|
|
83
|
+
};
|
|
84
|
+
const application = async (query) => {
|
|
85
|
+
const apps = rows(await call("coolify_list_applications"));
|
|
86
|
+
return apps.find((app) => [app.uuid, app.name, app.fqdn].some((value) => typeof value === "string" && value.toLowerCase().includes(query.toLowerCase())));
|
|
87
|
+
};
|
|
88
|
+
const serverByQuery = async (query) => {
|
|
89
|
+
const servers = rows(await call("coolify_list_servers"));
|
|
90
|
+
return servers.find((item) => [item.uuid, item.name, item.ip].some((value) => typeof value === "string" && value.toLowerCase().includes(query.toLowerCase())));
|
|
91
|
+
};
|
|
92
|
+
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 }));
|
|
93
|
+
register("coolify_get_infrastructure_overview", "Summarize Coolify infrastructure.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, overview);
|
|
94
|
+
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 }) => {
|
|
95
|
+
let app;
|
|
96
|
+
try {
|
|
97
|
+
app = await application(String(query));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
return { application: null, health: { status: "unknown", issues: [] }, logs: null, environment_variables: { count: 0, variables: [] }, recent_deployments: [], errors: [errorMessage(error)] };
|
|
101
|
+
}
|
|
102
|
+
if (!app?.uuid)
|
|
103
|
+
return { application: null, health: { status: "unknown", issues: [] }, logs: null, environment_variables: { count: 0, variables: [] }, recent_deployments: [], errors: [`No application found matching ${query}`] };
|
|
104
|
+
const names = ["application", "logs", "environment_variables", "deployments"];
|
|
105
|
+
const settled = await Promise.allSettled([
|
|
106
|
+
call("coolify_get_application_by_uuid", { uuid: app.uuid }), call("coolify_get_application_logs_by_uuid", { uuid: app.uuid, lines: 50 }), call("coolify_list_envs_by_application_uuid", { uuid: app.uuid }), call("coolify_list_deployments_by_app_uuid", { uuid: app.uuid }),
|
|
107
|
+
]);
|
|
108
|
+
const errors = [];
|
|
109
|
+
const value = (index) => { const entry = settled[index]; if (entry.status === "fulfilled")
|
|
110
|
+
return entry.value; errors.push(`${names[index]}: ${errorMessage(entry.reason)}`); return null; };
|
|
111
|
+
const details = value(0);
|
|
112
|
+
const rawLogs = value(1);
|
|
113
|
+
const envVars = environmentSummary(value(2));
|
|
114
|
+
const deployments = rows(value(3));
|
|
115
|
+
const appInfo = details ?? app;
|
|
116
|
+
const appStatus = status(appInfo);
|
|
117
|
+
const issues = [];
|
|
118
|
+
let health = "unknown";
|
|
119
|
+
if (/exited|unhealthy|error/iu.test(appStatus)) {
|
|
120
|
+
health = "unhealthy";
|
|
121
|
+
issues.push(`Status: ${appStatus}`);
|
|
122
|
+
}
|
|
123
|
+
else if (/running/iu.test(appStatus))
|
|
124
|
+
health = "healthy";
|
|
125
|
+
const failed = deployments.slice(0, 5).filter((item) => item.status === "failed").length;
|
|
126
|
+
if (failed) {
|
|
127
|
+
health = "unhealthy";
|
|
128
|
+
issues.push(`${failed} failed deployment(s) in last 5`);
|
|
129
|
+
}
|
|
130
|
+
const logs = typeof rawLogs === "string" ? rawLogs : rawLogs && typeof rawLogs === "object" && typeof rawLogs.logs === "string" ? rawLogs.logs : null;
|
|
131
|
+
return { application: { uuid: appInfo.uuid, name: appInfo.name, status: appStatus || "unknown", fqdn: appInfo.fqdn ?? null, git_repository: appInfo.git_repository ?? null, git_branch: appInfo.git_branch ?? null }, health: { status: health, issues }, logs: logs === null ? null : truncateLogs(logs), environment_variables: { count: envVars.length, variables: envVars }, recent_deployments: deployments.slice(0, 5).map((item) => ({ uuid: item.uuid, status: item.status, created_at: item.created_at })), ...(errors.length ? { errors } : {}) };
|
|
132
|
+
});
|
|
133
|
+
register("coolify_diagnose_server", "Diagnose a server by UUID, name, or IP.", z.object({ query: z.string() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ query }) => {
|
|
134
|
+
let found;
|
|
135
|
+
try {
|
|
136
|
+
found = await serverByQuery(String(query));
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], validation: null, errors: [errorMessage(error)] };
|
|
140
|
+
}
|
|
141
|
+
if (!found?.uuid)
|
|
142
|
+
return { server: null, health: { status: "unknown", issues: [] }, resources: [], domains: [], validation: null, errors: [`No server found matching ${query}`] };
|
|
143
|
+
const names = ["server", "resources", "domains", "validation"];
|
|
144
|
+
const settled = await Promise.allSettled([
|
|
145
|
+
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 }), call("coolify_validate_server_by_uuid", { uuid: found.uuid }),
|
|
146
|
+
]);
|
|
147
|
+
const errors = [];
|
|
148
|
+
const value = (index) => { const entry = settled[index]; if (entry.status === "fulfilled")
|
|
149
|
+
return entry.value; errors.push(`${names[index]}: ${errorMessage(entry.reason)}`); return null; };
|
|
150
|
+
const details = value(0) ?? found;
|
|
151
|
+
const resources = rows(value(1));
|
|
152
|
+
const issues = [];
|
|
153
|
+
if (details.is_reachable === false)
|
|
154
|
+
issues.push("Server is not reachable");
|
|
155
|
+
if (details.is_usable === false)
|
|
156
|
+
issues.push("Server is not usable");
|
|
157
|
+
const unhealthyResources = resources.filter(unhealthy).length;
|
|
158
|
+
if (unhealthyResources)
|
|
159
|
+
issues.push(`${unhealthyResources} unhealthy resource(s)`);
|
|
160
|
+
return { server: { uuid: details.uuid, name: details.name, ip: details.ip, status: details.status ?? null, is_reachable: details.is_reachable ?? null }, health: { status: issues.length ? "unhealthy" : details.is_reachable === true ? "healthy" : "unknown", issues }, resources: resources.map((item) => ({ uuid: item.uuid, name: item.name, type: item.type, status: item.status })), domains: rows(value(2)), validation: value(3), ...(errors.length ? { errors } : {}) };
|
|
161
|
+
});
|
|
162
|
+
register("coolify_find_issues", "Find unhealthy Coolify infrastructure.", z.object({}), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async () => {
|
|
163
|
+
const names = ["servers", "applications", "databases", "services"];
|
|
164
|
+
const { values, errors } = await settledRows(names);
|
|
165
|
+
const issues = [];
|
|
166
|
+
values[0].forEach((item) => { if (item.is_reachable === false || item.is_usable === false)
|
|
167
|
+
issues.push({ type: "server", uuid: item.uuid, name: item.name, issue: item.is_reachable === false ? "Server is not reachable" : "Server is not usable", status: status(item) || "unreachable" }); });
|
|
168
|
+
values.slice(1).forEach((items, index) => items.filter(unhealthy).forEach((item) => issues.push({ type: names[index + 1].slice(0, -1), uuid: item.uuid, name: item.name, issue: `${names[index + 1].slice(0, -1)} status: ${status(item)}`, status: status(item) })));
|
|
169
|
+
return { summary: { total_issues: issues.length, unhealthy_applications: issues.filter((item) => item.type === "application").length, unhealthy_databases: issues.filter((item) => item.type === "database").length, unhealthy_services: issues.filter((item) => item.type === "service").length, unreachable_servers: issues.filter((item) => item.type === "server").length }, issues, ...(errors.length ? { errors } : {}) };
|
|
170
|
+
});
|
|
171
|
+
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(rows(await call("coolify_list_applications")).filter((app) => app.project_uuid === project_uuid), (app) => call("coolify_restart_application_by_uuid", { uuid: app.uuid })));
|
|
172
|
+
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 }) => {
|
|
173
|
+
if (!app_uuids.length)
|
|
174
|
+
return batch([], async () => undefined);
|
|
175
|
+
const names = rows(await call("coolify_list_applications"));
|
|
176
|
+
const labels = new Map(names.map((item) => [item.uuid, item.name ?? item.uuid]));
|
|
177
|
+
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 } }));
|
|
178
|
+
});
|
|
179
|
+
register("coolify_stop_all_applications", "Emergency stop every running application.", z.object({ confirm_stop_all_applications: z.literal(true) }), { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, async () => batch(rows(await call("coolify_list_applications")).filter((app) => /running|healthy/iu.test(status(app))), (app) => call("coolify_stop_application_by_uuid", { uuid: app.uuid })));
|
|
180
|
+
register("coolify_redeploy_project_applications", "Redeploy every application in a project.", z.object({ project_uuid: z.string(), force: z.boolean().optional() }), { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ project_uuid, force }) => batch(rows(await call("coolify_list_applications")).filter((app) => app.project_uuid === project_uuid), (app) => call("coolify_deploy_by_tag_or_uuid", { uuid: app.uuid, force: force ?? true })));
|
|
181
|
+
register("coolify_get_environment", "Get an environment and its relevant database types.", z.object({ project_uuid: z.string(), environment_name_or_uuid: z.string() }), { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ project_uuid, environment_name_or_uuid }) => {
|
|
182
|
+
const settled = await Promise.allSettled([call("coolify_get_environment_by_name_or_uuid", { uuid: project_uuid, environment_name_or_uuid }), call("coolify_list_databases")]);
|
|
183
|
+
const errors = [];
|
|
184
|
+
const environment = settled[0].status === "fulfilled" ? settled[0].value : (errors.push(`environment: ${errorMessage(settled[0].reason)}`), null);
|
|
185
|
+
const databases = settled[1].status === "fulfilled" ? rows(settled[1].value) : (errors.push(`databases: ${errorMessage(settled[1].reason)}`), []);
|
|
186
|
+
if (!environment)
|
|
187
|
+
return { environment: null, missing_database_types: [...databaseTypes], ...(errors.length ? { errors } : {}) };
|
|
188
|
+
const matching = databases.filter((database) => (environment.id !== undefined && database.environment_id === environment.id) || (environment.uuid !== undefined && database.environment_uuid === environment.uuid) || (environment.name !== undefined && database.environment_name === environment.name));
|
|
189
|
+
const byType = new Map();
|
|
190
|
+
matching.forEach((database) => { const type = String(database.database_type ?? database.type ?? "").toLowerCase(); for (const expected of databaseTypes)
|
|
191
|
+
if (type.includes(expected))
|
|
192
|
+
byType.set(expected, [...(byType.get(expected) ?? []), database]); });
|
|
193
|
+
const missing = databaseTypes.filter((type) => !byType.has(type));
|
|
194
|
+
return { ...environment, ...Object.fromEntries(databaseTypes.filter((type) => byType.has(type)).map((type) => [`${type}s`, byType.get(type)])), missing_database_types: missing, ...(errors.length ? { errors } : {}) };
|
|
195
|
+
});
|
|
196
|
+
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" }) => {
|
|
197
|
+
const server = await call("coolify_get_server_by_uuid", { uuid: server_uuid }).catch(() => null);
|
|
198
|
+
const quote = (value) => `'${value.replace(/'/gu, "'\\''")}'`;
|
|
199
|
+
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" }] };
|
|
200
|
+
});
|
|
201
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GeneratedOperation } from "./generated/operations.js";
|
|
2
|
+
import type { CoolifyConfig } from "./config.js";
|
|
3
|
+
import { CoolifyApiError } from "./errors.js";
|
|
4
|
+
export interface ToolEnvelope<T> {
|
|
5
|
+
data: T;
|
|
6
|
+
status: number;
|
|
7
|
+
request: {
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface BinaryEnvelope {
|
|
13
|
+
encoding: "base64";
|
|
14
|
+
contentType: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
18
|
+
type Operation = Pick<GeneratedOperation, "method" | "path" | "parameters">;
|
|
19
|
+
export declare function redactSensitive<T>(value: T): T;
|
|
20
|
+
export declare class CoolifyClient {
|
|
21
|
+
private readonly config;
|
|
22
|
+
private readonly fetchImpl;
|
|
23
|
+
constructor(config: CoolifyConfig, fetchImpl?: FetchLike);
|
|
24
|
+
request<T>(operation: Operation, input: Record<string, unknown>): Promise<ToolEnvelope<T | null | string | BinaryEnvelope>>;
|
|
25
|
+
}
|
|
26
|
+
export { CoolifyApiError };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { CoolifyApiError } from "./errors.js";
|
|
2
|
+
const sensitiveKey = /(^value$|real_?value|private_?key|token|secret|password|authorization|cookie)/iu;
|
|
3
|
+
export function redactSensitive(value) {
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return value.map(redactSensitive);
|
|
6
|
+
if (!value || typeof value !== "object")
|
|
7
|
+
return value;
|
|
8
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [
|
|
9
|
+
key,
|
|
10
|
+
sensitiveKey.test(key) ? "[REDACTED]" : redactSensitive(child),
|
|
11
|
+
]));
|
|
12
|
+
}
|
|
13
|
+
function pathValue(value, name) {
|
|
14
|
+
if (value === undefined || value === null)
|
|
15
|
+
throw new Error(`Missing required path parameter: ${name}`);
|
|
16
|
+
return encodeURIComponent(String(value));
|
|
17
|
+
}
|
|
18
|
+
function appendQuery(url, name, value) {
|
|
19
|
+
if (value === undefined || value === null)
|
|
20
|
+
return;
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
for (const item of value)
|
|
23
|
+
url.searchParams.append(name, String(item));
|
|
24
|
+
else
|
|
25
|
+
url.searchParams.set(name, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
26
|
+
}
|
|
27
|
+
function base64(bytes) {
|
|
28
|
+
let text = "";
|
|
29
|
+
for (const byte of new Uint8Array(bytes))
|
|
30
|
+
text += String.fromCharCode(byte);
|
|
31
|
+
return btoa(text);
|
|
32
|
+
}
|
|
33
|
+
function normalizeKnownCollection(path, value) {
|
|
34
|
+
const collectionPaths = new Set(["/databases", "/resources", "/deployments", "/deployments/applications/{uuid}"]);
|
|
35
|
+
if (!collectionPaths.has(path))
|
|
36
|
+
return value;
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return value;
|
|
39
|
+
if (!value || typeof value !== "object")
|
|
40
|
+
throw new Error(`Coolify collection response for ${path} must be an array or named array wrapper`);
|
|
41
|
+
for (const key of ["data", "items", "results", "databases", "resources", "deployments"]) {
|
|
42
|
+
const nested = value[key];
|
|
43
|
+
if (Array.isArray(nested))
|
|
44
|
+
return nested;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Coolify collection response for ${path} must be an array or named array wrapper`);
|
|
47
|
+
}
|
|
48
|
+
export class CoolifyClient {
|
|
49
|
+
config;
|
|
50
|
+
fetchImpl;
|
|
51
|
+
constructor(config, fetchImpl = fetch) {
|
|
52
|
+
this.config = config;
|
|
53
|
+
this.fetchImpl = fetchImpl;
|
|
54
|
+
}
|
|
55
|
+
async request(operation, input) {
|
|
56
|
+
let path = operation.path;
|
|
57
|
+
for (const parameter of operation.parameters)
|
|
58
|
+
if (parameter.location === "path") {
|
|
59
|
+
path = path.replace(`{${parameter.name}}`, pathValue(input[parameter.name], parameter.name));
|
|
60
|
+
}
|
|
61
|
+
if (path.includes("{"))
|
|
62
|
+
throw new Error(`Unresolved path parameter in ${operation.path}`);
|
|
63
|
+
const url = new URL(this.config.baseUrl + path);
|
|
64
|
+
for (const parameter of operation.parameters)
|
|
65
|
+
if (parameter.location === "query")
|
|
66
|
+
appendQuery(url, parameter.name, input[parameter.name]);
|
|
67
|
+
const headers = new Headers({ accept: "application/json, text/plain, */*", authorization: `Bearer ${this.config.token}` });
|
|
68
|
+
const init = { method: operation.method, headers, signal: AbortSignal.timeout(this.config.timeoutMs) };
|
|
69
|
+
if (input.body !== undefined && !["GET", "HEAD"].includes(operation.method)) {
|
|
70
|
+
headers.set("content-type", "application/json");
|
|
71
|
+
init.body = JSON.stringify(input.body);
|
|
72
|
+
}
|
|
73
|
+
let response;
|
|
74
|
+
try {
|
|
75
|
+
response = await this.fetchImpl(url, init);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new CoolifyApiError(0, operation.method, path, `Coolify request failed for ${operation.method} ${path}: ${error instanceof Error ? error.message.replaceAll(this.config.token, "[REDACTED]") : "request error"}`);
|
|
79
|
+
}
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw new CoolifyApiError(response.status, operation.method, path, `Coolify API returned ${response.status} for ${operation.method} ${path}`);
|
|
82
|
+
let data = null;
|
|
83
|
+
if (response.status !== 204) {
|
|
84
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
85
|
+
if (contentType.includes("json"))
|
|
86
|
+
data = normalizeKnownCollection(operation.path, await response.json());
|
|
87
|
+
else if (contentType.startsWith("text/") || contentType.includes("xml"))
|
|
88
|
+
data = await response.text();
|
|
89
|
+
else
|
|
90
|
+
data = { encoding: "base64", contentType: contentType || "application/octet-stream", value: base64(await response.arrayBuffer()) };
|
|
91
|
+
}
|
|
92
|
+
return redactSensitive({ data, status: response.status, request: { method: operation.method, path } });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export { CoolifyApiError };
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CoolifyConfig {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
token: string;
|
|
4
|
+
timeoutMs: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class ConfigError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function loadConfig(env?: Record<string, string | undefined>): CoolifyConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class ConfigError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "ConfigError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function loadConfig(env = process.env) {
|
|
8
|
+
const rawUrl = env.COOLIFY_URL?.trim();
|
|
9
|
+
const token = env.COOLIFY_TOKEN?.trim();
|
|
10
|
+
if (!rawUrl)
|
|
11
|
+
throw new ConfigError("COOLIFY_URL is required");
|
|
12
|
+
if (!token)
|
|
13
|
+
throw new ConfigError("COOLIFY_TOKEN is required");
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(rawUrl);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new ConfigError("COOLIFY_URL must be a valid HTTP(S) URL");
|
|
20
|
+
}
|
|
21
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
22
|
+
throw new ConfigError("COOLIFY_URL must use http:// or https://");
|
|
23
|
+
}
|
|
24
|
+
if (url.search || url.hash)
|
|
25
|
+
throw new ConfigError("COOLIFY_URL must not include a query or fragment");
|
|
26
|
+
url.pathname = url.pathname.replace(/\/+$/u, "");
|
|
27
|
+
if (!url.pathname.endsWith("/api/v1"))
|
|
28
|
+
url.pathname = `${url.pathname}/api/v1`.replace(/^\/\//u, "/");
|
|
29
|
+
return { baseUrl: url.toString().replace(/\/$/u, ""), token, timeoutMs: 30_000 };
|
|
30
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED