@masonator/coolify-mcp 2.4.0 → 2.6.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 +23 -1
- package/dist/__tests__/coolify-client.test.js +145 -2
- package/dist/__tests__/mcp-server.test.js +169 -1
- package/dist/lib/coolify-client.d.ts +18 -2
- package/dist/lib/coolify-client.js +58 -4
- package/dist/lib/mcp-server.d.ts +12 -1
- package/dist/lib/mcp-server.js +227 -38
- package/dist/types/coolify.d.ts +37 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ The server uses **85% fewer tokens** than a naive implementation (6,600 vs 43,00
|
|
|
43
43
|
### Prerequisites
|
|
44
44
|
|
|
45
45
|
- Node.js >= 18
|
|
46
|
-
- A running Coolify instance
|
|
46
|
+
- A running Coolify instance (tested with v4.0.0-beta.460)
|
|
47
47
|
- Coolify API access token (generate in Coolify Settings > API)
|
|
48
48
|
|
|
49
49
|
### Claude Desktop
|
|
@@ -106,6 +106,28 @@ The Coolify API returns extremely verbose responses - a single application can c
|
|
|
106
106
|
| list_services | ~367KB | ~1.2KB | **99%** |
|
|
107
107
|
| list_servers | ~4KB | ~0.4KB | **90%** |
|
|
108
108
|
| list_application_envs | ~3KB/var | ~0.1KB/var | **97%** |
|
|
109
|
+
| deployment get | ~13KB | ~1KB | **92%** |
|
|
110
|
+
|
|
111
|
+
### HATEOAS-style Response Actions
|
|
112
|
+
|
|
113
|
+
Responses include contextual `_actions` suggesting relevant next steps:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"data": { "uuid": "abc123", "status": "running" },
|
|
118
|
+
"_actions": [
|
|
119
|
+
{ "tool": "application_logs", "args": { "uuid": "abc123" }, "hint": "View logs" },
|
|
120
|
+
{
|
|
121
|
+
"tool": "control",
|
|
122
|
+
"args": { "resource": "application", "action": "restart", "uuid": "abc123" },
|
|
123
|
+
"hint": "Restart"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"_pagination": { "next": { "tool": "list_applications", "args": { "page": 2 } } }
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This helps AI assistants understand logical next steps without consuming extra tokens.
|
|
109
131
|
|
|
110
132
|
### Recommended Workflow
|
|
111
133
|
|
|
@@ -84,6 +84,10 @@ describe('CoolifyClient', () => {
|
|
|
84
84
|
deployment_uuid: 'dep-123',
|
|
85
85
|
application_name: 'test-app',
|
|
86
86
|
status: 'finished',
|
|
87
|
+
force_rebuild: false,
|
|
88
|
+
is_webhook: false,
|
|
89
|
+
is_api: true,
|
|
90
|
+
restart_only: false,
|
|
87
91
|
created_at: '2024-01-01',
|
|
88
92
|
updated_at: '2024-01-01',
|
|
89
93
|
};
|
|
@@ -342,6 +346,18 @@ describe('CoolifyClient', () => {
|
|
|
342
346
|
expect(result).toEqual({ message: 'Deployed' });
|
|
343
347
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?tag=my-tag&force=true', expect.any(Object));
|
|
344
348
|
});
|
|
349
|
+
it('should deploy by Coolify UUID (24 char alphanumeric)', async () => {
|
|
350
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deployed' }));
|
|
351
|
+
// Coolify-style UUID: 24 lowercase alphanumeric chars
|
|
352
|
+
await client.deployByTagOrUuid('xs0sgs4gog044s4k4c88kgsc', false);
|
|
353
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?uuid=xs0sgs4gog044s4k4c88kgsc&force=false', expect.any(Object));
|
|
354
|
+
});
|
|
355
|
+
it('should deploy by standard UUID format', async () => {
|
|
356
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deployed' }));
|
|
357
|
+
// Standard UUID format with hyphens
|
|
358
|
+
await client.deployByTagOrUuid('a1b2c3d4-e5f6-7890-abcd-ef1234567890', true);
|
|
359
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?uuid=a1b2c3d4-e5f6-7890-abcd-ef1234567890&force=true', expect.any(Object));
|
|
360
|
+
});
|
|
345
361
|
});
|
|
346
362
|
describe('private keys', () => {
|
|
347
363
|
it('should list private keys', async () => {
|
|
@@ -574,6 +590,98 @@ describe('CoolifyClient', () => {
|
|
|
574
590
|
expect(result).toEqual(mockEnvironment);
|
|
575
591
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/projects/proj-uuid/production', expect.any(Object));
|
|
576
592
|
});
|
|
593
|
+
it('should get project environment with missing database types', async () => {
|
|
594
|
+
// Use environment_id to match (what real API uses)
|
|
595
|
+
const mockDbSummaries = [
|
|
596
|
+
{
|
|
597
|
+
uuid: 'pg-uuid',
|
|
598
|
+
name: 'pg-db',
|
|
599
|
+
type: 'postgresql',
|
|
600
|
+
status: 'running',
|
|
601
|
+
is_public: false,
|
|
602
|
+
environment_id: 1,
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
uuid: 'dragonfly-uuid',
|
|
606
|
+
name: 'dragonfly-cache',
|
|
607
|
+
type: 'standalone-dragonfly',
|
|
608
|
+
status: 'running',
|
|
609
|
+
is_public: false,
|
|
610
|
+
environment_id: 1,
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
uuid: 'other-env-db',
|
|
614
|
+
name: 'other-db',
|
|
615
|
+
type: 'standalone-keydb',
|
|
616
|
+
status: 'running',
|
|
617
|
+
is_public: false,
|
|
618
|
+
environment_id: 999, // different env
|
|
619
|
+
},
|
|
620
|
+
];
|
|
621
|
+
mockFetch
|
|
622
|
+
.mockResolvedValueOnce(mockResponse(mockEnvironment))
|
|
623
|
+
.mockResolvedValueOnce(mockResponse(mockDbSummaries));
|
|
624
|
+
const result = await client.getProjectEnvironmentWithDatabases('proj-uuid', 'production');
|
|
625
|
+
expect(result.uuid).toBe('env-uuid');
|
|
626
|
+
expect(result.dragonflys).toHaveLength(1);
|
|
627
|
+
expect(result.dragonflys[0].uuid).toBe('dragonfly-uuid');
|
|
628
|
+
expect(result.keydbs).toBeUndefined(); // other-env-db is in different env
|
|
629
|
+
});
|
|
630
|
+
it('should match databases by environment_uuid fallback', async () => {
|
|
631
|
+
const mockDbSummaries = [
|
|
632
|
+
{
|
|
633
|
+
uuid: 'keydb-uuid',
|
|
634
|
+
name: 'keydb-cache',
|
|
635
|
+
type: 'standalone-keydb',
|
|
636
|
+
status: 'running',
|
|
637
|
+
is_public: false,
|
|
638
|
+
environment_uuid: 'env-uuid', // matching by uuid
|
|
639
|
+
},
|
|
640
|
+
];
|
|
641
|
+
mockFetch
|
|
642
|
+
.mockResolvedValueOnce(mockResponse(mockEnvironment))
|
|
643
|
+
.mockResolvedValueOnce(mockResponse(mockDbSummaries));
|
|
644
|
+
const result = await client.getProjectEnvironmentWithDatabases('proj-uuid', 'production');
|
|
645
|
+
expect(result.keydbs).toHaveLength(1);
|
|
646
|
+
expect(result.keydbs[0].uuid).toBe('keydb-uuid');
|
|
647
|
+
});
|
|
648
|
+
it('should match databases by environment_name fallback', async () => {
|
|
649
|
+
const mockDbSummaries = [
|
|
650
|
+
{
|
|
651
|
+
uuid: 'clickhouse-uuid',
|
|
652
|
+
name: 'clickhouse-analytics',
|
|
653
|
+
type: 'standalone-clickhouse',
|
|
654
|
+
status: 'running',
|
|
655
|
+
is_public: false,
|
|
656
|
+
environment_name: 'production', // matching by name
|
|
657
|
+
},
|
|
658
|
+
];
|
|
659
|
+
mockFetch
|
|
660
|
+
.mockResolvedValueOnce(mockResponse(mockEnvironment))
|
|
661
|
+
.mockResolvedValueOnce(mockResponse(mockDbSummaries));
|
|
662
|
+
const result = await client.getProjectEnvironmentWithDatabases('proj-uuid', 'production');
|
|
663
|
+
expect(result.clickhouses).toHaveLength(1);
|
|
664
|
+
expect(result.clickhouses[0].uuid).toBe('clickhouse-uuid');
|
|
665
|
+
});
|
|
666
|
+
it('should not add empty arrays when no missing DB types exist', async () => {
|
|
667
|
+
const mockDbSummaries = [
|
|
668
|
+
{
|
|
669
|
+
uuid: 'pg-uuid',
|
|
670
|
+
name: 'pg-db',
|
|
671
|
+
type: 'postgresql', // not a "missing" type
|
|
672
|
+
status: 'running',
|
|
673
|
+
is_public: false,
|
|
674
|
+
environment_id: 1,
|
|
675
|
+
},
|
|
676
|
+
];
|
|
677
|
+
mockFetch
|
|
678
|
+
.mockResolvedValueOnce(mockResponse(mockEnvironment))
|
|
679
|
+
.mockResolvedValueOnce(mockResponse(mockDbSummaries));
|
|
680
|
+
const result = await client.getProjectEnvironmentWithDatabases('proj-uuid', 'production');
|
|
681
|
+
expect(result.dragonflys).toBeUndefined();
|
|
682
|
+
expect(result.keydbs).toBeUndefined();
|
|
683
|
+
expect(result.clickhouses).toBeUndefined();
|
|
684
|
+
});
|
|
577
685
|
it('should create a project environment', async () => {
|
|
578
686
|
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-env-uuid' }));
|
|
579
687
|
const result = await client.createProjectEnvironment('proj-uuid', { name: 'staging' });
|
|
@@ -1165,12 +1273,47 @@ describe('CoolifyClient', () => {
|
|
|
1165
1273
|
},
|
|
1166
1274
|
]);
|
|
1167
1275
|
});
|
|
1168
|
-
it('should get a deployment', async () => {
|
|
1276
|
+
it('should get a deployment (essential by default, no logs)', async () => {
|
|
1169
1277
|
mockFetch.mockResolvedValueOnce(mockResponse(mockDeployment));
|
|
1170
1278
|
const result = await client.getDeployment('dep-uuid');
|
|
1171
|
-
|
|
1279
|
+
// By default, returns DeploymentEssential without logs
|
|
1280
|
+
expect(result).toEqual({
|
|
1281
|
+
uuid: 'dep-uuid',
|
|
1282
|
+
deployment_uuid: 'dep-123',
|
|
1283
|
+
application_uuid: undefined,
|
|
1284
|
+
application_name: 'test-app',
|
|
1285
|
+
server_name: undefined,
|
|
1286
|
+
status: 'finished',
|
|
1287
|
+
commit: undefined,
|
|
1288
|
+
force_rebuild: false,
|
|
1289
|
+
is_webhook: false,
|
|
1290
|
+
is_api: true,
|
|
1291
|
+
created_at: '2024-01-01',
|
|
1292
|
+
updated_at: '2024-01-01',
|
|
1293
|
+
logs_available: false,
|
|
1294
|
+
logs_info: undefined,
|
|
1295
|
+
});
|
|
1172
1296
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/dep-uuid', expect.any(Object));
|
|
1173
1297
|
});
|
|
1298
|
+
it('should get a deployment with logs when includeLogs is true', async () => {
|
|
1299
|
+
const deploymentWithLogs = { ...mockDeployment, logs: 'Build started...' };
|
|
1300
|
+
mockFetch.mockResolvedValueOnce(mockResponse(deploymentWithLogs));
|
|
1301
|
+
const result = await client.getDeployment('dep-uuid', { includeLogs: true });
|
|
1302
|
+
// With includeLogs: true, returns full Deployment with logs
|
|
1303
|
+
expect(result).toEqual(deploymentWithLogs);
|
|
1304
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/dep-uuid', expect.any(Object));
|
|
1305
|
+
});
|
|
1306
|
+
it('should include logs_info when deployment has logs but includeLogs is false', async () => {
|
|
1307
|
+
const deploymentWithLogs = { ...mockDeployment, logs: 'Build started...' };
|
|
1308
|
+
mockFetch.mockResolvedValueOnce(mockResponse(deploymentWithLogs));
|
|
1309
|
+
const result = await client.getDeployment('dep-uuid');
|
|
1310
|
+
// Should have logs_info indicating logs are available
|
|
1311
|
+
expect(result).toMatchObject({
|
|
1312
|
+
uuid: 'dep-uuid',
|
|
1313
|
+
logs_available: true,
|
|
1314
|
+
logs_info: 'Logs available (16 chars). Use lines param to retrieve.',
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1174
1317
|
it('should list application deployments', async () => {
|
|
1175
1318
|
mockFetch.mockResolvedValueOnce(mockResponse([mockDeployment]));
|
|
1176
1319
|
const result = await client.listApplicationDeployments('app-uuid');
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* These tests verify MCP server instantiation and structure.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
9
|
-
import { CoolifyMcpServer } from '../lib/mcp-server.js';
|
|
9
|
+
import { CoolifyMcpServer, truncateLogs, getApplicationActions, getDeploymentActions, getPagination, } from '../lib/mcp-server.js';
|
|
10
10
|
describe('CoolifyMcpServer v2', () => {
|
|
11
11
|
let server;
|
|
12
12
|
beforeEach(() => {
|
|
@@ -47,6 +47,7 @@ describe('CoolifyMcpServer v2', () => {
|
|
|
47
47
|
// Environment operations
|
|
48
48
|
expect(typeof client.listProjectEnvironments).toBe('function');
|
|
49
49
|
expect(typeof client.getProjectEnvironment).toBe('function');
|
|
50
|
+
expect(typeof client.getProjectEnvironmentWithDatabases).toBe('function');
|
|
50
51
|
expect(typeof client.createProjectEnvironment).toBe('function');
|
|
51
52
|
expect(typeof client.deleteProjectEnvironment).toBe('function');
|
|
52
53
|
// Application operations
|
|
@@ -140,3 +141,170 @@ describe('CoolifyMcpServer v2', () => {
|
|
|
140
141
|
});
|
|
141
142
|
});
|
|
142
143
|
});
|
|
144
|
+
describe('truncateLogs', () => {
|
|
145
|
+
it('should return logs unchanged when within limits', () => {
|
|
146
|
+
const logs = 'line1\nline2\nline3';
|
|
147
|
+
const result = truncateLogs(logs, 200, 50000);
|
|
148
|
+
expect(result).toBe(logs);
|
|
149
|
+
});
|
|
150
|
+
it('should truncate to last N lines', () => {
|
|
151
|
+
const logs = 'line1\nline2\nline3\nline4\nline5';
|
|
152
|
+
const result = truncateLogs(logs, 3, 50000);
|
|
153
|
+
expect(result).toBe('line3\nline4\nline5');
|
|
154
|
+
});
|
|
155
|
+
it('should truncate by character limit when lines are huge', () => {
|
|
156
|
+
const hugeLine = 'x'.repeat(100);
|
|
157
|
+
const logs = `${hugeLine}\n${hugeLine}\n${hugeLine}`;
|
|
158
|
+
const result = truncateLogs(logs, 200, 50);
|
|
159
|
+
expect(result.length).toBeLessThanOrEqual(50);
|
|
160
|
+
expect(result.startsWith('...[truncated]...')).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
it('should not add truncation prefix when under char limit', () => {
|
|
163
|
+
const logs = 'line1\nline2\nline3';
|
|
164
|
+
const result = truncateLogs(logs, 200, 50000);
|
|
165
|
+
expect(result.startsWith('...[truncated]...')).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
it('should handle empty logs', () => {
|
|
168
|
+
const result = truncateLogs('', 200, 50000);
|
|
169
|
+
expect(result).toBe('');
|
|
170
|
+
});
|
|
171
|
+
it('should use default limits when not specified', () => {
|
|
172
|
+
const logs = 'line1\nline2';
|
|
173
|
+
const result = truncateLogs(logs);
|
|
174
|
+
expect(result).toBe(logs);
|
|
175
|
+
});
|
|
176
|
+
it('should respect custom line limit', () => {
|
|
177
|
+
const lines = Array.from({ length: 300 }, (_, i) => `line${i + 1}`).join('\n');
|
|
178
|
+
const result = truncateLogs(lines, 50, 50000);
|
|
179
|
+
const resultLines = result.split('\n');
|
|
180
|
+
expect(resultLines.length).toBe(50);
|
|
181
|
+
expect(resultLines[0]).toBe('line251');
|
|
182
|
+
expect(resultLines[49]).toBe('line300');
|
|
183
|
+
});
|
|
184
|
+
it('should respect custom char limit', () => {
|
|
185
|
+
const logs = 'x'.repeat(1000);
|
|
186
|
+
const result = truncateLogs(logs, 200, 100);
|
|
187
|
+
expect(result.length).toBe(100);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// Action Generators Tests
|
|
192
|
+
// =============================================================================
|
|
193
|
+
describe('getApplicationActions', () => {
|
|
194
|
+
it('should return view logs action for all apps', () => {
|
|
195
|
+
const actions = getApplicationActions('app-uuid', 'stopped');
|
|
196
|
+
expect(actions).toContainEqual({
|
|
197
|
+
tool: 'application_logs',
|
|
198
|
+
args: { uuid: 'app-uuid' },
|
|
199
|
+
hint: 'View logs',
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('should return restart/stop actions for running apps', () => {
|
|
203
|
+
const actions = getApplicationActions('app-uuid', 'running');
|
|
204
|
+
expect(actions).toContainEqual({
|
|
205
|
+
tool: 'control',
|
|
206
|
+
args: { resource: 'application', action: 'restart', uuid: 'app-uuid' },
|
|
207
|
+
hint: 'Restart',
|
|
208
|
+
});
|
|
209
|
+
expect(actions).toContainEqual({
|
|
210
|
+
tool: 'control',
|
|
211
|
+
args: { resource: 'application', action: 'stop', uuid: 'app-uuid' },
|
|
212
|
+
hint: 'Stop',
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
it('should return start action for stopped apps', () => {
|
|
216
|
+
const actions = getApplicationActions('app-uuid', 'stopped');
|
|
217
|
+
expect(actions).toContainEqual({
|
|
218
|
+
tool: 'control',
|
|
219
|
+
args: { resource: 'application', action: 'start', uuid: 'app-uuid' },
|
|
220
|
+
hint: 'Start',
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
it('should handle running:healthy status', () => {
|
|
224
|
+
const actions = getApplicationActions('app-uuid', 'running:healthy');
|
|
225
|
+
expect(actions.some((a) => a.hint === 'Restart')).toBe(true);
|
|
226
|
+
expect(actions.some((a) => a.hint === 'Stop')).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
it('should handle undefined status', () => {
|
|
229
|
+
const actions = getApplicationActions('app-uuid', undefined);
|
|
230
|
+
expect(actions).toContainEqual({
|
|
231
|
+
tool: 'control',
|
|
232
|
+
args: { resource: 'application', action: 'start', uuid: 'app-uuid' },
|
|
233
|
+
hint: 'Start',
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
describe('getDeploymentActions', () => {
|
|
238
|
+
it('should return cancel action for in_progress deployments', () => {
|
|
239
|
+
const actions = getDeploymentActions('dep-uuid', 'in_progress', 'app-uuid');
|
|
240
|
+
expect(actions).toContainEqual({
|
|
241
|
+
tool: 'deployment',
|
|
242
|
+
args: { action: 'cancel', uuid: 'dep-uuid' },
|
|
243
|
+
hint: 'Cancel',
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
it('should return cancel action for queued deployments', () => {
|
|
247
|
+
const actions = getDeploymentActions('dep-uuid', 'queued', 'app-uuid');
|
|
248
|
+
expect(actions).toContainEqual({
|
|
249
|
+
tool: 'deployment',
|
|
250
|
+
args: { action: 'cancel', uuid: 'dep-uuid' },
|
|
251
|
+
hint: 'Cancel',
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
it('should return app actions when appUuid provided', () => {
|
|
255
|
+
const actions = getDeploymentActions('dep-uuid', 'finished', 'app-uuid');
|
|
256
|
+
expect(actions).toContainEqual({
|
|
257
|
+
tool: 'get_application',
|
|
258
|
+
args: { uuid: 'app-uuid' },
|
|
259
|
+
hint: 'View app',
|
|
260
|
+
});
|
|
261
|
+
expect(actions).toContainEqual({
|
|
262
|
+
tool: 'application_logs',
|
|
263
|
+
args: { uuid: 'app-uuid' },
|
|
264
|
+
hint: 'App logs',
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
it('should not return cancel for finished deployments', () => {
|
|
268
|
+
const actions = getDeploymentActions('dep-uuid', 'finished', 'app-uuid');
|
|
269
|
+
expect(actions.some((a) => a.hint === 'Cancel')).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
it('should return empty actions when no appUuid and not in_progress', () => {
|
|
272
|
+
const actions = getDeploymentActions('dep-uuid', 'finished', undefined);
|
|
273
|
+
expect(actions).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
describe('getPagination', () => {
|
|
277
|
+
it('should return undefined when count is less than perPage and page is 1', () => {
|
|
278
|
+
const result = getPagination('list_apps', 1, 50, 30);
|
|
279
|
+
expect(result).toBeUndefined();
|
|
280
|
+
});
|
|
281
|
+
it('should return next when count equals or exceeds perPage', () => {
|
|
282
|
+
const result = getPagination('list_apps', 1, 50, 50);
|
|
283
|
+
expect(result).toEqual({
|
|
284
|
+
next: { tool: 'list_apps', args: { page: 2, per_page: 50 } },
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
it('should return both prev and next for middle pages', () => {
|
|
288
|
+
const result = getPagination('list_apps', 2, 50, 50);
|
|
289
|
+
expect(result).toEqual({
|
|
290
|
+
prev: { tool: 'list_apps', args: { page: 1, per_page: 50 } },
|
|
291
|
+
next: { tool: 'list_apps', args: { page: 3, per_page: 50 } },
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
it('should return prev when page > 1 and count < perPage', () => {
|
|
295
|
+
const result = getPagination('list_apps', 3, 50, 20);
|
|
296
|
+
expect(result).toEqual({
|
|
297
|
+
prev: { tool: 'list_apps', args: { page: 2, per_page: 50 } },
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
it('should use default page and perPage when undefined', () => {
|
|
301
|
+
const result = getPagination('list_apps', undefined, undefined, 100);
|
|
302
|
+
expect(result).toEqual({
|
|
303
|
+
next: { tool: 'list_apps', args: { page: 2, per_page: 50 } },
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
it('should return undefined when count is undefined', () => {
|
|
307
|
+
const result = getPagination('list_apps', 1, 50, undefined);
|
|
308
|
+
expect(result).toBeUndefined();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -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, GitHubApp, CreateGitHubAppRequest, UpdateGitHubAppRequest, GitHubAppUpdateResponse, 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, DeploymentEssential, 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;
|
|
@@ -35,6 +35,9 @@ export interface DatabaseSummary {
|
|
|
35
35
|
type: string;
|
|
36
36
|
status: string;
|
|
37
37
|
is_public: boolean;
|
|
38
|
+
environment_uuid?: string;
|
|
39
|
+
environment_name?: string;
|
|
40
|
+
environment_id?: number;
|
|
38
41
|
}
|
|
39
42
|
export interface ServiceSummary {
|
|
40
43
|
uuid: string;
|
|
@@ -89,6 +92,17 @@ export declare class CoolifyClient {
|
|
|
89
92
|
deleteProject(uuid: string): Promise<MessageResponse>;
|
|
90
93
|
listProjectEnvironments(projectUuid: string): Promise<Environment[]>;
|
|
91
94
|
getProjectEnvironment(projectUuid: string, environmentNameOrUuid: string): Promise<Environment>;
|
|
95
|
+
/**
|
|
96
|
+
* Get environment with missing database types (dragonfly, keydb, clickhouse).
|
|
97
|
+
* Coolify API omits these from the environment endpoint - we cross-reference
|
|
98
|
+
* with listDatabases using lightweight summaries.
|
|
99
|
+
* @see https://github.com/StuMason/coolify-mcp/issues/88
|
|
100
|
+
*/
|
|
101
|
+
getProjectEnvironmentWithDatabases(projectUuid: string, environmentNameOrUuid: string): Promise<Environment & {
|
|
102
|
+
dragonflys?: DatabaseSummary[];
|
|
103
|
+
keydbs?: DatabaseSummary[];
|
|
104
|
+
clickhouses?: DatabaseSummary[];
|
|
105
|
+
}>;
|
|
92
106
|
createProjectEnvironment(projectUuid: string, data: CreateEnvironmentRequest): Promise<UuidResponse>;
|
|
93
107
|
deleteProjectEnvironment(projectUuid: string, environmentNameOrUuid: string): Promise<MessageResponse>;
|
|
94
108
|
listApplications(options?: ListOptions): Promise<Application[] | ApplicationSummary[]>;
|
|
@@ -143,7 +157,9 @@ export declare class CoolifyClient {
|
|
|
143
157
|
updateServiceEnvVar(uuid: string, data: UpdateEnvVarRequest): Promise<MessageResponse>;
|
|
144
158
|
deleteServiceEnvVar(uuid: string, envUuid: string): Promise<MessageResponse>;
|
|
145
159
|
listDeployments(options?: ListOptions): Promise<Deployment[] | DeploymentSummary[]>;
|
|
146
|
-
getDeployment(uuid: string
|
|
160
|
+
getDeployment(uuid: string, options?: {
|
|
161
|
+
includeLogs?: boolean;
|
|
162
|
+
}): Promise<Deployment | DeploymentEssential>;
|
|
147
163
|
deployByTagOrUuid(tagOrUuid: string, force?: boolean): Promise<MessageResponse>;
|
|
148
164
|
listApplicationDeployments(appUuid: string): Promise<Deployment[]>;
|
|
149
165
|
listTeams(): Promise<Team[]>;
|
|
@@ -38,12 +38,17 @@ function toApplicationSummary(app) {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
function toDatabaseSummary(db) {
|
|
41
|
+
// API returns database_type not type, and environment_id not environment_uuid
|
|
42
|
+
const raw = db;
|
|
41
43
|
return {
|
|
42
44
|
uuid: db.uuid,
|
|
43
45
|
name: db.name,
|
|
44
|
-
type: db.type,
|
|
46
|
+
type: db.type || raw.database_type,
|
|
45
47
|
status: db.status,
|
|
46
48
|
is_public: db.is_public,
|
|
49
|
+
environment_uuid: db.environment_uuid,
|
|
50
|
+
environment_name: db.environment_name,
|
|
51
|
+
environment_id: raw.environment_id,
|
|
47
52
|
};
|
|
48
53
|
}
|
|
49
54
|
function toServiceSummary(svc) {
|
|
@@ -64,6 +69,26 @@ function toDeploymentSummary(dep) {
|
|
|
64
69
|
created_at: dep.created_at,
|
|
65
70
|
};
|
|
66
71
|
}
|
|
72
|
+
function toDeploymentEssential(dep) {
|
|
73
|
+
return {
|
|
74
|
+
uuid: dep.uuid,
|
|
75
|
+
deployment_uuid: dep.deployment_uuid,
|
|
76
|
+
application_uuid: dep.application_uuid,
|
|
77
|
+
application_name: dep.application_name,
|
|
78
|
+
server_name: dep.server_name,
|
|
79
|
+
status: dep.status,
|
|
80
|
+
commit: dep.commit,
|
|
81
|
+
force_rebuild: dep.force_rebuild,
|
|
82
|
+
is_webhook: dep.is_webhook,
|
|
83
|
+
is_api: dep.is_api,
|
|
84
|
+
created_at: dep.created_at,
|
|
85
|
+
updated_at: dep.updated_at,
|
|
86
|
+
logs_available: !!dep.logs,
|
|
87
|
+
logs_info: dep.logs
|
|
88
|
+
? `Logs available (${dep.logs.length} chars). Use lines param to retrieve.`
|
|
89
|
+
: undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
67
92
|
function toProjectSummary(proj) {
|
|
68
93
|
return {
|
|
69
94
|
uuid: proj.uuid,
|
|
@@ -256,6 +281,32 @@ export class CoolifyClient {
|
|
|
256
281
|
async getProjectEnvironment(projectUuid, environmentNameOrUuid) {
|
|
257
282
|
return this.request(`/projects/${projectUuid}/${environmentNameOrUuid}`);
|
|
258
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Get environment with missing database types (dragonfly, keydb, clickhouse).
|
|
286
|
+
* Coolify API omits these from the environment endpoint - we cross-reference
|
|
287
|
+
* with listDatabases using lightweight summaries.
|
|
288
|
+
* @see https://github.com/StuMason/coolify-mcp/issues/88
|
|
289
|
+
*/
|
|
290
|
+
async getProjectEnvironmentWithDatabases(projectUuid, environmentNameOrUuid) {
|
|
291
|
+
const [environment, dbSummaries] = await Promise.all([
|
|
292
|
+
this.getProjectEnvironment(projectUuid, environmentNameOrUuid),
|
|
293
|
+
this.listDatabases({ summary: true }),
|
|
294
|
+
]);
|
|
295
|
+
// Filter for this environment's missing database types
|
|
296
|
+
// API uses environment_id, not environment_uuid
|
|
297
|
+
const envDbs = dbSummaries.filter((db) => db.environment_id === environment.id ||
|
|
298
|
+
db.environment_uuid === environment.uuid ||
|
|
299
|
+
db.environment_name === environment.name);
|
|
300
|
+
const dragonflys = envDbs.filter((db) => db.type?.includes('dragonfly'));
|
|
301
|
+
const keydbs = envDbs.filter((db) => db.type?.includes('keydb'));
|
|
302
|
+
const clickhouses = envDbs.filter((db) => db.type?.includes('clickhouse'));
|
|
303
|
+
return {
|
|
304
|
+
...environment,
|
|
305
|
+
...(dragonflys.length > 0 && { dragonflys }),
|
|
306
|
+
...(keydbs.length > 0 && { keydbs }),
|
|
307
|
+
...(clickhouses.length > 0 && { clickhouses }),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
259
310
|
async createProjectEnvironment(projectUuid, data) {
|
|
260
311
|
return this.request(`/projects/${projectUuid}/environments`, {
|
|
261
312
|
method: 'POST',
|
|
@@ -569,11 +620,14 @@ export class CoolifyClient {
|
|
|
569
620
|
? deployments.map(toDeploymentSummary)
|
|
570
621
|
: deployments;
|
|
571
622
|
}
|
|
572
|
-
async getDeployment(uuid) {
|
|
573
|
-
|
|
623
|
+
async getDeployment(uuid, options) {
|
|
624
|
+
const deployment = await this.request(`/deployments/${uuid}`);
|
|
625
|
+
return options?.includeLogs ? deployment : toDeploymentEssential(deployment);
|
|
574
626
|
}
|
|
575
627
|
async deployByTagOrUuid(tagOrUuid, force = false) {
|
|
576
|
-
|
|
628
|
+
// Detect if the value looks like a UUID or a tag name
|
|
629
|
+
const param = this.isLikelyUuid(tagOrUuid) ? 'uuid' : 'tag';
|
|
630
|
+
return this.request(`/deploy?${param}=${encodeURIComponent(tagOrUuid)}&force=${force}`, { method: 'GET' });
|
|
577
631
|
}
|
|
578
632
|
async listApplicationDeployments(appUuid) {
|
|
579
633
|
return this.request(`/applications/${appUuid}/deployments`);
|
package/dist/lib/mcp-server.d.ts
CHANGED
|
@@ -4,7 +4,18 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
7
|
-
import type { CoolifyConfig } from '../types/coolify.js';
|
|
7
|
+
import type { CoolifyConfig, ResponseAction, ResponsePagination } from '../types/coolify.js';
|
|
8
|
+
/**
|
|
9
|
+
* Truncate logs by line count and character count.
|
|
10
|
+
* Exported for testing.
|
|
11
|
+
*/
|
|
12
|
+
export declare function truncateLogs(logs: string, lineLimit?: number, charLimit?: number): string;
|
|
13
|
+
/** Generate contextual actions for an application based on its status */
|
|
14
|
+
export declare function getApplicationActions(uuid: string, status?: string): ResponseAction[];
|
|
15
|
+
/** Generate contextual actions for a deployment */
|
|
16
|
+
export declare function getDeploymentActions(uuid: string, status: string, appUuid?: string): ResponseAction[];
|
|
17
|
+
/** Generate pagination info for list endpoints */
|
|
18
|
+
export declare function getPagination(tool: string, page?: number, perPage?: number, count?: number): ResponsePagination | undefined;
|
|
8
19
|
export declare class CoolifyMcpServer extends McpServer {
|
|
9
20
|
private readonly client;
|
|
10
21
|
constructor(config: CoolifyConfig);
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* Coolify MCP Server v2.4.0
|
|
3
3
|
* Consolidated tools for efficient token usage
|
|
4
4
|
*/
|
|
5
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
6
|
import { z } from 'zod';
|
|
8
7
|
import { CoolifyClient, } from './coolify-client.js';
|
|
9
|
-
const VERSION = '2.
|
|
8
|
+
const VERSION = '2.5.0';
|
|
10
9
|
/** Wrap handler with error handling */
|
|
11
10
|
function wrap(fn) {
|
|
12
11
|
return fn()
|
|
@@ -22,6 +21,100 @@ function wrap(fn) {
|
|
|
22
21
|
],
|
|
23
22
|
}));
|
|
24
23
|
}
|
|
24
|
+
const TRUNCATION_PREFIX = '...[truncated]...\n';
|
|
25
|
+
/**
|
|
26
|
+
* Truncate logs by line count and character count.
|
|
27
|
+
* Exported for testing.
|
|
28
|
+
*/
|
|
29
|
+
export function truncateLogs(logs, lineLimit = 200, charLimit = 50000) {
|
|
30
|
+
// First: limit by lines
|
|
31
|
+
const logLines = logs.split('\n');
|
|
32
|
+
const limitedLines = logLines.slice(-lineLimit);
|
|
33
|
+
let truncatedLogs = limitedLines.join('\n');
|
|
34
|
+
// Second: limit by characters (safety net for huge lines)
|
|
35
|
+
if (truncatedLogs.length > charLimit) {
|
|
36
|
+
// Account for prefix length to stay within limit
|
|
37
|
+
const prefixLen = TRUNCATION_PREFIX.length;
|
|
38
|
+
truncatedLogs = TRUNCATION_PREFIX + truncatedLogs.slice(-(charLimit - prefixLen));
|
|
39
|
+
}
|
|
40
|
+
return truncatedLogs;
|
|
41
|
+
}
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Action Generators for HATEOAS-style responses
|
|
44
|
+
// =============================================================================
|
|
45
|
+
/** Generate contextual actions for an application based on its status */
|
|
46
|
+
export function getApplicationActions(uuid, status) {
|
|
47
|
+
const actions = [
|
|
48
|
+
{ tool: 'application_logs', args: { uuid }, hint: 'View logs' },
|
|
49
|
+
];
|
|
50
|
+
const s = (status || '').toLowerCase();
|
|
51
|
+
if (s.includes('running')) {
|
|
52
|
+
actions.push({
|
|
53
|
+
tool: 'control',
|
|
54
|
+
args: { resource: 'application', action: 'restart', uuid },
|
|
55
|
+
hint: 'Restart',
|
|
56
|
+
});
|
|
57
|
+
actions.push({
|
|
58
|
+
tool: 'control',
|
|
59
|
+
args: { resource: 'application', action: 'stop', uuid },
|
|
60
|
+
hint: 'Stop',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
actions.push({
|
|
65
|
+
tool: 'control',
|
|
66
|
+
args: { resource: 'application', action: 'start', uuid },
|
|
67
|
+
hint: 'Start',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return actions;
|
|
71
|
+
}
|
|
72
|
+
/** Generate contextual actions for a deployment */
|
|
73
|
+
export function getDeploymentActions(uuid, status, appUuid) {
|
|
74
|
+
const actions = [];
|
|
75
|
+
if (status === 'in_progress' || status === 'queued') {
|
|
76
|
+
actions.push({ tool: 'deployment', args: { action: 'cancel', uuid }, hint: 'Cancel' });
|
|
77
|
+
}
|
|
78
|
+
if (appUuid) {
|
|
79
|
+
actions.push({ tool: 'get_application', args: { uuid: appUuid }, hint: 'View app' });
|
|
80
|
+
actions.push({ tool: 'application_logs', args: { uuid: appUuid }, hint: 'App logs' });
|
|
81
|
+
}
|
|
82
|
+
return actions;
|
|
83
|
+
}
|
|
84
|
+
/** Generate pagination info for list endpoints */
|
|
85
|
+
export function getPagination(tool, page, perPage, count) {
|
|
86
|
+
const p = page ?? 1;
|
|
87
|
+
const pp = perPage ?? 50;
|
|
88
|
+
if (!count || count < pp) {
|
|
89
|
+
return p > 1 ? { prev: { tool, args: { page: p - 1, per_page: pp } } } : undefined;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...(p > 1 && { prev: { tool, args: { page: p - 1, per_page: pp } } }),
|
|
93
|
+
next: { tool, args: { page: p + 1, per_page: pp } },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Wrap handler with error handling and HATEOAS actions */
|
|
97
|
+
function wrapWithActions(fn, getActions, getPaginationFn) {
|
|
98
|
+
return fn()
|
|
99
|
+
.then((result) => {
|
|
100
|
+
const actions = getActions?.(result) ?? [];
|
|
101
|
+
const pagination = getPaginationFn?.(result);
|
|
102
|
+
const response = { data: result };
|
|
103
|
+
if (actions.length > 0)
|
|
104
|
+
response._actions = actions;
|
|
105
|
+
if (pagination)
|
|
106
|
+
response._pagination = pagination;
|
|
107
|
+
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
108
|
+
})
|
|
109
|
+
.catch((error) => ({
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
25
118
|
export class CoolifyMcpServer extends McpServer {
|
|
26
119
|
constructor(config) {
|
|
27
120
|
super({ name: 'coolify', version: VERSION });
|
|
@@ -133,7 +226,7 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
133
226
|
// =========================================================================
|
|
134
227
|
// Environments (1 tool - consolidated CRUD)
|
|
135
228
|
// =========================================================================
|
|
136
|
-
this.tool('environments', 'Manage environments: list/get/create/delete', {
|
|
229
|
+
this.tool('environments', 'Manage environments: list/get/create/delete (get includes dragonfly/keydb/clickhouse DBs missing from API)', {
|
|
137
230
|
action: z.enum(['list', 'get', 'create', 'delete']),
|
|
138
231
|
project_uuid: z.string(),
|
|
139
232
|
name: z.string().optional(),
|
|
@@ -145,7 +238,8 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
145
238
|
case 'get':
|
|
146
239
|
if (!name)
|
|
147
240
|
return { content: [{ type: 'text', text: 'Error: name required' }] };
|
|
148
|
-
|
|
241
|
+
// Use enhanced method that includes missing DB types (#88)
|
|
242
|
+
return wrap(() => this.client.getProjectEnvironmentWithDatabases(project_uuid, name));
|
|
149
243
|
case 'create':
|
|
150
244
|
if (!name)
|
|
151
245
|
return { content: [{ type: 'text', text: 'Error: name required' }] };
|
|
@@ -159,8 +253,8 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
159
253
|
// =========================================================================
|
|
160
254
|
// Applications (4 tools)
|
|
161
255
|
// =========================================================================
|
|
162
|
-
this.tool('list_applications', 'List apps (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) =>
|
|
163
|
-
this.tool('get_application', 'App details', { uuid: z.string() }, async ({ uuid }) =>
|
|
256
|
+
this.tool('list_applications', 'List apps (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) => wrapWithActions(() => this.client.listApplications({ page, per_page, summary: true }), undefined, (result) => getPagination('list_applications', page, per_page, result.length)));
|
|
257
|
+
this.tool('get_application', 'App details', { uuid: z.string() }, async ({ uuid }) => wrapWithActions(() => this.client.getApplication(uuid), (app) => getApplicationActions(app.uuid, app.status)));
|
|
164
258
|
this.tool('application', 'Manage app: create/update/delete', {
|
|
165
259
|
action: z.enum([
|
|
166
260
|
'create_public',
|
|
@@ -205,8 +299,7 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
205
299
|
// Delete fields
|
|
206
300
|
delete_volumes: z.boolean().optional(),
|
|
207
301
|
}, async (args) => {
|
|
208
|
-
|
|
209
|
-
const { action, uuid, delete_volumes, ...apiData } = args;
|
|
302
|
+
const { action, uuid, delete_volumes } = args;
|
|
210
303
|
switch (action) {
|
|
211
304
|
case 'create_public':
|
|
212
305
|
if (!args.project_uuid ||
|
|
@@ -224,7 +317,19 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
224
317
|
],
|
|
225
318
|
};
|
|
226
319
|
}
|
|
227
|
-
return wrap(() => this.client.createApplicationPublic(
|
|
320
|
+
return wrap(() => this.client.createApplicationPublic({
|
|
321
|
+
project_uuid: args.project_uuid,
|
|
322
|
+
server_uuid: args.server_uuid,
|
|
323
|
+
git_repository: args.git_repository,
|
|
324
|
+
git_branch: args.git_branch,
|
|
325
|
+
build_pack: args.build_pack,
|
|
326
|
+
ports_exposes: args.ports_exposes,
|
|
327
|
+
environment_name: args.environment_name,
|
|
328
|
+
environment_uuid: args.environment_uuid,
|
|
329
|
+
name: args.name,
|
|
330
|
+
description: args.description,
|
|
331
|
+
fqdn: args.fqdn,
|
|
332
|
+
}));
|
|
228
333
|
case 'create_github':
|
|
229
334
|
if (!args.project_uuid ||
|
|
230
335
|
!args.server_uuid ||
|
|
@@ -240,7 +345,20 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
240
345
|
],
|
|
241
346
|
};
|
|
242
347
|
}
|
|
243
|
-
return wrap(() => this.client.createApplicationPrivateGH(
|
|
348
|
+
return wrap(() => this.client.createApplicationPrivateGH({
|
|
349
|
+
project_uuid: args.project_uuid,
|
|
350
|
+
server_uuid: args.server_uuid,
|
|
351
|
+
github_app_uuid: args.github_app_uuid,
|
|
352
|
+
git_repository: args.git_repository,
|
|
353
|
+
git_branch: args.git_branch,
|
|
354
|
+
build_pack: args.build_pack,
|
|
355
|
+
ports_exposes: args.ports_exposes,
|
|
356
|
+
environment_name: args.environment_name,
|
|
357
|
+
environment_uuid: args.environment_uuid,
|
|
358
|
+
name: args.name,
|
|
359
|
+
description: args.description,
|
|
360
|
+
fqdn: args.fqdn,
|
|
361
|
+
}));
|
|
244
362
|
case 'create_key':
|
|
245
363
|
if (!args.project_uuid ||
|
|
246
364
|
!args.server_uuid ||
|
|
@@ -256,7 +374,20 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
256
374
|
],
|
|
257
375
|
};
|
|
258
376
|
}
|
|
259
|
-
return wrap(() => this.client.createApplicationPrivateKey(
|
|
377
|
+
return wrap(() => this.client.createApplicationPrivateKey({
|
|
378
|
+
project_uuid: args.project_uuid,
|
|
379
|
+
server_uuid: args.server_uuid,
|
|
380
|
+
private_key_uuid: args.private_key_uuid,
|
|
381
|
+
git_repository: args.git_repository,
|
|
382
|
+
git_branch: args.git_branch,
|
|
383
|
+
build_pack: args.build_pack,
|
|
384
|
+
ports_exposes: args.ports_exposes,
|
|
385
|
+
environment_name: args.environment_name,
|
|
386
|
+
environment_uuid: args.environment_uuid,
|
|
387
|
+
name: args.name,
|
|
388
|
+
description: args.description,
|
|
389
|
+
fqdn: args.fqdn,
|
|
390
|
+
}));
|
|
260
391
|
case 'create_dockerimage':
|
|
261
392
|
if (!args.project_uuid ||
|
|
262
393
|
!args.server_uuid ||
|
|
@@ -271,11 +402,25 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
271
402
|
],
|
|
272
403
|
};
|
|
273
404
|
}
|
|
274
|
-
return wrap(() => this.client.createApplicationDockerImage(
|
|
275
|
-
|
|
405
|
+
return wrap(() => this.client.createApplicationDockerImage({
|
|
406
|
+
project_uuid: args.project_uuid,
|
|
407
|
+
server_uuid: args.server_uuid,
|
|
408
|
+
docker_registry_image_name: args.docker_registry_image_name,
|
|
409
|
+
ports_exposes: args.ports_exposes,
|
|
410
|
+
docker_registry_image_tag: args.docker_registry_image_tag,
|
|
411
|
+
environment_name: args.environment_name,
|
|
412
|
+
environment_uuid: args.environment_uuid,
|
|
413
|
+
name: args.name,
|
|
414
|
+
description: args.description,
|
|
415
|
+
fqdn: args.fqdn,
|
|
416
|
+
}));
|
|
417
|
+
case 'update': {
|
|
276
418
|
if (!uuid)
|
|
277
419
|
return { content: [{ type: 'text', text: 'Error: uuid required' }] };
|
|
278
|
-
|
|
420
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
421
|
+
const { action: _, uuid: __, delete_volumes: ___, ...updateData } = args;
|
|
422
|
+
return wrap(() => this.client.updateApplication(uuid, updateData));
|
|
423
|
+
}
|
|
279
424
|
case 'delete':
|
|
280
425
|
if (!uuid)
|
|
281
426
|
return { content: [{ type: 'text', text: 'Error: uuid required' }] };
|
|
@@ -378,8 +523,7 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
378
523
|
docker_compose_raw: z.string().optional(),
|
|
379
524
|
delete_volumes: z.boolean().optional(),
|
|
380
525
|
}, async (args) => {
|
|
381
|
-
|
|
382
|
-
const { action, uuid, delete_volumes, ...apiData } = args;
|
|
526
|
+
const { action, uuid, delete_volumes } = args;
|
|
383
527
|
switch (action) {
|
|
384
528
|
case 'create':
|
|
385
529
|
if (!args.server_uuid || !args.project_uuid) {
|
|
@@ -389,11 +533,23 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
389
533
|
],
|
|
390
534
|
};
|
|
391
535
|
}
|
|
392
|
-
return wrap(() => this.client.createService(
|
|
393
|
-
|
|
536
|
+
return wrap(() => this.client.createService({
|
|
537
|
+
project_uuid: args.project_uuid,
|
|
538
|
+
server_uuid: args.server_uuid,
|
|
539
|
+
type: args.type,
|
|
540
|
+
name: args.name,
|
|
541
|
+
description: args.description,
|
|
542
|
+
environment_name: args.environment_name,
|
|
543
|
+
instant_deploy: args.instant_deploy,
|
|
544
|
+
docker_compose_raw: args.docker_compose_raw,
|
|
545
|
+
}));
|
|
546
|
+
case 'update': {
|
|
394
547
|
if (!uuid)
|
|
395
548
|
return { content: [{ type: 'text', text: 'Error: uuid required' }] };
|
|
396
|
-
|
|
549
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
550
|
+
const { action: _, uuid: __, delete_volumes: ___, ...updateData } = args;
|
|
551
|
+
return wrap(() => this.client.updateService(uuid, updateData));
|
|
552
|
+
}
|
|
397
553
|
case 'delete':
|
|
398
554
|
if (!uuid)
|
|
399
555
|
return { content: [{ type: 'text', text: 'Error: uuid required' }] };
|
|
@@ -425,7 +581,36 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
425
581
|
restart: (u) => this.client.restartService(u),
|
|
426
582
|
},
|
|
427
583
|
};
|
|
428
|
-
|
|
584
|
+
// Generate contextual actions based on resource type and action taken
|
|
585
|
+
const getControlActions = () => {
|
|
586
|
+
const actions = [];
|
|
587
|
+
if (resource === 'application') {
|
|
588
|
+
actions.push({ tool: 'application_logs', args: { uuid }, hint: 'View logs' });
|
|
589
|
+
actions.push({ tool: 'get_application', args: { uuid }, hint: 'Check status' });
|
|
590
|
+
if (action === 'start' || action === 'restart') {
|
|
591
|
+
actions.push({
|
|
592
|
+
tool: 'control',
|
|
593
|
+
args: { resource: 'application', action: 'stop', uuid },
|
|
594
|
+
hint: 'Stop',
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
actions.push({
|
|
599
|
+
tool: 'control',
|
|
600
|
+
args: { resource: 'application', action: 'start', uuid },
|
|
601
|
+
hint: 'Start',
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else if (resource === 'database') {
|
|
606
|
+
actions.push({ tool: 'get_database', args: { uuid }, hint: 'Check status' });
|
|
607
|
+
}
|
|
608
|
+
else if (resource === 'service') {
|
|
609
|
+
actions.push({ tool: 'get_service', args: { uuid }, hint: 'Check status' });
|
|
610
|
+
}
|
|
611
|
+
return actions;
|
|
612
|
+
};
|
|
613
|
+
return wrapWithActions(() => methods[resource][action](uuid), getControlActions);
|
|
429
614
|
});
|
|
430
615
|
// =========================================================================
|
|
431
616
|
// Environment Variables (1 tool - consolidated)
|
|
@@ -437,8 +622,7 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
437
622
|
key: z.string().optional(),
|
|
438
623
|
value: z.string().optional(),
|
|
439
624
|
env_uuid: z.string().optional(),
|
|
440
|
-
|
|
441
|
-
}, async ({ resource, action, uuid, key, value, env_uuid, is_build_time }) => {
|
|
625
|
+
}, async ({ resource, action, uuid, key, value, env_uuid }) => {
|
|
442
626
|
if (resource === 'application') {
|
|
443
627
|
switch (action) {
|
|
444
628
|
case 'list':
|
|
@@ -446,7 +630,8 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
446
630
|
case 'create':
|
|
447
631
|
if (!key || !value)
|
|
448
632
|
return { content: [{ type: 'text', text: 'Error: key, value required' }] };
|
|
449
|
-
|
|
633
|
+
// Note: is_build_time is not passed - Coolify API rejects it for create action
|
|
634
|
+
return wrap(() => this.client.createApplicationEnvVar(uuid, { key, value }));
|
|
450
635
|
case 'update':
|
|
451
636
|
if (!key || !value)
|
|
452
637
|
return { content: [{ type: 'text', text: 'Error: key, value required' }] };
|
|
@@ -481,26 +666,30 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
481
666
|
// =========================================================================
|
|
482
667
|
// Deployments (3 tools)
|
|
483
668
|
// =========================================================================
|
|
484
|
-
this.tool('list_deployments', 'List deployments (summary)', { page: z.number().optional(), per_page: z.number().optional() }, async ({ page, per_page }) =>
|
|
485
|
-
this.tool('deploy', 'Deploy by tag/UUID', { tag_or_uuid: z.string(), force: z.boolean().optional() }, async ({ tag_or_uuid, force }) =>
|
|
486
|
-
this.tool('deployment', 'Manage deployment: get/cancel/list_for_app', {
|
|
669
|
+
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)));
|
|
670
|
+
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' }]));
|
|
671
|
+
this.tool('deployment', 'Manage deployment: get/cancel/list_for_app (logs excluded by default, use lines param to include)', {
|
|
487
672
|
action: z.enum(['get', 'cancel', 'list_for_app']),
|
|
488
673
|
uuid: z.string(),
|
|
489
|
-
lines: z.number().optional(), //
|
|
490
|
-
|
|
674
|
+
lines: z.number().optional(), // Include logs truncated to last N lines (omit for no logs)
|
|
675
|
+
max_chars: z.number().optional(), // Limit log output to last N chars (default: 50000)
|
|
676
|
+
}, async ({ action, uuid, lines, max_chars }) => {
|
|
491
677
|
switch (action) {
|
|
492
678
|
case 'get':
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
679
|
+
// If lines param specified, include logs and truncate
|
|
680
|
+
if (lines !== undefined) {
|
|
681
|
+
return wrapWithActions(async () => {
|
|
682
|
+
const deployment = (await this.client.getDeployment(uuid, {
|
|
683
|
+
includeLogs: true,
|
|
684
|
+
}));
|
|
685
|
+
if (deployment.logs) {
|
|
686
|
+
deployment.logs = truncateLogs(deployment.logs, lines, max_chars ?? 50000);
|
|
500
687
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
688
|
+
return deployment;
|
|
689
|
+
}, (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid));
|
|
690
|
+
}
|
|
691
|
+
// Otherwise return essential info without logs
|
|
692
|
+
return wrapWithActions(() => this.client.getDeployment(uuid), (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid));
|
|
504
693
|
case 'cancel':
|
|
505
694
|
return wrap(() => this.client.cancelDeployment(uuid));
|
|
506
695
|
case 'list_for_app':
|
package/dist/types/coolify.d.ts
CHANGED
|
@@ -224,11 +224,15 @@ export interface CreateApplicationPublicRequest {
|
|
|
224
224
|
start_command?: string;
|
|
225
225
|
instant_deploy?: boolean;
|
|
226
226
|
}
|
|
227
|
-
export interface CreateApplicationPrivateGHRequest extends CreateApplicationPublicRequest {
|
|
227
|
+
export interface CreateApplicationPrivateGHRequest extends Omit<CreateApplicationPublicRequest, 'build_pack' | 'ports_exposes'> {
|
|
228
228
|
github_app_uuid: string;
|
|
229
|
+
build_pack?: BuildPack;
|
|
230
|
+
ports_exposes?: string;
|
|
229
231
|
}
|
|
230
|
-
export interface CreateApplicationPrivateKeyRequest extends CreateApplicationPublicRequest {
|
|
232
|
+
export interface CreateApplicationPrivateKeyRequest extends Omit<CreateApplicationPublicRequest, 'build_pack' | 'ports_exposes'> {
|
|
231
233
|
private_key_uuid: string;
|
|
234
|
+
build_pack?: BuildPack;
|
|
235
|
+
ports_exposes?: string;
|
|
232
236
|
}
|
|
233
237
|
export interface CreateApplicationDockerfileRequest {
|
|
234
238
|
project_uuid: string;
|
|
@@ -897,3 +901,34 @@ export interface BatchOperationResult {
|
|
|
897
901
|
error: string;
|
|
898
902
|
}>;
|
|
899
903
|
}
|
|
904
|
+
export interface ResponseAction {
|
|
905
|
+
tool: string;
|
|
906
|
+
args: Record<string, string | number | boolean>;
|
|
907
|
+
hint: string;
|
|
908
|
+
}
|
|
909
|
+
export interface ResponsePagination {
|
|
910
|
+
next?: {
|
|
911
|
+
tool: string;
|
|
912
|
+
args: Record<string, number>;
|
|
913
|
+
};
|
|
914
|
+
prev?: {
|
|
915
|
+
tool: string;
|
|
916
|
+
args: Record<string, number>;
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
export interface DeploymentEssential {
|
|
920
|
+
uuid: string;
|
|
921
|
+
deployment_uuid: string;
|
|
922
|
+
application_uuid?: string;
|
|
923
|
+
application_name?: string;
|
|
924
|
+
server_name?: string;
|
|
925
|
+
status: string;
|
|
926
|
+
commit?: string;
|
|
927
|
+
force_rebuild: boolean;
|
|
928
|
+
is_webhook: boolean;
|
|
929
|
+
is_api: boolean;
|
|
930
|
+
created_at: string;
|
|
931
|
+
updated_at: string;
|
|
932
|
+
logs_available?: boolean;
|
|
933
|
+
logs_info?: string;
|
|
934
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@masonator/coolify-mcp",
|
|
3
3
|
"scope": "@masonator",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.6.0",
|
|
5
5
|
"description": "MCP server implementation for Coolify",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"globals": "^17.0.0",
|
|
60
60
|
"husky": "^9.0.11",
|
|
61
61
|
"jest": "^29.7.0",
|
|
62
|
+
"jest-junit": "^16.0.0",
|
|
62
63
|
"lint-staged": "^16.2.7",
|
|
63
64
|
"markdownlint-cli2": "^0.20.0",
|
|
64
65
|
"prettier": "^3.5.3",
|