@masonator/coolify-mcp 2.3.0 → 2.4.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
 
@@ -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');
@@ -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');
@@ -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,5 +1,5 @@
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';
@@ -1,12 +1,12 @@
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
  /* eslint-disable @typescript-eslint/no-explicit-any */
6
6
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
7
  import { z } from 'zod';
8
8
  import { CoolifyClient, } from './coolify-client.js';
9
- const VERSION = '2.3.0';
9
+ const VERSION = '2.4.0';
10
10
  /** Wrap handler with error handling */
11
11
  function wrap(fn) {
12
12
  return fn()
@@ -205,7 +205,8 @@ export class CoolifyMcpServer extends McpServer {
205
205
  // Delete fields
206
206
  delete_volumes: z.boolean().optional(),
207
207
  }, async (args) => {
208
- const { action, uuid } = args;
208
+ // Strip MCP-internal fields before passing to API (fixes #76)
209
+ const { action, uuid, delete_volumes, ...apiData } = args;
209
210
  switch (action) {
210
211
  case 'create_public':
211
212
  if (!args.project_uuid ||
@@ -223,7 +224,7 @@ export class CoolifyMcpServer extends McpServer {
223
224
  ],
224
225
  };
225
226
  }
226
- return wrap(() => this.client.createApplicationPublic(args));
227
+ return wrap(() => this.client.createApplicationPublic(apiData));
227
228
  case 'create_github':
228
229
  if (!args.project_uuid ||
229
230
  !args.server_uuid ||
@@ -239,7 +240,7 @@ export class CoolifyMcpServer extends McpServer {
239
240
  ],
240
241
  };
241
242
  }
242
- return wrap(() => this.client.createApplicationPrivateGH(args));
243
+ return wrap(() => this.client.createApplicationPrivateGH(apiData));
243
244
  case 'create_key':
244
245
  if (!args.project_uuid ||
245
246
  !args.server_uuid ||
@@ -255,7 +256,7 @@ export class CoolifyMcpServer extends McpServer {
255
256
  ],
256
257
  };
257
258
  }
258
- return wrap(() => this.client.createApplicationPrivateKey(args));
259
+ return wrap(() => this.client.createApplicationPrivateKey(apiData));
259
260
  case 'create_dockerimage':
260
261
  if (!args.project_uuid ||
261
262
  !args.server_uuid ||
@@ -270,15 +271,15 @@ export class CoolifyMcpServer extends McpServer {
270
271
  ],
271
272
  };
272
273
  }
273
- return wrap(() => this.client.createApplicationDockerImage(args));
274
+ return wrap(() => this.client.createApplicationDockerImage(apiData));
274
275
  case 'update':
275
276
  if (!uuid)
276
277
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
277
- return wrap(() => this.client.updateApplication(uuid, args));
278
+ return wrap(() => this.client.updateApplication(uuid, apiData));
278
279
  case 'delete':
279
280
  if (!uuid)
280
281
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
281
- return wrap(() => this.client.deleteApplication(uuid, { deleteVolumes: args.delete_volumes }));
282
+ return wrap(() => this.client.deleteApplication(uuid, { deleteVolumes: delete_volumes }));
282
283
  }
283
284
  });
284
285
  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 +378,8 @@ export class CoolifyMcpServer extends McpServer {
377
378
  docker_compose_raw: z.string().optional(),
378
379
  delete_volumes: z.boolean().optional(),
379
380
  }, async (args) => {
380
- const { action, uuid } = args;
381
+ // Strip MCP-internal fields before passing to API (fixes #76)
382
+ const { action, uuid, delete_volumes, ...apiData } = args;
381
383
  switch (action) {
382
384
  case 'create':
383
385
  if (!args.server_uuid || !args.project_uuid) {
@@ -387,15 +389,15 @@ export class CoolifyMcpServer extends McpServer {
387
389
  ],
388
390
  };
389
391
  }
390
- return wrap(() => this.client.createService(args));
392
+ return wrap(() => this.client.createService(apiData));
391
393
  case 'update':
392
394
  if (!uuid)
393
395
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
394
- return wrap(() => this.client.updateService(uuid, args));
396
+ return wrap(() => this.client.updateService(uuid, apiData));
395
397
  case 'delete':
396
398
  if (!uuid)
397
399
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
398
- return wrap(() => this.client.deleteService(uuid, { deleteVolumes: args.delete_volumes }));
400
+ return wrap(() => this.client.deleteService(uuid, { deleteVolumes: delete_volumes }));
399
401
  }
400
402
  });
401
403
  // =========================================================================
@@ -541,6 +543,90 @@ export class CoolifyMcpServer extends McpServer {
541
543
  }
542
544
  });
543
545
  // =========================================================================
546
+ // GitHub Apps (1 tool - consolidated)
547
+ // =========================================================================
548
+ this.tool('github_apps', 'Manage GitHub Apps: list/get/create/update/delete', {
549
+ action: z.enum(['list', 'get', 'create', 'update', 'delete']),
550
+ // GitHub apps use integer id, not uuid
551
+ id: z.number().optional(),
552
+ // Create/Update fields
553
+ name: z.string().optional(),
554
+ organization: z.string().optional(),
555
+ api_url: z.string().optional(),
556
+ html_url: z.string().optional(),
557
+ custom_user: z.string().optional(),
558
+ custom_port: z.number().optional(),
559
+ app_id: z.number().optional(),
560
+ installation_id: z.number().optional(),
561
+ client_id: z.string().optional(),
562
+ client_secret: z.string().optional(),
563
+ webhook_secret: z.string().optional(),
564
+ private_key_uuid: z.string().optional(),
565
+ is_system_wide: z.boolean().optional(),
566
+ }, async (args) => {
567
+ const { action, id, ...apiData } = args;
568
+ switch (action) {
569
+ case 'list':
570
+ return wrap(async () => {
571
+ const apps = (await this.client.listGitHubApps({
572
+ summary: true,
573
+ }));
574
+ return apps;
575
+ });
576
+ case 'get':
577
+ if (!id)
578
+ return { content: [{ type: 'text', text: 'Error: id required' }] };
579
+ return wrap(async () => {
580
+ const apps = (await this.client.listGitHubApps());
581
+ const app = apps.find((a) => a.id === id);
582
+ if (!app)
583
+ throw new Error(`GitHub App with id ${id} not found`);
584
+ return app;
585
+ });
586
+ case 'create':
587
+ if (!apiData.name ||
588
+ !apiData.api_url ||
589
+ !apiData.html_url ||
590
+ !apiData.app_id ||
591
+ !apiData.installation_id ||
592
+ !apiData.client_id ||
593
+ !apiData.client_secret ||
594
+ !apiData.private_key_uuid) {
595
+ return {
596
+ content: [
597
+ {
598
+ type: 'text',
599
+ text: 'Error: name, api_url, html_url, app_id, installation_id, client_id, client_secret, private_key_uuid required',
600
+ },
601
+ ],
602
+ };
603
+ }
604
+ return wrap(() => this.client.createGitHubApp({
605
+ name: apiData.name,
606
+ api_url: apiData.api_url,
607
+ html_url: apiData.html_url,
608
+ app_id: apiData.app_id,
609
+ installation_id: apiData.installation_id,
610
+ client_id: apiData.client_id,
611
+ client_secret: apiData.client_secret,
612
+ private_key_uuid: apiData.private_key_uuid,
613
+ organization: apiData.organization,
614
+ custom_user: apiData.custom_user,
615
+ custom_port: apiData.custom_port,
616
+ webhook_secret: apiData.webhook_secret,
617
+ is_system_wide: apiData.is_system_wide,
618
+ }));
619
+ case 'update':
620
+ if (!id)
621
+ return { content: [{ type: 'text', text: 'Error: id required' }] };
622
+ return wrap(() => this.client.updateGitHubApp(id, apiData));
623
+ case 'delete':
624
+ if (!id)
625
+ return { content: [{ type: 'text', text: 'Error: id required' }] };
626
+ return wrap(() => this.client.deleteGitHubApp(id));
627
+ }
628
+ });
629
+ // =========================================================================
544
630
  // Database Backups (1 tool - consolidated)
545
631
  // =========================================================================
546
632
  this.tool('database_backups', 'Manage backups: list_schedules/get_schedule/list_executions/get_execution/create/update/delete', {
@@ -718,6 +718,64 @@ export interface UpdatePrivateKeyRequest {
718
718
  description?: string;
719
719
  private_key?: string;
720
720
  }
721
+ export interface GitHubApp {
722
+ id: number;
723
+ uuid: string;
724
+ name: string;
725
+ organization: string | null;
726
+ api_url: string;
727
+ html_url: string;
728
+ custom_user: string;
729
+ custom_port: number;
730
+ app_id: number | null;
731
+ installation_id: number | null;
732
+ client_id: string | null;
733
+ is_system_wide: boolean;
734
+ is_public: boolean;
735
+ private_key_id: number | null;
736
+ team_id: number;
737
+ type: string;
738
+ administration: string | null;
739
+ contents: string | null;
740
+ metadata: string | null;
741
+ pull_requests: string | null;
742
+ created_at: string;
743
+ updated_at: string;
744
+ }
745
+ export interface CreateGitHubAppRequest {
746
+ name: string;
747
+ api_url: string;
748
+ html_url: string;
749
+ app_id: number;
750
+ installation_id: number;
751
+ client_id: string;
752
+ client_secret: string;
753
+ private_key_uuid: string;
754
+ organization?: string;
755
+ custom_user?: string;
756
+ custom_port?: number;
757
+ webhook_secret?: string;
758
+ is_system_wide?: boolean;
759
+ }
760
+ export interface UpdateGitHubAppRequest {
761
+ name?: string;
762
+ organization?: string;
763
+ api_url?: string;
764
+ html_url?: string;
765
+ custom_user?: string;
766
+ custom_port?: number;
767
+ app_id?: number;
768
+ installation_id?: number;
769
+ client_id?: string;
770
+ client_secret?: string;
771
+ webhook_secret?: string;
772
+ private_key_uuid?: string;
773
+ is_system_wide?: boolean;
774
+ }
775
+ export interface GitHubAppUpdateResponse {
776
+ message: string;
777
+ data: GitHubApp;
778
+ }
721
779
  export type CloudProvider = 'hetzner' | 'digitalocean';
722
780
  export interface CloudToken {
723
781
  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.4.0",
5
5
  "description": "MCP server implementation for Coolify",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",