@masonator/coolify-mcp 2.6.4 → 2.6.6
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.
|
@@ -148,41 +148,46 @@ describe('CoolifyMcpServer v2', () => {
|
|
|
148
148
|
});
|
|
149
149
|
});
|
|
150
150
|
describe('truncateLogs', () => {
|
|
151
|
+
// Plain text log tests
|
|
151
152
|
it('should return logs unchanged when within limits', () => {
|
|
152
153
|
const logs = 'line1\nline2\nline3';
|
|
153
154
|
const result = truncateLogs(logs, 200, 50000);
|
|
154
|
-
expect(result).toBe(logs);
|
|
155
|
+
expect(result.logs).toBe(logs);
|
|
156
|
+
expect(result.total).toBe(3);
|
|
155
157
|
});
|
|
156
158
|
it('should truncate to last N lines', () => {
|
|
157
159
|
const logs = 'line1\nline2\nline3\nline4\nline5';
|
|
158
160
|
const result = truncateLogs(logs, 3, 50000);
|
|
159
|
-
expect(result).toBe('line3\nline4\nline5');
|
|
161
|
+
expect(result.logs).toBe('line3\nline4\nline5');
|
|
162
|
+
expect(result.total).toBe(5);
|
|
163
|
+
expect(result.showing_start).toBe(3);
|
|
164
|
+
expect(result.showing_end).toBe(5);
|
|
160
165
|
});
|
|
161
166
|
it('should truncate by character limit when lines are huge', () => {
|
|
162
167
|
const hugeLine = 'x'.repeat(100);
|
|
163
168
|
const logs = `${hugeLine}\n${hugeLine}\n${hugeLine}`;
|
|
164
169
|
const result = truncateLogs(logs, 200, 50);
|
|
165
|
-
expect(result.length).toBeLessThanOrEqual(50);
|
|
166
|
-
expect(result.startsWith('...[truncated]...')).toBe(true);
|
|
170
|
+
expect(result.logs.length).toBeLessThanOrEqual(50);
|
|
171
|
+
expect(result.logs.startsWith('...[truncated]...')).toBe(true);
|
|
167
172
|
});
|
|
168
173
|
it('should not add truncation prefix when under char limit', () => {
|
|
169
174
|
const logs = 'line1\nline2\nline3';
|
|
170
175
|
const result = truncateLogs(logs, 200, 50000);
|
|
171
|
-
expect(result.startsWith('...[truncated]...')).toBe(false);
|
|
176
|
+
expect(result.logs.startsWith('...[truncated]...')).toBe(false);
|
|
172
177
|
});
|
|
173
178
|
it('should handle empty logs', () => {
|
|
174
179
|
const result = truncateLogs('', 200, 50000);
|
|
175
|
-
expect(result).toBe('');
|
|
180
|
+
expect(result.logs).toBe('');
|
|
176
181
|
});
|
|
177
182
|
it('should use default limits when not specified', () => {
|
|
178
183
|
const logs = 'line1\nline2';
|
|
179
184
|
const result = truncateLogs(logs);
|
|
180
|
-
expect(result).toBe(logs);
|
|
185
|
+
expect(result.logs).toBe(logs);
|
|
181
186
|
});
|
|
182
187
|
it('should respect custom line limit', () => {
|
|
183
188
|
const lines = Array.from({ length: 300 }, (_, i) => `line${i + 1}`).join('\n');
|
|
184
189
|
const result = truncateLogs(lines, 50, 50000);
|
|
185
|
-
const resultLines = result.split('\n');
|
|
190
|
+
const resultLines = result.logs.split('\n');
|
|
186
191
|
expect(resultLines.length).toBe(50);
|
|
187
192
|
expect(resultLines[0]).toBe('line251');
|
|
188
193
|
expect(resultLines[49]).toBe('line300');
|
|
@@ -190,7 +195,90 @@ describe('truncateLogs', () => {
|
|
|
190
195
|
it('should respect custom char limit', () => {
|
|
191
196
|
const logs = 'x'.repeat(1000);
|
|
192
197
|
const result = truncateLogs(logs, 200, 100);
|
|
193
|
-
expect(result.length).toBe(100);
|
|
198
|
+
expect(result.logs.length).toBe(100);
|
|
199
|
+
});
|
|
200
|
+
// Pagination tests (plain text)
|
|
201
|
+
it('should paginate plain text logs (page 2 = older entries)', () => {
|
|
202
|
+
const logs = Array.from({ length: 30 }, (_, i) => `line${i + 1}`).join('\n');
|
|
203
|
+
const page1 = truncateLogs(logs, 10, 50000, 1);
|
|
204
|
+
const page2 = truncateLogs(logs, 10, 50000, 2);
|
|
205
|
+
const page3 = truncateLogs(logs, 10, 50000, 3);
|
|
206
|
+
expect(page1.logs).toContain('line30');
|
|
207
|
+
expect(page1.logs).toContain('line21');
|
|
208
|
+
expect(page1.logs).not.toContain('line20');
|
|
209
|
+
expect(page2.logs).toContain('line20');
|
|
210
|
+
expect(page2.logs).toContain('line11');
|
|
211
|
+
expect(page2.logs).not.toContain('line10');
|
|
212
|
+
expect(page3.logs).toContain('line10');
|
|
213
|
+
expect(page3.logs).toContain('line1');
|
|
214
|
+
expect(page1.showing_start).toBe(21);
|
|
215
|
+
expect(page1.showing_end).toBe(30);
|
|
216
|
+
});
|
|
217
|
+
// JSON array format tests (Coolify deployment logs)
|
|
218
|
+
it('should parse JSON array logs and return last N visible entries', () => {
|
|
219
|
+
const entries = [
|
|
220
|
+
{ output: 'Building...', timestamp: '2026-01-01T00:00:01Z', hidden: false },
|
|
221
|
+
{ output: 'docker pull', timestamp: '2026-01-01T00:00:02Z', hidden: true },
|
|
222
|
+
{ output: 'Compiling...', timestamp: '2026-01-01T00:00:03Z', hidden: false },
|
|
223
|
+
{ output: 'Done.', timestamp: '2026-01-01T00:00:04Z', hidden: false },
|
|
224
|
+
];
|
|
225
|
+
const result = truncateLogs(JSON.stringify(entries), 2, 50000);
|
|
226
|
+
expect(result.logs).toContain('Compiling...');
|
|
227
|
+
expect(result.logs).toContain('Done.');
|
|
228
|
+
expect(result.logs).not.toContain('Building...');
|
|
229
|
+
expect(result.logs).not.toContain('docker pull');
|
|
230
|
+
expect(result.total).toBe(3); // 3 visible entries
|
|
231
|
+
});
|
|
232
|
+
it('should filter hidden entries from JSON logs', () => {
|
|
233
|
+
const entries = [
|
|
234
|
+
{ output: 'visible1', timestamp: '2026-01-01T00:00:01Z', hidden: false },
|
|
235
|
+
{ output: 'hidden1', timestamp: '2026-01-01T00:00:02Z', hidden: true },
|
|
236
|
+
{ output: 'hidden2', timestamp: '2026-01-01T00:00:03Z', hidden: true },
|
|
237
|
+
{ output: 'visible2', timestamp: '2026-01-01T00:00:04Z', hidden: false },
|
|
238
|
+
];
|
|
239
|
+
const result = truncateLogs(JSON.stringify(entries), 200, 50000);
|
|
240
|
+
expect(result.logs).toContain('visible1');
|
|
241
|
+
expect(result.logs).toContain('visible2');
|
|
242
|
+
expect(result.logs).not.toContain('hidden1');
|
|
243
|
+
expect(result.logs).not.toContain('hidden2');
|
|
244
|
+
});
|
|
245
|
+
it('should format JSON log entries with timestamp and output', () => {
|
|
246
|
+
const entries = [
|
|
247
|
+
{ output: 'Starting deploy', timestamp: '2026-01-01T10:00:00Z', hidden: false },
|
|
248
|
+
];
|
|
249
|
+
const result = truncateLogs(JSON.stringify(entries), 200, 50000);
|
|
250
|
+
expect(result.logs).toBe('[2026-01-01T10:00:00Z] Starting deploy');
|
|
251
|
+
});
|
|
252
|
+
it('should paginate JSON logs (page 2 = older entries)', () => {
|
|
253
|
+
const entries = Array.from({ length: 30 }, (_, i) => ({
|
|
254
|
+
output: `step ${i + 1}`,
|
|
255
|
+
timestamp: `2026-01-01T00:00:${String(i).padStart(2, '0')}Z`,
|
|
256
|
+
hidden: false,
|
|
257
|
+
}));
|
|
258
|
+
const page1 = truncateLogs(JSON.stringify(entries), 10, 50000, 1);
|
|
259
|
+
const page2 = truncateLogs(JSON.stringify(entries), 10, 50000, 2);
|
|
260
|
+
expect(page1.logs).toContain('step 30');
|
|
261
|
+
expect(page1.logs).toContain('step 21');
|
|
262
|
+
expect(page1.logs).not.toContain('step 20');
|
|
263
|
+
expect(page2.logs).toContain('step 20');
|
|
264
|
+
expect(page2.logs).toContain('step 11');
|
|
265
|
+
expect(page2.logs).not.toContain('step 10');
|
|
266
|
+
expect(page1.total).toBe(30);
|
|
267
|
+
expect(page1.showing_start).toBe(21);
|
|
268
|
+
expect(page1.showing_end).toBe(30);
|
|
269
|
+
expect(page2.showing_start).toBe(11);
|
|
270
|
+
expect(page2.showing_end).toBe(20);
|
|
271
|
+
});
|
|
272
|
+
it('should return metadata with total and showing range', () => {
|
|
273
|
+
const entries = Array.from({ length: 50 }, (_, i) => ({
|
|
274
|
+
output: `step ${i}`,
|
|
275
|
+
timestamp: `2026-01-01T00:00:${String(i).padStart(2, '0')}Z`,
|
|
276
|
+
hidden: false,
|
|
277
|
+
}));
|
|
278
|
+
const result = truncateLogs(JSON.stringify(entries), 10, 50000);
|
|
279
|
+
expect(result.total).toBe(50);
|
|
280
|
+
expect(result.showing_start).toBe(41);
|
|
281
|
+
expect(result.showing_end).toBe(50);
|
|
194
282
|
});
|
|
195
283
|
});
|
|
196
284
|
// =============================================================================
|
package/dist/lib/mcp-server.d.ts
CHANGED
|
@@ -6,11 +6,19 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
6
6
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
7
7
|
import type { CoolifyConfig, ResponseAction, ResponsePagination } from '../types/coolify.js';
|
|
8
8
|
export declare const VERSION: string;
|
|
9
|
+
export interface TruncatedLogsResult {
|
|
10
|
+
logs: string;
|
|
11
|
+
total: number;
|
|
12
|
+
showing_start: number;
|
|
13
|
+
showing_end: number;
|
|
14
|
+
}
|
|
9
15
|
/**
|
|
10
|
-
* Truncate logs by
|
|
16
|
+
* Truncate logs by entry count with pagination support.
|
|
17
|
+
* Handles both JSON array format (Coolify deployment logs) and plain text.
|
|
18
|
+
* Page 1 = most recent entries, page 2 = next older batch, etc.
|
|
11
19
|
* Exported for testing.
|
|
12
20
|
*/
|
|
13
|
-
export declare function truncateLogs(logs: string, lineLimit?: number, charLimit?: number):
|
|
21
|
+
export declare function truncateLogs(logs: string, lineLimit?: number, charLimit?: number, page?: number): TruncatedLogsResult;
|
|
14
22
|
/** Generate contextual actions for an application based on its status */
|
|
15
23
|
export declare function getApplicationActions(uuid: string, status?: string): ResponseAction[];
|
|
16
24
|
/** Generate contextual actions for a deployment */
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -25,21 +25,55 @@ function wrap(fn) {
|
|
|
25
25
|
}
|
|
26
26
|
const TRUNCATION_PREFIX = '...[truncated]...\n';
|
|
27
27
|
/**
|
|
28
|
-
* Truncate logs by
|
|
28
|
+
* Truncate logs by entry count with pagination support.
|
|
29
|
+
* Handles both JSON array format (Coolify deployment logs) and plain text.
|
|
30
|
+
* Page 1 = most recent entries, page 2 = next older batch, etc.
|
|
29
31
|
* Exported for testing.
|
|
30
32
|
*/
|
|
31
|
-
export function truncateLogs(logs, lineLimit = 200, charLimit = 50000) {
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
export function truncateLogs(logs, lineLimit = 200, charLimit = 50000, page = 1) {
|
|
34
|
+
// Try parsing as JSON array (Coolify deployment log format)
|
|
35
|
+
let lines;
|
|
36
|
+
let total;
|
|
37
|
+
try {
|
|
38
|
+
const entries = JSON.parse(logs);
|
|
39
|
+
if (Array.isArray(entries)) {
|
|
40
|
+
const visible = entries.filter((e) => !e.hidden);
|
|
41
|
+
total = visible.length;
|
|
42
|
+
const end = total - (page - 1) * lineLimit;
|
|
43
|
+
const start = Math.max(0, end - lineLimit);
|
|
44
|
+
const slice = visible.slice(start, end);
|
|
45
|
+
lines = slice.map((e) => `[${e.timestamp ?? ''}] ${e.output ?? ''}`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const allLines = logs.split('\n');
|
|
49
|
+
total = allLines.length;
|
|
50
|
+
const end = total - (page - 1) * lineLimit;
|
|
51
|
+
const start = Math.max(0, end - lineLimit);
|
|
52
|
+
lines = allLines.slice(start, end);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Plain text logs — split by newlines
|
|
57
|
+
const allLines = logs.split('\n');
|
|
58
|
+
total = allLines.length;
|
|
59
|
+
const end = total - (page - 1) * lineLimit;
|
|
60
|
+
const start = Math.max(0, end - lineLimit);
|
|
61
|
+
lines = allLines.slice(start, end);
|
|
62
|
+
}
|
|
63
|
+
const end = total - (page - 1) * lineLimit;
|
|
64
|
+
const start = Math.max(0, end - lineLimit);
|
|
65
|
+
let result = lines.join('\n');
|
|
66
|
+
// Safety net: limit by characters
|
|
67
|
+
if (result.length > charLimit) {
|
|
39
68
|
const prefixLen = TRUNCATION_PREFIX.length;
|
|
40
|
-
|
|
69
|
+
result = TRUNCATION_PREFIX + result.slice(-(charLimit - prefixLen));
|
|
41
70
|
}
|
|
42
|
-
return
|
|
71
|
+
return {
|
|
72
|
+
logs: result,
|
|
73
|
+
total,
|
|
74
|
+
showing_start: start + 1,
|
|
75
|
+
showing_end: Math.min(end, total),
|
|
76
|
+
};
|
|
43
77
|
}
|
|
44
78
|
// =============================================================================
|
|
45
79
|
// Action Generators for HATEOAS-style responses
|
|
@@ -676,22 +710,48 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
676
710
|
this.tool('deployment', 'Manage deployment: get/cancel/list_for_app (logs excluded by default, use lines param to include)', {
|
|
677
711
|
action: z.enum(['get', 'cancel', 'list_for_app']),
|
|
678
712
|
uuid: z.string(),
|
|
679
|
-
lines: z.number().optional(), // Include logs truncated to last N
|
|
713
|
+
lines: z.number().optional(), // Include logs truncated to last N entries (omit for no logs)
|
|
714
|
+
page: z.number().optional(), // Log page (1=most recent, 2=older, etc.)
|
|
680
715
|
max_chars: z.number().optional(), // Limit log output to last N chars (default: 50000)
|
|
681
|
-
}, async ({ action, uuid, lines, max_chars }) => {
|
|
716
|
+
}, async ({ action, uuid, lines, page, max_chars }) => {
|
|
682
717
|
switch (action) {
|
|
683
718
|
case 'get':
|
|
684
719
|
// If lines param specified, include logs and truncate
|
|
685
720
|
if (lines !== undefined) {
|
|
721
|
+
const p = page ?? 1;
|
|
722
|
+
const ll = lines;
|
|
686
723
|
return wrapWithActions(async () => {
|
|
687
724
|
const deployment = (await this.client.getDeployment(uuid, {
|
|
688
725
|
includeLogs: true,
|
|
689
726
|
}));
|
|
690
727
|
if (deployment.logs) {
|
|
691
|
-
|
|
728
|
+
const result = truncateLogs(deployment.logs, ll, max_chars ?? 50000, p);
|
|
729
|
+
deployment.logs = result.logs;
|
|
730
|
+
return {
|
|
731
|
+
...deployment,
|
|
732
|
+
logs_meta: {
|
|
733
|
+
total_entries: result.total,
|
|
734
|
+
showing: `${result.showing_start}-${result.showing_end} of ${result.total}`,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
692
737
|
}
|
|
693
|
-
return deployment;
|
|
694
|
-
}, (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid))
|
|
738
|
+
return { ...deployment, logs_meta: undefined };
|
|
739
|
+
}, (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid), (dep) => {
|
|
740
|
+
const total = dep.logs_meta?.total_entries ?? 0;
|
|
741
|
+
const hasOlder = p * ll < total;
|
|
742
|
+
const pagination = {};
|
|
743
|
+
if (hasOlder)
|
|
744
|
+
pagination.next = {
|
|
745
|
+
tool: 'deployment',
|
|
746
|
+
args: { action: 'get', uuid, lines: ll, page: p + 1 },
|
|
747
|
+
};
|
|
748
|
+
if (p > 1)
|
|
749
|
+
pagination.prev = {
|
|
750
|
+
tool: 'deployment',
|
|
751
|
+
args: { action: 'get', uuid, lines: ll, page: p - 1 },
|
|
752
|
+
};
|
|
753
|
+
return Object.keys(pagination).length > 0 ? pagination : undefined;
|
|
754
|
+
});
|
|
695
755
|
}
|
|
696
756
|
// Otherwise return essential info without logs
|
|
697
757
|
return wrapWithActions(() => this.client.getDeployment(uuid), (dep) => getDeploymentActions(dep.uuid, dep.status, dep.application_uuid));
|
package/dist/types/coolify.d.ts
CHANGED
|
@@ -907,11 +907,11 @@ export interface ResponseAction {
|
|
|
907
907
|
export interface ResponsePagination {
|
|
908
908
|
next?: {
|
|
909
909
|
tool: string;
|
|
910
|
-
args: Record<string, number>;
|
|
910
|
+
args: Record<string, string | number>;
|
|
911
911
|
};
|
|
912
912
|
prev?: {
|
|
913
913
|
tool: string;
|
|
914
|
-
args: Record<string, number>;
|
|
914
|
+
args: Record<string, string | number>;
|
|
915
915
|
};
|
|
916
916
|
}
|
|
917
917
|
export interface DeploymentEssential {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@masonator/coolify-mcp",
|
|
3
3
|
"scope": "@masonator",
|
|
4
|
-
"version": "2.6.
|
|
4
|
+
"version": "2.6.6",
|
|
5
5
|
"description": "MCP server implementation for Coolify",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -49,12 +49,13 @@
|
|
|
49
49
|
"zod": "^4.3.5"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@eslint/js": "^9.39.3",
|
|
52
53
|
"@types/jest": "^29.5.14",
|
|
53
54
|
"@types/node": "^25.0.3",
|
|
54
55
|
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
55
56
|
"@typescript-eslint/parser": "^8.51.0",
|
|
56
57
|
"dotenv": "^17.2.3",
|
|
57
|
-
"eslint": "^
|
|
58
|
+
"eslint": "^10.0.0",
|
|
58
59
|
"eslint-config-prettier": "^10.1.8",
|
|
59
60
|
"globals": "^17.0.0",
|
|
60
61
|
"husky": "^9.0.11",
|