@masonator/coolify-mcp 2.7.3 → 2.8.1

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
@@ -85,6 +85,34 @@ claude mcp add coolify \
85
85
  env COOLIFY_ACCESS_TOKEN=your-api-token COOLIFY_BASE_URL=https://your-coolify-instance.com npx -y @masonator/coolify-mcp
86
86
  ```
87
87
 
88
+ ### Custom HTTP Headers (Cloudflare Zero Trust, Auth Proxies)
89
+
90
+ If your Coolify instance sits behind a Cloudflare Access tunnel or other auth-proxy middleware, pass extra headers on every outbound request with `--header`:
91
+
92
+ ```json
93
+ {
94
+ "mcpServers": {
95
+ "coolify": {
96
+ "command": "npx",
97
+ "args": [
98
+ "-y",
99
+ "@masonator/coolify-mcp",
100
+ "--header",
101
+ "CF-Access-Client-Id: abc123.access",
102
+ "--header",
103
+ "CF-Access-Client-Secret: your-secret"
104
+ ],
105
+ "env": {
106
+ "COOLIFY_ACCESS_TOKEN": "your-api-token",
107
+ "COOLIFY_BASE_URL": "https://your-coolify-instance.com"
108
+ }
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ Multiple `--header` flags can be combined. The reserved headers `Authorization` and `Content-Type` are filtered (with a warning) to prevent silently overriding the Coolify bearer token.
115
+
88
116
  ## Context-Optimized Responses
89
117
 
90
118
  ### Why This Matters
@@ -103,13 +131,14 @@ The Coolify API returns extremely verbose responses - a single application can c
103
131
 
104
132
  ### Response Size Comparison
105
133
 
106
- | Endpoint | Full Response | Summary Response | Reduction |
107
- | --------------------- | ------------- | ---------------- | --------- |
108
- | list_applications | ~170KB | ~4.4KB | **97%** |
109
- | list_services | ~367KB | ~1.2KB | **99%** |
110
- | list_servers | ~4KB | ~0.4KB | **90%** |
111
- | list_application_envs | ~3KB/var | ~0.1KB/var | **97%** |
112
- | deployment get | ~13KB | ~1KB | **92%** |
134
+ | Endpoint | Full Response | Summary Response | Reduction |
135
+ | ----------------------- | ------------- | ---------------- | --------- |
136
+ | list_applications | ~170KB | ~4.4KB | **97%** |
137
+ | list_services | ~367KB | ~1.2KB | **99%** |
138
+ | list_servers | ~4KB | ~0.4KB | **90%** |
139
+ | list_application_envs | ~3KB/var | ~0.1KB/var | **97%** |
140
+ | deployment get | ~13KB | ~1KB | **92%** |
141
+ | deployment list_for_app | ~1MB | ~4KB | **99.6%** |
113
142
 
114
143
  ### HATEOAS-style Response Actions
115
144
 
@@ -126,6 +126,42 @@ describe('CoolifyClient', () => {
126
126
  c.getVersion();
127
127
  expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/version', expect.any(Object));
128
128
  });
129
+ it('should merge customHeaders into outgoing requests', async () => {
130
+ const c = new CoolifyClient({
131
+ baseUrl: 'http://localhost:3000',
132
+ accessToken: 'test-token',
133
+ customHeaders: { 'CF-Access-Client-Id': 'abc', 'CF-Access-Client-Secret': 'xyz' },
134
+ });
135
+ mockFetch.mockResolvedValueOnce(mockResponse([{ uuid: 's1', name: 'srv' }]));
136
+ await c.listServers();
137
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
138
+ headers: expect.objectContaining({
139
+ 'CF-Access-Client-Id': 'abc',
140
+ 'CF-Access-Client-Secret': 'xyz',
141
+ Authorization: 'Bearer test-token',
142
+ }),
143
+ }));
144
+ });
145
+ it('should filter reserved headers from customHeaders', async () => {
146
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
147
+ const c = new CoolifyClient({
148
+ baseUrl: 'http://localhost:3000',
149
+ accessToken: 'test-token',
150
+ customHeaders: {
151
+ Authorization: 'Bearer override',
152
+ 'Content-Type': 'text/plain',
153
+ 'X-Safe-Header': 'allowed',
154
+ },
155
+ });
156
+ mockFetch.mockResolvedValueOnce(mockResponse([{ uuid: 's1', name: 'srv' }]));
157
+ await c.listServers();
158
+ const headers = mockFetch.mock.calls[0][1]?.headers;
159
+ expect(headers['Authorization']).toBe('Bearer test-token');
160
+ expect(headers['Content-Type']).toBe('application/json');
161
+ expect(headers['X-Safe-Header']).toBe('allowed');
162
+ expect(warnSpy).toHaveBeenCalledTimes(2);
163
+ warnSpy.mockRestore();
164
+ });
129
165
  });
130
166
  describe('listServers', () => {
131
167
  it('should return a list of servers', async () => {
@@ -849,6 +885,87 @@ describe('CoolifyClient', () => {
849
885
  expect(callBody.domains).toBe('https://app.example.com');
850
886
  expect(callBody.fqdn).toBeUndefined();
851
887
  });
888
+ it('should map fqdn to domains in createApplicationDockerImage', async () => {
889
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
890
+ await client.createApplicationDockerImage({
891
+ project_uuid: 'proj-uuid',
892
+ server_uuid: 'server-uuid',
893
+ docker_registry_image_name: 'traefik/whoami',
894
+ ports_exposes: '80',
895
+ fqdn: 'https://whoami.example.com',
896
+ });
897
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
898
+ expect(callBody.domains).toBe('https://whoami.example.com');
899
+ expect(callBody.fqdn).toBeUndefined();
900
+ });
901
+ it('should map fqdn to domains in createApplicationDockerfile', async () => {
902
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
903
+ await client.createApplicationDockerfile({
904
+ project_uuid: 'proj-uuid',
905
+ server_uuid: 'server-uuid',
906
+ dockerfile: 'FROM nginx',
907
+ fqdn: 'https://app.example.com',
908
+ });
909
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
910
+ expect(callBody.domains).toBe('https://app.example.com');
911
+ expect(callBody.fqdn).toBeUndefined();
912
+ });
913
+ it('should map fqdn to domains in createApplicationDockerCompose', async () => {
914
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
915
+ await client.createApplicationDockerCompose({
916
+ project_uuid: 'proj-uuid',
917
+ server_uuid: 'server-uuid',
918
+ docker_compose_raw: 'version: "3"\n',
919
+ fqdn: 'https://compose.example.com',
920
+ });
921
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
922
+ expect(callBody.domains).toBe('https://compose.example.com');
923
+ expect(callBody.fqdn).toBeUndefined();
924
+ });
925
+ it('should accept explicit domains and prefer it over fqdn', async () => {
926
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
927
+ await client.createApplicationDockerImage({
928
+ project_uuid: 'proj-uuid',
929
+ server_uuid: 'server-uuid',
930
+ docker_registry_image_name: 'traefik/whoami',
931
+ ports_exposes: '80',
932
+ fqdn: 'https://from-fqdn.example.com',
933
+ domains: 'https://from-domains.example.com',
934
+ });
935
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
936
+ expect(callBody.domains).toBe('https://from-domains.example.com');
937
+ expect(callBody.fqdn).toBeUndefined();
938
+ });
939
+ it('should pass instant_deploy and custom_* fields through createApplicationDockerImage', async () => {
940
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
941
+ await client.createApplicationDockerImage({
942
+ project_uuid: 'proj-uuid',
943
+ server_uuid: 'server-uuid',
944
+ docker_registry_image_name: 'traefik/whoami',
945
+ ports_exposes: '80',
946
+ instant_deploy: true,
947
+ custom_docker_run_options: '--network=my-net',
948
+ custom_labels: 'dHJhZWZpaw==',
949
+ });
950
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
951
+ expect(callBody.instant_deploy).toBe(true);
952
+ expect(callBody.custom_docker_run_options).toBe('--network=my-net');
953
+ expect(callBody.custom_labels).toBe('dHJhZWZpaw==');
954
+ });
955
+ it('should pass destination_uuid through in createApplicationPublic', async () => {
956
+ mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
957
+ await client.createApplicationPublic({
958
+ project_uuid: 'proj-uuid',
959
+ server_uuid: 'server-uuid',
960
+ destination_uuid: 'dest-uuid',
961
+ git_repository: 'https://github.com/user/repo',
962
+ git_branch: 'main',
963
+ build_pack: 'nixpacks',
964
+ ports_exposes: '3000',
965
+ });
966
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
967
+ expect(callBody.destination_uuid).toBe('dest-uuid');
968
+ });
852
969
  it('should map fqdn to domains in updateApplication', async () => {
853
970
  mockFetch.mockResolvedValueOnce(mockResponse(mockApplication));
854
971
  await client.updateApplication('app-uuid', { fqdn: 'https://new.example.com' });
@@ -1457,12 +1574,32 @@ describe('CoolifyClient', () => {
1457
1574
  logs_info: 'Logs available (16 chars). Use lines param to retrieve.',
1458
1575
  });
1459
1576
  });
1460
- it('should list application deployments', async () => {
1461
- mockFetch.mockResolvedValueOnce(mockResponse([mockDeployment]));
1577
+ it('should list application deployments as essential by default (no logs)', async () => {
1578
+ const withLogs = { ...mockDeployment, logs: 'x'.repeat(30000) };
1579
+ // Real Coolify wraps the list in an envelope: { count, deployments: [...] }
1580
+ mockFetch.mockResolvedValueOnce(mockResponse({ count: 1, deployments: [withLogs] }));
1462
1581
  const result = await client.listApplicationDeployments('app-uuid');
1463
- expect(result).toEqual([mockDeployment]);
1582
+ expect(result.count).toBe(1);
1583
+ expect(result.deployments).toHaveLength(1);
1584
+ const [first] = result.deployments;
1585
+ // Essential projection: no raw logs, but a `logs_available` breadcrumb.
1586
+ expect(first.logs).toBeUndefined();
1587
+ expect(first.logs_available).toBe(true);
1588
+ // Essential also drops fields like `id`
1589
+ expect(first.id).toBeUndefined();
1464
1590
  expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/applications/app-uuid', expect.any(Object));
1465
1591
  });
1592
+ it('should return full deployments when includeLogs is true', async () => {
1593
+ const withLogs = { ...mockDeployment, logs: 'build log stream' };
1594
+ mockFetch.mockResolvedValueOnce(mockResponse({ count: 1, deployments: [withLogs] }));
1595
+ const result = await client.listApplicationDeployments('app-uuid', { includeLogs: true });
1596
+ expect(result).toEqual({ count: 1, deployments: [withLogs] });
1597
+ });
1598
+ it('should tolerate a malformed envelope (missing deployments array)', async () => {
1599
+ mockFetch.mockResolvedValueOnce(mockResponse({}));
1600
+ const result = await client.listApplicationDeployments('app-uuid');
1601
+ expect(result).toEqual({ count: 0, deployments: [] });
1602
+ });
1466
1603
  });
1467
1604
  // =========================================================================
1468
1605
  // Team endpoints - extended coverage
@@ -1906,7 +2043,7 @@ describe('CoolifyClient', () => {
1906
2043
  .mockResolvedValueOnce(mockResponse(mockApp))
1907
2044
  .mockResolvedValueOnce(mockResponse(mockLogs))
1908
2045
  .mockResolvedValueOnce(mockResponse(mockEnvVars))
1909
- .mockResolvedValueOnce(mockResponse(mockDeployments));
2046
+ .mockResolvedValueOnce(mockResponse({ count: mockDeployments.length, deployments: mockDeployments }));
1910
2047
  const result = await client.diagnoseApplication(testAppUuid);
1911
2048
  expect(result.application).toEqual({
1912
2049
  uuid: testAppUuid,
@@ -1932,7 +2069,7 @@ describe('CoolifyClient', () => {
1932
2069
  .mockResolvedValueOnce(mockResponse(unhealthyApp))
1933
2070
  .mockResolvedValueOnce(mockResponse(mockLogs))
1934
2071
  .mockResolvedValueOnce(mockResponse(mockEnvVars))
1935
- .mockResolvedValueOnce(mockResponse([]));
2072
+ .mockResolvedValueOnce(mockResponse({ count: 0, deployments: [] }));
1936
2073
  const result = await client.diagnoseApplication(testAppUuid);
1937
2074
  expect(result.health.status).toBe('unhealthy');
1938
2075
  expect(result.health.issues).toContain('Status: exited:unhealthy');
@@ -1946,7 +2083,7 @@ describe('CoolifyClient', () => {
1946
2083
  .mockResolvedValueOnce(mockResponse(mockApp))
1947
2084
  .mockResolvedValueOnce(mockResponse(mockLogs))
1948
2085
  .mockResolvedValueOnce(mockResponse(mockEnvVars))
1949
- .mockResolvedValueOnce(mockResponse(failedDeployments));
2086
+ .mockResolvedValueOnce(mockResponse({ count: failedDeployments.length, deployments: failedDeployments }));
1950
2087
  const result = await client.diagnoseApplication(testAppUuid);
1951
2088
  expect(result.health.issues).toContain('2 failed deployment(s) in last 5');
1952
2089
  });
@@ -1955,7 +2092,7 @@ describe('CoolifyClient', () => {
1955
2092
  .mockResolvedValueOnce(mockResponse(mockApp))
1956
2093
  .mockRejectedValueOnce(new Error('Logs unavailable'))
1957
2094
  .mockResolvedValueOnce(mockResponse(mockEnvVars))
1958
- .mockResolvedValueOnce(mockResponse(mockDeployments));
2095
+ .mockResolvedValueOnce(mockResponse({ count: mockDeployments.length, deployments: mockDeployments }));
1959
2096
  const result = await client.diagnoseApplication(testAppUuid);
1960
2097
  expect(result.application).not.toBeNull();
1961
2098
  expect(result.logs).toBeNull();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { parseHeaders } from '../lib/parse-headers.js';
3
+ describe('parseHeaders', () => {
4
+ it('should parse a single header', () => {
5
+ const result = parseHeaders(['--header', 'X-Custom: value']);
6
+ expect(result).toEqual({ 'X-Custom': 'value' });
7
+ });
8
+ it('should parse multiple headers', () => {
9
+ const result = parseHeaders(['--header', 'X-First: one', '--header', 'X-Second: two']);
10
+ expect(result).toEqual({ 'X-First': 'one', 'X-Second': 'two' });
11
+ });
12
+ it('should ignore malformed headers without a colon', () => {
13
+ const result = parseHeaders(['--header', 'no-colon-here']);
14
+ expect(result).toEqual({});
15
+ });
16
+ it('should trim whitespace from key and value', () => {
17
+ const result = parseHeaders(['--header', ' X-Spaced : some value ']);
18
+ expect(result).toEqual({ 'X-Spaced': 'some value' });
19
+ });
20
+ it('should handle header value containing colons', () => {
21
+ const result = parseHeaders(['--header', 'Authorization: Bearer abc:def:ghi']);
22
+ expect(result).toEqual({ Authorization: 'Bearer abc:def:ghi' });
23
+ });
24
+ it('should return empty object when no headers provided', () => {
25
+ expect(parseHeaders([])).toEqual({});
26
+ expect(parseHeaders(['--other', 'flag'])).toEqual({});
27
+ });
28
+ it('should ignore --header without a following value', () => {
29
+ const result = parseHeaders(['--header']);
30
+ expect(result).toEqual({});
31
+ });
32
+ });
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { CoolifyMcpServer } from './lib/mcp-server.js';
4
+ import { parseHeaders } from './lib/parse-headers.js';
4
5
  async function main() {
6
+ const customHeaders = parseHeaders(process.argv);
5
7
  const config = {
6
8
  baseUrl: process.env.COOLIFY_BASE_URL || 'http://localhost:3000',
7
9
  accessToken: process.env.COOLIFY_ACCESS_TOKEN || '',
10
+ customHeaders: Object.keys(customHeaders).length > 0 ? customHeaders : undefined,
8
11
  };
9
12
  if (!config.accessToken) {
10
13
  throw new Error('COOLIFY_ACCESS_TOKEN environment variable is required');
@@ -72,6 +72,7 @@ export interface GitHubAppSummary {
72
72
  export declare class CoolifyClient {
73
73
  private readonly baseUrl;
74
74
  private readonly accessToken;
75
+ private readonly customHeaders;
75
76
  private cachedVersion;
76
77
  constructor(config: CoolifyConfig);
77
78
  private request;
@@ -163,7 +164,23 @@ export declare class CoolifyClient {
163
164
  includeLogs?: boolean;
164
165
  }): Promise<Deployment | DeploymentEssential>;
165
166
  deployByTagOrUuid(tagOrUuid: string, force?: boolean): Promise<MessageResponse>;
166
- listApplicationDeployments(appUuid: string): Promise<Deployment[]>;
167
+ /**
168
+ * List deployments for an application.
169
+ *
170
+ * Coolify returns `{ count, deployments: Deployment[] }` for this endpoint
171
+ * (NOT a raw array — upstream @masonator type was incorrect).
172
+ *
173
+ * By default returns a DeploymentEssential summary (no `logs` field) because
174
+ * each deployment's log blob can be 30–100KB, and a typical list has 20–35
175
+ * deployments — exceeding MCP response token limits. Pass `includeLogs: true`
176
+ * to get raw Deployment objects with full build logs.
177
+ */
178
+ listApplicationDeployments(appUuid: string, options?: {
179
+ includeLogs?: boolean;
180
+ }): Promise<{
181
+ count: number;
182
+ deployments: Deployment[] | DeploymentEssential[];
183
+ }>;
167
184
  listTeams(): Promise<Team[]>;
168
185
  getTeam(id: number): Promise<Team>;
169
186
  getTeamMembers(id: number): Promise<TeamMember[]>;
@@ -35,6 +35,12 @@ function toBase64(value) {
35
35
  */
36
36
  function mapFqdnToDomains(data) {
37
37
  const { fqdn, ...rest } = data;
38
+ // Explicit `domains` always wins. `fqdn` is only used when `domains` was
39
+ // not provided — kept for backward compatibility because `get_application`
40
+ // surfaces the field as `fqdn` in responses.
41
+ if (rest.domains !== undefined) {
42
+ return rest;
43
+ }
38
44
  if (fqdn === undefined) {
39
45
  return rest;
40
46
  }
@@ -143,8 +149,11 @@ function toEnvVarSummary(envVar) {
143
149
  * HTTP client for the Coolify API
144
150
  */
145
151
  export class CoolifyClient {
152
+ baseUrl;
153
+ accessToken;
154
+ customHeaders;
155
+ cachedVersion = null;
146
156
  constructor(config) {
147
- this.cachedVersion = null;
148
157
  if (!config.baseUrl) {
149
158
  throw new Error('Coolify base URL is required');
150
159
  }
@@ -153,6 +162,18 @@ export class CoolifyClient {
153
162
  }
154
163
  this.baseUrl = config.baseUrl.replace(/\/$/, '');
155
164
  this.accessToken = config.accessToken;
165
+ const reserved = new Set(['authorization', 'content-type']);
166
+ const raw = config.customHeaders ?? {};
167
+ const filtered = {};
168
+ for (const [key, value] of Object.entries(raw)) {
169
+ if (reserved.has(key.toLowerCase())) {
170
+ console.warn(`Custom header "${key}" ignored: reserved by the Coolify client`);
171
+ }
172
+ else {
173
+ filtered[key] = value;
174
+ }
175
+ }
176
+ this.customHeaders = filtered;
156
177
  }
157
178
  // ===========================================================================
158
179
  // Private HTTP methods
@@ -165,6 +186,7 @@ export class CoolifyClient {
165
186
  headers: {
166
187
  'Content-Type': 'application/json',
167
188
  Authorization: `Bearer ${this.accessToken}`,
189
+ ...this.customHeaders,
168
190
  ...options.headers,
169
191
  },
170
192
  });
@@ -187,7 +209,7 @@ export class CoolifyClient {
187
209
  }
188
210
  catch (error) {
189
211
  if (error instanceof TypeError && error.message.includes('fetch')) {
190
- throw new Error(`Failed to connect to Coolify server at ${this.baseUrl}. Please check if the server is running and accessible.`);
212
+ throw new Error(`Failed to connect to Coolify server at ${this.baseUrl}. Please check if the server is running and accessible.`, { cause: error });
191
213
  }
192
214
  throw error;
193
215
  }
@@ -214,6 +236,7 @@ export class CoolifyClient {
214
236
  const response = await fetch(url, {
215
237
  headers: {
216
238
  Authorization: `Bearer ${this.accessToken}`,
239
+ ...this.customHeaders,
217
240
  },
218
241
  });
219
242
  if (!response.ok) {
@@ -231,7 +254,7 @@ export class CoolifyClient {
231
254
  await this.getVersion();
232
255
  }
233
256
  catch (error) {
234
- throw new Error(`Failed to connect to Coolify server: ${error instanceof Error ? error.message : 'Unknown error'}`);
257
+ throw new Error(`Failed to connect to Coolify server: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
235
258
  }
236
259
  }
237
260
  // ===========================================================================
@@ -386,17 +409,18 @@ export class CoolifyClient {
386
409
  async createApplicationDockerfile(data) {
387
410
  return this.request('/applications/dockerfile', {
388
411
  method: 'POST',
389
- body: JSON.stringify(data),
412
+ body: JSON.stringify(mapFqdnToDomains(data)),
390
413
  });
391
414
  }
392
415
  async createApplicationDockerImage(data) {
393
416
  return this.request('/applications/dockerimage', {
394
417
  method: 'POST',
395
- body: JSON.stringify(data),
418
+ body: JSON.stringify(mapFqdnToDomains(data)),
396
419
  });
397
420
  }
398
421
  async createApplicationDockerCompose(data) {
399
- const payload = { ...data };
422
+ const mapped = mapFqdnToDomains(data);
423
+ const payload = { ...mapped };
400
424
  if (payload.docker_compose_raw) {
401
425
  payload.docker_compose_raw = toBase64(payload.docker_compose_raw);
402
426
  }
@@ -679,8 +703,24 @@ export class CoolifyClient {
679
703
  const param = this.isLikelyUuid(tagOrUuid) ? 'uuid' : 'tag';
680
704
  return this.request(`/deploy?${param}=${encodeURIComponent(tagOrUuid)}&force=${force}`, { method: 'GET' });
681
705
  }
682
- async listApplicationDeployments(appUuid) {
683
- return this.request(`/deployments/applications/${appUuid}`);
706
+ /**
707
+ * List deployments for an application.
708
+ *
709
+ * Coolify returns `{ count, deployments: Deployment[] }` for this endpoint
710
+ * (NOT a raw array — upstream @masonator type was incorrect).
711
+ *
712
+ * By default returns a DeploymentEssential summary (no `logs` field) because
713
+ * each deployment's log blob can be 30–100KB, and a typical list has 20–35
714
+ * deployments — exceeding MCP response token limits. Pass `includeLogs: true`
715
+ * to get raw Deployment objects with full build logs.
716
+ */
717
+ async listApplicationDeployments(appUuid, options) {
718
+ const envelope = await this.request(`/deployments/applications/${appUuid}`);
719
+ const deployments = Array.isArray(envelope?.deployments) ? envelope.deployments : [];
720
+ return {
721
+ count: typeof envelope?.count === 'number' ? envelope.count : deployments.length,
722
+ deployments: options?.includeLogs ? deployments : deployments.map(toDeploymentEssential),
723
+ };
684
724
  }
685
725
  // ===========================================================================
686
726
  // Team endpoints
@@ -932,7 +972,10 @@ export class CoolifyClient {
932
972
  const app = extract(results[0], 'application');
933
973
  const logs = extract(results[1], 'logs');
934
974
  const envVars = extract(results[2], 'environment_variables');
935
- const deployments = extract(results[3], 'deployments');
975
+ // listApplicationDeployments now returns { count, deployments: [...] } —
976
+ // flatten back to the array that the diagnostics consumer expects.
977
+ const deploymentsEnvelope = extract(results[3], 'deployments');
978
+ const deployments = deploymentsEnvelope?.deployments ?? [];
936
979
  // Determine health status and issues
937
980
  const issues = [];
938
981
  let healthStatus = 'unknown';
@@ -7,11 +7,9 @@ const DOCS_BASE_URL = 'https://coolify.io';
7
7
  * The LLM calling this tool handles semantic understanding — we just need good ranking.
8
8
  */
9
9
  export class DocsSearchEngine {
10
- constructor() {
11
- this.index = null;
12
- this.chunks = [];
13
- this.loading = null;
14
- }
10
+ index = null;
11
+ chunks = [];
12
+ loading = null;
15
13
  async ensureLoaded() {
16
14
  if (this.index)
17
15
  return;
@@ -23,7 +21,7 @@ export class DocsSearchEngine {
23
21
  async loadAndIndex() {
24
22
  try {
25
23
  const controller = new AbortController();
26
- const timeout = setTimeout(() => controller.abort(), 15000);
24
+ const timeout = setTimeout(() => controller.abort(), 15_000);
27
25
  let response;
28
26
  try {
29
27
  response = await fetch(DOCS_FULL_URL, { signal: controller.signal });
@@ -153,9 +153,10 @@ function wrapWithActions(fn, getActions, getPaginationFn) {
153
153
  }));
154
154
  }
155
155
  export class CoolifyMcpServer extends McpServer {
156
+ client;
157
+ docsSearch = new DocsSearchEngine();
156
158
  constructor(config) {
157
159
  super({ name: 'coolify', version: VERSION });
158
- this.docsSearch = new DocsSearchEngine();
159
160
  this.client = new CoolifyClient(config);
160
161
  this.registerTools();
161
162
  }
@@ -308,6 +309,7 @@ export class CoolifyMcpServer extends McpServer {
308
309
  server_uuid: z.string().optional(),
309
310
  github_app_uuid: z.string().optional(),
310
311
  private_key_uuid: z.string().optional(),
312
+ destination_uuid: z.string().optional(),
311
313
  git_repository: z.string().optional(),
312
314
  git_branch: z.string().optional(),
313
315
  environment_name: z.string().optional(),
@@ -321,6 +323,10 @@ export class CoolifyMcpServer extends McpServer {
321
323
  name: z.string().optional(),
322
324
  description: z.string().optional(),
323
325
  fqdn: z.string().optional(),
326
+ domains: z.string().optional(),
327
+ custom_docker_run_options: z.string().optional(),
328
+ custom_labels: z.string().optional(),
329
+ instant_deploy: z.boolean().optional(),
324
330
  // Health check fields
325
331
  health_check_enabled: z.boolean().optional(),
326
332
  health_check_path: z.string().optional(),
@@ -358,6 +364,7 @@ export class CoolifyMcpServer extends McpServer {
358
364
  return wrap(() => this.client.createApplicationPublic({
359
365
  project_uuid: args.project_uuid,
360
366
  server_uuid: args.server_uuid,
367
+ destination_uuid: args.destination_uuid,
361
368
  git_repository: args.git_repository,
362
369
  git_branch: args.git_branch,
363
370
  build_pack: args.build_pack,
@@ -367,6 +374,10 @@ export class CoolifyMcpServer extends McpServer {
367
374
  name: args.name,
368
375
  description: args.description,
369
376
  fqdn: args.fqdn,
377
+ domains: args.domains,
378
+ custom_docker_run_options: args.custom_docker_run_options,
379
+ custom_labels: args.custom_labels,
380
+ instant_deploy: args.instant_deploy,
370
381
  }));
371
382
  case 'create_github':
372
383
  if (!args.project_uuid ||
@@ -387,6 +398,7 @@ export class CoolifyMcpServer extends McpServer {
387
398
  project_uuid: args.project_uuid,
388
399
  server_uuid: args.server_uuid,
389
400
  github_app_uuid: args.github_app_uuid,
401
+ destination_uuid: args.destination_uuid,
390
402
  git_repository: args.git_repository,
391
403
  git_branch: args.git_branch,
392
404
  build_pack: args.build_pack,
@@ -396,6 +408,10 @@ export class CoolifyMcpServer extends McpServer {
396
408
  name: args.name,
397
409
  description: args.description,
398
410
  fqdn: args.fqdn,
411
+ domains: args.domains,
412
+ custom_docker_run_options: args.custom_docker_run_options,
413
+ custom_labels: args.custom_labels,
414
+ instant_deploy: args.instant_deploy,
399
415
  }));
400
416
  case 'create_key':
401
417
  if (!args.project_uuid ||
@@ -416,6 +432,7 @@ export class CoolifyMcpServer extends McpServer {
416
432
  project_uuid: args.project_uuid,
417
433
  server_uuid: args.server_uuid,
418
434
  private_key_uuid: args.private_key_uuid,
435
+ destination_uuid: args.destination_uuid,
419
436
  git_repository: args.git_repository,
420
437
  git_branch: args.git_branch,
421
438
  build_pack: args.build_pack,
@@ -425,6 +442,10 @@ export class CoolifyMcpServer extends McpServer {
425
442
  name: args.name,
426
443
  description: args.description,
427
444
  fqdn: args.fqdn,
445
+ domains: args.domains,
446
+ custom_docker_run_options: args.custom_docker_run_options,
447
+ custom_labels: args.custom_labels,
448
+ instant_deploy: args.instant_deploy,
428
449
  }));
429
450
  case 'create_dockerimage':
430
451
  if (!args.project_uuid ||
@@ -443,6 +464,7 @@ export class CoolifyMcpServer extends McpServer {
443
464
  return wrap(() => this.client.createApplicationDockerImage({
444
465
  project_uuid: args.project_uuid,
445
466
  server_uuid: args.server_uuid,
467
+ destination_uuid: args.destination_uuid,
446
468
  docker_registry_image_name: args.docker_registry_image_name,
447
469
  ports_exposes: args.ports_exposes,
448
470
  docker_registry_image_tag: args.docker_registry_image_tag,
@@ -451,6 +473,10 @@ export class CoolifyMcpServer extends McpServer {
451
473
  name: args.name,
452
474
  description: args.description,
453
475
  fqdn: args.fqdn,
476
+ domains: args.domains,
477
+ custom_docker_run_options: args.custom_docker_run_options,
478
+ custom_labels: args.custom_labels,
479
+ instant_deploy: args.instant_deploy,
454
480
  }));
455
481
  case 'update': {
456
482
  if (!uuid)
@@ -709,13 +735,14 @@ export class CoolifyMcpServer extends McpServer {
709
735
  // =========================================================================
710
736
  this.tool('list_deployments', 'List deployments (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrapWithActions(() => this.client.listDeployments({ page, per_page, summary: true }), undefined, (result) => getPagination('list_deployments', page, per_page, result.length)));
711
737
  this.tool('deploy', 'Deploy by tag/UUID', { tag_or_uuid: z.string(), force: z.boolean().optional() }, async ({ tag_or_uuid, force }) => wrapWithActions(() => this.client.deployByTagOrUuid(tag_or_uuid, force), () => [{ tool: 'list_deployments', args: {}, hint: 'Check deployment status' }]));
712
- this.tool('deployment', 'Manage deployment: get/cancel/list_for_app (logs excluded by default, use lines param to include)', {
738
+ this.tool('deployment', 'Manage deployment: get/cancel/list_for_app. Logs excluded by default on all actions — for get use `lines` (paginated tail), for list_for_app use `include_logs: true` to include raw build-log blobs.', {
713
739
  action: z.enum(['get', 'cancel', 'list_for_app']),
714
740
  uuid: z.string(),
715
741
  lines: z.number().optional(), // Include logs truncated to last N entries (omit for no logs)
716
742
  page: z.number().optional(), // Log page (1=most recent, 2=older, etc.)
717
743
  max_chars: z.number().optional(), // Limit log output to last N chars (default: 50000)
718
- }, async ({ action, uuid, lines, page, max_chars }) => {
744
+ include_logs: z.boolean().optional(), // list_for_app only: include raw build logs (default false; upstream returns ~30KB per deployment)
745
+ }, async ({ action, uuid, lines, page, max_chars, include_logs }) => {
719
746
  switch (action) {
720
747
  case 'get':
721
748
  // If lines param specified, include logs and truncate
@@ -760,7 +787,7 @@ export class CoolifyMcpServer extends McpServer {
760
787
  case 'cancel':
761
788
  return wrap(() => this.client.cancelDeployment(uuid));
762
789
  case 'list_for_app':
763
- return wrap(() => this.client.listApplicationDeployments(uuid));
790
+ return wrap(() => this.client.listApplicationDeployments(uuid, { includeLogs: include_logs }));
764
791
  }
765
792
  });
766
793
  // =========================================================================
@@ -0,0 +1 @@
1
+ export declare function parseHeaders(argv: string[]): Record<string, string>;
@@ -0,0 +1,14 @@
1
+ export function parseHeaders(argv) {
2
+ const headers = {};
3
+ for (let i = 0; i < argv.length; i++) {
4
+ if (argv[i] === '--header' && i + 1 < argv.length) {
5
+ const value = argv[i + 1];
6
+ const colonIndex = value.indexOf(':');
7
+ if (colonIndex > 0) {
8
+ headers[value.slice(0, colonIndex).trim()] = value.slice(colonIndex + 1).trim();
9
+ }
10
+ i++;
11
+ }
12
+ }
13
+ return headers;
14
+ }
@@ -5,6 +5,7 @@
5
5
  export interface CoolifyConfig {
6
6
  baseUrl: string;
7
7
  accessToken: string;
8
+ customHeaders?: Record<string, string>;
8
9
  }
9
10
  export interface ErrorResponse {
10
11
  error?: string;
@@ -211,6 +212,7 @@ export interface CreateApplicationPublicRequest {
211
212
  name?: string;
212
213
  description?: string;
213
214
  fqdn?: string;
215
+ domains?: string;
214
216
  git_repository: string;
215
217
  git_branch: string;
216
218
  git_commit_sha?: string;
@@ -222,6 +224,8 @@ export interface CreateApplicationPublicRequest {
222
224
  install_command?: string;
223
225
  build_command?: string;
224
226
  start_command?: string;
227
+ custom_docker_run_options?: string;
228
+ custom_labels?: string;
225
229
  instant_deploy?: boolean;
226
230
  }
227
231
  export interface CreateApplicationPrivateGHRequest extends Omit<CreateApplicationPublicRequest, 'build_pack' | 'ports_exposes'> {
@@ -243,11 +247,14 @@ export interface CreateApplicationDockerfileRequest {
243
247
  name?: string;
244
248
  description?: string;
245
249
  fqdn?: string;
250
+ domains?: string;
246
251
  dockerfile: string;
247
252
  dockerfile_location?: string;
248
253
  ports_exposes?: string;
249
254
  ports_mappings?: string;
250
255
  base_directory?: string;
256
+ custom_docker_run_options?: string;
257
+ custom_labels?: string;
251
258
  instant_deploy?: boolean;
252
259
  }
253
260
  export interface CreateApplicationDockerImageRequest {
@@ -259,10 +266,13 @@ export interface CreateApplicationDockerImageRequest {
259
266
  name?: string;
260
267
  description?: string;
261
268
  fqdn?: string;
269
+ domains?: string;
262
270
  docker_registry_image_name: string;
263
271
  docker_registry_image_tag?: string;
264
272
  ports_exposes: string;
265
273
  ports_mappings?: string;
274
+ custom_docker_run_options?: string;
275
+ custom_labels?: string;
266
276
  instant_deploy?: boolean;
267
277
  }
268
278
  export interface CreateApplicationDockerComposeRequest {
@@ -273,16 +283,23 @@ export interface CreateApplicationDockerComposeRequest {
273
283
  destination_uuid?: string;
274
284
  name?: string;
275
285
  description?: string;
286
+ fqdn?: string;
287
+ domains?: string;
276
288
  docker_compose_raw: string;
277
289
  docker_compose_location?: string;
278
290
  docker_compose_custom_start_command?: string;
279
291
  docker_compose_custom_build_command?: string;
292
+ custom_docker_run_options?: string;
293
+ custom_labels?: string;
280
294
  instant_deploy?: boolean;
281
295
  }
282
296
  export interface UpdateApplicationRequest {
283
297
  name?: string;
284
298
  description?: string;
285
299
  fqdn?: string;
300
+ domains?: string;
301
+ custom_docker_run_options?: string;
302
+ custom_labels?: string;
286
303
  git_repository?: string;
287
304
  git_branch?: string;
288
305
  git_commit_sha?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@masonator/coolify-mcp",
3
3
  "scope": "@masonator",
4
- "version": "2.7.3",
4
+ "version": "2.8.1",
5
5
  "mcpName": "io.github.StuMason/coolify",
6
6
  "description": "MCP server for Coolify — 38 optimized tools for infrastructure management, diagnostics, and documentation search",
7
7
  "type": "module",
@@ -64,8 +64,8 @@
64
64
  "zod": "^4.3.5"
65
65
  },
66
66
  "devDependencies": {
67
- "@eslint/js": "^9.39.3",
68
- "@types/jest": "^29.5.14",
67
+ "@eslint/js": "^10.0.1",
68
+ "@types/jest": "^30.0.0",
69
69
  "@types/node": "^25.0.3",
70
70
  "@typescript-eslint/eslint-plugin": "^8.51.0",
71
71
  "@typescript-eslint/parser": "^8.51.0",
@@ -74,10 +74,10 @@
74
74
  "eslint-config-prettier": "^10.1.8",
75
75
  "globals": "^17.0.0",
76
76
  "husky": "^9.0.11",
77
- "jest": "^29.7.0",
77
+ "jest": "^30.3.0",
78
78
  "jest-junit": "^16.0.0",
79
79
  "lint-staged": "^16.2.7",
80
- "markdownlint-cli2": "^0.21.0",
80
+ "markdownlint-cli2": "^0.22.0",
81
81
  "prettier": "^3.5.3",
82
82
  "shx": "^0.4.0",
83
83
  "ts-jest": "^29.2.6",
@@ -85,7 +85,10 @@
85
85
  "typescript-eslint": "^8.51.0"
86
86
  },
87
87
  "engines": {
88
- "node": ">=18"
88
+ "node": ">=20"
89
+ },
90
+ "overrides": {
91
+ "handlebars": "^4.7.9"
89
92
  },
90
93
  "lint-staged": {
91
94
  "*.{ts,js,json,md,yaml,yml}": "prettier --write",