@masonator/coolify-mcp 2.4.0 → 2.5.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
@@ -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 } from '../lib/mcp-server.js';
10
10
  describe('CoolifyMcpServer v2', () => {
11
11
  let server;
12
12
  beforeEach(() => {
@@ -140,3 +140,49 @@ describe('CoolifyMcpServer v2', () => {
140
140
  });
141
141
  });
142
142
  });
143
+ describe('truncateLogs', () => {
144
+ it('should return logs unchanged when within limits', () => {
145
+ const logs = 'line1\nline2\nline3';
146
+ const result = truncateLogs(logs, 200, 50000);
147
+ expect(result).toBe(logs);
148
+ });
149
+ it('should truncate to last N lines', () => {
150
+ const logs = 'line1\nline2\nline3\nline4\nline5';
151
+ const result = truncateLogs(logs, 3, 50000);
152
+ expect(result).toBe('line3\nline4\nline5');
153
+ });
154
+ it('should truncate by character limit when lines are huge', () => {
155
+ const hugeLine = 'x'.repeat(100);
156
+ const logs = `${hugeLine}\n${hugeLine}\n${hugeLine}`;
157
+ const result = truncateLogs(logs, 200, 50);
158
+ expect(result.length).toBeLessThanOrEqual(50);
159
+ expect(result.startsWith('...[truncated]...')).toBe(true);
160
+ });
161
+ it('should not add truncation prefix when under char limit', () => {
162
+ const logs = 'line1\nline2\nline3';
163
+ const result = truncateLogs(logs, 200, 50000);
164
+ expect(result.startsWith('...[truncated]...')).toBe(false);
165
+ });
166
+ it('should handle empty logs', () => {
167
+ const result = truncateLogs('', 200, 50000);
168
+ expect(result).toBe('');
169
+ });
170
+ it('should use default limits when not specified', () => {
171
+ const logs = 'line1\nline2';
172
+ const result = truncateLogs(logs);
173
+ expect(result).toBe(logs);
174
+ });
175
+ it('should respect custom line limit', () => {
176
+ const lines = Array.from({ length: 300 }, (_, i) => `line${i + 1}`).join('\n');
177
+ const result = truncateLogs(lines, 50, 50000);
178
+ const resultLines = result.split('\n');
179
+ expect(resultLines.length).toBe(50);
180
+ expect(resultLines[0]).toBe('line251');
181
+ expect(resultLines[49]).toBe('line300');
182
+ });
183
+ it('should respect custom char limit', () => {
184
+ const logs = 'x'.repeat(1000);
185
+ const result = truncateLogs(logs, 200, 100);
186
+ expect(result.length).toBe(100);
187
+ });
188
+ });
@@ -5,6 +5,11 @@
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
7
7
  import type { CoolifyConfig } 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;
8
13
  export declare class CoolifyMcpServer extends McpServer {
9
14
  private readonly client;
10
15
  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,24 @@ 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
+ }
25
42
  export class CoolifyMcpServer extends McpServer {
26
43
  constructor(config) {
27
44
  super({ name: 'coolify', version: VERSION });
@@ -205,8 +222,7 @@ export class CoolifyMcpServer extends McpServer {
205
222
  // Delete fields
206
223
  delete_volumes: z.boolean().optional(),
207
224
  }, async (args) => {
208
- // Strip MCP-internal fields before passing to API (fixes #76)
209
- const { action, uuid, delete_volumes, ...apiData } = args;
225
+ const { action, uuid, delete_volumes } = args;
210
226
  switch (action) {
211
227
  case 'create_public':
212
228
  if (!args.project_uuid ||
@@ -224,7 +240,19 @@ export class CoolifyMcpServer extends McpServer {
224
240
  ],
225
241
  };
226
242
  }
227
- return wrap(() => this.client.createApplicationPublic(apiData));
243
+ return wrap(() => this.client.createApplicationPublic({
244
+ project_uuid: args.project_uuid,
245
+ server_uuid: args.server_uuid,
246
+ git_repository: args.git_repository,
247
+ git_branch: args.git_branch,
248
+ build_pack: args.build_pack,
249
+ ports_exposes: args.ports_exposes,
250
+ environment_name: args.environment_name,
251
+ environment_uuid: args.environment_uuid,
252
+ name: args.name,
253
+ description: args.description,
254
+ fqdn: args.fqdn,
255
+ }));
228
256
  case 'create_github':
229
257
  if (!args.project_uuid ||
230
258
  !args.server_uuid ||
@@ -240,7 +268,20 @@ export class CoolifyMcpServer extends McpServer {
240
268
  ],
241
269
  };
242
270
  }
243
- return wrap(() => this.client.createApplicationPrivateGH(apiData));
271
+ return wrap(() => this.client.createApplicationPrivateGH({
272
+ project_uuid: args.project_uuid,
273
+ server_uuid: args.server_uuid,
274
+ github_app_uuid: args.github_app_uuid,
275
+ git_repository: args.git_repository,
276
+ git_branch: args.git_branch,
277
+ build_pack: args.build_pack,
278
+ ports_exposes: args.ports_exposes,
279
+ environment_name: args.environment_name,
280
+ environment_uuid: args.environment_uuid,
281
+ name: args.name,
282
+ description: args.description,
283
+ fqdn: args.fqdn,
284
+ }));
244
285
  case 'create_key':
245
286
  if (!args.project_uuid ||
246
287
  !args.server_uuid ||
@@ -256,7 +297,20 @@ export class CoolifyMcpServer extends McpServer {
256
297
  ],
257
298
  };
258
299
  }
259
- return wrap(() => this.client.createApplicationPrivateKey(apiData));
300
+ return wrap(() => this.client.createApplicationPrivateKey({
301
+ project_uuid: args.project_uuid,
302
+ server_uuid: args.server_uuid,
303
+ private_key_uuid: args.private_key_uuid,
304
+ git_repository: args.git_repository,
305
+ git_branch: args.git_branch,
306
+ build_pack: args.build_pack,
307
+ ports_exposes: args.ports_exposes,
308
+ environment_name: args.environment_name,
309
+ environment_uuid: args.environment_uuid,
310
+ name: args.name,
311
+ description: args.description,
312
+ fqdn: args.fqdn,
313
+ }));
260
314
  case 'create_dockerimage':
261
315
  if (!args.project_uuid ||
262
316
  !args.server_uuid ||
@@ -271,11 +325,25 @@ export class CoolifyMcpServer extends McpServer {
271
325
  ],
272
326
  };
273
327
  }
274
- return wrap(() => this.client.createApplicationDockerImage(apiData));
275
- case 'update':
328
+ return wrap(() => this.client.createApplicationDockerImage({
329
+ project_uuid: args.project_uuid,
330
+ server_uuid: args.server_uuid,
331
+ docker_registry_image_name: args.docker_registry_image_name,
332
+ ports_exposes: args.ports_exposes,
333
+ docker_registry_image_tag: args.docker_registry_image_tag,
334
+ environment_name: args.environment_name,
335
+ environment_uuid: args.environment_uuid,
336
+ name: args.name,
337
+ description: args.description,
338
+ fqdn: args.fqdn,
339
+ }));
340
+ case 'update': {
276
341
  if (!uuid)
277
342
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
278
- return wrap(() => this.client.updateApplication(uuid, apiData));
343
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
344
+ const { action: _, uuid: __, delete_volumes: ___, ...updateData } = args;
345
+ return wrap(() => this.client.updateApplication(uuid, updateData));
346
+ }
279
347
  case 'delete':
280
348
  if (!uuid)
281
349
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
@@ -378,8 +446,7 @@ export class CoolifyMcpServer extends McpServer {
378
446
  docker_compose_raw: z.string().optional(),
379
447
  delete_volumes: z.boolean().optional(),
380
448
  }, async (args) => {
381
- // Strip MCP-internal fields before passing to API (fixes #76)
382
- const { action, uuid, delete_volumes, ...apiData } = args;
449
+ const { action, uuid, delete_volumes } = args;
383
450
  switch (action) {
384
451
  case 'create':
385
452
  if (!args.server_uuid || !args.project_uuid) {
@@ -389,11 +456,23 @@ export class CoolifyMcpServer extends McpServer {
389
456
  ],
390
457
  };
391
458
  }
392
- return wrap(() => this.client.createService(apiData));
393
- case 'update':
459
+ return wrap(() => this.client.createService({
460
+ project_uuid: args.project_uuid,
461
+ server_uuid: args.server_uuid,
462
+ type: args.type,
463
+ name: args.name,
464
+ description: args.description,
465
+ environment_name: args.environment_name,
466
+ instant_deploy: args.instant_deploy,
467
+ docker_compose_raw: args.docker_compose_raw,
468
+ }));
469
+ case 'update': {
394
470
  if (!uuid)
395
471
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
396
- return wrap(() => this.client.updateService(uuid, apiData));
472
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
473
+ const { action: _, uuid: __, delete_volumes: ___, ...updateData } = args;
474
+ return wrap(() => this.client.updateService(uuid, updateData));
475
+ }
397
476
  case 'delete':
398
477
  if (!uuid)
399
478
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
@@ -483,21 +562,18 @@ export class CoolifyMcpServer extends McpServer {
483
562
  // =========================================================================
484
563
  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
564
  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', {
565
+ this.tool('deployment', 'Manage deployment: get/cancel/list_for_app (logs truncated to last 200 lines/50K chars by default)', {
487
566
  action: z.enum(['get', 'cancel', 'list_for_app']),
488
567
  uuid: z.string(),
489
- lines: z.number().optional(), // Limit log output to last N lines (for 'get' action)
490
- }, async ({ action, uuid, lines }) => {
568
+ lines: z.number().optional(), // Limit log output to last N lines (default: 200)
569
+ max_chars: z.number().optional(), // Limit log output to last N chars (default: 50000)
570
+ }, async ({ action, uuid, lines, max_chars }) => {
491
571
  switch (action) {
492
572
  case 'get':
493
573
  return wrap(async () => {
494
574
  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');
500
- }
575
+ if (deployment.logs) {
576
+ deployment.logs = truncateLogs(deployment.logs, lines ?? 200, max_chars ?? 50000);
501
577
  }
502
578
  return deployment;
503
579
  });
@@ -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;
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.5.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",