@masonator/coolify-mcp 2.4.0 → 2.6.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/README.md CHANGED
@@ -43,7 +43,7 @@ The server uses **85% fewer tokens** than a naive implementation (6,600 vs 43,00
43
43
  ### Prerequisites
44
44
 
45
45
  - Node.js >= 18
46
- - A running Coolify instance
46
+ - A running Coolify instance (tested with v4.0.0-beta.460)
47
47
  - Coolify API access token (generate in Coolify Settings > API)
48
48
 
49
49
  ### Claude Desktop
@@ -106,6 +106,28 @@ The Coolify API returns extremely verbose responses - a single application can c
106
106
  | list_services | ~367KB | ~1.2KB | **99%** |
107
107
  | list_servers | ~4KB | ~0.4KB | **90%** |
108
108
  | list_application_envs | ~3KB/var | ~0.1KB/var | **97%** |
109
+ | deployment get | ~13KB | ~1KB | **92%** |
110
+
111
+ ### HATEOAS-style Response Actions
112
+
113
+ Responses include contextual `_actions` suggesting relevant next steps:
114
+
115
+ ```json
116
+ {
117
+ "data": { "uuid": "abc123", "status": "running" },
118
+ "_actions": [
119
+ { "tool": "application_logs", "args": { "uuid": "abc123" }, "hint": "View logs" },
120
+ {
121
+ "tool": "control",
122
+ "args": { "resource": "application", "action": "restart", "uuid": "abc123" },
123
+ "hint": "Restart"
124
+ }
125
+ ],
126
+ "_pagination": { "next": { "tool": "list_applications", "args": { "page": 2 } } }
127
+ }
128
+ ```
129
+
130
+ This helps AI assistants understand logical next steps without consuming extra tokens.
109
131
 
110
132
  ### Recommended Workflow
111
133
 
@@ -84,6 +84,10 @@ describe('CoolifyClient', () => {
84
84
  deployment_uuid: 'dep-123',
85
85
  application_name: 'test-app',
86
86
  status: 'finished',
87
+ force_rebuild: false,
88
+ is_webhook: false,
89
+ is_api: true,
90
+ restart_only: false,
87
91
  created_at: '2024-01-01',
88
92
  updated_at: '2024-01-01',
89
93
  };
@@ -342,6 +346,18 @@ describe('CoolifyClient', () => {
342
346
  expect(result).toEqual({ message: 'Deployed' });
343
347
  expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?tag=my-tag&force=true', expect.any(Object));
344
348
  });
349
+ it('should deploy by Coolify UUID (24 char alphanumeric)', async () => {
350
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deployed' }));
351
+ // Coolify-style UUID: 24 lowercase alphanumeric chars
352
+ await client.deployByTagOrUuid('xs0sgs4gog044s4k4c88kgsc', false);
353
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?uuid=xs0sgs4gog044s4k4c88kgsc&force=false', expect.any(Object));
354
+ });
355
+ it('should deploy by standard UUID format', async () => {
356
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deployed' }));
357
+ // Standard UUID format with hyphens
358
+ await client.deployByTagOrUuid('a1b2c3d4-e5f6-7890-abcd-ef1234567890', true);
359
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?uuid=a1b2c3d4-e5f6-7890-abcd-ef1234567890&force=true', expect.any(Object));
360
+ });
345
361
  });
346
362
  describe('private keys', () => {
347
363
  it('should list private keys', async () => {
@@ -574,6 +590,98 @@ describe('CoolifyClient', () => {
574
590
  expect(result).toEqual(mockEnvironment);
575
591
  expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/projects/proj-uuid/production', expect.any(Object));
576
592
  });
593
+ it('should get project environment with missing database types', async () => {
594
+ // Use environment_id to match (what real API uses)
595
+ const mockDbSummaries = [
596
+ {
597
+ uuid: 'pg-uuid',
598
+ name: 'pg-db',
599
+ type: 'postgresql',
600
+ status: 'running',
601
+ is_public: false,
602
+ environment_id: 1,
603
+ },
604
+ {
605
+ uuid: 'dragonfly-uuid',
606
+ name: 'dragonfly-cache',
607
+ type: 'standalone-dragonfly',
608
+ status: 'running',
609
+ is_public: false,
610
+ environment_id: 1,
611
+ },
612
+ {
613
+ uuid: 'other-env-db',
614
+ name: 'other-db',
615
+ type: 'standalone-keydb',
616
+ status: 'running',
617
+ is_public: false,
618
+ environment_id: 999, // different env
619
+ },
620
+ ];
621
+ mockFetch
622
+ .mockResolvedValueOnce(mockResponse(mockEnvironment))
623
+ .mockResolvedValueOnce(mockResponse(mockDbSummaries));
624
+ const result = await client.getProjectEnvironmentWithDatabases('proj-uuid', 'production');
625
+ expect(result.uuid).toBe('env-uuid');
626
+ expect(result.dragonflys).toHaveLength(1);
627
+ expect(result.dragonflys[0].uuid).toBe('dragonfly-uuid');
628
+ expect(result.keydbs).toBeUndefined(); // other-env-db is in different env
629
+ });
630
+ it('should match databases by environment_uuid fallback', async () => {
631
+ const mockDbSummaries = [
632
+ {
633
+ uuid: 'keydb-uuid',
634
+ name: 'keydb-cache',
635
+ type: 'standalone-keydb',
636
+ status: 'running',
637
+ is_public: false,
638
+ environment_uuid: 'env-uuid', // matching by uuid
639
+ },
640
+ ];
641
+ mockFetch
642
+ .mockResolvedValueOnce(mockResponse(mockEnvironment))
643
+ .mockResolvedValueOnce(mockResponse(mockDbSummaries));
644
+ const result = await client.getProjectEnvironmentWithDatabases('proj-uuid', 'production');
645
+ expect(result.keydbs).toHaveLength(1);
646
+ expect(result.keydbs[0].uuid).toBe('keydb-uuid');
647
+ });
648
+ it('should match databases by environment_name fallback', async () => {
649
+ const mockDbSummaries = [
650
+ {
651
+ uuid: 'clickhouse-uuid',
652
+ name: 'clickhouse-analytics',
653
+ type: 'standalone-clickhouse',
654
+ status: 'running',
655
+ is_public: false,
656
+ environment_name: 'production', // matching by name
657
+ },
658
+ ];
659
+ mockFetch
660
+ .mockResolvedValueOnce(mockResponse(mockEnvironment))
661
+ .mockResolvedValueOnce(mockResponse(mockDbSummaries));
662
+ const result = await client.getProjectEnvironmentWithDatabases('proj-uuid', 'production');
663
+ expect(result.clickhouses).toHaveLength(1);
664
+ expect(result.clickhouses[0].uuid).toBe('clickhouse-uuid');
665
+ });
666
+ it('should not add empty arrays when no missing DB types exist', async () => {
667
+ const mockDbSummaries = [
668
+ {
669
+ uuid: 'pg-uuid',
670
+ name: 'pg-db',
671
+ type: 'postgresql', // not a "missing" type
672
+ status: 'running',
673
+ is_public: false,
674
+ environment_id: 1,
675
+ },
676
+ ];
677
+ mockFetch
678
+ .mockResolvedValueOnce(mockResponse(mockEnvironment))
679
+ .mockResolvedValueOnce(mockResponse(mockDbSummaries));
680
+ const result = await client.getProjectEnvironmentWithDatabases('proj-uuid', 'production');
681
+ expect(result.dragonflys).toBeUndefined();
682
+ expect(result.keydbs).toBeUndefined();
683
+ expect(result.clickhouses).toBeUndefined();
684
+ });
577
685
  it('should create a project environment', async () => {
578
686
  mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-env-uuid' }));
579
687
  const result = await client.createProjectEnvironment('proj-uuid', { name: 'staging' });
@@ -1165,12 +1273,47 @@ describe('CoolifyClient', () => {
1165
1273
  },
1166
1274
  ]);
1167
1275
  });
1168
- it('should get a deployment', async () => {
1276
+ it('should get a deployment (essential by default, no logs)', async () => {
1169
1277
  mockFetch.mockResolvedValueOnce(mockResponse(mockDeployment));
1170
1278
  const result = await client.getDeployment('dep-uuid');
1171
- expect(result).toEqual(mockDeployment);
1279
+ // By default, returns DeploymentEssential without logs
1280
+ expect(result).toEqual({
1281
+ uuid: 'dep-uuid',
1282
+ deployment_uuid: 'dep-123',
1283
+ application_uuid: undefined,
1284
+ application_name: 'test-app',
1285
+ server_name: undefined,
1286
+ status: 'finished',
1287
+ commit: undefined,
1288
+ force_rebuild: false,
1289
+ is_webhook: false,
1290
+ is_api: true,
1291
+ created_at: '2024-01-01',
1292
+ updated_at: '2024-01-01',
1293
+ logs_available: false,
1294
+ logs_info: undefined,
1295
+ });
1172
1296
  expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/dep-uuid', expect.any(Object));
1173
1297
  });
1298
+ it('should get a deployment with logs when includeLogs is true', async () => {
1299
+ const deploymentWithLogs = { ...mockDeployment, logs: 'Build started...' };
1300
+ mockFetch.mockResolvedValueOnce(mockResponse(deploymentWithLogs));
1301
+ const result = await client.getDeployment('dep-uuid', { includeLogs: true });
1302
+ // With includeLogs: true, returns full Deployment with logs
1303
+ expect(result).toEqual(deploymentWithLogs);
1304
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/dep-uuid', expect.any(Object));
1305
+ });
1306
+ it('should include logs_info when deployment has logs but includeLogs is false', async () => {
1307
+ const deploymentWithLogs = { ...mockDeployment, logs: 'Build started...' };
1308
+ mockFetch.mockResolvedValueOnce(mockResponse(deploymentWithLogs));
1309
+ const result = await client.getDeployment('dep-uuid');
1310
+ // Should have logs_info indicating logs are available
1311
+ expect(result).toMatchObject({
1312
+ uuid: 'dep-uuid',
1313
+ logs_available: true,
1314
+ logs_info: 'Logs available (16 chars). Use lines param to retrieve.',
1315
+ });
1316
+ });
1174
1317
  it('should list application deployments', async () => {
1175
1318
  mockFetch.mockResolvedValueOnce(mockResponse([mockDeployment]));
1176
1319
  const result = await client.listApplicationDeployments('app-uuid');
@@ -6,7 +6,7 @@
6
6
  * These tests verify MCP server instantiation and structure.
7
7
  */
8
8
  import { describe, it, expect, beforeEach } from '@jest/globals';
9
- import { CoolifyMcpServer } from '../lib/mcp-server.js';
9
+ import { CoolifyMcpServer, truncateLogs, getApplicationActions, getDeploymentActions, getPagination, } from '../lib/mcp-server.js';
10
10
  describe('CoolifyMcpServer v2', () => {
11
11
  let server;
12
12
  beforeEach(() => {
@@ -47,6 +47,7 @@ describe('CoolifyMcpServer v2', () => {
47
47
  // Environment operations
48
48
  expect(typeof client.listProjectEnvironments).toBe('function');
49
49
  expect(typeof client.getProjectEnvironment).toBe('function');
50
+ expect(typeof client.getProjectEnvironmentWithDatabases).toBe('function');
50
51
  expect(typeof client.createProjectEnvironment).toBe('function');
51
52
  expect(typeof client.deleteProjectEnvironment).toBe('function');
52
53
  // Application operations
@@ -140,3 +141,170 @@ describe('CoolifyMcpServer v2', () => {
140
141
  });
141
142
  });
142
143
  });
144
+ describe('truncateLogs', () => {
145
+ it('should return logs unchanged when within limits', () => {
146
+ const logs = 'line1\nline2\nline3';
147
+ const result = truncateLogs(logs, 200, 50000);
148
+ expect(result).toBe(logs);
149
+ });
150
+ it('should truncate to last N lines', () => {
151
+ const logs = 'line1\nline2\nline3\nline4\nline5';
152
+ const result = truncateLogs(logs, 3, 50000);
153
+ expect(result).toBe('line3\nline4\nline5');
154
+ });
155
+ it('should truncate by character limit when lines are huge', () => {
156
+ const hugeLine = 'x'.repeat(100);
157
+ const logs = `${hugeLine}\n${hugeLine}\n${hugeLine}`;
158
+ const result = truncateLogs(logs, 200, 50);
159
+ expect(result.length).toBeLessThanOrEqual(50);
160
+ expect(result.startsWith('...[truncated]...')).toBe(true);
161
+ });
162
+ it('should not add truncation prefix when under char limit', () => {
163
+ const logs = 'line1\nline2\nline3';
164
+ const result = truncateLogs(logs, 200, 50000);
165
+ expect(result.startsWith('...[truncated]...')).toBe(false);
166
+ });
167
+ it('should handle empty logs', () => {
168
+ const result = truncateLogs('', 200, 50000);
169
+ expect(result).toBe('');
170
+ });
171
+ it('should use default limits when not specified', () => {
172
+ const logs = 'line1\nline2';
173
+ const result = truncateLogs(logs);
174
+ expect(result).toBe(logs);
175
+ });
176
+ it('should respect custom line limit', () => {
177
+ const lines = Array.from({ length: 300 }, (_, i) => `line${i + 1}`).join('\n');
178
+ const result = truncateLogs(lines, 50, 50000);
179
+ const resultLines = result.split('\n');
180
+ expect(resultLines.length).toBe(50);
181
+ expect(resultLines[0]).toBe('line251');
182
+ expect(resultLines[49]).toBe('line300');
183
+ });
184
+ it('should respect custom char limit', () => {
185
+ const logs = 'x'.repeat(1000);
186
+ const result = truncateLogs(logs, 200, 100);
187
+ expect(result.length).toBe(100);
188
+ });
189
+ });
190
+ // =============================================================================
191
+ // Action Generators Tests
192
+ // =============================================================================
193
+ describe('getApplicationActions', () => {
194
+ it('should return view logs action for all apps', () => {
195
+ const actions = getApplicationActions('app-uuid', 'stopped');
196
+ expect(actions).toContainEqual({
197
+ tool: 'application_logs',
198
+ args: { uuid: 'app-uuid' },
199
+ hint: 'View logs',
200
+ });
201
+ });
202
+ it('should return restart/stop actions for running apps', () => {
203
+ const actions = getApplicationActions('app-uuid', 'running');
204
+ expect(actions).toContainEqual({
205
+ tool: 'control',
206
+ args: { resource: 'application', action: 'restart', uuid: 'app-uuid' },
207
+ hint: 'Restart',
208
+ });
209
+ expect(actions).toContainEqual({
210
+ tool: 'control',
211
+ args: { resource: 'application', action: 'stop', uuid: 'app-uuid' },
212
+ hint: 'Stop',
213
+ });
214
+ });
215
+ it('should return start action for stopped apps', () => {
216
+ const actions = getApplicationActions('app-uuid', 'stopped');
217
+ expect(actions).toContainEqual({
218
+ tool: 'control',
219
+ args: { resource: 'application', action: 'start', uuid: 'app-uuid' },
220
+ hint: 'Start',
221
+ });
222
+ });
223
+ it('should handle running:healthy status', () => {
224
+ const actions = getApplicationActions('app-uuid', 'running:healthy');
225
+ expect(actions.some((a) => a.hint === 'Restart')).toBe(true);
226
+ expect(actions.some((a) => a.hint === 'Stop')).toBe(true);
227
+ });
228
+ it('should handle undefined status', () => {
229
+ const actions = getApplicationActions('app-uuid', undefined);
230
+ expect(actions).toContainEqual({
231
+ tool: 'control',
232
+ args: { resource: 'application', action: 'start', uuid: 'app-uuid' },
233
+ hint: 'Start',
234
+ });
235
+ });
236
+ });
237
+ describe('getDeploymentActions', () => {
238
+ it('should return cancel action for in_progress deployments', () => {
239
+ const actions = getDeploymentActions('dep-uuid', 'in_progress', 'app-uuid');
240
+ expect(actions).toContainEqual({
241
+ tool: 'deployment',
242
+ args: { action: 'cancel', uuid: 'dep-uuid' },
243
+ hint: 'Cancel',
244
+ });
245
+ });
246
+ it('should return cancel action for queued deployments', () => {
247
+ const actions = getDeploymentActions('dep-uuid', 'queued', 'app-uuid');
248
+ expect(actions).toContainEqual({
249
+ tool: 'deployment',
250
+ args: { action: 'cancel', uuid: 'dep-uuid' },
251
+ hint: 'Cancel',
252
+ });
253
+ });
254
+ it('should return app actions when appUuid provided', () => {
255
+ const actions = getDeploymentActions('dep-uuid', 'finished', 'app-uuid');
256
+ expect(actions).toContainEqual({
257
+ tool: 'get_application',
258
+ args: { uuid: 'app-uuid' },
259
+ hint: 'View app',
260
+ });
261
+ expect(actions).toContainEqual({
262
+ tool: 'application_logs',
263
+ args: { uuid: 'app-uuid' },
264
+ hint: 'App logs',
265
+ });
266
+ });
267
+ it('should not return cancel for finished deployments', () => {
268
+ const actions = getDeploymentActions('dep-uuid', 'finished', 'app-uuid');
269
+ expect(actions.some((a) => a.hint === 'Cancel')).toBe(false);
270
+ });
271
+ it('should return empty actions when no appUuid and not in_progress', () => {
272
+ const actions = getDeploymentActions('dep-uuid', 'finished', undefined);
273
+ expect(actions).toEqual([]);
274
+ });
275
+ });
276
+ describe('getPagination', () => {
277
+ it('should return undefined when count is less than perPage and page is 1', () => {
278
+ const result = getPagination('list_apps', 1, 50, 30);
279
+ expect(result).toBeUndefined();
280
+ });
281
+ it('should return next when count equals or exceeds perPage', () => {
282
+ const result = getPagination('list_apps', 1, 50, 50);
283
+ expect(result).toEqual({
284
+ next: { tool: 'list_apps', args: { page: 2, per_page: 50 } },
285
+ });
286
+ });
287
+ it('should return both prev and next for middle pages', () => {
288
+ const result = getPagination('list_apps', 2, 50, 50);
289
+ expect(result).toEqual({
290
+ prev: { tool: 'list_apps', args: { page: 1, per_page: 50 } },
291
+ next: { tool: 'list_apps', args: { page: 3, per_page: 50 } },
292
+ });
293
+ });
294
+ it('should return prev when page > 1 and count < perPage', () => {
295
+ const result = getPagination('list_apps', 3, 50, 20);
296
+ expect(result).toEqual({
297
+ prev: { tool: 'list_apps', args: { page: 2, per_page: 50 } },
298
+ });
299
+ });
300
+ it('should use default page and perPage when undefined', () => {
301
+ const result = getPagination('list_apps', undefined, undefined, 100);
302
+ expect(result).toEqual({
303
+ next: { tool: 'list_apps', args: { page: 2, per_page: 50 } },
304
+ });
305
+ });
306
+ it('should return undefined when count is undefined', () => {
307
+ const result = getPagination('list_apps', 1, 50, undefined);
308
+ expect(result).toBeUndefined();
309
+ });
310
+ });
@@ -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, EnvVarSummary, CreateEnvVarRequest, UpdateEnvVarRequest, BulkUpdateEnvVarsRequest, Database, UpdateDatabaseRequest, CreatePostgresqlRequest, CreateMysqlRequest, CreateMariadbRequest, CreateMongodbRequest, CreateRedisRequest, CreateKeydbRequest, CreateClickhouseRequest, CreateDragonflyRequest, CreateDatabaseResponse, DatabaseBackup, BackupExecution, CreateDatabaseBackupRequest, UpdateDatabaseBackupRequest, Service, CreateServiceRequest, UpdateServiceRequest, ServiceCreateResponse, Deployment, Team, TeamMember, PrivateKey, CreatePrivateKeyRequest, UpdatePrivateKeyRequest, GitHubApp, CreateGitHubAppRequest, UpdateGitHubAppRequest, GitHubAppUpdateResponse, CloudToken, CreateCloudTokenRequest, UpdateCloudTokenRequest, CloudTokenValidation, Version, ApplicationDiagnostic, ServerDiagnostic, InfrastructureIssuesReport, BatchOperationResult } 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, EnvVarSummary, CreateEnvVarRequest, UpdateEnvVarRequest, BulkUpdateEnvVarsRequest, Database, UpdateDatabaseRequest, CreatePostgresqlRequest, CreateMysqlRequest, CreateMariadbRequest, CreateMongodbRequest, CreateRedisRequest, CreateKeydbRequest, CreateClickhouseRequest, CreateDragonflyRequest, CreateDatabaseResponse, DatabaseBackup, BackupExecution, CreateDatabaseBackupRequest, UpdateDatabaseBackupRequest, Service, CreateServiceRequest, UpdateServiceRequest, ServiceCreateResponse, Deployment, DeploymentEssential, Team, TeamMember, PrivateKey, CreatePrivateKeyRequest, UpdatePrivateKeyRequest, GitHubApp, CreateGitHubAppRequest, UpdateGitHubAppRequest, GitHubAppUpdateResponse, CloudToken, CreateCloudTokenRequest, UpdateCloudTokenRequest, CloudTokenValidation, Version, ApplicationDiagnostic, ServerDiagnostic, InfrastructureIssuesReport, BatchOperationResult } from '../types/coolify.js';
6
6
  export interface ListOptions {
7
7
  page?: number;
8
8
  per_page?: number;
@@ -35,6 +35,9 @@ export interface DatabaseSummary {
35
35
  type: string;
36
36
  status: string;
37
37
  is_public: boolean;
38
+ environment_uuid?: string;
39
+ environment_name?: string;
40
+ environment_id?: number;
38
41
  }
39
42
  export interface ServiceSummary {
40
43
  uuid: string;
@@ -89,6 +92,17 @@ export declare class CoolifyClient {
89
92
  deleteProject(uuid: string): Promise<MessageResponse>;
90
93
  listProjectEnvironments(projectUuid: string): Promise<Environment[]>;
91
94
  getProjectEnvironment(projectUuid: string, environmentNameOrUuid: string): Promise<Environment>;
95
+ /**
96
+ * Get environment with missing database types (dragonfly, keydb, clickhouse).
97
+ * Coolify API omits these from the environment endpoint - we cross-reference
98
+ * with listDatabases using lightweight summaries.
99
+ * @see https://github.com/StuMason/coolify-mcp/issues/88
100
+ */
101
+ getProjectEnvironmentWithDatabases(projectUuid: string, environmentNameOrUuid: string): Promise<Environment & {
102
+ dragonflys?: DatabaseSummary[];
103
+ keydbs?: DatabaseSummary[];
104
+ clickhouses?: DatabaseSummary[];
105
+ }>;
92
106
  createProjectEnvironment(projectUuid: string, data: CreateEnvironmentRequest): Promise<UuidResponse>;
93
107
  deleteProjectEnvironment(projectUuid: string, environmentNameOrUuid: string): Promise<MessageResponse>;
94
108
  listApplications(options?: ListOptions): Promise<Application[] | ApplicationSummary[]>;
@@ -143,7 +157,9 @@ export declare class CoolifyClient {
143
157
  updateServiceEnvVar(uuid: string, data: UpdateEnvVarRequest): Promise<MessageResponse>;
144
158
  deleteServiceEnvVar(uuid: string, envUuid: string): Promise<MessageResponse>;
145
159
  listDeployments(options?: ListOptions): Promise<Deployment[] | DeploymentSummary[]>;
146
- getDeployment(uuid: string): Promise<Deployment>;
160
+ getDeployment(uuid: string, options?: {
161
+ includeLogs?: boolean;
162
+ }): Promise<Deployment | DeploymentEssential>;
147
163
  deployByTagOrUuid(tagOrUuid: string, force?: boolean): Promise<MessageResponse>;
148
164
  listApplicationDeployments(appUuid: string): Promise<Deployment[]>;
149
165
  listTeams(): Promise<Team[]>;
@@ -38,12 +38,17 @@ function toApplicationSummary(app) {
38
38
  };
39
39
  }
40
40
  function toDatabaseSummary(db) {
41
+ // API returns database_type not type, and environment_id not environment_uuid
42
+ const raw = db;
41
43
  return {
42
44
  uuid: db.uuid,
43
45
  name: db.name,
44
- type: db.type,
46
+ type: db.type || raw.database_type,
45
47
  status: db.status,
46
48
  is_public: db.is_public,
49
+ environment_uuid: db.environment_uuid,
50
+ environment_name: db.environment_name,
51
+ environment_id: raw.environment_id,
47
52
  };
48
53
  }
49
54
  function toServiceSummary(svc) {
@@ -64,6 +69,26 @@ function toDeploymentSummary(dep) {
64
69
  created_at: dep.created_at,
65
70
  };
66
71
  }
72
+ function toDeploymentEssential(dep) {
73
+ return {
74
+ uuid: dep.uuid,
75
+ deployment_uuid: dep.deployment_uuid,
76
+ application_uuid: dep.application_uuid,
77
+ application_name: dep.application_name,
78
+ server_name: dep.server_name,
79
+ status: dep.status,
80
+ commit: dep.commit,
81
+ force_rebuild: dep.force_rebuild,
82
+ is_webhook: dep.is_webhook,
83
+ is_api: dep.is_api,
84
+ created_at: dep.created_at,
85
+ updated_at: dep.updated_at,
86
+ logs_available: !!dep.logs,
87
+ logs_info: dep.logs
88
+ ? `Logs available (${dep.logs.length} chars). Use lines param to retrieve.`
89
+ : undefined,
90
+ };
91
+ }
67
92
  function toProjectSummary(proj) {
68
93
  return {
69
94
  uuid: proj.uuid,
@@ -256,6 +281,32 @@ export class CoolifyClient {
256
281
  async getProjectEnvironment(projectUuid, environmentNameOrUuid) {
257
282
  return this.request(`/projects/${projectUuid}/${environmentNameOrUuid}`);
258
283
  }
284
+ /**
285
+ * Get environment with missing database types (dragonfly, keydb, clickhouse).
286
+ * Coolify API omits these from the environment endpoint - we cross-reference
287
+ * with listDatabases using lightweight summaries.
288
+ * @see https://github.com/StuMason/coolify-mcp/issues/88
289
+ */
290
+ async getProjectEnvironmentWithDatabases(projectUuid, environmentNameOrUuid) {
291
+ const [environment, dbSummaries] = await Promise.all([
292
+ this.getProjectEnvironment(projectUuid, environmentNameOrUuid),
293
+ this.listDatabases({ summary: true }),
294
+ ]);
295
+ // Filter for this environment's missing database types
296
+ // API uses environment_id, not environment_uuid
297
+ const envDbs = dbSummaries.filter((db) => db.environment_id === environment.id ||
298
+ db.environment_uuid === environment.uuid ||
299
+ db.environment_name === environment.name);
300
+ const dragonflys = envDbs.filter((db) => db.type?.includes('dragonfly'));
301
+ const keydbs = envDbs.filter((db) => db.type?.includes('keydb'));
302
+ const clickhouses = envDbs.filter((db) => db.type?.includes('clickhouse'));
303
+ return {
304
+ ...environment,
305
+ ...(dragonflys.length > 0 && { dragonflys }),
306
+ ...(keydbs.length > 0 && { keydbs }),
307
+ ...(clickhouses.length > 0 && { clickhouses }),
308
+ };
309
+ }
259
310
  async createProjectEnvironment(projectUuid, data) {
260
311
  return this.request(`/projects/${projectUuid}/environments`, {
261
312
  method: 'POST',
@@ -569,11 +620,14 @@ export class CoolifyClient {
569
620
  ? deployments.map(toDeploymentSummary)
570
621
  : deployments;
571
622
  }
572
- async getDeployment(uuid) {
573
- return this.request(`/deployments/${uuid}`);
623
+ async getDeployment(uuid, options) {
624
+ const deployment = await this.request(`/deployments/${uuid}`);
625
+ return options?.includeLogs ? deployment : toDeploymentEssential(deployment);
574
626
  }
575
627
  async deployByTagOrUuid(tagOrUuid, force = false) {
576
- return this.request(`/deploy?tag=${encodeURIComponent(tagOrUuid)}&force=${force}`, { method: 'GET' });
628
+ // Detect if the value looks like a UUID or a tag name
629
+ const param = this.isLikelyUuid(tagOrUuid) ? 'uuid' : 'tag';
630
+ return this.request(`/deploy?${param}=${encodeURIComponent(tagOrUuid)}&force=${force}`, { method: 'GET' });
577
631
  }
578
632
  async listApplicationDeployments(appUuid) {
579
633
  return this.request(`/applications/${appUuid}/deployments`);
@@ -4,7 +4,18 @@
4
4
  */
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
7
- import type { CoolifyConfig } from '../types/coolify.js';
7
+ import type { CoolifyConfig, ResponseAction, ResponsePagination } from '../types/coolify.js';
8
+ /**
9
+ * Truncate logs by line count and character count.
10
+ * Exported for testing.
11
+ */
12
+ export declare function truncateLogs(logs: string, lineLimit?: number, charLimit?: number): string;
13
+ /** Generate contextual actions for an application based on its status */
14
+ export declare function getApplicationActions(uuid: string, status?: string): ResponseAction[];
15
+ /** Generate contextual actions for a deployment */
16
+ export declare function getDeploymentActions(uuid: string, status: string, appUuid?: string): ResponseAction[];
17
+ /** Generate pagination info for list endpoints */
18
+ export declare function getPagination(tool: string, page?: number, perPage?: number, count?: number): ResponsePagination | undefined;
8
19
  export declare class CoolifyMcpServer extends McpServer {
9
20
  private readonly client;
10
21
  constructor(config: CoolifyConfig);
@@ -2,11 +2,10 @@
2
2
  * Coolify MCP Server v2.4.0
3
3
  * Consolidated tools for efficient token usage
4
4
  */
5
- /* eslint-disable @typescript-eslint/no-explicit-any */
6
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
6
  import { z } from 'zod';
8
7
  import { CoolifyClient, } from './coolify-client.js';
9
- const VERSION = '2.4.0';
8
+ const VERSION = '2.5.0';
10
9
  /** Wrap handler with error handling */
11
10
  function wrap(fn) {
12
11
  return fn()
@@ -22,6 +21,100 @@ function wrap(fn) {
22
21
  ],
23
22
  }));
24
23
  }
24
+ const TRUNCATION_PREFIX = '...[truncated]...\n';
25
+ /**
26
+ * Truncate logs by line count and character count.
27
+ * Exported for testing.
28
+ */
29
+ export function truncateLogs(logs, lineLimit = 200, charLimit = 50000) {
30
+ // First: limit by lines
31
+ const logLines = logs.split('\n');
32
+ const limitedLines = logLines.slice(-lineLimit);
33
+ let truncatedLogs = limitedLines.join('\n');
34
+ // Second: limit by characters (safety net for huge lines)
35
+ if (truncatedLogs.length > charLimit) {
36
+ // Account for prefix length to stay within limit
37
+ const prefixLen = TRUNCATION_PREFIX.length;
38
+ truncatedLogs = TRUNCATION_PREFIX + truncatedLogs.slice(-(charLimit - prefixLen));
39
+ }
40
+ return truncatedLogs;
41
+ }
42
+ // =============================================================================
43
+ // Action Generators for HATEOAS-style responses
44
+ // =============================================================================
45
+ /** Generate contextual actions for an application based on its status */
46
+ export function getApplicationActions(uuid, status) {
47
+ const actions = [
48
+ { tool: 'application_logs', args: { uuid }, hint: 'View logs' },
49
+ ];
50
+ const s = (status || '').toLowerCase();
51
+ if (s.includes('running')) {
52
+ actions.push({
53
+ tool: 'control',
54
+ args: { resource: 'application', action: 'restart', uuid },
55
+ hint: 'Restart',
56
+ });
57
+ actions.push({
58
+ tool: 'control',
59
+ args: { resource: 'application', action: 'stop', uuid },
60
+ hint: 'Stop',
61
+ });
62
+ }
63
+ else {
64
+ actions.push({
65
+ tool: 'control',
66
+ args: { resource: 'application', action: 'start', uuid },
67
+ hint: 'Start',
68
+ });
69
+ }
70
+ return actions;
71
+ }
72
+ /** Generate contextual actions for a deployment */
73
+ export function getDeploymentActions(uuid, status, appUuid) {
74
+ const actions = [];
75
+ if (status === 'in_progress' || status === 'queued') {
76
+ actions.push({ tool: 'deployment', args: { action: 'cancel', uuid }, hint: 'Cancel' });
77
+ }
78
+ if (appUuid) {
79
+ actions.push({ tool: 'get_application', args: { uuid: appUuid }, hint: 'View app' });
80
+ actions.push({ tool: 'application_logs', args: { uuid: appUuid }, hint: 'App logs' });
81
+ }
82
+ return actions;
83
+ }
84
+ /** Generate pagination info for list endpoints */
85
+ export function getPagination(tool, page, perPage, count) {
86
+ const p = page ?? 1;
87
+ const pp = perPage ?? 50;
88
+ if (!count || count < pp) {
89
+ return p > 1 ? { prev: { tool, args: { page: p - 1, per_page: pp } } } : undefined;
90
+ }
91
+ return {
92
+ ...(p > 1 && { prev: { tool, args: { page: p - 1, per_page: pp } } }),
93
+ next: { tool, args: { page: p + 1, per_page: pp } },
94
+ };
95
+ }
96
+ /** Wrap handler with error handling and HATEOAS actions */
97
+ function wrapWithActions(fn, getActions, getPaginationFn) {
98
+ return fn()
99
+ .then((result) => {
100
+ const actions = getActions?.(result) ?? [];
101
+ const pagination = getPaginationFn?.(result);
102
+ const response = { data: result };
103
+ if (actions.length > 0)
104
+ response._actions = actions;
105
+ if (pagination)
106
+ response._pagination = pagination;
107
+ return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
108
+ })
109
+ .catch((error) => ({
110
+ content: [
111
+ {
112
+ type: 'text',
113
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
114
+ },
115
+ ],
116
+ }));
117
+ }
25
118
  export class CoolifyMcpServer extends McpServer {
26
119
  constructor(config) {
27
120
  super({ name: 'coolify', version: VERSION });
@@ -133,7 +226,7 @@ export class CoolifyMcpServer extends McpServer {
133
226
  // =========================================================================
134
227
  // Environments (1 tool - consolidated CRUD)
135
228
  // =========================================================================
136
- this.tool('environments', 'Manage environments: list/get/create/delete', {
229
+ this.tool('environments', 'Manage environments: list/get/create/delete (get includes dragonfly/keydb/clickhouse DBs missing from API)', {
137
230
  action: z.enum(['list', 'get', 'create', 'delete']),
138
231
  project_uuid: z.string(),
139
232
  name: z.string().optional(),
@@ -145,7 +238,8 @@ export class CoolifyMcpServer extends McpServer {
145
238
  case 'get':
146
239
  if (!name)
147
240
  return { content: [{ type: 'text', text: 'Error: name required' }] };
148
- return wrap(() => this.client.getProjectEnvironment(project_uuid, name));
241
+ // Use enhanced method that includes missing DB types (#88)
242
+ return wrap(() => this.client.getProjectEnvironmentWithDatabases(project_uuid, name));
149
243
  case 'create':
150
244
  if (!name)
151
245
  return { content: [{ type: 'text', text: 'Error: name required' }] };
@@ -159,8 +253,8 @@ export class CoolifyMcpServer extends McpServer {
159
253
  // =========================================================================
160
254
  // Applications (4 tools)
161
255
  // =========================================================================
162
- this.tool('list_applications', 'List apps (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrap(() => this.client.listApplications({ page, per_page, summary: true })));
163
- this.tool('get_application', 'App details', { uuid: z.string() }, async ({ uuid }) => wrap(() => this.client.getApplication(uuid)));
256
+ this.tool('list_applications', 'List apps (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrapWithActions(() => this.client.listApplications({ page, per_page, summary: true }), undefined, (result) => getPagination('list_applications', page, per_page, result.length)));
257
+ this.tool('get_application', 'App details', { uuid: z.string() }, async ({ uuid }) => wrapWithActions(() => this.client.getApplication(uuid), (app) => getApplicationActions(app.uuid, app.status)));
164
258
  this.tool('application', 'Manage app: create/update/delete', {
165
259
  action: z.enum([
166
260
  'create_public',
@@ -205,8 +299,7 @@ export class CoolifyMcpServer extends McpServer {
205
299
  // Delete fields
206
300
  delete_volumes: z.boolean().optional(),
207
301
  }, async (args) => {
208
- // Strip MCP-internal fields before passing to API (fixes #76)
209
- const { action, uuid, delete_volumes, ...apiData } = args;
302
+ const { action, uuid, delete_volumes } = args;
210
303
  switch (action) {
211
304
  case 'create_public':
212
305
  if (!args.project_uuid ||
@@ -224,7 +317,19 @@ export class CoolifyMcpServer extends McpServer {
224
317
  ],
225
318
  };
226
319
  }
227
- return wrap(() => this.client.createApplicationPublic(apiData));
320
+ return wrap(() => this.client.createApplicationPublic({
321
+ project_uuid: args.project_uuid,
322
+ server_uuid: args.server_uuid,
323
+ git_repository: args.git_repository,
324
+ git_branch: args.git_branch,
325
+ build_pack: args.build_pack,
326
+ ports_exposes: args.ports_exposes,
327
+ environment_name: args.environment_name,
328
+ environment_uuid: args.environment_uuid,
329
+ name: args.name,
330
+ description: args.description,
331
+ fqdn: args.fqdn,
332
+ }));
228
333
  case 'create_github':
229
334
  if (!args.project_uuid ||
230
335
  !args.server_uuid ||
@@ -240,7 +345,20 @@ export class CoolifyMcpServer extends McpServer {
240
345
  ],
241
346
  };
242
347
  }
243
- return wrap(() => this.client.createApplicationPrivateGH(apiData));
348
+ return wrap(() => this.client.createApplicationPrivateGH({
349
+ project_uuid: args.project_uuid,
350
+ server_uuid: args.server_uuid,
351
+ github_app_uuid: args.github_app_uuid,
352
+ git_repository: args.git_repository,
353
+ git_branch: args.git_branch,
354
+ build_pack: args.build_pack,
355
+ ports_exposes: args.ports_exposes,
356
+ environment_name: args.environment_name,
357
+ environment_uuid: args.environment_uuid,
358
+ name: args.name,
359
+ description: args.description,
360
+ fqdn: args.fqdn,
361
+ }));
244
362
  case 'create_key':
245
363
  if (!args.project_uuid ||
246
364
  !args.server_uuid ||
@@ -256,7 +374,20 @@ export class CoolifyMcpServer extends McpServer {
256
374
  ],
257
375
  };
258
376
  }
259
- return wrap(() => this.client.createApplicationPrivateKey(apiData));
377
+ return wrap(() => this.client.createApplicationPrivateKey({
378
+ project_uuid: args.project_uuid,
379
+ server_uuid: args.server_uuid,
380
+ private_key_uuid: args.private_key_uuid,
381
+ git_repository: args.git_repository,
382
+ git_branch: args.git_branch,
383
+ build_pack: args.build_pack,
384
+ ports_exposes: args.ports_exposes,
385
+ environment_name: args.environment_name,
386
+ environment_uuid: args.environment_uuid,
387
+ name: args.name,
388
+ description: args.description,
389
+ fqdn: args.fqdn,
390
+ }));
260
391
  case 'create_dockerimage':
261
392
  if (!args.project_uuid ||
262
393
  !args.server_uuid ||
@@ -271,11 +402,25 @@ export class CoolifyMcpServer extends McpServer {
271
402
  ],
272
403
  };
273
404
  }
274
- return wrap(() => this.client.createApplicationDockerImage(apiData));
275
- case 'update':
405
+ return wrap(() => this.client.createApplicationDockerImage({
406
+ project_uuid: args.project_uuid,
407
+ server_uuid: args.server_uuid,
408
+ docker_registry_image_name: args.docker_registry_image_name,
409
+ ports_exposes: args.ports_exposes,
410
+ docker_registry_image_tag: args.docker_registry_image_tag,
411
+ environment_name: args.environment_name,
412
+ environment_uuid: args.environment_uuid,
413
+ name: args.name,
414
+ description: args.description,
415
+ fqdn: args.fqdn,
416
+ }));
417
+ case 'update': {
276
418
  if (!uuid)
277
419
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
278
- return wrap(() => this.client.updateApplication(uuid, apiData));
420
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
421
+ const { action: _, uuid: __, delete_volumes: ___, ...updateData } = args;
422
+ return wrap(() => this.client.updateApplication(uuid, updateData));
423
+ }
279
424
  case 'delete':
280
425
  if (!uuid)
281
426
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
@@ -378,8 +523,7 @@ export class CoolifyMcpServer extends McpServer {
378
523
  docker_compose_raw: z.string().optional(),
379
524
  delete_volumes: z.boolean().optional(),
380
525
  }, async (args) => {
381
- // Strip MCP-internal fields before passing to API (fixes #76)
382
- const { action, uuid, delete_volumes, ...apiData } = args;
526
+ const { action, uuid, delete_volumes } = args;
383
527
  switch (action) {
384
528
  case 'create':
385
529
  if (!args.server_uuid || !args.project_uuid) {
@@ -389,11 +533,23 @@ export class CoolifyMcpServer extends McpServer {
389
533
  ],
390
534
  };
391
535
  }
392
- return wrap(() => this.client.createService(apiData));
393
- case 'update':
536
+ return wrap(() => this.client.createService({
537
+ project_uuid: args.project_uuid,
538
+ server_uuid: args.server_uuid,
539
+ type: args.type,
540
+ name: args.name,
541
+ description: args.description,
542
+ environment_name: args.environment_name,
543
+ instant_deploy: args.instant_deploy,
544
+ docker_compose_raw: args.docker_compose_raw,
545
+ }));
546
+ case 'update': {
394
547
  if (!uuid)
395
548
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
396
- return wrap(() => this.client.updateService(uuid, apiData));
549
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
550
+ const { action: _, uuid: __, delete_volumes: ___, ...updateData } = args;
551
+ return wrap(() => this.client.updateService(uuid, updateData));
552
+ }
397
553
  case 'delete':
398
554
  if (!uuid)
399
555
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
@@ -425,7 +581,36 @@ export class CoolifyMcpServer extends McpServer {
425
581
  restart: (u) => this.client.restartService(u),
426
582
  },
427
583
  };
428
- return wrap(() => methods[resource][action](uuid));
584
+ // Generate contextual actions based on resource type and action taken
585
+ const getControlActions = () => {
586
+ const actions = [];
587
+ if (resource === 'application') {
588
+ actions.push({ tool: 'application_logs', args: { uuid }, hint: 'View logs' });
589
+ actions.push({ tool: 'get_application', args: { uuid }, hint: 'Check status' });
590
+ if (action === 'start' || action === 'restart') {
591
+ actions.push({
592
+ tool: 'control',
593
+ args: { resource: 'application', action: 'stop', uuid },
594
+ hint: 'Stop',
595
+ });
596
+ }
597
+ else {
598
+ actions.push({
599
+ tool: 'control',
600
+ args: { resource: 'application', action: 'start', uuid },
601
+ hint: 'Start',
602
+ });
603
+ }
604
+ }
605
+ else if (resource === 'database') {
606
+ actions.push({ tool: 'get_database', args: { uuid }, hint: 'Check status' });
607
+ }
608
+ else if (resource === 'service') {
609
+ actions.push({ tool: 'get_service', args: { uuid }, hint: 'Check status' });
610
+ }
611
+ return actions;
612
+ };
613
+ return wrapWithActions(() => methods[resource][action](uuid), getControlActions);
429
614
  });
430
615
  // =========================================================================
431
616
  // Environment Variables (1 tool - consolidated)
@@ -437,8 +622,7 @@ export class CoolifyMcpServer extends McpServer {
437
622
  key: z.string().optional(),
438
623
  value: z.string().optional(),
439
624
  env_uuid: z.string().optional(),
440
- is_build_time: z.boolean().optional(),
441
- }, async ({ resource, action, uuid, key, value, env_uuid, is_build_time }) => {
625
+ }, async ({ resource, action, uuid, key, value, env_uuid }) => {
442
626
  if (resource === 'application') {
443
627
  switch (action) {
444
628
  case 'list':
@@ -446,7 +630,8 @@ export class CoolifyMcpServer extends McpServer {
446
630
  case 'create':
447
631
  if (!key || !value)
448
632
  return { content: [{ type: 'text', text: 'Error: key, value required' }] };
449
- return wrap(() => this.client.createApplicationEnvVar(uuid, { key, value, is_build_time }));
633
+ // Note: is_build_time is not passed - Coolify API rejects it for create action
634
+ return wrap(() => this.client.createApplicationEnvVar(uuid, { key, value }));
450
635
  case 'update':
451
636
  if (!key || !value)
452
637
  return { content: [{ type: 'text', text: 'Error: key, value required' }] };
@@ -481,26 +666,30 @@ export class CoolifyMcpServer extends McpServer {
481
666
  // =========================================================================
482
667
  // Deployments (3 tools)
483
668
  // =========================================================================
484
- this.tool('list_deployments', 'List deployments (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrap(() => this.client.listDeployments({ page, per_page, summary: true })));
485
- this.tool('deploy', 'Deploy by tag/UUID', { tag_or_uuid: z.string(), force: z.boolean().optional() }, async ({ tag_or_uuid, force }) => wrap(() => this.client.deployByTagOrUuid(tag_or_uuid, force)));
486
- this.tool('deployment', 'Manage deployment: get/cancel/list_for_app', {
669
+ this.tool('list_deployments', 'List deployments (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrapWithActions(() => this.client.listDeployments({ page, per_page, summary: true }), undefined, (result) => getPagination('list_deployments', page, per_page, result.length)));
670
+ this.tool('deploy', 'Deploy by tag/UUID', { tag_or_uuid: z.string(), force: z.boolean().optional() }, async ({ tag_or_uuid, force }) => wrapWithActions(() => this.client.deployByTagOrUuid(tag_or_uuid, force), () => [{ tool: 'list_deployments', args: {}, hint: 'Check deployment status' }]));
671
+ this.tool('deployment', 'Manage deployment: get/cancel/list_for_app (logs excluded by default, use lines param to include)', {
487
672
  action: z.enum(['get', 'cancel', 'list_for_app']),
488
673
  uuid: z.string(),
489
- lines: z.number().optional(), // Limit log output to last N lines (for 'get' action)
490
- }, async ({ action, uuid, lines }) => {
674
+ lines: z.number().optional(), // Include logs truncated to last N lines (omit for no logs)
675
+ max_chars: z.number().optional(), // Limit log output to last N chars (default: 50000)
676
+ }, async ({ action, uuid, lines, max_chars }) => {
491
677
  switch (action) {
492
678
  case 'get':
493
- return wrap(async () => {
494
- const deployment = await this.client.getDeployment(uuid);
495
- // Truncate logs to last N lines if specified
496
- if (lines && deployment.logs) {
497
- const logLines = deployment.logs.split('\n');
498
- if (logLines.length > lines) {
499
- deployment.logs = logLines.slice(-lines).join('\n');
679
+ // If lines param specified, include logs and truncate
680
+ if (lines !== undefined) {
681
+ return wrapWithActions(async () => {
682
+ const deployment = (await this.client.getDeployment(uuid, {
683
+ includeLogs: true,
684
+ }));
685
+ if (deployment.logs) {
686
+ deployment.logs = truncateLogs(deployment.logs, lines, max_chars ?? 50000);
500
687
  }
501
- }
502
- return deployment;
503
- });
688
+ return deployment;
689
+ }, (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid));
690
+ }
691
+ // Otherwise return essential info without logs
692
+ return wrapWithActions(() => this.client.getDeployment(uuid), (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid));
504
693
  case 'cancel':
505
694
  return wrap(() => this.client.cancelDeployment(uuid));
506
695
  case 'list_for_app':
@@ -224,11 +224,15 @@ export interface CreateApplicationPublicRequest {
224
224
  start_command?: string;
225
225
  instant_deploy?: boolean;
226
226
  }
227
- export interface CreateApplicationPrivateGHRequest extends CreateApplicationPublicRequest {
227
+ export interface CreateApplicationPrivateGHRequest extends Omit<CreateApplicationPublicRequest, 'build_pack' | 'ports_exposes'> {
228
228
  github_app_uuid: string;
229
+ build_pack?: BuildPack;
230
+ ports_exposes?: string;
229
231
  }
230
- export interface CreateApplicationPrivateKeyRequest extends CreateApplicationPublicRequest {
232
+ export interface CreateApplicationPrivateKeyRequest extends Omit<CreateApplicationPublicRequest, 'build_pack' | 'ports_exposes'> {
231
233
  private_key_uuid: string;
234
+ build_pack?: BuildPack;
235
+ ports_exposes?: string;
232
236
  }
233
237
  export interface CreateApplicationDockerfileRequest {
234
238
  project_uuid: string;
@@ -897,3 +901,34 @@ export interface BatchOperationResult {
897
901
  error: string;
898
902
  }>;
899
903
  }
904
+ export interface ResponseAction {
905
+ tool: string;
906
+ args: Record<string, string | number | boolean>;
907
+ hint: string;
908
+ }
909
+ export interface ResponsePagination {
910
+ next?: {
911
+ tool: string;
912
+ args: Record<string, number>;
913
+ };
914
+ prev?: {
915
+ tool: string;
916
+ args: Record<string, number>;
917
+ };
918
+ }
919
+ export interface DeploymentEssential {
920
+ uuid: string;
921
+ deployment_uuid: string;
922
+ application_uuid?: string;
923
+ application_name?: string;
924
+ server_name?: string;
925
+ status: string;
926
+ commit?: string;
927
+ force_rebuild: boolean;
928
+ is_webhook: boolean;
929
+ is_api: boolean;
930
+ created_at: string;
931
+ updated_at: string;
932
+ logs_available?: boolean;
933
+ logs_info?: string;
934
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@masonator/coolify-mcp",
3
3
  "scope": "@masonator",
4
- "version": "2.4.0",
4
+ "version": "2.6.0",
5
5
  "description": "MCP server implementation for Coolify",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -59,6 +59,7 @@
59
59
  "globals": "^17.0.0",
60
60
  "husky": "^9.0.11",
61
61
  "jest": "^29.7.0",
62
+ "jest-junit": "^16.0.0",
62
63
  "lint-staged": "^16.2.7",
63
64
  "markdownlint-cli2": "^0.20.0",
64
65
  "prettier": "^3.5.3",