@masonator/coolify-mcp 0.6.0 → 0.8.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.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Integration tests for diagnostic tools.
3
+ *
4
+ * These tests hit the real Coolify API to verify diagnostic methods work correctly.
5
+ * They are skipped in CI and should be run manually when testing against a real instance.
6
+ *
7
+ * Prerequisites:
8
+ * - COOLIFY_URL and COOLIFY_TOKEN environment variables set (from .env)
9
+ * - Access to a running Coolify instance
10
+ *
11
+ * Run with: npm run test:integration
12
+ */
13
+ import { config } from 'dotenv';
14
+ import { CoolifyClient } from '../../lib/coolify-client.js';
15
+ // Load environment variables from .env file
16
+ config();
17
+ const COOLIFY_URL = process.env.COOLIFY_URL;
18
+ const COOLIFY_TOKEN = process.env.COOLIFY_TOKEN;
19
+ // Skip all tests if environment variables are not set
20
+ const shouldRun = COOLIFY_URL && COOLIFY_TOKEN;
21
+ // Test data - UUIDs from actual infrastructure
22
+ // These should be updated to match your test environment
23
+ const TEST_DATA = {
24
+ // Server: coolify-apps (running, reachable)
25
+ SERVER_UUID: 'ggkk8w4c08gw48oowsg4g0oc',
26
+ // Application: test-system (running)
27
+ APP_UUID_HEALTHY: 'xs0sgs4gog044s4k4c88kgsc',
28
+ // Application: Bumnail Benerator (exited:unhealthy)
29
+ APP_UUID_UNHEALTHY: 't444wg40s4kkwcc04s084wgw',
30
+ };
31
+ const describeFn = shouldRun ? describe : describe.skip;
32
+ describeFn('Diagnostic Integration Tests', () => {
33
+ let client;
34
+ beforeAll(() => {
35
+ if (!COOLIFY_URL || !COOLIFY_TOKEN) {
36
+ throw new Error('COOLIFY_URL and COOLIFY_TOKEN must be set for integration tests');
37
+ }
38
+ client = new CoolifyClient({
39
+ baseUrl: COOLIFY_URL,
40
+ accessToken: COOLIFY_TOKEN,
41
+ });
42
+ });
43
+ describe('diagnoseApplication', () => {
44
+ it('should return diagnostic data for a healthy application', async () => {
45
+ const result = await client.diagnoseApplication(TEST_DATA.APP_UUID_HEALTHY);
46
+ // Should have application info
47
+ expect(result.application).not.toBeNull();
48
+ expect(result.application?.uuid).toBe(TEST_DATA.APP_UUID_HEALTHY);
49
+ expect(result.application?.name).toBeDefined();
50
+ // Should have health assessment
51
+ expect(result.health).toBeDefined();
52
+ expect(['healthy', 'unhealthy', 'unknown']).toContain(result.health.status);
53
+ // Should have environment variables (even if empty)
54
+ expect(result.environment_variables).toBeDefined();
55
+ expect(typeof result.environment_variables.count).toBe('number');
56
+ expect(Array.isArray(result.environment_variables.variables)).toBe(true);
57
+ // Values should be hidden (only key and is_build_time exposed)
58
+ if (result.environment_variables.variables.length > 0) {
59
+ const firstVar = result.environment_variables.variables[0];
60
+ expect(firstVar).toHaveProperty('key');
61
+ expect(firstVar).toHaveProperty('is_build_time');
62
+ expect(firstVar).not.toHaveProperty('value');
63
+ }
64
+ // Should have recent deployments array
65
+ expect(Array.isArray(result.recent_deployments)).toBe(true);
66
+ // Should not have errors if all calls succeeded
67
+ // (errors array might be present if some endpoints failed)
68
+ console.log('Healthy app diagnostic result:', JSON.stringify(result, null, 2));
69
+ }, 30000);
70
+ it('should detect issues in an unhealthy application', async () => {
71
+ const result = await client.diagnoseApplication(TEST_DATA.APP_UUID_UNHEALTHY);
72
+ expect(result.application).not.toBeNull();
73
+ // Should detect unhealthy status
74
+ if (result.application?.status?.includes('exited') ||
75
+ result.application?.status?.includes('unhealthy')) {
76
+ expect(result.health.status).toBe('unhealthy');
77
+ expect(result.health.issues.length).toBeGreaterThan(0);
78
+ }
79
+ console.log('Unhealthy app diagnostic result:', JSON.stringify(result, null, 2));
80
+ }, 30000);
81
+ it('should handle non-existent application gracefully', async () => {
82
+ const result = await client.diagnoseApplication('non-existent-uuid');
83
+ // Should have errors but not throw
84
+ expect(result.errors).toBeDefined();
85
+ expect(result.errors.length).toBeGreaterThan(0);
86
+ expect(result.application).toBeNull();
87
+ }, 30000);
88
+ });
89
+ describe('diagnoseServer', () => {
90
+ it('should return diagnostic data for a server', async () => {
91
+ const result = await client.diagnoseServer(TEST_DATA.SERVER_UUID);
92
+ // Should have server info
93
+ expect(result.server).not.toBeNull();
94
+ expect(result.server?.uuid).toBe(TEST_DATA.SERVER_UUID);
95
+ expect(result.server?.name).toBeDefined();
96
+ expect(result.server?.ip).toBeDefined();
97
+ // Should have health assessment
98
+ expect(result.health).toBeDefined();
99
+ expect(['healthy', 'unhealthy', 'unknown']).toContain(result.health.status);
100
+ // Should have resources array
101
+ expect(Array.isArray(result.resources)).toBe(true);
102
+ // Should have domains array
103
+ expect(Array.isArray(result.domains)).toBe(true);
104
+ // Should have validation result
105
+ expect(result.validation).toBeDefined();
106
+ console.log('Server diagnostic result:', JSON.stringify(result, null, 2));
107
+ }, 30000);
108
+ it('should handle non-existent server gracefully', async () => {
109
+ const result = await client.diagnoseServer('non-existent-uuid');
110
+ expect(result.errors).toBeDefined();
111
+ expect(result.errors.length).toBeGreaterThan(0);
112
+ expect(result.server).toBeNull();
113
+ }, 30000);
114
+ });
115
+ describe('findInfrastructureIssues', () => {
116
+ it('should return infrastructure issues report', async () => {
117
+ const result = await client.findInfrastructureIssues();
118
+ // Should have summary
119
+ expect(result.summary).toBeDefined();
120
+ expect(typeof result.summary.total_issues).toBe('number');
121
+ expect(typeof result.summary.unhealthy_applications).toBe('number');
122
+ expect(typeof result.summary.unhealthy_databases).toBe('number');
123
+ expect(typeof result.summary.unhealthy_services).toBe('number');
124
+ expect(typeof result.summary.unreachable_servers).toBe('number');
125
+ // Should have issues array
126
+ expect(Array.isArray(result.issues)).toBe(true);
127
+ // Each issue should have required fields
128
+ for (const issue of result.issues) {
129
+ expect(['application', 'database', 'service', 'server']).toContain(issue.type);
130
+ expect(issue.uuid).toBeDefined();
131
+ expect(issue.name).toBeDefined();
132
+ expect(issue.issue).toBeDefined();
133
+ expect(issue.status).toBeDefined();
134
+ }
135
+ // Summary counts should match issues array
136
+ expect(result.summary.total_issues).toBe(result.issues.length);
137
+ console.log('Infrastructure issues report:', JSON.stringify(result, null, 2));
138
+ }, 60000);
139
+ });
140
+ });
@@ -12,6 +12,9 @@ const mockListProjects = jest.fn();
12
12
  const mockListApplications = jest.fn();
13
13
  const mockListDatabases = jest.fn();
14
14
  const mockListServices = jest.fn();
15
+ const mockDiagnoseApplication = jest.fn();
16
+ const mockDiagnoseServer = jest.fn();
17
+ const mockFindInfrastructureIssues = jest.fn();
15
18
  // Mock the CoolifyClient module
16
19
  jest.mock('../lib/coolify-client.js', () => ({
17
20
  CoolifyClient: jest.fn().mockImplementation(() => ({
@@ -20,6 +23,9 @@ jest.mock('../lib/coolify-client.js', () => ({
20
23
  listApplications: mockListApplications,
21
24
  listDatabases: mockListDatabases,
22
25
  listServices: mockListServices,
26
+ diagnoseApplication: mockDiagnoseApplication,
27
+ diagnoseServer: mockDiagnoseServer,
28
+ findInfrastructureIssues: mockFindInfrastructureIssues,
23
29
  getVersion: jest.fn(),
24
30
  })),
25
31
  }));
@@ -138,4 +144,164 @@ describe('CoolifyMcpServer', () => {
138
144
  await expect(mockListServers({ summary: true })).rejects.toThrow('Connection failed');
139
145
  });
140
146
  });
147
+ describe('diagnostic tools', () => {
148
+ beforeEach(() => {
149
+ new CoolifyMcpServer({
150
+ baseUrl: 'http://localhost:3000',
151
+ accessToken: 'test-token',
152
+ });
153
+ });
154
+ describe('diagnose_app', () => {
155
+ it('should call diagnoseApplication with the query', async () => {
156
+ mockDiagnoseApplication.mockResolvedValue({
157
+ application: {
158
+ uuid: 'app-uuid-123',
159
+ name: 'test-app',
160
+ status: 'running',
161
+ fqdn: 'https://test.example.com',
162
+ git_repository: 'org/repo',
163
+ git_branch: 'main',
164
+ },
165
+ health: { status: 'healthy', issues: [] },
166
+ logs: 'Application started',
167
+ environment_variables: {
168
+ count: 2,
169
+ variables: [{ key: 'NODE_ENV', is_build_time: false }],
170
+ },
171
+ recent_deployments: [],
172
+ });
173
+ await mockDiagnoseApplication('test-app');
174
+ expect(mockDiagnoseApplication).toHaveBeenCalledWith('test-app');
175
+ });
176
+ it('should call diagnoseApplication with a domain', async () => {
177
+ mockDiagnoseApplication.mockResolvedValue({
178
+ application: null,
179
+ health: { status: 'unknown', issues: [] },
180
+ logs: null,
181
+ environment_variables: { count: 0, variables: [] },
182
+ recent_deployments: [],
183
+ errors: ['Application not found'],
184
+ });
185
+ await mockDiagnoseApplication('tidylinker.com');
186
+ expect(mockDiagnoseApplication).toHaveBeenCalledWith('tidylinker.com');
187
+ });
188
+ it('should call diagnoseApplication with a UUID', async () => {
189
+ mockDiagnoseApplication.mockResolvedValue({
190
+ application: {
191
+ uuid: 'xs0sgs4gog044s4k4c88kgsc',
192
+ name: 'test-app',
193
+ status: 'running',
194
+ fqdn: null,
195
+ git_repository: null,
196
+ git_branch: null,
197
+ },
198
+ health: { status: 'healthy', issues: [] },
199
+ logs: null,
200
+ environment_variables: { count: 0, variables: [] },
201
+ recent_deployments: [],
202
+ });
203
+ await mockDiagnoseApplication('xs0sgs4gog044s4k4c88kgsc');
204
+ expect(mockDiagnoseApplication).toHaveBeenCalledWith('xs0sgs4gog044s4k4c88kgsc');
205
+ });
206
+ });
207
+ describe('diagnose_server', () => {
208
+ it('should call diagnoseServer with the query', async () => {
209
+ mockDiagnoseServer.mockResolvedValue({
210
+ server: {
211
+ uuid: 'srv-uuid-123',
212
+ name: 'production-server',
213
+ ip: '192.168.1.100',
214
+ status: 'running',
215
+ is_reachable: true,
216
+ },
217
+ health: { status: 'healthy', issues: [] },
218
+ resources: [],
219
+ domains: [],
220
+ validation: { message: 'Server is reachable' },
221
+ });
222
+ await mockDiagnoseServer('production-server');
223
+ expect(mockDiagnoseServer).toHaveBeenCalledWith('production-server');
224
+ });
225
+ it('should call diagnoseServer with an IP address', async () => {
226
+ mockDiagnoseServer.mockResolvedValue({
227
+ server: {
228
+ uuid: 'srv-uuid-123',
229
+ name: 'production-server',
230
+ ip: '192.168.1.100',
231
+ status: 'running',
232
+ is_reachable: true,
233
+ },
234
+ health: { status: 'healthy', issues: [] },
235
+ resources: [],
236
+ domains: [],
237
+ validation: { message: 'Server is reachable' },
238
+ });
239
+ await mockDiagnoseServer('192.168.1.100');
240
+ expect(mockDiagnoseServer).toHaveBeenCalledWith('192.168.1.100');
241
+ });
242
+ it('should call diagnoseServer with a UUID', async () => {
243
+ mockDiagnoseServer.mockResolvedValue({
244
+ server: {
245
+ uuid: 'ggkk8w4c08gw48oowsg4g0oc',
246
+ name: 'coolify-apps',
247
+ ip: '10.0.0.1',
248
+ status: 'running',
249
+ is_reachable: true,
250
+ },
251
+ health: { status: 'healthy', issues: [] },
252
+ resources: [],
253
+ domains: [],
254
+ validation: { message: 'Server is reachable' },
255
+ });
256
+ await mockDiagnoseServer('ggkk8w4c08gw48oowsg4g0oc');
257
+ expect(mockDiagnoseServer).toHaveBeenCalledWith('ggkk8w4c08gw48oowsg4g0oc');
258
+ });
259
+ });
260
+ describe('find_issues', () => {
261
+ it('should call findInfrastructureIssues', async () => {
262
+ mockFindInfrastructureIssues.mockResolvedValue({
263
+ summary: {
264
+ total_issues: 2,
265
+ unhealthy_applications: 1,
266
+ unhealthy_databases: 0,
267
+ unhealthy_services: 1,
268
+ unreachable_servers: 0,
269
+ },
270
+ issues: [
271
+ {
272
+ type: 'application',
273
+ uuid: 'app-1',
274
+ name: 'broken-app',
275
+ issue: 'Application is unhealthy',
276
+ status: 'exited:unhealthy',
277
+ },
278
+ {
279
+ type: 'service',
280
+ uuid: 'svc-1',
281
+ name: 'broken-service',
282
+ issue: 'Service has exited',
283
+ status: 'exited',
284
+ },
285
+ ],
286
+ });
287
+ await mockFindInfrastructureIssues();
288
+ expect(mockFindInfrastructureIssues).toHaveBeenCalled();
289
+ });
290
+ it('should return empty issues when infrastructure is healthy', async () => {
291
+ mockFindInfrastructureIssues.mockResolvedValue({
292
+ summary: {
293
+ total_issues: 0,
294
+ unhealthy_applications: 0,
295
+ unhealthy_databases: 0,
296
+ unhealthy_services: 0,
297
+ unreachable_servers: 0,
298
+ },
299
+ issues: [],
300
+ });
301
+ const result = await mockFindInfrastructureIssues();
302
+ expect(result.summary.total_issues).toBe(0);
303
+ expect(result.issues).toHaveLength(0);
304
+ });
305
+ });
306
+ });
141
307
  });
@@ -2,7 +2,7 @@
2
2
  * Coolify API Client
3
3
  * Complete HTTP client for the Coolify API v1
4
4
  */
5
- import type { CoolifyConfig, DeleteOptions, MessageResponse, UuidResponse, Server, ServerResource, ServerDomain, ServerValidation, CreateServerRequest, UpdateServerRequest, Project, CreateProjectRequest, UpdateProjectRequest, Environment, CreateEnvironmentRequest, Application, CreateApplicationPublicRequest, CreateApplicationPrivateGHRequest, CreateApplicationPrivateKeyRequest, CreateApplicationDockerfileRequest, CreateApplicationDockerImageRequest, CreateApplicationDockerComposeRequest, UpdateApplicationRequest, ApplicationActionResponse, EnvironmentVariable, CreateEnvVarRequest, UpdateEnvVarRequest, BulkUpdateEnvVarsRequest, Database, UpdateDatabaseRequest, DatabaseBackup, CreateDatabaseBackupRequest, Service, CreateServiceRequest, UpdateServiceRequest, ServiceCreateResponse, Deployment, Team, TeamMember, PrivateKey, CreatePrivateKeyRequest, UpdatePrivateKeyRequest, CloudToken, CreateCloudTokenRequest, UpdateCloudTokenRequest, CloudTokenValidation, Version } from '../types/coolify.js';
5
+ import type { CoolifyConfig, DeleteOptions, MessageResponse, UuidResponse, Server, ServerResource, ServerDomain, ServerValidation, CreateServerRequest, UpdateServerRequest, Project, CreateProjectRequest, UpdateProjectRequest, Environment, CreateEnvironmentRequest, Application, CreateApplicationPublicRequest, CreateApplicationPrivateGHRequest, CreateApplicationPrivateKeyRequest, CreateApplicationDockerfileRequest, CreateApplicationDockerImageRequest, CreateApplicationDockerComposeRequest, UpdateApplicationRequest, ApplicationActionResponse, EnvironmentVariable, CreateEnvVarRequest, UpdateEnvVarRequest, BulkUpdateEnvVarsRequest, Database, UpdateDatabaseRequest, DatabaseBackup, BackupExecution, Service, CreateServiceRequest, UpdateServiceRequest, ServiceCreateResponse, Deployment, Team, TeamMember, PrivateKey, CreatePrivateKeyRequest, UpdatePrivateKeyRequest, CloudToken, CreateCloudTokenRequest, UpdateCloudTokenRequest, CloudTokenValidation, Version, ApplicationDiagnostic, ServerDiagnostic, InfrastructureIssuesReport } from '../types/coolify.js';
6
6
  export interface ListOptions {
7
7
  page?: number;
8
8
  per_page?: number;
@@ -112,8 +112,6 @@ export declare class CoolifyClient {
112
112
  startDatabase(uuid: string): Promise<MessageResponse>;
113
113
  stopDatabase(uuid: string): Promise<MessageResponse>;
114
114
  restartDatabase(uuid: string): Promise<MessageResponse>;
115
- listDatabaseBackups(uuid: string): Promise<DatabaseBackup[]>;
116
- createDatabaseBackup(uuid: string, data: CreateDatabaseBackupRequest): Promise<UuidResponse & MessageResponse>;
117
115
  listServices(options?: ListOptions): Promise<Service[] | ServiceSummary[]>;
118
116
  getService(uuid: string): Promise<Service>;
119
117
  createService(data: CreateServiceRequest): Promise<ServiceCreateResponse>;
@@ -146,4 +144,42 @@ export declare class CoolifyClient {
146
144
  updateCloudToken(uuid: string, data: UpdateCloudTokenRequest): Promise<CloudToken>;
147
145
  deleteCloudToken(uuid: string): Promise<MessageResponse>;
148
146
  validateCloudToken(uuid: string): Promise<CloudTokenValidation>;
147
+ listDatabaseBackups(databaseUuid: string): Promise<DatabaseBackup[]>;
148
+ getDatabaseBackup(databaseUuid: string, backupUuid: string): Promise<DatabaseBackup>;
149
+ listBackupExecutions(databaseUuid: string, backupUuid: string): Promise<BackupExecution[]>;
150
+ getBackupExecution(databaseUuid: string, backupUuid: string, executionUuid: string): Promise<BackupExecution>;
151
+ cancelDeployment(uuid: string): Promise<MessageResponse>;
152
+ /**
153
+ * Check if a string looks like a UUID (Coolify format or standard format).
154
+ * Coolify UUIDs are alphanumeric strings, typically 24 chars like "xs0sgs4gog044s4k4c88kgsc"
155
+ * Also accepts standard UUID format with hyphens like "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
156
+ */
157
+ private isLikelyUuid;
158
+ /**
159
+ * Find an application by UUID, name, or domain (FQDN).
160
+ * Returns the UUID if found, throws if not found or multiple matches.
161
+ */
162
+ resolveApplicationUuid(query: string): Promise<string>;
163
+ /**
164
+ * Find a server by UUID, name, or IP address.
165
+ * Returns the UUID if found, throws if not found or multiple matches.
166
+ */
167
+ resolveServerUuid(query: string): Promise<string>;
168
+ /**
169
+ * Get comprehensive diagnostic info for an application.
170
+ * Aggregates: application details, logs, env vars, recent deployments.
171
+ * @param query - Application UUID, name, or domain (FQDN)
172
+ */
173
+ diagnoseApplication(query: string): Promise<ApplicationDiagnostic>;
174
+ /**
175
+ * Get comprehensive diagnostic info for a server.
176
+ * Aggregates: server details, resources, domains, validation.
177
+ * @param query - Server UUID, name, or IP address
178
+ */
179
+ diagnoseServer(query: string): Promise<ServerDiagnostic>;
180
+ /**
181
+ * Scan infrastructure for common issues.
182
+ * Finds: unreachable servers, unhealthy apps, exited databases, stopped services.
183
+ */
184
+ findInfrastructureIssues(): Promise<InfrastructureIssuesReport>;
149
185
  }