@masonator/coolify-mcp 2.2.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
@@ -227,7 +230,9 @@ These tools accept human-friendly identifiers instead of just UUIDs:
227
230
  - `list_applications` - List all applications (returns summary)
228
231
  - `get_application` - Get application details
229
232
  - `application_logs` - Get application logs
230
- - `application` - Create, update, or delete apps with `action: create_github|create_key|update|delete`
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');
@@ -52,6 +52,7 @@ describe('CoolifyMcpServer v2', () => {
52
52
  // Application operations
53
53
  expect(typeof client.listApplications).toBe('function');
54
54
  expect(typeof client.getApplication).toBe('function');
55
+ expect(typeof client.createApplicationPublic).toBe('function');
55
56
  expect(typeof client.createApplicationPrivateGH).toBe('function');
56
57
  expect(typeof client.createApplicationPrivateKey).toBe('function');
57
58
  expect(typeof client.createApplicationDockerImage).toBe('function');
@@ -106,6 +107,11 @@ describe('CoolifyMcpServer v2', () => {
106
107
  expect(typeof client.createPrivateKey).toBe('function');
107
108
  expect(typeof client.updatePrivateKey).toBe('function');
108
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');
109
115
  // Backup operations
110
116
  expect(typeof client.listDatabaseBackups).toBe('function');
111
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.2.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.2.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.2.0';
9
+ const VERSION = '2.4.0';
10
10
  /** Wrap handler with error handling */
11
11
  function wrap(fn) {
12
12
  return fn()
@@ -162,7 +162,14 @@ export class CoolifyMcpServer extends McpServer {
162
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
163
  this.tool('get_application', 'App details', { uuid: z.string() }, async ({ uuid }) => wrap(() => this.client.getApplication(uuid)));
164
164
  this.tool('application', 'Manage app: create/update/delete', {
165
- action: z.enum(['create_github', 'create_key', 'create_dockerimage', 'update', 'delete']),
165
+ action: z.enum([
166
+ 'create_public',
167
+ 'create_github',
168
+ 'create_key',
169
+ 'create_dockerimage',
170
+ 'update',
171
+ 'delete',
172
+ ]),
166
173
  uuid: z.string().optional(),
167
174
  // Create fields
168
175
  project_uuid: z.string().optional(),
@@ -172,6 +179,7 @@ export class CoolifyMcpServer extends McpServer {
172
179
  git_repository: z.string().optional(),
173
180
  git_branch: z.string().optional(),
174
181
  environment_name: z.string().optional(),
182
+ environment_uuid: z.string().optional(),
175
183
  build_pack: z.string().optional(),
176
184
  ports_exposes: z.string().optional(),
177
185
  // Docker image fields
@@ -197,8 +205,26 @@ export class CoolifyMcpServer extends McpServer {
197
205
  // Delete fields
198
206
  delete_volumes: z.boolean().optional(),
199
207
  }, async (args) => {
200
- const { action, uuid } = args;
208
+ // Strip MCP-internal fields before passing to API (fixes #76)
209
+ const { action, uuid, delete_volumes, ...apiData } = args;
201
210
  switch (action) {
211
+ case 'create_public':
212
+ if (!args.project_uuid ||
213
+ !args.server_uuid ||
214
+ !args.git_repository ||
215
+ !args.git_branch ||
216
+ !args.build_pack ||
217
+ !args.ports_exposes) {
218
+ return {
219
+ content: [
220
+ {
221
+ type: 'text',
222
+ text: 'Error: project_uuid, server_uuid, git_repository, git_branch, build_pack, ports_exposes required',
223
+ },
224
+ ],
225
+ };
226
+ }
227
+ return wrap(() => this.client.createApplicationPublic(apiData));
202
228
  case 'create_github':
203
229
  if (!args.project_uuid ||
204
230
  !args.server_uuid ||
@@ -214,7 +240,7 @@ export class CoolifyMcpServer extends McpServer {
214
240
  ],
215
241
  };
216
242
  }
217
- return wrap(() => this.client.createApplicationPrivateGH(args));
243
+ return wrap(() => this.client.createApplicationPrivateGH(apiData));
218
244
  case 'create_key':
219
245
  if (!args.project_uuid ||
220
246
  !args.server_uuid ||
@@ -230,7 +256,7 @@ export class CoolifyMcpServer extends McpServer {
230
256
  ],
231
257
  };
232
258
  }
233
- return wrap(() => this.client.createApplicationPrivateKey(args));
259
+ return wrap(() => this.client.createApplicationPrivateKey(apiData));
234
260
  case 'create_dockerimage':
235
261
  if (!args.project_uuid ||
236
262
  !args.server_uuid ||
@@ -245,15 +271,15 @@ export class CoolifyMcpServer extends McpServer {
245
271
  ],
246
272
  };
247
273
  }
248
- return wrap(() => this.client.createApplicationDockerImage(args));
274
+ return wrap(() => this.client.createApplicationDockerImage(apiData));
249
275
  case 'update':
250
276
  if (!uuid)
251
277
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
252
- return wrap(() => this.client.updateApplication(uuid, args));
278
+ return wrap(() => this.client.updateApplication(uuid, apiData));
253
279
  case 'delete':
254
280
  if (!uuid)
255
281
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
256
- return wrap(() => this.client.deleteApplication(uuid, { deleteVolumes: args.delete_volumes }));
282
+ return wrap(() => this.client.deleteApplication(uuid, { deleteVolumes: delete_volumes }));
257
283
  }
258
284
  });
259
285
  this.tool('application_logs', 'Get app logs', { uuid: z.string(), lines: z.number().optional() }, async ({ uuid, lines }) => wrap(() => this.client.getApplicationLogs(uuid, lines)));
@@ -352,7 +378,8 @@ export class CoolifyMcpServer extends McpServer {
352
378
  docker_compose_raw: z.string().optional(),
353
379
  delete_volumes: z.boolean().optional(),
354
380
  }, async (args) => {
355
- const { action, uuid } = args;
381
+ // Strip MCP-internal fields before passing to API (fixes #76)
382
+ const { action, uuid, delete_volumes, ...apiData } = args;
356
383
  switch (action) {
357
384
  case 'create':
358
385
  if (!args.server_uuid || !args.project_uuid) {
@@ -362,15 +389,15 @@ export class CoolifyMcpServer extends McpServer {
362
389
  ],
363
390
  };
364
391
  }
365
- return wrap(() => this.client.createService(args));
392
+ return wrap(() => this.client.createService(apiData));
366
393
  case 'update':
367
394
  if (!uuid)
368
395
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
369
- return wrap(() => this.client.updateService(uuid, args));
396
+ return wrap(() => this.client.updateService(uuid, apiData));
370
397
  case 'delete':
371
398
  if (!uuid)
372
399
  return { content: [{ type: 'text', text: 'Error: uuid required' }] };
373
- return wrap(() => this.client.deleteService(uuid, { deleteVolumes: args.delete_volumes }));
400
+ return wrap(() => this.client.deleteService(uuid, { deleteVolumes: delete_volumes }));
374
401
  }
375
402
  });
376
403
  // =========================================================================
@@ -516,6 +543,90 @@ export class CoolifyMcpServer extends McpServer {
516
543
  }
517
544
  });
518
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
+ // =========================================================================
519
630
  // Database Backups (1 tool - consolidated)
520
631
  // =========================================================================
521
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.2.0",
4
+ "version": "2.4.0",
5
5
  "description": "MCP server implementation for Coolify",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",