@masonator/coolify-mcp 2.3.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
@@ -9,13 +9,13 @@
9
9
  [![codecov](https://codecov.io/gh/StuMason/coolify-mcp/branch/main/graph/badge.svg)](https://codecov.io/gh/StuMason/coolify-mcp)
10
10
  [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/stumason-coolify-mcp-badge.png)](https://mseep.ai/app/stumason-coolify-mcp)
11
11
 
12
- > **The most comprehensive MCP server for Coolify** - 34 optimized tools, smart diagnostics, and batch operations for managing your self-hosted PaaS through AI assistants.
12
+ > **The most comprehensive MCP server for Coolify** - 35 optimized tools, smart diagnostics, and batch operations for managing your self-hosted PaaS through AI assistants.
13
13
 
14
14
  A Model Context Protocol (MCP) server for [Coolify](https://coolify.io/), enabling AI assistants to manage and debug your Coolify instances through natural language.
15
15
 
16
16
  ## Features
17
17
 
18
- This MCP server provides **34 token-optimized tools** for **debugging, management, and deployment**:
18
+ This MCP server provides **35 token-optimized tools** for **debugging, management, and deployment**:
19
19
 
20
20
  | Category | Tools |
21
21
  | -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
@@ -32,6 +32,7 @@ This MCP server provides **34 token-optimized tools** for **debugging, managemen
32
32
  | **Env Vars** | `env_vars` (CRUD for application and service env vars) |
33
33
  | **Deployments** | `list_deployments`, `deploy`, `deployment` (get, cancel, list_for_app) |
34
34
  | **Private Keys** | `private_keys` (list, get, create, update, delete via action param) |
35
+ | **GitHub Apps** | `github_apps` (list, get, create, update, delete via action param) |
35
36
 
36
37
  ### Token-Optimized Design
37
38
 
@@ -42,7 +43,7 @@ The server uses **85% fewer tokens** than a naive implementation (6,600 vs 43,00
42
43
  ### Prerequisites
43
44
 
44
45
  - Node.js >= 18
45
- - A running Coolify instance
46
+ - A running Coolify instance (tested with v4.0.0-beta.460)
46
47
  - Coolify API access token (generate in Coolify Settings > API)
47
48
 
48
49
  ### Claude Desktop
@@ -111,7 +112,7 @@ The Coolify API returns extremely verbose responses - a single application can c
111
112
  1. **Start with overview**: `get_infrastructure_overview` - see everything at once
112
113
  2. **Find your target**: `list_applications` - get UUIDs of what you need
113
114
  3. **Dive deep**: `get_application(uuid)` - full details for one resource
114
- 4. **Take action**: `restart_application(uuid)`, `get_application_logs(uuid)`, etc.
115
+ 4. **Take action**: `control(resource: 'application', action: 'restart')`, `application_logs(uuid)`, etc.
115
116
 
116
117
  ### Pagination
117
118
 
@@ -161,6 +162,8 @@ Update the DATABASE_URL env var for application {uuid}
161
162
  Create a new project called "my-app"
162
163
  Create a staging environment in project {uuid}
163
164
  Deploy my app from private GitHub repo org/repo on branch main
165
+ Deploy nginx:latest from Docker Hub
166
+ Deploy from public repo https://github.com/org/repo
164
167
  ```
165
168
 
166
169
  ## Environment Variables
@@ -228,6 +231,8 @@ These tools accept human-friendly identifiers instead of just UUIDs:
228
231
  - `get_application` - Get application details
229
232
  - `application_logs` - Get application logs
230
233
  - `application` - Create, update, or delete apps with `action: create_public|create_github|create_key|create_dockerimage|update|delete`
234
+ - Deploy from public repos, private GitHub, SSH keys, or Docker images
235
+ - Configure health checks (path, interval, retries, etc.)
231
236
  - `env_vars` - Manage env vars with `resource: application, action: list|create|update|delete`
232
237
  - `control` - Start/stop/restart with `resource: application, action: start|stop|restart`
233
238
 
@@ -254,7 +259,7 @@ These tools accept human-friendly identifiers instead of just UUIDs:
254
259
 
255
260
  - `list_deployments` - List running deployments (returns summary)
256
261
  - `deploy` - Deploy by tag or UUID
257
- - `deployment` - Manage deployments with `action: get|cancel|list_for_app`
262
+ - `deployment` - Manage deployments with `action: get|cancel|list_for_app` (supports `lines` param to limit log output)
258
263
 
259
264
  ### Private Keys
260
265
 
@@ -359,6 +359,83 @@ describe('CoolifyClient', () => {
359
359
  expect(result).toEqual({ uuid: 'new-key-uuid' });
360
360
  });
361
361
  });
362
+ describe('github apps', () => {
363
+ const mockGitHubApp = {
364
+ id: 1,
365
+ uuid: 'gh-app-uuid',
366
+ name: 'my-github-app',
367
+ organization: null,
368
+ api_url: 'https://api.github.com',
369
+ html_url: 'https://github.com',
370
+ custom_user: 'git',
371
+ custom_port: 22,
372
+ app_id: 12345,
373
+ installation_id: 67890,
374
+ client_id: 'client-123',
375
+ is_system_wide: false,
376
+ is_public: false,
377
+ private_key_id: 1,
378
+ team_id: 0,
379
+ type: 'github',
380
+ administration: null,
381
+ contents: null,
382
+ metadata: null,
383
+ pull_requests: null,
384
+ created_at: '2024-01-01',
385
+ updated_at: '2024-01-01',
386
+ };
387
+ it('should list github apps', async () => {
388
+ mockFetch.mockResolvedValueOnce(mockResponse([mockGitHubApp]));
389
+ const result = await client.listGitHubApps();
390
+ expect(result).toEqual([mockGitHubApp]);
391
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/github-apps', expect.any(Object));
392
+ });
393
+ it('should list github apps with summary', async () => {
394
+ mockFetch.mockResolvedValueOnce(mockResponse([mockGitHubApp]));
395
+ const result = await client.listGitHubApps({ summary: true });
396
+ expect(result).toEqual([
397
+ {
398
+ id: 1,
399
+ uuid: 'gh-app-uuid',
400
+ name: 'my-github-app',
401
+ organization: null,
402
+ is_public: false,
403
+ app_id: 12345,
404
+ },
405
+ ]);
406
+ });
407
+ it('should create a github app', async () => {
408
+ mockFetch.mockResolvedValueOnce(mockResponse(mockGitHubApp));
409
+ const result = await client.createGitHubApp({
410
+ name: 'my-github-app',
411
+ api_url: 'https://api.github.com',
412
+ html_url: 'https://github.com',
413
+ app_id: 12345,
414
+ installation_id: 67890,
415
+ client_id: 'client-123',
416
+ client_secret: 'secret-456',
417
+ private_key_uuid: 'key-uuid',
418
+ });
419
+ expect(result).toEqual(mockGitHubApp);
420
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/github-apps', expect.objectContaining({ method: 'POST' }));
421
+ });
422
+ it('should update a github app', async () => {
423
+ const updateResponse = { message: 'GitHub app updated successfully', data: mockGitHubApp };
424
+ mockFetch.mockResolvedValueOnce(mockResponse(updateResponse));
425
+ const result = await client.updateGitHubApp(1, { name: 'updated-app' });
426
+ expect(result).toEqual(updateResponse);
427
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/github-apps/1', expect.objectContaining({
428
+ method: 'PATCH',
429
+ body: JSON.stringify({ name: 'updated-app' }),
430
+ }));
431
+ });
432
+ it('should delete a github app', async () => {
433
+ mockFetch.mockResolvedValueOnce(mockResponse({ message: 'GitHub app deleted successfully' }));
434
+ const result = await client.deleteGitHubApp(1);
435
+ expect(result).toEqual({ message: 'GitHub app deleted successfully' });
436
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/github-apps/1', expect.objectContaining({ method: 'DELETE' }));
437
+ });
438
+ });
362
439
  describe('error handling', () => {
363
440
  it('should handle network errors', async () => {
364
441
  mockFetch.mockRejectedValueOnce(new TypeError('fetch failed'));
@@ -610,11 +687,76 @@ describe('CoolifyClient', () => {
610
687
  expect(result).toEqual({ uuid: 'new-app-uuid' });
611
688
  expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/dockercompose', expect.objectContaining({ method: 'POST' }));
612
689
  });
690
+ /**
691
+ * Issue #76 - Client Layer Behavior Test
692
+ *
693
+ * This test documents that the client passes through whatever data it receives.
694
+ * The client itself is NOT buggy - it correctly sends all fields to the API.
695
+ *
696
+ * The FIX for #76 is in mcp-server.ts which now strips 'action' before
697
+ * calling client methods. This test ensures the client behavior remains
698
+ * predictable (pass-through) so the MCP server layer must handle filtering.
699
+ */
700
+ it('client passes through action field when included in create data (documents #76 fix location)', async () => {
701
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
702
+ // This simulates what mcp-server.ts does: passing full args with action
703
+ const argsFromMcpTool = {
704
+ action: 'create_public', // This should NOT be sent to the API
705
+ project_uuid: 'proj-uuid',
706
+ server_uuid: 'server-uuid',
707
+ git_repository: 'https://github.com/user/repo',
708
+ git_branch: 'main',
709
+ build_pack: 'nixpacks',
710
+ ports_exposes: '3000',
711
+ };
712
+ await client.createApplicationPublic(argsFromMcpTool);
713
+ // This assertion proves the bug: 'action' IS included in the request body
714
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/public', expect.objectContaining({
715
+ method: 'POST',
716
+ body: expect.stringContaining('"action":"create_public"'),
717
+ }));
718
+ });
613
719
  it('should update an application', async () => {
614
720
  mockFetch.mockResolvedValueOnce(mockResponse({ ...mockApplication, name: 'updated-app' }));
615
721
  const result = await client.updateApplication('app-uuid', { name: 'updated-app' });
616
722
  expect(result.name).toBe('updated-app');
617
723
  });
724
+ it('should update an application and verify request body', async () => {
725
+ mockFetch.mockResolvedValueOnce(mockResponse({ ...mockApplication, name: 'updated-app' }));
726
+ await client.updateApplication('app-uuid', { name: 'updated-app', description: 'new desc' });
727
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid', expect.objectContaining({
728
+ method: 'PATCH',
729
+ body: JSON.stringify({ name: 'updated-app', description: 'new desc' }),
730
+ }));
731
+ });
732
+ /**
733
+ * Issue #76 - Client Layer Behavior Test
734
+ *
735
+ * This test documents that the client passes through whatever data it receives.
736
+ * The client itself is NOT buggy - it correctly sends all fields to the API.
737
+ *
738
+ * The FIX for #76 is in mcp-server.ts which now strips 'action' before
739
+ * calling client methods. This test ensures the client behavior remains
740
+ * predictable (pass-through) so the MCP server layer must handle filtering.
741
+ */
742
+ it('client passes through action field when included in update data (documents #76 fix location)', async () => {
743
+ mockFetch.mockResolvedValueOnce(mockResponse({ ...mockApplication, name: 'updated-app' }));
744
+ // This simulates what mcp-server.ts does: passing the full args object including 'action'
745
+ const argsFromMcpTool = {
746
+ action: 'update', // This should NOT be sent to the API
747
+ uuid: 'app-uuid', // This is extracted separately
748
+ name: 'updated-app',
749
+ description: 'new desc',
750
+ };
751
+ // The client passes whatever it receives to the API
752
+ await client.updateApplication('app-uuid', argsFromMcpTool);
753
+ // This assertion proves the bug: 'action' IS included in the request body
754
+ // The Coolify API will reject this with "action: This field is not allowed"
755
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid', expect.objectContaining({
756
+ method: 'PATCH',
757
+ body: expect.stringContaining('"action":"update"'),
758
+ }));
759
+ });
618
760
  it('should delete an application', async () => {
619
761
  mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted' }));
620
762
  const result = await client.deleteApplication('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 } from '../lib/mcp-server.js';
10
10
  describe('CoolifyMcpServer v2', () => {
11
11
  let server;
12
12
  beforeEach(() => {
@@ -107,6 +107,11 @@ describe('CoolifyMcpServer v2', () => {
107
107
  expect(typeof client.createPrivateKey).toBe('function');
108
108
  expect(typeof client.updatePrivateKey).toBe('function');
109
109
  expect(typeof client.deletePrivateKey).toBe('function');
110
+ // GitHub App operations
111
+ expect(typeof client.listGitHubApps).toBe('function');
112
+ expect(typeof client.createGitHubApp).toBe('function');
113
+ expect(typeof client.updateGitHubApp).toBe('function');
114
+ expect(typeof client.deleteGitHubApp).toBe('function');
110
115
  // Backup operations
111
116
  expect(typeof client.listDatabaseBackups).toBe('function');
112
117
  expect(typeof client.getDatabaseBackup).toBe('function');
@@ -135,3 +140,49 @@ describe('CoolifyMcpServer v2', () => {
135
140
  });
136
141
  });
137
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
+ });
@@ -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, 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, 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;
@@ -55,6 +55,14 @@ export interface ProjectSummary {
55
55
  name: string;
56
56
  description?: string;
57
57
  }
58
+ export interface GitHubAppSummary {
59
+ id: number;
60
+ uuid: string;
61
+ name: string;
62
+ organization: string | null;
63
+ is_public: boolean;
64
+ app_id: number | null;
65
+ }
58
66
  /**
59
67
  * HTTP client for the Coolify API
60
68
  */
@@ -148,6 +156,10 @@ export declare class CoolifyClient {
148
156
  createPrivateKey(data: CreatePrivateKeyRequest): Promise<UuidResponse>;
149
157
  updatePrivateKey(uuid: string, data: UpdatePrivateKeyRequest): Promise<PrivateKey>;
150
158
  deletePrivateKey(uuid: string): Promise<MessageResponse>;
159
+ listGitHubApps(options?: ListOptions): Promise<GitHubApp[] | GitHubAppSummary[]>;
160
+ createGitHubApp(data: CreateGitHubAppRequest): Promise<GitHubApp>;
161
+ updateGitHubApp(id: number, data: UpdateGitHubAppRequest): Promise<GitHubAppUpdateResponse>;
162
+ deleteGitHubApp(id: number): Promise<MessageResponse>;
151
163
  listCloudTokens(): Promise<CloudToken[]>;
152
164
  getCloudToken(uuid: string): Promise<CloudToken>;
153
165
  createCloudToken(data: CreateCloudTokenRequest): Promise<UuidResponse>;
@@ -71,6 +71,16 @@ function toProjectSummary(proj) {
71
71
  description: proj.description,
72
72
  };
73
73
  }
74
+ function toGitHubAppSummary(app) {
75
+ return {
76
+ id: app.id,
77
+ uuid: app.uuid,
78
+ name: app.name,
79
+ organization: app.organization,
80
+ is_public: app.is_public,
81
+ app_id: app.app_id,
82
+ };
83
+ }
74
84
  function toEnvVarSummary(envVar) {
75
85
  return {
76
86
  uuid: envVar.uuid,
@@ -613,6 +623,30 @@ export class CoolifyClient {
613
623
  });
614
624
  }
615
625
  // ===========================================================================
626
+ // GitHub App endpoints
627
+ // ===========================================================================
628
+ async listGitHubApps(options) {
629
+ const apps = await this.request('/github-apps');
630
+ return options?.summary && Array.isArray(apps) ? apps.map(toGitHubAppSummary) : apps;
631
+ }
632
+ async createGitHubApp(data) {
633
+ return this.request('/github-apps', {
634
+ method: 'POST',
635
+ body: JSON.stringify(data),
636
+ });
637
+ }
638
+ async updateGitHubApp(id, data) {
639
+ return this.request(`/github-apps/${id}`, {
640
+ method: 'PATCH',
641
+ body: JSON.stringify(cleanRequestData(data)),
642
+ });
643
+ }
644
+ async deleteGitHubApp(id) {
645
+ return this.request(`/github-apps/${id}`, {
646
+ method: 'DELETE',
647
+ });
648
+ }
649
+ // ===========================================================================
616
650
  // Cloud Token endpoints (Hetzner, DigitalOcean)
617
651
  // ===========================================================================
618
652
  async listCloudTokens() {
@@ -1,10 +1,15 @@
1
1
  /**
2
- * Coolify MCP Server v2.3.0
2
+ * Coolify MCP Server v2.4.0
3
3
  * Consolidated tools for efficient token usage
4
4
  */
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);
@@ -1,12 +1,11 @@
1
1
  /**
2
- * Coolify MCP Server v2.3.0
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.3.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,7 +222,7 @@ export class CoolifyMcpServer extends McpServer {
205
222
  // Delete fields
206
223
  delete_volumes: z.boolean().optional(),
207
224
  }, async (args) => {
208
- const { action, uuid } = args;
225
+ const { action, uuid, delete_volumes } = args;
209
226
  switch (action) {
210
227
  case 'create_public':
211
228
  if (!args.project_uuid ||
@@ -223,7 +240,19 @@ export class CoolifyMcpServer extends McpServer {
223
240
  ],
224
241
  };
225
242
  }
226
- return wrap(() => this.client.createApplicationPublic(args));
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
+ }));
227
256
  case 'create_github':
228
257
  if (!args.project_uuid ||
229
258
  !args.server_uuid ||
@@ -239,7 +268,20 @@ export class CoolifyMcpServer extends McpServer {
239
268
  ],
240
269
  };
241
270
  }
242
- return wrap(() => this.client.createApplicationPrivateGH(args));
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
+ }));
243
285
  case 'create_key':
244
286
  if (!args.project_uuid ||
245
287
  !args.server_uuid ||
@@ -255,7 +297,20 @@ export class CoolifyMcpServer extends McpServer {
255
297
  ],
256
298
  };
257
299
  }
258
- return wrap(() => this.client.createApplicationPrivateKey(args));
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
+ }));
259
314
  case 'create_dockerimage':
260
315
  if (!args.project_uuid ||
261
316
  !args.server_uuid ||
@@ -270,15 +325,29 @@ export class CoolifyMcpServer extends McpServer {
270
325
  ],
271
326
  };
272
327
  }
273
- return wrap(() => this.client.createApplicationDockerImage(args));
274
- 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': {
275
341
  if (!uuid)
276
342
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
277
- return wrap(() => this.client.updateApplication(uuid, args));
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
+ }
278
347
  case 'delete':
279
348
  if (!uuid)
280
349
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
281
- return wrap(() => this.client.deleteApplication(uuid, { deleteVolumes: args.delete_volumes }));
350
+ return wrap(() => this.client.deleteApplication(uuid, { deleteVolumes: delete_volumes }));
282
351
  }
283
352
  });
284
353
  this.tool('application_logs', 'Get app logs', { uuid: z.string(), lines: z.number().optional() }, async ({ uuid, lines }) => wrap(() => this.client.getApplicationLogs(uuid, lines)));
@@ -377,7 +446,7 @@ export class CoolifyMcpServer extends McpServer {
377
446
  docker_compose_raw: z.string().optional(),
378
447
  delete_volumes: z.boolean().optional(),
379
448
  }, async (args) => {
380
- const { action, uuid } = args;
449
+ const { action, uuid, delete_volumes } = args;
381
450
  switch (action) {
382
451
  case 'create':
383
452
  if (!args.server_uuid || !args.project_uuid) {
@@ -387,15 +456,27 @@ export class CoolifyMcpServer extends McpServer {
387
456
  ],
388
457
  };
389
458
  }
390
- return wrap(() => this.client.createService(args));
391
- 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': {
392
470
  if (!uuid)
393
471
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
394
- return wrap(() => this.client.updateService(uuid, args));
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
+ }
395
476
  case 'delete':
396
477
  if (!uuid)
397
478
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
398
- return wrap(() => this.client.deleteService(uuid, { deleteVolumes: args.delete_volumes }));
479
+ return wrap(() => this.client.deleteService(uuid, { deleteVolumes: delete_volumes }));
399
480
  }
400
481
  });
401
482
  // =========================================================================
@@ -481,21 +562,18 @@ export class CoolifyMcpServer extends McpServer {
481
562
  // =========================================================================
482
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 })));
483
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)));
484
- 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)', {
485
566
  action: z.enum(['get', 'cancel', 'list_for_app']),
486
567
  uuid: z.string(),
487
- lines: z.number().optional(), // Limit log output to last N lines (for 'get' action)
488
- }, 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 }) => {
489
571
  switch (action) {
490
572
  case 'get':
491
573
  return wrap(async () => {
492
574
  const deployment = await this.client.getDeployment(uuid);
493
- // Truncate logs to last N lines if specified
494
- if (lines && deployment.logs) {
495
- const logLines = deployment.logs.split('\n');
496
- if (logLines.length > lines) {
497
- deployment.logs = logLines.slice(-lines).join('\n');
498
- }
575
+ if (deployment.logs) {
576
+ deployment.logs = truncateLogs(deployment.logs, lines ?? 200, max_chars ?? 50000);
499
577
  }
500
578
  return deployment;
501
579
  });
@@ -541,6 +619,90 @@ export class CoolifyMcpServer extends McpServer {
541
619
  }
542
620
  });
543
621
  // =========================================================================
622
+ // GitHub Apps (1 tool - consolidated)
623
+ // =========================================================================
624
+ this.tool('github_apps', 'Manage GitHub Apps: list/get/create/update/delete', {
625
+ action: z.enum(['list', 'get', 'create', 'update', 'delete']),
626
+ // GitHub apps use integer id, not uuid
627
+ id: z.number().optional(),
628
+ // Create/Update fields
629
+ name: z.string().optional(),
630
+ organization: z.string().optional(),
631
+ api_url: z.string().optional(),
632
+ html_url: z.string().optional(),
633
+ custom_user: z.string().optional(),
634
+ custom_port: z.number().optional(),
635
+ app_id: z.number().optional(),
636
+ installation_id: z.number().optional(),
637
+ client_id: z.string().optional(),
638
+ client_secret: z.string().optional(),
639
+ webhook_secret: z.string().optional(),
640
+ private_key_uuid: z.string().optional(),
641
+ is_system_wide: z.boolean().optional(),
642
+ }, async (args) => {
643
+ const { action, id, ...apiData } = args;
644
+ switch (action) {
645
+ case 'list':
646
+ return wrap(async () => {
647
+ const apps = (await this.client.listGitHubApps({
648
+ summary: true,
649
+ }));
650
+ return apps;
651
+ });
652
+ case 'get':
653
+ if (!id)
654
+ return { content: [{ type: 'text', text: 'Error: id required' }] };
655
+ return wrap(async () => {
656
+ const apps = (await this.client.listGitHubApps());
657
+ const app = apps.find((a) => a.id === id);
658
+ if (!app)
659
+ throw new Error(`GitHub App with id ${id} not found`);
660
+ return app;
661
+ });
662
+ case 'create':
663
+ if (!apiData.name ||
664
+ !apiData.api_url ||
665
+ !apiData.html_url ||
666
+ !apiData.app_id ||
667
+ !apiData.installation_id ||
668
+ !apiData.client_id ||
669
+ !apiData.client_secret ||
670
+ !apiData.private_key_uuid) {
671
+ return {
672
+ content: [
673
+ {
674
+ type: 'text',
675
+ text: 'Error: name, api_url, html_url, app_id, installation_id, client_id, client_secret, private_key_uuid required',
676
+ },
677
+ ],
678
+ };
679
+ }
680
+ return wrap(() => this.client.createGitHubApp({
681
+ name: apiData.name,
682
+ api_url: apiData.api_url,
683
+ html_url: apiData.html_url,
684
+ app_id: apiData.app_id,
685
+ installation_id: apiData.installation_id,
686
+ client_id: apiData.client_id,
687
+ client_secret: apiData.client_secret,
688
+ private_key_uuid: apiData.private_key_uuid,
689
+ organization: apiData.organization,
690
+ custom_user: apiData.custom_user,
691
+ custom_port: apiData.custom_port,
692
+ webhook_secret: apiData.webhook_secret,
693
+ is_system_wide: apiData.is_system_wide,
694
+ }));
695
+ case 'update':
696
+ if (!id)
697
+ return { content: [{ type: 'text', text: 'Error: id required' }] };
698
+ return wrap(() => this.client.updateGitHubApp(id, apiData));
699
+ case 'delete':
700
+ if (!id)
701
+ return { content: [{ type: 'text', text: 'Error: id required' }] };
702
+ return wrap(() => this.client.deleteGitHubApp(id));
703
+ }
704
+ });
705
+ // =========================================================================
544
706
  // Database Backups (1 tool - consolidated)
545
707
  // =========================================================================
546
708
  this.tool('database_backups', 'Manage backups: list_schedules/get_schedule/list_executions/get_execution/create/update/delete', {
@@ -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;
@@ -718,6 +722,64 @@ export interface UpdatePrivateKeyRequest {
718
722
  description?: string;
719
723
  private_key?: string;
720
724
  }
725
+ export interface GitHubApp {
726
+ id: number;
727
+ uuid: string;
728
+ name: string;
729
+ organization: string | null;
730
+ api_url: string;
731
+ html_url: string;
732
+ custom_user: string;
733
+ custom_port: number;
734
+ app_id: number | null;
735
+ installation_id: number | null;
736
+ client_id: string | null;
737
+ is_system_wide: boolean;
738
+ is_public: boolean;
739
+ private_key_id: number | null;
740
+ team_id: number;
741
+ type: string;
742
+ administration: string | null;
743
+ contents: string | null;
744
+ metadata: string | null;
745
+ pull_requests: string | null;
746
+ created_at: string;
747
+ updated_at: string;
748
+ }
749
+ export interface CreateGitHubAppRequest {
750
+ name: string;
751
+ api_url: string;
752
+ html_url: string;
753
+ app_id: number;
754
+ installation_id: number;
755
+ client_id: string;
756
+ client_secret: string;
757
+ private_key_uuid: string;
758
+ organization?: string;
759
+ custom_user?: string;
760
+ custom_port?: number;
761
+ webhook_secret?: string;
762
+ is_system_wide?: boolean;
763
+ }
764
+ export interface UpdateGitHubAppRequest {
765
+ name?: string;
766
+ organization?: string;
767
+ api_url?: string;
768
+ html_url?: string;
769
+ custom_user?: string;
770
+ custom_port?: number;
771
+ app_id?: number;
772
+ installation_id?: number;
773
+ client_id?: string;
774
+ client_secret?: string;
775
+ webhook_secret?: string;
776
+ private_key_uuid?: string;
777
+ is_system_wide?: boolean;
778
+ }
779
+ export interface GitHubAppUpdateResponse {
780
+ message: string;
781
+ data: GitHubApp;
782
+ }
721
783
  export type CloudProvider = 'hetzner' | 'digitalocean';
722
784
  export interface CloudToken {
723
785
  id: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@masonator/coolify-mcp",
3
3
  "scope": "@masonator",
4
- "version": "2.3.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",