@masonator/coolify-mcp
Advanced tools
| /** | ||
| * Integration tests for the #336 field-test follow-ups. | ||
| * | ||
| * These verify against a real Coolify instance that: | ||
| * 1. `listDatabases` reports every database `/resources` knows about — the | ||
| * per-type id collision upstream made `GET /databases` silently drop whole | ||
| * types (2 Postgres + 2 Dragonfly with ids 1,2 returned only the Dragonflys). | ||
| * 2. `findInfrastructureIssues` severity accounting is internally consistent, | ||
| * and warnings (unknown health, available proxy updates) never inflate the | ||
| * critical counts. | ||
| * 3. `diagnoseApplication` env-var accounting is internally consistent — | ||
| * preview twins are surfaced, not deduped. | ||
| * | ||
| * Prerequisites: | ||
| * - COOLIFY_URL and COOLIFY_TOKEN environment variables set (from .env) | ||
| * | ||
| * Run with: npm run test:integration | ||
| */ | ||
| export {}; |
| /** | ||
| * Integration tests for the #336 field-test follow-ups. | ||
| * | ||
| * These verify against a real Coolify instance that: | ||
| * 1. `listDatabases` reports every database `/resources` knows about — the | ||
| * per-type id collision upstream made `GET /databases` silently drop whole | ||
| * types (2 Postgres + 2 Dragonfly with ids 1,2 returned only the Dragonflys). | ||
| * 2. `findInfrastructureIssues` severity accounting is internally consistent, | ||
| * and warnings (unknown health, available proxy updates) never inflate the | ||
| * critical counts. | ||
| * 3. `diagnoseApplication` env-var accounting is internally consistent — | ||
| * preview twins are surfaced, not deduped. | ||
| * | ||
| * Prerequisites: | ||
| * - COOLIFY_URL and COOLIFY_TOKEN environment variables set (from .env) | ||
| * | ||
| * Run with: npm run test:integration | ||
| */ | ||
| import { CoolifyClient } from '../../lib/coolify-client.js'; | ||
| import { COOLIFY_URL, COOLIFY_TOKEN, warnIfSkipped } from './helpers.js'; | ||
| warnIfSkipped('issue-336.integration'); | ||
| const shouldRun = COOLIFY_URL && COOLIFY_TOKEN; | ||
| const describeFn = shouldRun ? describe : describe.skip; | ||
| describeFn('#336 follow-ups (live)', () => { | ||
| let client; | ||
| beforeAll(() => { | ||
| client = new CoolifyClient({ | ||
| baseUrl: COOLIFY_URL, | ||
| accessToken: COOLIFY_TOKEN, | ||
| }); | ||
| }); | ||
| describe('list_databases collision merge', () => { | ||
| it('reports every standalone-* resource /resources knows about', async () => { | ||
| const [databases, resources] = await Promise.all([ | ||
| client.listDatabases({ summary: true }), | ||
| client.listResources(), | ||
| ]); | ||
| const resourceDbUuids = resources | ||
| .filter((r) => typeof r.type === 'string' && r.type.startsWith('standalone-')) | ||
| .map((r) => r.uuid) | ||
| .sort(); | ||
| const listedUuids = new Set(databases.map((db) => db.uuid)); | ||
| const dropped = resourceDbUuids.filter((uuid) => !listedUuids.has(uuid)); | ||
| expect(dropped).toEqual([]); | ||
| }); | ||
| it('does not duplicate a database the plain endpoint already returned', async () => { | ||
| const databases = await client.listDatabases({ summary: true }); | ||
| const uuids = databases.map((db) => db.uuid); | ||
| expect(new Set(uuids).size).toBe(uuids.length); | ||
| }); | ||
| it('every merged row carries a type, so overview counts stay typed', async () => { | ||
| const databases = await client.listDatabases({ summary: true }); | ||
| for (const db of databases) { | ||
| expect(typeof db.type).toBe('string'); | ||
| expect(db.type.length).toBeGreaterThan(0); | ||
| } | ||
| }); | ||
| }); | ||
| describe('find_issues severity accounting', () => { | ||
| it('classifies every issue and keeps critical counts free of warnings', async () => { | ||
| const report = await client.findInfrastructureIssues(); | ||
| for (const issue of report.issues) { | ||
| expect(['critical', 'warning']).toContain(issue.severity); | ||
| } | ||
| const critical = report.issues.filter((i) => i.severity === 'critical'); | ||
| const warnings = report.issues.filter((i) => i.severity === 'warning'); | ||
| expect(report.summary.warnings).toBe(warnings.length); | ||
| expect(report.summary.total_issues).toBe(report.issues.length); | ||
| expect(report.summary.unhealthy_applications).toBe(critical.filter((i) => i.type === 'application').length); | ||
| expect(report.summary.unhealthy_databases).toBe(critical.filter((i) => i.type === 'database').length); | ||
| expect(report.summary.unhealthy_services).toBe(critical.filter((i) => i.type === 'service').length); | ||
| // Proxy-update warnings are type 'server' but must not count as unreachable. | ||
| expect(report.summary.unreachable_servers).toBe(critical.filter((i) => i.type === 'server').length); | ||
| }); | ||
| it('flags resources running with unknown health when the estate has any', async () => { | ||
| const [report, resources] = await Promise.all([ | ||
| client.findInfrastructureIssues(), | ||
| client.listResources(), | ||
| ]); | ||
| const unknown = resources.filter((r) => r.status?.startsWith('running') && r.status?.endsWith(':unknown')); | ||
| const unknownWarnings = report.issues.filter((i) => i.severity === 'warning' && i.issue.includes('health unknown')); | ||
| // Every running:unknown resource find_issues can see must produce a | ||
| // warning. (>= because /resources also lists service sub-containers.) | ||
| if (unknown.length === 0) { | ||
| expect(unknownWarnings).toEqual([]); | ||
| } | ||
| else { | ||
| expect(unknownWarnings.length).toBeGreaterThan(0); | ||
| } | ||
| }); | ||
| }); | ||
| describe('diagnose_app env-var accounting', () => { | ||
| it('splits preview twins out without deduping the raw rows', async () => { | ||
| const apps = (await client.listApplications({ summary: true })); | ||
| if (apps.length === 0) { | ||
| console.warn('[issue-336.integration] no applications on this estate — nothing to diagnose'); | ||
| return; | ||
| } | ||
| const diag = await client.diagnoseApplication(apps[0].uuid); | ||
| const env = diag.environment_variables; | ||
| expect(env.count).toBe(env.variables.length); | ||
| expect(env.production_count + env.preview_count).toBe(env.count); | ||
| expect(env.distinct_keys).toBeLessThanOrEqual(env.count); | ||
| expect(env.distinct_keys).toBe(new Set(env.variables.map((v) => v.key)).size); | ||
| for (const variable of env.variables) { | ||
| expect(typeof variable.is_preview).toBe('boolean'); | ||
| } | ||
| }); | ||
| }); | ||
| }); |
@@ -265,2 +265,14 @@ /** | ||
| deleteApplicationEnvVar(uuid: string, envUuid: string): Promise<MessageResponse>; | ||
| /** | ||
| * List databases, augmented from `/resources`. | ||
| * | ||
| * Coolify keeps a per-type id sequence and `GET /databases` merges the | ||
| * per-type collections keyed on that id, so two database types sharing ids | ||
| * silently shadow each other (verified live: 2 Postgres + 2 Dragonfly, both | ||
| * pairs ids 1 and 2, returns only the Dragonflys). `/resources` reports every | ||
| * database correctly, so rows it knows about that `/databases` dropped are | ||
| * merged back in by uuid. When `/databases` is complete the merge is a no-op; | ||
| * if `/resources` fails the plain `/databases` result is returned unchanged. | ||
| * @see https://github.com/StuMason/coolify-mcp/issues/336 | ||
| */ | ||
| listDatabases(options?: ListOptions): Promise<Database[] | DatabaseSummary[]>; | ||
@@ -444,3 +456,12 @@ getDatabase(uuid: string, options?: { | ||
| /** | ||
| * Normalize a name/FQDN candidate for exact comparison: lowercase, trimmed, | ||
| * scheme and trailing slashes stripped. "https://app.example.com/" and | ||
| * "app.example.com" are the same address to a human asking about it. | ||
| */ | ||
| private static normalizeHostLike; | ||
| /** | ||
| * Find an application by UUID, name, or domain (FQDN). | ||
| * An exact name or FQDN match wins outright; substring matching only runs | ||
| * when nothing matches exactly, so "api.example.com" resolves even when | ||
| * "api.example.com.staging" also exists (#336). | ||
| * Returns the UUID if found, throws if not found or multiple matches. | ||
@@ -468,3 +489,6 @@ */ | ||
| * Scan infrastructure for common issues. | ||
| * Finds: unreachable servers, unhealthy apps, exited databases, stopped services. | ||
| * Critical: unreachable servers, unhealthy apps, exited databases, stopped | ||
| * services. Warnings (#336): resources running with unknown health, and | ||
| * servers with an available proxy update (`traefik_outdated_info`, only on | ||
| * GET /servers/{uuid}, so each listed server is fetched individually). | ||
| */ | ||
@@ -471,0 +495,0 @@ findInfrastructureIssues(): Promise<InfrastructureIssuesReport>; |
@@ -49,2 +49,4 @@ /** | ||
| proxy_status?: string; | ||
| detected_traefik_version?: string | null; | ||
| traefik_outdated_info?: TraefikOutdatedInfo | null; | ||
| settings?: ServerSettings; | ||
@@ -55,2 +57,15 @@ team_id?: number; | ||
| } | ||
| /** | ||
| * Shape of `traefik_outdated_info` on GET /servers/{uuid} (verified live | ||
| * against Coolify v4.3.7). `type` is the semver distance of the available | ||
| * update, e.g. "patch_update". | ||
| */ | ||
| export interface TraefikOutdatedInfo { | ||
| current: string; | ||
| latest: string; | ||
| type?: string; | ||
| checked_at?: string; | ||
| newer_branch_target?: string; | ||
| newer_branch_latest?: string; | ||
| } | ||
| export interface ServerSettings { | ||
@@ -1003,2 +1018,5 @@ id: number; | ||
| count: number; | ||
| distinct_keys: number; | ||
| production_count: number; | ||
| preview_count: number; | ||
| variables: Array<{ | ||
@@ -1008,2 +1026,3 @@ key: string; | ||
| is_runtime: boolean; | ||
| is_preview: boolean; | ||
| }>; | ||
@@ -1052,2 +1071,3 @@ }; | ||
| status: string; | ||
| severity: 'critical' | 'warning'; | ||
| } | ||
@@ -1061,2 +1081,3 @@ export interface InfrastructureIssuesReport { | ||
| unreachable_servers: number; | ||
| warnings: number; | ||
| }; | ||
@@ -1063,0 +1084,0 @@ issues: InfrastructureIssue[]; |
+2
-2
| { | ||
| "name": "@masonator/coolify-mcp", | ||
| "scope": "@masonator", | ||
| "version": "2.19.3", | ||
| "version": "2.19.4", | ||
| "mcpName": "io.github.StuMason/coolify", | ||
@@ -109,3 +109,3 @@ "description": "MCP server for Coolify — 44 optimized tools for infrastructure management, diagnostics, and documentation search", | ||
| "picomatch": "^4.0.4", | ||
| "js-yaml": "^4.2.0", | ||
| "js-yaml": "^4.3.1", | ||
| "markdown-it": "^14.2.0" | ||
@@ -112,0 +112,0 @@ }, |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
877264
4.45%45
4.65%17241
4.08%