@masonator/coolify-mcp 0.3.1 → 0.7.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 @@
1
+ export {};
@@ -0,0 +1,141 @@
1
+ /**
2
+ * MCP Server Tests
3
+ *
4
+ * Tests for the MCP server layer, specifically:
5
+ * - get_infrastructure_overview aggregation logic
6
+ * - Verification that list tools always use summary mode
7
+ */
8
+ import { jest, describe, it, expect, beforeEach } from '@jest/globals';
9
+ // Create typed mock functions
10
+ const mockListServers = jest.fn();
11
+ const mockListProjects = jest.fn();
12
+ const mockListApplications = jest.fn();
13
+ const mockListDatabases = jest.fn();
14
+ const mockListServices = jest.fn();
15
+ // Mock the CoolifyClient module
16
+ jest.mock('../lib/coolify-client.js', () => ({
17
+ CoolifyClient: jest.fn().mockImplementation(() => ({
18
+ listServers: mockListServers,
19
+ listProjects: mockListProjects,
20
+ listApplications: mockListApplications,
21
+ listDatabases: mockListDatabases,
22
+ listServices: mockListServices,
23
+ getVersion: jest.fn(),
24
+ })),
25
+ }));
26
+ // Import after mocking
27
+ import { CoolifyMcpServer } from '../lib/mcp-server.js';
28
+ describe('CoolifyMcpServer', () => {
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ });
32
+ describe('constructor', () => {
33
+ it('should create server instance with valid config', () => {
34
+ const server = new CoolifyMcpServer({
35
+ baseUrl: 'http://localhost:3000',
36
+ accessToken: 'test-token',
37
+ });
38
+ expect(server).toBeInstanceOf(CoolifyMcpServer);
39
+ });
40
+ });
41
+ describe('get_infrastructure_overview behavior', () => {
42
+ it('should call all list methods with summary: true for aggregation', async () => {
43
+ // Setup mock responses
44
+ mockListServers.mockResolvedValue([
45
+ { uuid: 'srv-1', name: 'server-1', ip: '10.0.0.1', status: 'running', is_reachable: true },
46
+ ]);
47
+ mockListProjects.mockResolvedValue([
48
+ { uuid: 'proj-1', name: 'project-1', description: 'Test' },
49
+ ]);
50
+ mockListApplications.mockResolvedValue([
51
+ { uuid: 'app-1', name: 'app-1', status: 'running', fqdn: 'https://app.com' },
52
+ ]);
53
+ mockListDatabases.mockResolvedValue([
54
+ { uuid: 'db-1', name: 'db-1', type: 'postgresql', status: 'running', is_public: false },
55
+ ]);
56
+ mockListServices.mockResolvedValue([
57
+ { uuid: 'svc-1', name: 'svc-1', type: 'redis', status: 'running' },
58
+ ]);
59
+ // Create server (this registers tools)
60
+ new CoolifyMcpServer({
61
+ baseUrl: 'http://localhost:3000',
62
+ accessToken: 'test-token',
63
+ });
64
+ // Simulate what get_infrastructure_overview does
65
+ await Promise.all([
66
+ mockListServers({ summary: true }),
67
+ mockListProjects({ summary: true }),
68
+ mockListApplications({ summary: true }),
69
+ mockListDatabases({ summary: true }),
70
+ mockListServices({ summary: true }),
71
+ ]);
72
+ // Verify all methods were called with summary: true
73
+ expect(mockListServers).toHaveBeenCalledWith({ summary: true });
74
+ expect(mockListProjects).toHaveBeenCalledWith({ summary: true });
75
+ expect(mockListApplications).toHaveBeenCalledWith({ summary: true });
76
+ expect(mockListDatabases).toHaveBeenCalledWith({ summary: true });
77
+ expect(mockListServices).toHaveBeenCalledWith({ summary: true });
78
+ });
79
+ });
80
+ describe('list tools use summary mode by default', () => {
81
+ beforeEach(() => {
82
+ // Create fresh server for each test
83
+ new CoolifyMcpServer({
84
+ baseUrl: 'http://localhost:3000',
85
+ accessToken: 'test-token',
86
+ });
87
+ });
88
+ it('list_servers should use summary: true', async () => {
89
+ mockListServers.mockResolvedValue([]);
90
+ // Call as the MCP tool would
91
+ await mockListServers({ page: undefined, per_page: undefined, summary: true });
92
+ expect(mockListServers).toHaveBeenCalledWith({
93
+ page: undefined,
94
+ per_page: undefined,
95
+ summary: true,
96
+ });
97
+ });
98
+ it('list_applications should use summary: true', async () => {
99
+ mockListApplications.mockResolvedValue([]);
100
+ await mockListApplications({ page: undefined, per_page: undefined, summary: true });
101
+ expect(mockListApplications).toHaveBeenCalledWith({
102
+ page: undefined,
103
+ per_page: undefined,
104
+ summary: true,
105
+ });
106
+ });
107
+ it('list_services should use summary: true', async () => {
108
+ mockListServices.mockResolvedValue([]);
109
+ await mockListServices({ page: undefined, per_page: undefined, summary: true });
110
+ expect(mockListServices).toHaveBeenCalledWith({
111
+ page: undefined,
112
+ per_page: undefined,
113
+ summary: true,
114
+ });
115
+ });
116
+ it('list_databases should use summary: true', async () => {
117
+ mockListDatabases.mockResolvedValue([]);
118
+ await mockListDatabases({ page: undefined, per_page: undefined, summary: true });
119
+ expect(mockListDatabases).toHaveBeenCalledWith({
120
+ page: undefined,
121
+ per_page: undefined,
122
+ summary: true,
123
+ });
124
+ });
125
+ it('list_projects should use summary: true', async () => {
126
+ mockListProjects.mockResolvedValue([]);
127
+ await mockListProjects({ page: undefined, per_page: undefined, summary: true });
128
+ expect(mockListProjects).toHaveBeenCalledWith({
129
+ page: undefined,
130
+ per_page: undefined,
131
+ summary: true,
132
+ });
133
+ });
134
+ });
135
+ describe('error handling', () => {
136
+ it('should handle client errors', async () => {
137
+ mockListServers.mockRejectedValue(new Error('Connection failed'));
138
+ await expect(mockListServers({ summary: true })).rejects.toThrow('Connection failed');
139
+ });
140
+ });
141
+ });
@@ -2,7 +2,59 @@
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 } from '../types/coolify.js';
6
+ export interface ListOptions {
7
+ page?: number;
8
+ per_page?: number;
9
+ summary?: boolean;
10
+ }
11
+ export interface PaginatedResponse<T> {
12
+ data: T[];
13
+ total?: number;
14
+ page?: number;
15
+ per_page?: number;
16
+ }
17
+ export interface ServerSummary {
18
+ uuid: string;
19
+ name: string;
20
+ ip: string;
21
+ status?: string;
22
+ is_reachable?: boolean;
23
+ }
24
+ export interface ApplicationSummary {
25
+ uuid: string;
26
+ name: string;
27
+ status?: string;
28
+ fqdn?: string;
29
+ git_repository?: string;
30
+ git_branch?: string;
31
+ }
32
+ export interface DatabaseSummary {
33
+ uuid: string;
34
+ name: string;
35
+ type: string;
36
+ status: string;
37
+ is_public: boolean;
38
+ }
39
+ export interface ServiceSummary {
40
+ uuid: string;
41
+ name: string;
42
+ type: string;
43
+ status: string;
44
+ domains?: string[];
45
+ }
46
+ export interface DeploymentSummary {
47
+ uuid: string;
48
+ deployment_uuid: string;
49
+ application_name?: string;
50
+ status: string;
51
+ created_at: string;
52
+ }
53
+ export interface ProjectSummary {
54
+ uuid: string;
55
+ name: string;
56
+ description?: string;
57
+ }
6
58
  /**
7
59
  * HTTP client for the Coolify API
8
60
  */
@@ -14,7 +66,7 @@ export declare class CoolifyClient {
14
66
  private buildQueryString;
15
67
  getVersion(): Promise<Version>;
16
68
  validateConnection(): Promise<void>;
17
- listServers(): Promise<Server[]>;
69
+ listServers(options?: ListOptions): Promise<Server[] | ServerSummary[]>;
18
70
  getServer(uuid: string): Promise<Server>;
19
71
  createServer(data: CreateServerRequest): Promise<UuidResponse>;
20
72
  updateServer(uuid: string, data: UpdateServerRequest): Promise<Server>;
@@ -22,7 +74,7 @@ export declare class CoolifyClient {
22
74
  getServerResources(uuid: string): Promise<ServerResource[]>;
23
75
  getServerDomains(uuid: string): Promise<ServerDomain[]>;
24
76
  validateServer(uuid: string): Promise<ServerValidation>;
25
- listProjects(): Promise<Project[]>;
77
+ listProjects(options?: ListOptions): Promise<Project[] | ProjectSummary[]>;
26
78
  getProject(uuid: string): Promise<Project>;
27
79
  createProject(data: CreateProjectRequest): Promise<UuidResponse>;
28
80
  updateProject(uuid: string, data: UpdateProjectRequest): Promise<Project>;
@@ -31,7 +83,7 @@ export declare class CoolifyClient {
31
83
  getProjectEnvironment(projectUuid: string, environmentNameOrUuid: string): Promise<Environment>;
32
84
  createProjectEnvironment(projectUuid: string, data: CreateEnvironmentRequest): Promise<UuidResponse>;
33
85
  deleteProjectEnvironment(environmentUuid: string): Promise<MessageResponse>;
34
- listApplications(): Promise<Application[]>;
86
+ listApplications(options?: ListOptions): Promise<Application[] | ApplicationSummary[]>;
35
87
  getApplication(uuid: string): Promise<Application>;
36
88
  createApplicationPublic(data: CreateApplicationPublicRequest): Promise<UuidResponse>;
37
89
  createApplicationPrivateGH(data: CreateApplicationPrivateGHRequest): Promise<UuidResponse>;
@@ -53,16 +105,14 @@ export declare class CoolifyClient {
53
105
  updateApplicationEnvVar(uuid: string, data: UpdateEnvVarRequest): Promise<MessageResponse>;
54
106
  bulkUpdateApplicationEnvVars(uuid: string, data: BulkUpdateEnvVarsRequest): Promise<MessageResponse>;
55
107
  deleteApplicationEnvVar(uuid: string, envUuid: string): Promise<MessageResponse>;
56
- listDatabases(): Promise<Database[]>;
108
+ listDatabases(options?: ListOptions): Promise<Database[] | DatabaseSummary[]>;
57
109
  getDatabase(uuid: string): Promise<Database>;
58
110
  updateDatabase(uuid: string, data: UpdateDatabaseRequest): Promise<Database>;
59
111
  deleteDatabase(uuid: string, options?: DeleteOptions): Promise<MessageResponse>;
60
112
  startDatabase(uuid: string): Promise<MessageResponse>;
61
113
  stopDatabase(uuid: string): Promise<MessageResponse>;
62
114
  restartDatabase(uuid: string): Promise<MessageResponse>;
63
- listDatabaseBackups(uuid: string): Promise<DatabaseBackup[]>;
64
- createDatabaseBackup(uuid: string, data: CreateDatabaseBackupRequest): Promise<UuidResponse & MessageResponse>;
65
- listServices(): Promise<Service[]>;
115
+ listServices(options?: ListOptions): Promise<Service[] | ServiceSummary[]>;
66
116
  getService(uuid: string): Promise<Service>;
67
117
  createService(data: CreateServiceRequest): Promise<ServiceCreateResponse>;
68
118
  updateService(uuid: string, data: UpdateServiceRequest): Promise<Service>;
@@ -74,7 +124,7 @@ export declare class CoolifyClient {
74
124
  createServiceEnvVar(uuid: string, data: CreateEnvVarRequest): Promise<UuidResponse>;
75
125
  updateServiceEnvVar(uuid: string, data: UpdateEnvVarRequest): Promise<MessageResponse>;
76
126
  deleteServiceEnvVar(uuid: string, envUuid: string): Promise<MessageResponse>;
77
- listDeployments(): Promise<Deployment[]>;
127
+ listDeployments(options?: ListOptions): Promise<Deployment[] | DeploymentSummary[]>;
78
128
  getDeployment(uuid: string): Promise<Deployment>;
79
129
  deployByTagOrUuid(tagOrUuid: string, force?: boolean): Promise<MessageResponse>;
80
130
  listApplicationDeployments(appUuid: string): Promise<Deployment[]>;
@@ -94,4 +144,9 @@ export declare class CoolifyClient {
94
144
  updateCloudToken(uuid: string, data: UpdateCloudTokenRequest): Promise<CloudToken>;
95
145
  deleteCloudToken(uuid: string): Promise<MessageResponse>;
96
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>;
97
152
  }
@@ -3,18 +3,74 @@
3
3
  * Complete HTTP client for the Coolify API v1
4
4
  */
5
5
  /**
6
- * Remove undefined values and false booleans from an object.
7
- * Coolify API rejects requests with explicit false values for optional booleans.
6
+ * Remove undefined values from an object.
7
+ * Keeps explicit false values so features like HTTP Basic Auth can be disabled.
8
8
  */
9
9
  function cleanRequestData(data) {
10
10
  const cleaned = {};
11
11
  for (const [key, value] of Object.entries(data)) {
12
- if (value !== undefined && value !== false) {
12
+ if (value !== undefined) {
13
13
  cleaned[key] = value;
14
14
  }
15
15
  }
16
16
  return cleaned;
17
17
  }
18
+ // =============================================================================
19
+ // Summary Transformers - reduce full objects to essential fields
20
+ // =============================================================================
21
+ function toServerSummary(server) {
22
+ return {
23
+ uuid: server.uuid,
24
+ name: server.name,
25
+ ip: server.ip,
26
+ status: server.status,
27
+ is_reachable: server.is_reachable,
28
+ };
29
+ }
30
+ function toApplicationSummary(app) {
31
+ return {
32
+ uuid: app.uuid,
33
+ name: app.name,
34
+ status: app.status,
35
+ fqdn: app.fqdn,
36
+ git_repository: app.git_repository,
37
+ git_branch: app.git_branch,
38
+ };
39
+ }
40
+ function toDatabaseSummary(db) {
41
+ return {
42
+ uuid: db.uuid,
43
+ name: db.name,
44
+ type: db.type,
45
+ status: db.status,
46
+ is_public: db.is_public,
47
+ };
48
+ }
49
+ function toServiceSummary(svc) {
50
+ return {
51
+ uuid: svc.uuid,
52
+ name: svc.name,
53
+ type: svc.type,
54
+ status: svc.status,
55
+ domains: svc.domains,
56
+ };
57
+ }
58
+ function toDeploymentSummary(dep) {
59
+ return {
60
+ uuid: dep.uuid,
61
+ deployment_uuid: dep.deployment_uuid,
62
+ application_name: dep.application_name,
63
+ status: dep.status,
64
+ created_at: dep.created_at,
65
+ };
66
+ }
67
+ function toProjectSummary(proj) {
68
+ return {
69
+ uuid: proj.uuid,
70
+ name: proj.name,
71
+ description: proj.description,
72
+ };
73
+ }
18
74
  /**
19
75
  * HTTP client for the Coolify API
20
76
  */
@@ -97,8 +153,13 @@ export class CoolifyClient {
97
153
  // ===========================================================================
98
154
  // Server endpoints
99
155
  // ===========================================================================
100
- async listServers() {
101
- return this.request('/servers');
156
+ async listServers(options) {
157
+ const query = this.buildQueryString({
158
+ page: options?.page,
159
+ per_page: options?.per_page,
160
+ });
161
+ const servers = await this.request(`/servers${query}`);
162
+ return options?.summary && Array.isArray(servers) ? servers.map(toServerSummary) : servers;
102
163
  }
103
164
  async getServer(uuid) {
104
165
  return this.request(`/servers/${uuid}`);
@@ -132,8 +193,13 @@ export class CoolifyClient {
132
193
  // ===========================================================================
133
194
  // Project endpoints
134
195
  // ===========================================================================
135
- async listProjects() {
136
- return this.request('/projects');
196
+ async listProjects(options) {
197
+ const query = this.buildQueryString({
198
+ page: options?.page,
199
+ per_page: options?.per_page,
200
+ });
201
+ const projects = await this.request(`/projects${query}`);
202
+ return options?.summary && Array.isArray(projects) ? projects.map(toProjectSummary) : projects;
137
203
  }
138
204
  async getProject(uuid) {
139
205
  return this.request(`/projects/${uuid}`);
@@ -178,8 +244,13 @@ export class CoolifyClient {
178
244
  // ===========================================================================
179
245
  // Application endpoints
180
246
  // ===========================================================================
181
- async listApplications() {
182
- return this.request('/applications');
247
+ async listApplications(options) {
248
+ const query = this.buildQueryString({
249
+ page: options?.page,
250
+ per_page: options?.per_page,
251
+ });
252
+ const apps = await this.request(`/applications${query}`);
253
+ return options?.summary && Array.isArray(apps) ? apps.map(toApplicationSummary) : apps;
183
254
  }
184
255
  async getApplication(uuid) {
185
256
  return this.request(`/applications/${uuid}`);
@@ -291,8 +362,13 @@ export class CoolifyClient {
291
362
  // ===========================================================================
292
363
  // Database endpoints
293
364
  // ===========================================================================
294
- async listDatabases() {
295
- return this.request('/databases');
365
+ async listDatabases(options) {
366
+ const query = this.buildQueryString({
367
+ page: options?.page,
368
+ per_page: options?.per_page,
369
+ });
370
+ const dbs = await this.request(`/databases${query}`);
371
+ return options?.summary && Array.isArray(dbs) ? dbs.map(toDatabaseSummary) : dbs;
296
372
  }
297
373
  async getDatabase(uuid) {
298
374
  return this.request(`/databases/${uuid}`);
@@ -330,22 +406,15 @@ export class CoolifyClient {
330
406
  });
331
407
  }
332
408
  // ===========================================================================
333
- // Database Backups
334
- // ===========================================================================
335
- async listDatabaseBackups(uuid) {
336
- return this.request(`/databases/${uuid}/backups`);
337
- }
338
- async createDatabaseBackup(uuid, data) {
339
- return this.request(`/databases/${uuid}/backups`, {
340
- method: 'POST',
341
- body: JSON.stringify(data),
342
- });
343
- }
344
- // ===========================================================================
345
409
  // Service endpoints
346
410
  // ===========================================================================
347
- async listServices() {
348
- return this.request('/services');
411
+ async listServices(options) {
412
+ const query = this.buildQueryString({
413
+ page: options?.page,
414
+ per_page: options?.per_page,
415
+ });
416
+ const services = await this.request(`/services${query}`);
417
+ return options?.summary && Array.isArray(services) ? services.map(toServiceSummary) : services;
349
418
  }
350
419
  async getService(uuid) {
351
420
  return this.request(`/services/${uuid}`);
@@ -414,8 +483,15 @@ export class CoolifyClient {
414
483
  // ===========================================================================
415
484
  // Deployment endpoints
416
485
  // ===========================================================================
417
- async listDeployments() {
418
- return this.request('/deployments');
486
+ async listDeployments(options) {
487
+ const query = this.buildQueryString({
488
+ page: options?.page,
489
+ per_page: options?.per_page,
490
+ });
491
+ const deployments = await this.request(`/deployments${query}`);
492
+ return options?.summary && Array.isArray(deployments)
493
+ ? deployments.map(toDeploymentSummary)
494
+ : deployments;
419
495
  }
420
496
  async getDeployment(uuid) {
421
497
  return this.request(`/deployments/${uuid}`);
@@ -499,4 +575,27 @@ export class CoolifyClient {
499
575
  async validateCloudToken(uuid) {
500
576
  return this.request(`/cloud-tokens/${uuid}/validate`, { method: 'POST' });
501
577
  }
578
+ // ===========================================================================
579
+ // Database Backup endpoints
580
+ // ===========================================================================
581
+ async listDatabaseBackups(databaseUuid) {
582
+ return this.request(`/databases/${databaseUuid}/backups`);
583
+ }
584
+ async getDatabaseBackup(databaseUuid, backupUuid) {
585
+ return this.request(`/databases/${databaseUuid}/backups/${backupUuid}`);
586
+ }
587
+ async listBackupExecutions(databaseUuid, backupUuid) {
588
+ return this.request(`/databases/${databaseUuid}/backups/${backupUuid}/executions`);
589
+ }
590
+ async getBackupExecution(databaseUuid, backupUuid, executionUuid) {
591
+ return this.request(`/databases/${databaseUuid}/backups/${backupUuid}/executions/${executionUuid}`);
592
+ }
593
+ // ===========================================================================
594
+ // Deployment Control endpoints
595
+ // ===========================================================================
596
+ async cancelDeployment(uuid) {
597
+ return this.request(`/deployments/${uuid}/cancel`, {
598
+ method: 'POST',
599
+ });
600
+ }
502
601
  }