@masonator/coolify-mcp 2.8.1 → 2.10.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/dist/__tests__/coolify-client.test.js +373 -19
- package/dist/__tests__/integration/diagnostics.integration.test.js +3 -2
- package/dist/__tests__/mcp-server.test.js +263 -1
- package/dist/lib/coolify-client.d.ts +21 -3
- package/dist/lib/coolify-client.js +86 -9
- package/dist/lib/mcp-server.js +112 -10
- package/dist/types/coolify.d.ts +38 -5
- package/package.json +3 -3
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
|
|
2
2
|
import { CoolifyClient } from '../lib/coolify-client.js';
|
|
3
3
|
// Helper to create mock response
|
|
4
|
-
function mockResponse(data, ok = true, status = 200) {
|
|
4
|
+
function mockResponse(data, ok = true, status = 200, contentType = 'application/json') {
|
|
5
|
+
const body = contentType.includes('application/json') ? JSON.stringify(data) : String(data);
|
|
5
6
|
return {
|
|
6
7
|
ok,
|
|
7
8
|
status,
|
|
8
9
|
statusText: ok ? 'OK' : 'Error',
|
|
9
|
-
|
|
10
|
+
headers: new Headers({ 'Content-Type': contentType }),
|
|
11
|
+
text: async () => body,
|
|
10
12
|
};
|
|
11
13
|
}
|
|
12
14
|
const mockFetch = jest.fn();
|
|
@@ -521,6 +523,21 @@ describe('CoolifyClient', () => {
|
|
|
521
523
|
const result = await client.deleteServer('test-uuid');
|
|
522
524
|
expect(result).toEqual({});
|
|
523
525
|
});
|
|
526
|
+
it('should return plain text responses without JSON parsing', async () => {
|
|
527
|
+
mockFetch.mockResolvedValueOnce(mockResponse('log line 1\nlog line 2', true, 200, 'text/plain; charset=utf-8'));
|
|
528
|
+
const result = await client.getApplicationLogs('app-uuid', 50);
|
|
529
|
+
expect(result).toBe('log line 1\nlog line 2');
|
|
530
|
+
});
|
|
531
|
+
it('should fall back to raw text when JSON responses are malformed', async () => {
|
|
532
|
+
mockFetch.mockResolvedValueOnce({
|
|
533
|
+
ok: true,
|
|
534
|
+
status: 200,
|
|
535
|
+
headers: new Headers({ 'Content-Type': 'application/json' }),
|
|
536
|
+
text: async () => 'not valid json',
|
|
537
|
+
});
|
|
538
|
+
const result = await client.getApplicationLogs('app-uuid', 50);
|
|
539
|
+
expect(result).toBe('not valid json');
|
|
540
|
+
});
|
|
524
541
|
it('should handle API errors without message', async () => {
|
|
525
542
|
mockFetch.mockResolvedValueOnce(mockResponse({}, false, 500));
|
|
526
543
|
await expect(client.listServers()).rejects.toThrow('HTTP 500: Error');
|
|
@@ -952,6 +969,131 @@ describe('CoolifyClient', () => {
|
|
|
952
969
|
expect(callBody.custom_docker_run_options).toBe('--network=my-net');
|
|
953
970
|
expect(callBody.custom_labels).toBe('dHJhZWZpaw==');
|
|
954
971
|
});
|
|
972
|
+
// Regression for #178 — verify build-config and health_check_* fields reach
|
|
973
|
+
// the wire, not just zod-accepted then silently stripped by the hand-pick.
|
|
974
|
+
it('should pass build-config and health_check fields through createApplicationPublic', async () => {
|
|
975
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
|
|
976
|
+
await client.createApplicationPublic({
|
|
977
|
+
project_uuid: 'proj-uuid',
|
|
978
|
+
server_uuid: 'server-uuid',
|
|
979
|
+
git_repository: 'https://github.com/user/monorepo',
|
|
980
|
+
git_branch: 'main',
|
|
981
|
+
build_pack: 'dockerfile',
|
|
982
|
+
ports_exposes: '3000',
|
|
983
|
+
base_directory: '/apps/api',
|
|
984
|
+
publish_directory: '/dist',
|
|
985
|
+
install_command: 'pnpm install',
|
|
986
|
+
build_command: 'pnpm build',
|
|
987
|
+
start_command: 'node dist/main.js',
|
|
988
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
989
|
+
watch_paths: 'apps/api/**',
|
|
990
|
+
health_check_enabled: true,
|
|
991
|
+
health_check_path: '/health',
|
|
992
|
+
health_check_port: 3000,
|
|
993
|
+
health_check_start_period: 60,
|
|
994
|
+
});
|
|
995
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
|
|
996
|
+
expect(callBody.base_directory).toBe('/apps/api');
|
|
997
|
+
expect(callBody.publish_directory).toBe('/dist');
|
|
998
|
+
expect(callBody.install_command).toBe('pnpm install');
|
|
999
|
+
expect(callBody.build_command).toBe('pnpm build');
|
|
1000
|
+
expect(callBody.start_command).toBe('node dist/main.js');
|
|
1001
|
+
expect(callBody.dockerfile_location).toBe('/apps/api/Dockerfile');
|
|
1002
|
+
expect(callBody.watch_paths).toBe('apps/api/**');
|
|
1003
|
+
expect(callBody.health_check_enabled).toBe(true);
|
|
1004
|
+
expect(callBody.health_check_path).toBe('/health');
|
|
1005
|
+
expect(callBody.health_check_port).toBe(3000);
|
|
1006
|
+
expect(callBody.health_check_start_period).toBe(60);
|
|
1007
|
+
});
|
|
1008
|
+
it('should pass build-config and health_check fields through createApplicationPrivateGH', async () => {
|
|
1009
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
|
|
1010
|
+
await client.createApplicationPrivateGH({
|
|
1011
|
+
project_uuid: 'proj-uuid',
|
|
1012
|
+
server_uuid: 'server-uuid',
|
|
1013
|
+
github_app_uuid: 'gh-app-uuid',
|
|
1014
|
+
git_repository: 'org/monorepo',
|
|
1015
|
+
git_branch: 'main',
|
|
1016
|
+
base_directory: '/apps/api',
|
|
1017
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
1018
|
+
watch_paths: 'apps/api/**',
|
|
1019
|
+
health_check_enabled: true,
|
|
1020
|
+
health_check_path: '/health',
|
|
1021
|
+
});
|
|
1022
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
|
|
1023
|
+
expect(callBody.base_directory).toBe('/apps/api');
|
|
1024
|
+
expect(callBody.dockerfile_location).toBe('/apps/api/Dockerfile');
|
|
1025
|
+
expect(callBody.watch_paths).toBe('apps/api/**');
|
|
1026
|
+
expect(callBody.health_check_enabled).toBe(true);
|
|
1027
|
+
expect(callBody.health_check_path).toBe('/health');
|
|
1028
|
+
});
|
|
1029
|
+
it('should pass build-config and health_check fields through createApplicationPrivateKey', async () => {
|
|
1030
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
|
|
1031
|
+
await client.createApplicationPrivateKey({
|
|
1032
|
+
project_uuid: 'proj-uuid',
|
|
1033
|
+
server_uuid: 'server-uuid',
|
|
1034
|
+
private_key_uuid: 'key-uuid',
|
|
1035
|
+
git_repository: 'git@github.com:org/monorepo.git',
|
|
1036
|
+
git_branch: 'main',
|
|
1037
|
+
base_directory: '/apps/api',
|
|
1038
|
+
publish_directory: '/dist',
|
|
1039
|
+
install_command: 'pnpm install',
|
|
1040
|
+
build_command: 'pnpm build',
|
|
1041
|
+
start_command: 'node dist/main.js',
|
|
1042
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
1043
|
+
watch_paths: 'apps/api/**',
|
|
1044
|
+
health_check_enabled: true,
|
|
1045
|
+
health_check_path: '/health',
|
|
1046
|
+
health_check_port: 3000,
|
|
1047
|
+
});
|
|
1048
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
|
|
1049
|
+
expect(callBody.base_directory).toBe('/apps/api');
|
|
1050
|
+
expect(callBody.dockerfile_location).toBe('/apps/api/Dockerfile');
|
|
1051
|
+
expect(callBody.watch_paths).toBe('apps/api/**');
|
|
1052
|
+
expect(callBody.health_check_path).toBe('/health');
|
|
1053
|
+
expect(callBody.health_check_port).toBe(3000);
|
|
1054
|
+
});
|
|
1055
|
+
it('should pass health_check fields through createApplicationDockerImage (build-config N/A)', async () => {
|
|
1056
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
|
|
1057
|
+
await client.createApplicationDockerImage({
|
|
1058
|
+
project_uuid: 'proj-uuid',
|
|
1059
|
+
server_uuid: 'server-uuid',
|
|
1060
|
+
docker_registry_image_name: 'traefik/whoami',
|
|
1061
|
+
ports_exposes: '80',
|
|
1062
|
+
health_check_enabled: true,
|
|
1063
|
+
health_check_path: '/health',
|
|
1064
|
+
health_check_port: 80,
|
|
1065
|
+
health_check_method: 'GET',
|
|
1066
|
+
health_check_scheme: 'http',
|
|
1067
|
+
health_check_return_code: 200,
|
|
1068
|
+
health_check_interval: 30,
|
|
1069
|
+
health_check_timeout: 5,
|
|
1070
|
+
health_check_retries: 3,
|
|
1071
|
+
health_check_start_period: 60,
|
|
1072
|
+
});
|
|
1073
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
|
|
1074
|
+
expect(callBody.health_check_enabled).toBe(true);
|
|
1075
|
+
expect(callBody.health_check_path).toBe('/health');
|
|
1076
|
+
expect(callBody.health_check_port).toBe(80);
|
|
1077
|
+
expect(callBody.health_check_method).toBe('GET');
|
|
1078
|
+
expect(callBody.health_check_scheme).toBe('http');
|
|
1079
|
+
expect(callBody.health_check_return_code).toBe(200);
|
|
1080
|
+
expect(callBody.health_check_interval).toBe(30);
|
|
1081
|
+
expect(callBody.health_check_timeout).toBe(5);
|
|
1082
|
+
expect(callBody.health_check_retries).toBe(3);
|
|
1083
|
+
expect(callBody.health_check_start_period).toBe(60);
|
|
1084
|
+
});
|
|
1085
|
+
it('should pass dockerfile_target_build through updateApplication (PATCH-only field)', async () => {
|
|
1086
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApplication));
|
|
1087
|
+
await client.updateApplication('app-uuid', {
|
|
1088
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
1089
|
+
dockerfile_target_build: 'production',
|
|
1090
|
+
base_directory: '/apps/api',
|
|
1091
|
+
});
|
|
1092
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1]?.body);
|
|
1093
|
+
expect(callBody.dockerfile_location).toBe('/apps/api/Dockerfile');
|
|
1094
|
+
expect(callBody.dockerfile_target_build).toBe('production');
|
|
1095
|
+
expect(callBody.base_directory).toBe('/apps/api');
|
|
1096
|
+
});
|
|
955
1097
|
it('should pass destination_uuid through in createApplicationPublic', async () => {
|
|
956
1098
|
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-app-uuid' }));
|
|
957
1099
|
await client.createApplicationPublic({
|
|
@@ -1154,21 +1296,72 @@ describe('CoolifyClient', () => {
|
|
|
1154
1296
|
uuid: 'env-var-uuid',
|
|
1155
1297
|
key: 'API_KEY',
|
|
1156
1298
|
value: 'secret123',
|
|
1157
|
-
|
|
1299
|
+
is_buildtime: false,
|
|
1300
|
+
is_runtime: true,
|
|
1158
1301
|
};
|
|
1159
|
-
it('should list application env vars', async () => {
|
|
1302
|
+
it('should list application env vars with values masked by default (#159)', async () => {
|
|
1160
1303
|
mockFetch.mockResolvedValueOnce(mockResponse([mockEnvVar]));
|
|
1161
1304
|
const result = await client.listApplicationEnvVars('app-uuid');
|
|
1162
|
-
|
|
1305
|
+
// value masked, metadata preserved
|
|
1306
|
+
expect(result).toEqual([
|
|
1307
|
+
{
|
|
1308
|
+
uuid: 'env-var-uuid',
|
|
1309
|
+
key: 'API_KEY',
|
|
1310
|
+
value: '***',
|
|
1311
|
+
is_buildtime: false,
|
|
1312
|
+
is_runtime: true,
|
|
1313
|
+
},
|
|
1314
|
+
]);
|
|
1163
1315
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/envs', expect.any(Object));
|
|
1164
1316
|
});
|
|
1165
|
-
it('should list application env vars with
|
|
1317
|
+
it('should list application env vars with real_value also masked on the full projection (#159)', async () => {
|
|
1166
1318
|
const fullEnvVar = {
|
|
1167
1319
|
id: 1,
|
|
1168
1320
|
uuid: 'env-var-uuid',
|
|
1169
1321
|
key: 'API_KEY',
|
|
1170
1322
|
value: 'secret123',
|
|
1171
|
-
|
|
1323
|
+
real_value: 'secret123',
|
|
1324
|
+
is_buildtime: false,
|
|
1325
|
+
is_runtime: true,
|
|
1326
|
+
is_literal: true,
|
|
1327
|
+
is_multiline: false,
|
|
1328
|
+
is_preview: false,
|
|
1329
|
+
is_shared: false,
|
|
1330
|
+
is_shown_once: false,
|
|
1331
|
+
application_id: 1,
|
|
1332
|
+
created_at: '2024-01-01',
|
|
1333
|
+
updated_at: '2024-01-01',
|
|
1334
|
+
};
|
|
1335
|
+
mockFetch.mockResolvedValueOnce(mockResponse([fullEnvVar]));
|
|
1336
|
+
const result = (await client.listApplicationEnvVars('app-uuid'));
|
|
1337
|
+
expect(result[0].value).toBe('***');
|
|
1338
|
+
expect(result[0].real_value).toBe('***');
|
|
1339
|
+
// Metadata stays intact
|
|
1340
|
+
expect(result[0]).toMatchObject({
|
|
1341
|
+
uuid: 'env-var-uuid',
|
|
1342
|
+
key: 'API_KEY',
|
|
1343
|
+
is_buildtime: false,
|
|
1344
|
+
is_runtime: true,
|
|
1345
|
+
is_literal: true,
|
|
1346
|
+
is_preview: false,
|
|
1347
|
+
application_id: 1,
|
|
1348
|
+
created_at: '2024-01-01',
|
|
1349
|
+
updated_at: '2024-01-01',
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
it('should list application env vars with real values when reveal=true (#159)', async () => {
|
|
1353
|
+
mockFetch.mockResolvedValueOnce(mockResponse([mockEnvVar]));
|
|
1354
|
+
const result = await client.listApplicationEnvVars('app-uuid', { reveal: true });
|
|
1355
|
+
expect(result).toEqual([mockEnvVar]);
|
|
1356
|
+
});
|
|
1357
|
+
it('should list application env vars with summary, masked by default (#159)', async () => {
|
|
1358
|
+
const fullEnvVar = {
|
|
1359
|
+
id: 1,
|
|
1360
|
+
uuid: 'env-var-uuid',
|
|
1361
|
+
key: 'API_KEY',
|
|
1362
|
+
value: 'secret123',
|
|
1363
|
+
is_buildtime: false,
|
|
1364
|
+
is_runtime: true,
|
|
1172
1365
|
is_literal: true,
|
|
1173
1366
|
is_multiline: false,
|
|
1174
1367
|
is_preview: false,
|
|
@@ -1180,13 +1373,46 @@ describe('CoolifyClient', () => {
|
|
|
1180
1373
|
};
|
|
1181
1374
|
mockFetch.mockResolvedValueOnce(mockResponse([fullEnvVar]));
|
|
1182
1375
|
const result = await client.listApplicationEnvVars('app-uuid', { summary: true });
|
|
1183
|
-
// Summary should only include uuid, key, value,
|
|
1376
|
+
// Summary should only include uuid, key, value, is_buildtime, is_runtime — and value masked
|
|
1377
|
+
expect(result).toEqual([
|
|
1378
|
+
{
|
|
1379
|
+
uuid: 'env-var-uuid',
|
|
1380
|
+
key: 'API_KEY',
|
|
1381
|
+
value: '***',
|
|
1382
|
+
is_buildtime: false,
|
|
1383
|
+
is_runtime: true,
|
|
1384
|
+
},
|
|
1385
|
+
]);
|
|
1386
|
+
});
|
|
1387
|
+
it('should list application env vars with summary and reveal=true returning real values (#159)', async () => {
|
|
1388
|
+
const fullEnvVar = {
|
|
1389
|
+
id: 1,
|
|
1390
|
+
uuid: 'env-var-uuid',
|
|
1391
|
+
key: 'API_KEY',
|
|
1392
|
+
value: 'secret123',
|
|
1393
|
+
is_buildtime: false,
|
|
1394
|
+
is_runtime: true,
|
|
1395
|
+
is_literal: true,
|
|
1396
|
+
is_multiline: false,
|
|
1397
|
+
is_preview: false,
|
|
1398
|
+
is_shared: false,
|
|
1399
|
+
is_shown_once: false,
|
|
1400
|
+
application_id: 1,
|
|
1401
|
+
created_at: '2024-01-01',
|
|
1402
|
+
updated_at: '2024-01-01',
|
|
1403
|
+
};
|
|
1404
|
+
mockFetch.mockResolvedValueOnce(mockResponse([fullEnvVar]));
|
|
1405
|
+
const result = await client.listApplicationEnvVars('app-uuid', {
|
|
1406
|
+
summary: true,
|
|
1407
|
+
reveal: true,
|
|
1408
|
+
});
|
|
1184
1409
|
expect(result).toEqual([
|
|
1185
1410
|
{
|
|
1186
1411
|
uuid: 'env-var-uuid',
|
|
1187
1412
|
key: 'API_KEY',
|
|
1188
1413
|
value: 'secret123',
|
|
1189
|
-
|
|
1414
|
+
is_buildtime: false,
|
|
1415
|
+
is_runtime: true,
|
|
1190
1416
|
},
|
|
1191
1417
|
]);
|
|
1192
1418
|
});
|
|
@@ -1195,10 +1421,31 @@ describe('CoolifyClient', () => {
|
|
|
1195
1421
|
const result = await client.createApplicationEnvVar('app-uuid', {
|
|
1196
1422
|
key: 'NEW_VAR',
|
|
1197
1423
|
value: 'new-value',
|
|
1198
|
-
|
|
1424
|
+
is_buildtime: true,
|
|
1199
1425
|
});
|
|
1200
1426
|
expect(result).toEqual({ uuid: 'new-env-uuid' });
|
|
1201
1427
|
});
|
|
1428
|
+
it('should create runtime-only env var (no Dockerfile ARG injection)', async () => {
|
|
1429
|
+
// Regression for #135: setting is_buildtime=false avoids multiline values
|
|
1430
|
+
// (PEM keys, etc.) being injected as Dockerfile ARG and breaking the build.
|
|
1431
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-env-uuid' }));
|
|
1432
|
+
await client.createApplicationEnvVar('app-uuid', {
|
|
1433
|
+
key: 'PASSPORT_PRIVATE_KEY',
|
|
1434
|
+
value: '-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----',
|
|
1435
|
+
is_buildtime: false,
|
|
1436
|
+
is_runtime: true,
|
|
1437
|
+
});
|
|
1438
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/envs', expect.objectContaining({
|
|
1439
|
+
method: 'POST',
|
|
1440
|
+
body: expect.stringContaining('"is_buildtime":false'),
|
|
1441
|
+
}));
|
|
1442
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
1443
|
+
expect(body).toMatchObject({
|
|
1444
|
+
key: 'PASSPORT_PRIVATE_KEY',
|
|
1445
|
+
is_buildtime: false,
|
|
1446
|
+
is_runtime: true,
|
|
1447
|
+
});
|
|
1448
|
+
});
|
|
1202
1449
|
it('should update application env var', async () => {
|
|
1203
1450
|
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
|
|
1204
1451
|
const result = await client.updateApplicationEnvVar('app-uuid', {
|
|
@@ -1207,6 +1454,22 @@ describe('CoolifyClient', () => {
|
|
|
1207
1454
|
});
|
|
1208
1455
|
expect(result).toEqual({ message: 'Updated' });
|
|
1209
1456
|
});
|
|
1457
|
+
it('should update env var to runtime-only (flip is_buildtime=false)', async () => {
|
|
1458
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
|
|
1459
|
+
await client.updateApplicationEnvVar('app-uuid', {
|
|
1460
|
+
key: 'NODE_ENV',
|
|
1461
|
+
value: 'production',
|
|
1462
|
+
is_buildtime: false,
|
|
1463
|
+
is_runtime: true,
|
|
1464
|
+
});
|
|
1465
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
1466
|
+
expect(body).toEqual({
|
|
1467
|
+
key: 'NODE_ENV',
|
|
1468
|
+
value: 'production',
|
|
1469
|
+
is_buildtime: false,
|
|
1470
|
+
is_runtime: true,
|
|
1471
|
+
});
|
|
1472
|
+
});
|
|
1210
1473
|
it('should bulk update application env vars', async () => {
|
|
1211
1474
|
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
|
|
1212
1475
|
const result = await client.bulkUpdateApplicationEnvVars('app-uuid', {
|
|
@@ -1482,12 +1745,54 @@ describe('CoolifyClient', () => {
|
|
|
1482
1745
|
key: 'SVC_KEY',
|
|
1483
1746
|
value: 'svc-value',
|
|
1484
1747
|
};
|
|
1485
|
-
it('should list service env vars', async () => {
|
|
1748
|
+
it('should list service env vars with values masked by default (#159)', async () => {
|
|
1486
1749
|
mockFetch.mockResolvedValueOnce(mockResponse([mockEnvVar]));
|
|
1487
1750
|
const result = await client.listServiceEnvVars('test-uuid');
|
|
1488
|
-
|
|
1751
|
+
// value masked, metadata (uuid, key) preserved
|
|
1752
|
+
expect(result).toEqual([
|
|
1753
|
+
{
|
|
1754
|
+
uuid: 'svc-env-uuid',
|
|
1755
|
+
key: 'SVC_KEY',
|
|
1756
|
+
value: '***',
|
|
1757
|
+
},
|
|
1758
|
+
]);
|
|
1489
1759
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid/envs', expect.any(Object));
|
|
1490
1760
|
});
|
|
1761
|
+
it('should list service env vars with real_value masked on the full projection (#159)', async () => {
|
|
1762
|
+
const fullEnvVar = {
|
|
1763
|
+
id: 1,
|
|
1764
|
+
uuid: 'svc-env-uuid',
|
|
1765
|
+
key: 'SVC_KEY',
|
|
1766
|
+
value: 'svc-value',
|
|
1767
|
+
real_value: 'svc-value',
|
|
1768
|
+
is_buildtime: false,
|
|
1769
|
+
is_runtime: true,
|
|
1770
|
+
is_literal: true,
|
|
1771
|
+
is_multiline: false,
|
|
1772
|
+
is_preview: false,
|
|
1773
|
+
is_shared: false,
|
|
1774
|
+
is_shown_once: false,
|
|
1775
|
+
service_id: 42,
|
|
1776
|
+
created_at: '2024-01-01',
|
|
1777
|
+
updated_at: '2024-01-01',
|
|
1778
|
+
};
|
|
1779
|
+
mockFetch.mockResolvedValueOnce(mockResponse([fullEnvVar]));
|
|
1780
|
+
const result = await client.listServiceEnvVars('test-uuid');
|
|
1781
|
+
expect(result[0].value).toBe('***');
|
|
1782
|
+
expect(result[0].real_value).toBe('***');
|
|
1783
|
+
expect(result[0]).toMatchObject({
|
|
1784
|
+
uuid: 'svc-env-uuid',
|
|
1785
|
+
key: 'SVC_KEY',
|
|
1786
|
+
is_buildtime: false,
|
|
1787
|
+
is_runtime: true,
|
|
1788
|
+
service_id: 42,
|
|
1789
|
+
});
|
|
1790
|
+
});
|
|
1791
|
+
it('should list service env vars with real values when reveal=true (#159)', async () => {
|
|
1792
|
+
mockFetch.mockResolvedValueOnce(mockResponse([mockEnvVar]));
|
|
1793
|
+
const result = await client.listServiceEnvVars('test-uuid', { reveal: true });
|
|
1794
|
+
expect(result).toEqual([mockEnvVar]);
|
|
1795
|
+
});
|
|
1491
1796
|
it('should create service env var', async () => {
|
|
1492
1797
|
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-env-uuid' }));
|
|
1493
1798
|
const result = await client.createServiceEnvVar('test-uuid', {
|
|
@@ -2008,9 +2313,17 @@ describe('CoolifyClient', () => {
|
|
|
2008
2313
|
uuid: 'env-1',
|
|
2009
2314
|
key: 'DATABASE_URL',
|
|
2010
2315
|
value: 'postgres://...',
|
|
2011
|
-
|
|
2316
|
+
is_buildtime: false,
|
|
2317
|
+
is_runtime: true,
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
id: 2,
|
|
2321
|
+
uuid: 'env-2',
|
|
2322
|
+
key: 'NODE_ENV',
|
|
2323
|
+
value: 'production',
|
|
2324
|
+
is_buildtime: true,
|
|
2325
|
+
is_runtime: true,
|
|
2012
2326
|
},
|
|
2013
|
-
{ id: 2, uuid: 'env-2', key: 'NODE_ENV', value: 'production', is_build_time: true },
|
|
2014
2327
|
];
|
|
2015
2328
|
const mockDeployments = [
|
|
2016
2329
|
{
|
|
@@ -2057,12 +2370,28 @@ describe('CoolifyClient', () => {
|
|
|
2057
2370
|
expect(result.logs).toBe(mockLogs);
|
|
2058
2371
|
expect(result.environment_variables.count).toBe(2);
|
|
2059
2372
|
expect(result.environment_variables.variables).toEqual([
|
|
2060
|
-
{ key: 'DATABASE_URL',
|
|
2061
|
-
{ key: 'NODE_ENV',
|
|
2373
|
+
{ key: 'DATABASE_URL', is_buildtime: false, is_runtime: true },
|
|
2374
|
+
{ key: 'NODE_ENV', is_buildtime: true, is_runtime: true },
|
|
2062
2375
|
]);
|
|
2063
2376
|
expect(result.recent_deployments).toHaveLength(2);
|
|
2064
2377
|
expect(result.errors).toBeUndefined();
|
|
2065
2378
|
});
|
|
2379
|
+
it('should apply default flags when env var omits is_buildtime/is_runtime', async () => {
|
|
2380
|
+
// Hits the `?? false` / `?? true` fallback branches in the diagnose mapping
|
|
2381
|
+
// for legacy Coolify responses that don't carry both flags explicitly.
|
|
2382
|
+
const envVarsMissingFlags = [
|
|
2383
|
+
{ id: 1, uuid: 'env-1', key: 'LEGACY_VAR', value: 'x' },
|
|
2384
|
+
];
|
|
2385
|
+
mockFetch
|
|
2386
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
2387
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
2388
|
+
.mockResolvedValueOnce(mockResponse(envVarsMissingFlags))
|
|
2389
|
+
.mockResolvedValueOnce(mockResponse({ count: 0, deployments: [] }));
|
|
2390
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
2391
|
+
expect(result.environment_variables.variables).toEqual([
|
|
2392
|
+
{ key: 'LEGACY_VAR', is_buildtime: false, is_runtime: true },
|
|
2393
|
+
]);
|
|
2394
|
+
});
|
|
2066
2395
|
it('should detect unhealthy application status', async () => {
|
|
2067
2396
|
const unhealthyApp = { ...mockApp, status: 'exited:unhealthy' };
|
|
2068
2397
|
mockFetch
|
|
@@ -2543,15 +2872,40 @@ describe('CoolifyClient', () => {
|
|
|
2543
2872
|
// No API calls should be made
|
|
2544
2873
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
2545
2874
|
});
|
|
2546
|
-
it('should send
|
|
2875
|
+
it('should send buildtime flag when specified', async () => {
|
|
2547
2876
|
mockFetch
|
|
2548
2877
|
.mockResolvedValueOnce(mockResponse(mockApps))
|
|
2549
2878
|
.mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
|
|
2550
2879
|
await client.bulkEnvUpdate(['app-1'], 'BUILD_VAR', 'value', true);
|
|
2551
|
-
// Verify the PATCH call was made with
|
|
2880
|
+
// Verify the PATCH call was made with is_buildtime (one word — Coolify API field name)
|
|
2881
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-1/envs', expect.objectContaining({
|
|
2882
|
+
method: 'PATCH',
|
|
2883
|
+
body: JSON.stringify({ key: 'BUILD_VAR', value: 'value', is_buildtime: true }),
|
|
2884
|
+
}));
|
|
2885
|
+
});
|
|
2886
|
+
it('should send both buildtime and runtime flags for runtime-only vars', async () => {
|
|
2887
|
+
mockFetch
|
|
2888
|
+
.mockResolvedValueOnce(mockResponse(mockApps))
|
|
2889
|
+
.mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
|
|
2890
|
+
await client.bulkEnvUpdate(['app-1'], 'PEM_KEY', 'multiline-value', false, true);
|
|
2891
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-1/envs', expect.objectContaining({
|
|
2892
|
+
method: 'PATCH',
|
|
2893
|
+
body: JSON.stringify({
|
|
2894
|
+
key: 'PEM_KEY',
|
|
2895
|
+
value: 'multiline-value',
|
|
2896
|
+
is_buildtime: false,
|
|
2897
|
+
is_runtime: true,
|
|
2898
|
+
}),
|
|
2899
|
+
}));
|
|
2900
|
+
});
|
|
2901
|
+
it('should send only is_runtime when buildtime is left undefined', async () => {
|
|
2902
|
+
mockFetch
|
|
2903
|
+
.mockResolvedValueOnce(mockResponse(mockApps))
|
|
2904
|
+
.mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
|
|
2905
|
+
await client.bulkEnvUpdate(['app-1'], 'API_KEY', 'val', undefined, false);
|
|
2552
2906
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-1/envs', expect.objectContaining({
|
|
2553
2907
|
method: 'PATCH',
|
|
2554
|
-
body: JSON.stringify({ key: '
|
|
2908
|
+
body: JSON.stringify({ key: 'API_KEY', value: 'val', is_runtime: false }),
|
|
2555
2909
|
}));
|
|
2556
2910
|
});
|
|
2557
2911
|
});
|
|
@@ -54,11 +54,12 @@ describeFn('Diagnostic Integration Tests', () => {
|
|
|
54
54
|
expect(result.environment_variables).toBeDefined();
|
|
55
55
|
expect(typeof result.environment_variables.count).toBe('number');
|
|
56
56
|
expect(Array.isArray(result.environment_variables.variables)).toBe(true);
|
|
57
|
-
// Values should be hidden (only key
|
|
57
|
+
// Values should be hidden (only key, is_buildtime, is_runtime exposed)
|
|
58
58
|
if (result.environment_variables.variables.length > 0) {
|
|
59
59
|
const firstVar = result.environment_variables.variables[0];
|
|
60
60
|
expect(firstVar).toHaveProperty('key');
|
|
61
|
-
expect(firstVar).toHaveProperty('
|
|
61
|
+
expect(firstVar).toHaveProperty('is_buildtime');
|
|
62
|
+
expect(firstVar).toHaveProperty('is_runtime');
|
|
62
63
|
expect(firstVar).not.toHaveProperty('value');
|
|
63
64
|
}
|
|
64
65
|
// Should have recent deployments array
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* These tests verify MCP server instantiation and structure.
|
|
7
7
|
*/
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
|
-
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
9
|
+
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
10
10
|
import { CoolifyMcpServer, VERSION, truncateLogs, getApplicationActions, getDeploymentActions, getPagination, } from '../lib/mcp-server.js';
|
|
11
11
|
describe('CoolifyMcpServer v2', () => {
|
|
12
12
|
let server;
|
|
@@ -161,6 +161,268 @@ describe('CoolifyMcpServer v2', () => {
|
|
|
161
161
|
expect(client['accessToken']).toBe('test-token');
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
|
+
describe('env_vars tool handler', () => {
|
|
165
|
+
// Reach the SDK-registered handler so the is_buildtime / is_runtime
|
|
166
|
+
// passthrough lines are actually executed (not just type-checked).
|
|
167
|
+
const callEnvVars = async (srv, args) => {
|
|
168
|
+
const tool = srv._registeredTools['env_vars'];
|
|
169
|
+
return tool.handler(args, {});
|
|
170
|
+
};
|
|
171
|
+
it('forwards is_buildtime/is_runtime to createApplicationEnvVar', async () => {
|
|
172
|
+
const spy = jest
|
|
173
|
+
.spyOn(server['client'], 'createApplicationEnvVar')
|
|
174
|
+
.mockResolvedValue({ uuid: 'env-1' });
|
|
175
|
+
await callEnvVars(server, {
|
|
176
|
+
resource: 'application',
|
|
177
|
+
action: 'create',
|
|
178
|
+
uuid: 'app-uuid',
|
|
179
|
+
key: 'PEM_KEY',
|
|
180
|
+
value: '-----BEGIN-----',
|
|
181
|
+
is_buildtime: false,
|
|
182
|
+
is_runtime: true,
|
|
183
|
+
});
|
|
184
|
+
expect(spy).toHaveBeenCalledWith('app-uuid', {
|
|
185
|
+
key: 'PEM_KEY',
|
|
186
|
+
value: '-----BEGIN-----',
|
|
187
|
+
is_buildtime: false,
|
|
188
|
+
is_runtime: true,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
it('forwards is_buildtime/is_runtime to updateApplicationEnvVar', async () => {
|
|
192
|
+
const spy = jest
|
|
193
|
+
.spyOn(server['client'], 'updateApplicationEnvVar')
|
|
194
|
+
.mockResolvedValue({ message: 'Updated' });
|
|
195
|
+
await callEnvVars(server, {
|
|
196
|
+
resource: 'application',
|
|
197
|
+
action: 'update',
|
|
198
|
+
uuid: 'app-uuid',
|
|
199
|
+
key: 'NODE_ENV',
|
|
200
|
+
value: 'production',
|
|
201
|
+
is_buildtime: false,
|
|
202
|
+
is_runtime: true,
|
|
203
|
+
});
|
|
204
|
+
expect(spy).toHaveBeenCalledWith('app-uuid', {
|
|
205
|
+
key: 'NODE_ENV',
|
|
206
|
+
value: 'production',
|
|
207
|
+
is_buildtime: false,
|
|
208
|
+
is_runtime: true,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
it('forwards is_buildtime/is_runtime to createServiceEnvVar', async () => {
|
|
212
|
+
const spy = jest
|
|
213
|
+
.spyOn(server['client'], 'createServiceEnvVar')
|
|
214
|
+
.mockResolvedValue({ uuid: 'env-1' });
|
|
215
|
+
await callEnvVars(server, {
|
|
216
|
+
resource: 'service',
|
|
217
|
+
action: 'create',
|
|
218
|
+
uuid: 'svc-uuid',
|
|
219
|
+
key: 'API_KEY',
|
|
220
|
+
value: 'secret',
|
|
221
|
+
is_buildtime: true,
|
|
222
|
+
is_runtime: undefined,
|
|
223
|
+
});
|
|
224
|
+
expect(spy).toHaveBeenCalledWith('svc-uuid', {
|
|
225
|
+
key: 'API_KEY',
|
|
226
|
+
value: 'secret',
|
|
227
|
+
is_buildtime: true,
|
|
228
|
+
is_runtime: undefined,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
it('returns key/value error when create is missing required fields', async () => {
|
|
232
|
+
const result = (await callEnvVars(server, {
|
|
233
|
+
resource: 'application',
|
|
234
|
+
action: 'create',
|
|
235
|
+
uuid: 'app-uuid',
|
|
236
|
+
}));
|
|
237
|
+
expect(result.content[0].text).toContain('key, value required');
|
|
238
|
+
});
|
|
239
|
+
it('returns key/value error when service create is missing required fields', async () => {
|
|
240
|
+
const result = (await callEnvVars(server, {
|
|
241
|
+
resource: 'service',
|
|
242
|
+
action: 'create',
|
|
243
|
+
uuid: 'svc-uuid',
|
|
244
|
+
}));
|
|
245
|
+
expect(result.content[0].text).toContain('key, value required');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
describe('bulk_env_update tool handler', () => {
|
|
249
|
+
it('forwards is_buildtime/is_runtime to bulkEnvUpdate', async () => {
|
|
250
|
+
const spy = jest.spyOn(server['client'], 'bulkEnvUpdate').mockResolvedValue({
|
|
251
|
+
summary: { total: 2, succeeded: 2, failed: 0 },
|
|
252
|
+
succeeded: [],
|
|
253
|
+
failed: [],
|
|
254
|
+
});
|
|
255
|
+
const tool = server._registeredTools['bulk_env_update'];
|
|
256
|
+
await tool.handler({
|
|
257
|
+
app_uuids: ['app-1', 'app-2'],
|
|
258
|
+
key: 'PEM_KEY',
|
|
259
|
+
value: 'multiline',
|
|
260
|
+
is_buildtime: false,
|
|
261
|
+
is_runtime: true,
|
|
262
|
+
}, {});
|
|
263
|
+
expect(spy).toHaveBeenCalledWith(['app-1', 'app-2'], 'PEM_KEY', 'multiline', false, true);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
describe('application tool handler', () => {
|
|
267
|
+
// Regression for #178 — verify the application tool's create_* hand-picks
|
|
268
|
+
// forward build-config and health_check_* fields to the client. Previously
|
|
269
|
+
// these fields were accepted by zod but silently dropped by the hand-pick.
|
|
270
|
+
const callApplication = async (srv, args) => {
|
|
271
|
+
const tool = srv._registeredTools['application'];
|
|
272
|
+
return tool.handler(args, {});
|
|
273
|
+
};
|
|
274
|
+
const baseCreatePublic = {
|
|
275
|
+
action: 'create_public',
|
|
276
|
+
project_uuid: 'proj-uuid',
|
|
277
|
+
server_uuid: 'server-uuid',
|
|
278
|
+
git_repository: 'https://github.com/org/monorepo',
|
|
279
|
+
git_branch: 'main',
|
|
280
|
+
build_pack: 'dockerfile',
|
|
281
|
+
ports_exposes: '3000',
|
|
282
|
+
};
|
|
283
|
+
it('forwards build-config and health_check fields in create_public', async () => {
|
|
284
|
+
const spy = jest
|
|
285
|
+
.spyOn(server['client'], 'createApplicationPublic')
|
|
286
|
+
.mockResolvedValue({ uuid: 'app-1' });
|
|
287
|
+
await callApplication(server, {
|
|
288
|
+
...baseCreatePublic,
|
|
289
|
+
base_directory: '/apps/api',
|
|
290
|
+
publish_directory: '/dist',
|
|
291
|
+
install_command: 'pnpm install',
|
|
292
|
+
build_command: 'pnpm build',
|
|
293
|
+
start_command: 'node dist/main.js',
|
|
294
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
295
|
+
watch_paths: 'apps/api/**',
|
|
296
|
+
health_check_enabled: true,
|
|
297
|
+
health_check_path: '/health',
|
|
298
|
+
health_check_port: 3000,
|
|
299
|
+
health_check_start_period: 60,
|
|
300
|
+
});
|
|
301
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({
|
|
302
|
+
base_directory: '/apps/api',
|
|
303
|
+
publish_directory: '/dist',
|
|
304
|
+
install_command: 'pnpm install',
|
|
305
|
+
build_command: 'pnpm build',
|
|
306
|
+
start_command: 'node dist/main.js',
|
|
307
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
308
|
+
watch_paths: 'apps/api/**',
|
|
309
|
+
health_check_enabled: true,
|
|
310
|
+
health_check_path: '/health',
|
|
311
|
+
health_check_port: 3000,
|
|
312
|
+
health_check_start_period: 60,
|
|
313
|
+
}));
|
|
314
|
+
});
|
|
315
|
+
it('forwards build-config and health_check fields in create_github', async () => {
|
|
316
|
+
const spy = jest
|
|
317
|
+
.spyOn(server['client'], 'createApplicationPrivateGH')
|
|
318
|
+
.mockResolvedValue({ uuid: 'app-2' });
|
|
319
|
+
await callApplication(server, {
|
|
320
|
+
action: 'create_github',
|
|
321
|
+
project_uuid: 'proj-uuid',
|
|
322
|
+
server_uuid: 'server-uuid',
|
|
323
|
+
github_app_uuid: 'gh-app-uuid',
|
|
324
|
+
git_repository: 'org/monorepo',
|
|
325
|
+
git_branch: 'main',
|
|
326
|
+
base_directory: '/apps/api',
|
|
327
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
328
|
+
watch_paths: 'apps/api/**',
|
|
329
|
+
health_check_enabled: true,
|
|
330
|
+
health_check_path: '/health',
|
|
331
|
+
});
|
|
332
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({
|
|
333
|
+
base_directory: '/apps/api',
|
|
334
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
335
|
+
watch_paths: 'apps/api/**',
|
|
336
|
+
health_check_enabled: true,
|
|
337
|
+
health_check_path: '/health',
|
|
338
|
+
}));
|
|
339
|
+
});
|
|
340
|
+
it('forwards build-config and health_check fields in create_key', async () => {
|
|
341
|
+
const spy = jest
|
|
342
|
+
.spyOn(server['client'], 'createApplicationPrivateKey')
|
|
343
|
+
.mockResolvedValue({ uuid: 'app-3' });
|
|
344
|
+
await callApplication(server, {
|
|
345
|
+
action: 'create_key',
|
|
346
|
+
project_uuid: 'proj-uuid',
|
|
347
|
+
server_uuid: 'server-uuid',
|
|
348
|
+
private_key_uuid: 'key-uuid',
|
|
349
|
+
git_repository: 'git@github.com:org/monorepo.git',
|
|
350
|
+
git_branch: 'main',
|
|
351
|
+
base_directory: '/apps/api',
|
|
352
|
+
publish_directory: '/dist',
|
|
353
|
+
install_command: 'pnpm install',
|
|
354
|
+
build_command: 'pnpm build',
|
|
355
|
+
start_command: 'node dist/main.js',
|
|
356
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
357
|
+
watch_paths: 'apps/api/**',
|
|
358
|
+
health_check_enabled: true,
|
|
359
|
+
health_check_path: '/health',
|
|
360
|
+
health_check_port: 3000,
|
|
361
|
+
});
|
|
362
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({
|
|
363
|
+
base_directory: '/apps/api',
|
|
364
|
+
publish_directory: '/dist',
|
|
365
|
+
install_command: 'pnpm install',
|
|
366
|
+
build_command: 'pnpm build',
|
|
367
|
+
start_command: 'node dist/main.js',
|
|
368
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
369
|
+
watch_paths: 'apps/api/**',
|
|
370
|
+
health_check_enabled: true,
|
|
371
|
+
health_check_path: '/health',
|
|
372
|
+
health_check_port: 3000,
|
|
373
|
+
}));
|
|
374
|
+
});
|
|
375
|
+
it('forwards health_check fields in create_dockerimage (build-config intentionally dropped)', async () => {
|
|
376
|
+
const spy = jest
|
|
377
|
+
.spyOn(server['client'], 'createApplicationDockerImage')
|
|
378
|
+
.mockResolvedValue({ uuid: 'app-4' });
|
|
379
|
+
// Caller passes both healthcheck AND build-config. Coolify's /applications/dockerimage
|
|
380
|
+
// endpoint doesn't accept build-config (pre-built image), so handler must drop those.
|
|
381
|
+
await callApplication(server, {
|
|
382
|
+
action: 'create_dockerimage',
|
|
383
|
+
project_uuid: 'proj-uuid',
|
|
384
|
+
server_uuid: 'server-uuid',
|
|
385
|
+
docker_registry_image_name: 'traefik/whoami',
|
|
386
|
+
ports_exposes: '80',
|
|
387
|
+
// Should be forwarded:
|
|
388
|
+
health_check_enabled: true,
|
|
389
|
+
health_check_path: '/health',
|
|
390
|
+
health_check_port: 80,
|
|
391
|
+
// Should NOT be forwarded (build-config not applicable to prebuilt image):
|
|
392
|
+
base_directory: '/should-be-dropped',
|
|
393
|
+
install_command: 'should-be-dropped',
|
|
394
|
+
dockerfile_location: '/should-be-dropped',
|
|
395
|
+
});
|
|
396
|
+
const forwarded = spy.mock.calls[0]?.[0];
|
|
397
|
+
expect(forwarded).toEqual(expect.objectContaining({
|
|
398
|
+
health_check_enabled: true,
|
|
399
|
+
health_check_path: '/health',
|
|
400
|
+
health_check_port: 80,
|
|
401
|
+
}));
|
|
402
|
+
expect(forwarded).not.toHaveProperty('base_directory');
|
|
403
|
+
expect(forwarded).not.toHaveProperty('install_command');
|
|
404
|
+
expect(forwarded).not.toHaveProperty('dockerfile_location');
|
|
405
|
+
});
|
|
406
|
+
it('forwards dockerfile_target_build through update (PATCH-only)', async () => {
|
|
407
|
+
const spy = jest.spyOn(server['client'], 'updateApplication').mockResolvedValue({});
|
|
408
|
+
await callApplication(server, {
|
|
409
|
+
action: 'update',
|
|
410
|
+
uuid: 'app-uuid',
|
|
411
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
412
|
+
dockerfile_target_build: 'production',
|
|
413
|
+
base_directory: '/apps/api',
|
|
414
|
+
});
|
|
415
|
+
expect(spy).toHaveBeenCalledWith('app-uuid', expect.objectContaining({
|
|
416
|
+
dockerfile_location: '/apps/api/Dockerfile',
|
|
417
|
+
dockerfile_target_build: 'production',
|
|
418
|
+
base_directory: '/apps/api',
|
|
419
|
+
}));
|
|
420
|
+
// Confirm the update spread strips routing fields.
|
|
421
|
+
const updateData = spy.mock.calls[0]?.[1];
|
|
422
|
+
expect(updateData).not.toHaveProperty('action');
|
|
423
|
+
expect(updateData).not.toHaveProperty('uuid');
|
|
424
|
+
});
|
|
425
|
+
});
|
|
164
426
|
});
|
|
165
427
|
describe('truncateLogs', () => {
|
|
166
428
|
// Plain text log tests
|
|
@@ -125,8 +125,16 @@ export declare class CoolifyClient {
|
|
|
125
125
|
}): Promise<ApplicationActionResponse>;
|
|
126
126
|
stopApplication(uuid: string): Promise<ApplicationActionResponse>;
|
|
127
127
|
restartApplication(uuid: string): Promise<ApplicationActionResponse>;
|
|
128
|
+
/**
|
|
129
|
+
* List env vars for an application.
|
|
130
|
+
*
|
|
131
|
+
* Default behaviour masks `value` (and `real_value` on the full projection)
|
|
132
|
+
* with a sentinel string so secrets are not leaked to MCP clients. Pass
|
|
133
|
+
* `reveal: true` when the caller explicitly needs the plaintext value.
|
|
134
|
+
*/
|
|
128
135
|
listApplicationEnvVars(uuid: string, options?: {
|
|
129
136
|
summary?: boolean;
|
|
137
|
+
reveal?: boolean;
|
|
130
138
|
}): Promise<EnvironmentVariable[] | EnvVarSummary[]>;
|
|
131
139
|
createApplicationEnvVar(uuid: string, data: CreateEnvVarRequest): Promise<UuidResponse>;
|
|
132
140
|
updateApplicationEnvVar(uuid: string, data: UpdateEnvVarRequest): Promise<MessageResponse>;
|
|
@@ -155,7 +163,16 @@ export declare class CoolifyClient {
|
|
|
155
163
|
startService(uuid: string): Promise<MessageResponse>;
|
|
156
164
|
stopService(uuid: string): Promise<MessageResponse>;
|
|
157
165
|
restartService(uuid: string): Promise<MessageResponse>;
|
|
158
|
-
|
|
166
|
+
/**
|
|
167
|
+
* List env vars for a service.
|
|
168
|
+
*
|
|
169
|
+
* Default behaviour masks `value` (and `real_value`) with a sentinel string
|
|
170
|
+
* so secrets are not leaked to MCP clients. Pass `reveal: true` when the
|
|
171
|
+
* caller explicitly needs the plaintext value.
|
|
172
|
+
*/
|
|
173
|
+
listServiceEnvVars(uuid: string, options?: {
|
|
174
|
+
reveal?: boolean;
|
|
175
|
+
}): Promise<EnvironmentVariable[]>;
|
|
159
176
|
createServiceEnvVar(uuid: string, data: CreateEnvVarRequest): Promise<UuidResponse>;
|
|
160
177
|
updateServiceEnvVar(uuid: string, data: UpdateEnvVarRequest): Promise<MessageResponse>;
|
|
161
178
|
deleteServiceEnvVar(uuid: string, envUuid: string): Promise<MessageResponse>;
|
|
@@ -257,9 +274,10 @@ export declare class CoolifyClient {
|
|
|
257
274
|
* @param appUuids - Array of application UUIDs
|
|
258
275
|
* @param key - Environment variable key
|
|
259
276
|
* @param value - Environment variable value
|
|
260
|
-
* @param
|
|
277
|
+
* @param isBuildtime - Sets the build-time flag on the variable when provided
|
|
278
|
+
* @param isRuntime - Sets the runtime flag on the variable when provided
|
|
261
279
|
*/
|
|
262
|
-
bulkEnvUpdate(appUuids: string[], key: string, value: string,
|
|
280
|
+
bulkEnvUpdate(appUuids: string[], key: string, value: string, isBuildtime?: boolean, isRuntime?: boolean): Promise<BatchOperationResult>;
|
|
263
281
|
/**
|
|
264
282
|
* Emergency stop all running applications across entire infrastructure.
|
|
265
283
|
*/
|
|
@@ -142,7 +142,42 @@ function toEnvVarSummary(envVar) {
|
|
|
142
142
|
uuid: envVar.uuid,
|
|
143
143
|
key: envVar.key,
|
|
144
144
|
value: envVar.value,
|
|
145
|
-
|
|
145
|
+
is_buildtime: envVar.is_buildtime,
|
|
146
|
+
is_runtime: envVar.is_runtime,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Sentinel string used to replace plaintext env var values when masking.
|
|
151
|
+
* Exported via behaviour, not as a public API — clients should treat any
|
|
152
|
+
* non-real string as "value not returned".
|
|
153
|
+
*/
|
|
154
|
+
const MASKED_VALUE = '***';
|
|
155
|
+
/**
|
|
156
|
+
* Mask the `value` and `real_value` fields on a full {@link EnvironmentVariable}.
|
|
157
|
+
* All other metadata (uuid, key, flags, timestamps, ids) is preserved verbatim.
|
|
158
|
+
*
|
|
159
|
+
* Applied at the API boundary so callers cannot accidentally leak secrets to
|
|
160
|
+
* an LLM client by forgetting to strip values downstream. Pair with the
|
|
161
|
+
* `reveal: true` opt-in on list methods when the caller genuinely needs the
|
|
162
|
+
* plaintext (e.g. "what is FOO set to right now?").
|
|
163
|
+
*/
|
|
164
|
+
function maskEnvVar(envVar) {
|
|
165
|
+
const masked = {
|
|
166
|
+
...envVar,
|
|
167
|
+
value: MASKED_VALUE,
|
|
168
|
+
};
|
|
169
|
+
if (envVar.real_value !== undefined) {
|
|
170
|
+
masked.real_value = MASKED_VALUE;
|
|
171
|
+
}
|
|
172
|
+
return masked;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Mask the `value` field on an {@link EnvVarSummary}. Metadata is preserved.
|
|
176
|
+
*/
|
|
177
|
+
function maskEnvVarSummary(envVar) {
|
|
178
|
+
return {
|
|
179
|
+
...envVar,
|
|
180
|
+
value: MASKED_VALUE,
|
|
146
181
|
};
|
|
147
182
|
}
|
|
148
183
|
/**
|
|
@@ -192,7 +227,22 @@ export class CoolifyClient {
|
|
|
192
227
|
});
|
|
193
228
|
// Handle empty responses (204 No Content, etc.)
|
|
194
229
|
const text = await response.text();
|
|
195
|
-
const
|
|
230
|
+
const contentType = response.headers?.get('Content-Type')?.toLowerCase() ?? '';
|
|
231
|
+
const isJsonResponse = !contentType || contentType.includes('application/json') || contentType.includes('+json');
|
|
232
|
+
let data = {};
|
|
233
|
+
if (text) {
|
|
234
|
+
if (isJsonResponse) {
|
|
235
|
+
try {
|
|
236
|
+
data = JSON.parse(text);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
data = text;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
data = text;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
196
246
|
if (!response.ok) {
|
|
197
247
|
const error = data;
|
|
198
248
|
// Include validation errors if present
|
|
@@ -476,9 +526,21 @@ export class CoolifyClient {
|
|
|
476
526
|
// ===========================================================================
|
|
477
527
|
// Application Environment Variables
|
|
478
528
|
// ===========================================================================
|
|
529
|
+
/**
|
|
530
|
+
* List env vars for an application.
|
|
531
|
+
*
|
|
532
|
+
* Default behaviour masks `value` (and `real_value` on the full projection)
|
|
533
|
+
* with a sentinel string so secrets are not leaked to MCP clients. Pass
|
|
534
|
+
* `reveal: true` when the caller explicitly needs the plaintext value.
|
|
535
|
+
*/
|
|
479
536
|
async listApplicationEnvVars(uuid, options) {
|
|
480
537
|
const envVars = await this.request(`/applications/${uuid}/envs`);
|
|
481
|
-
|
|
538
|
+
const reveal = options?.reveal === true;
|
|
539
|
+
if (options?.summary) {
|
|
540
|
+
const summaries = envVars.map(toEnvVarSummary);
|
|
541
|
+
return reveal ? summaries : summaries.map(maskEnvVarSummary);
|
|
542
|
+
}
|
|
543
|
+
return reveal ? envVars : envVars.map(maskEnvVar);
|
|
482
544
|
}
|
|
483
545
|
async createApplicationEnvVar(uuid, data) {
|
|
484
546
|
return this.request(`/applications/${uuid}/envs`, {
|
|
@@ -661,8 +723,16 @@ export class CoolifyClient {
|
|
|
661
723
|
// ===========================================================================
|
|
662
724
|
// Service Environment Variables
|
|
663
725
|
// ===========================================================================
|
|
664
|
-
|
|
665
|
-
|
|
726
|
+
/**
|
|
727
|
+
* List env vars for a service.
|
|
728
|
+
*
|
|
729
|
+
* Default behaviour masks `value` (and `real_value`) with a sentinel string
|
|
730
|
+
* so secrets are not leaked to MCP clients. Pass `reveal: true` when the
|
|
731
|
+
* caller explicitly needs the plaintext value.
|
|
732
|
+
*/
|
|
733
|
+
async listServiceEnvVars(uuid, options) {
|
|
734
|
+
const envVars = await this.request(`/services/${uuid}/envs`);
|
|
735
|
+
return options?.reveal === true ? envVars : envVars.map(maskEnvVar);
|
|
666
736
|
}
|
|
667
737
|
async createServiceEnvVar(uuid, data) {
|
|
668
738
|
return this.request(`/services/${uuid}/envs`, {
|
|
@@ -1026,7 +1096,8 @@ export class CoolifyClient {
|
|
|
1026
1096
|
count: envVars?.length || 0,
|
|
1027
1097
|
variables: (envVars || []).map((v) => ({
|
|
1028
1098
|
key: v.key,
|
|
1029
|
-
|
|
1099
|
+
is_buildtime: v.is_buildtime ?? false,
|
|
1100
|
+
is_runtime: v.is_runtime ?? true,
|
|
1030
1101
|
})),
|
|
1031
1102
|
},
|
|
1032
1103
|
recent_deployments: (deployments || []).slice(0, 5).map((d) => ({
|
|
@@ -1292,9 +1363,10 @@ export class CoolifyClient {
|
|
|
1292
1363
|
* @param appUuids - Array of application UUIDs
|
|
1293
1364
|
* @param key - Environment variable key
|
|
1294
1365
|
* @param value - Environment variable value
|
|
1295
|
-
* @param
|
|
1366
|
+
* @param isBuildtime - Sets the build-time flag on the variable when provided
|
|
1367
|
+
* @param isRuntime - Sets the runtime flag on the variable when provided
|
|
1296
1368
|
*/
|
|
1297
|
-
async bulkEnvUpdate(appUuids, key, value,
|
|
1369
|
+
async bulkEnvUpdate(appUuids, key, value, isBuildtime, isRuntime) {
|
|
1298
1370
|
// Early return for empty array - avoid unnecessary API call
|
|
1299
1371
|
if (appUuids.length === 0) {
|
|
1300
1372
|
return {
|
|
@@ -1311,7 +1383,12 @@ export class CoolifyClient {
|
|
|
1311
1383
|
uuid,
|
|
1312
1384
|
name: appMap.get(uuid) || uuid,
|
|
1313
1385
|
}));
|
|
1314
|
-
const results = await Promise.allSettled(appUuids.map((uuid) => this.updateApplicationEnvVar(uuid, {
|
|
1386
|
+
const results = await Promise.allSettled(appUuids.map((uuid) => this.updateApplicationEnvVar(uuid, {
|
|
1387
|
+
key,
|
|
1388
|
+
value,
|
|
1389
|
+
is_buildtime: isBuildtime,
|
|
1390
|
+
is_runtime: isRuntime,
|
|
1391
|
+
})));
|
|
1315
1392
|
return this.aggregateBatchResults(resources, results);
|
|
1316
1393
|
}
|
|
1317
1394
|
/**
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -340,6 +340,18 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
340
340
|
health_check_timeout: z.number().optional(),
|
|
341
341
|
health_check_retries: z.number().optional(),
|
|
342
342
|
health_check_start_period: z.number().optional(),
|
|
343
|
+
// Build configuration fields (accepted on create_public/github/key + update;
|
|
344
|
+
// create_dockerimage ignores these — pre-built image, no build step)
|
|
345
|
+
base_directory: z.string().optional(),
|
|
346
|
+
publish_directory: z.string().optional(),
|
|
347
|
+
install_command: z.string().optional(),
|
|
348
|
+
build_command: z.string().optional(),
|
|
349
|
+
start_command: z.string().optional(),
|
|
350
|
+
dockerfile_location: z.string().optional(),
|
|
351
|
+
watch_paths: z.string().optional(),
|
|
352
|
+
// Update-only: Coolify strips dockerfile_target_build on every create endpoint
|
|
353
|
+
// (controller $allowedFields line 1014) but accepts on PATCH (line 2497).
|
|
354
|
+
dockerfile_target_build: z.string().optional(),
|
|
343
355
|
// Delete fields
|
|
344
356
|
delete_volumes: z.boolean().optional(),
|
|
345
357
|
}, async (args) => {
|
|
@@ -375,6 +387,25 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
375
387
|
description: args.description,
|
|
376
388
|
fqdn: args.fqdn,
|
|
377
389
|
domains: args.domains,
|
|
390
|
+
base_directory: args.base_directory,
|
|
391
|
+
publish_directory: args.publish_directory,
|
|
392
|
+
install_command: args.install_command,
|
|
393
|
+
build_command: args.build_command,
|
|
394
|
+
start_command: args.start_command,
|
|
395
|
+
dockerfile_location: args.dockerfile_location,
|
|
396
|
+
watch_paths: args.watch_paths,
|
|
397
|
+
health_check_enabled: args.health_check_enabled,
|
|
398
|
+
health_check_path: args.health_check_path,
|
|
399
|
+
health_check_port: args.health_check_port,
|
|
400
|
+
health_check_host: args.health_check_host,
|
|
401
|
+
health_check_method: args.health_check_method,
|
|
402
|
+
health_check_return_code: args.health_check_return_code,
|
|
403
|
+
health_check_scheme: args.health_check_scheme,
|
|
404
|
+
health_check_response_text: args.health_check_response_text,
|
|
405
|
+
health_check_interval: args.health_check_interval,
|
|
406
|
+
health_check_timeout: args.health_check_timeout,
|
|
407
|
+
health_check_retries: args.health_check_retries,
|
|
408
|
+
health_check_start_period: args.health_check_start_period,
|
|
378
409
|
custom_docker_run_options: args.custom_docker_run_options,
|
|
379
410
|
custom_labels: args.custom_labels,
|
|
380
411
|
instant_deploy: args.instant_deploy,
|
|
@@ -409,6 +440,25 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
409
440
|
description: args.description,
|
|
410
441
|
fqdn: args.fqdn,
|
|
411
442
|
domains: args.domains,
|
|
443
|
+
base_directory: args.base_directory,
|
|
444
|
+
publish_directory: args.publish_directory,
|
|
445
|
+
install_command: args.install_command,
|
|
446
|
+
build_command: args.build_command,
|
|
447
|
+
start_command: args.start_command,
|
|
448
|
+
dockerfile_location: args.dockerfile_location,
|
|
449
|
+
watch_paths: args.watch_paths,
|
|
450
|
+
health_check_enabled: args.health_check_enabled,
|
|
451
|
+
health_check_path: args.health_check_path,
|
|
452
|
+
health_check_port: args.health_check_port,
|
|
453
|
+
health_check_host: args.health_check_host,
|
|
454
|
+
health_check_method: args.health_check_method,
|
|
455
|
+
health_check_return_code: args.health_check_return_code,
|
|
456
|
+
health_check_scheme: args.health_check_scheme,
|
|
457
|
+
health_check_response_text: args.health_check_response_text,
|
|
458
|
+
health_check_interval: args.health_check_interval,
|
|
459
|
+
health_check_timeout: args.health_check_timeout,
|
|
460
|
+
health_check_retries: args.health_check_retries,
|
|
461
|
+
health_check_start_period: args.health_check_start_period,
|
|
412
462
|
custom_docker_run_options: args.custom_docker_run_options,
|
|
413
463
|
custom_labels: args.custom_labels,
|
|
414
464
|
instant_deploy: args.instant_deploy,
|
|
@@ -443,6 +493,25 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
443
493
|
description: args.description,
|
|
444
494
|
fqdn: args.fqdn,
|
|
445
495
|
domains: args.domains,
|
|
496
|
+
base_directory: args.base_directory,
|
|
497
|
+
publish_directory: args.publish_directory,
|
|
498
|
+
install_command: args.install_command,
|
|
499
|
+
build_command: args.build_command,
|
|
500
|
+
start_command: args.start_command,
|
|
501
|
+
dockerfile_location: args.dockerfile_location,
|
|
502
|
+
watch_paths: args.watch_paths,
|
|
503
|
+
health_check_enabled: args.health_check_enabled,
|
|
504
|
+
health_check_path: args.health_check_path,
|
|
505
|
+
health_check_port: args.health_check_port,
|
|
506
|
+
health_check_host: args.health_check_host,
|
|
507
|
+
health_check_method: args.health_check_method,
|
|
508
|
+
health_check_return_code: args.health_check_return_code,
|
|
509
|
+
health_check_scheme: args.health_check_scheme,
|
|
510
|
+
health_check_response_text: args.health_check_response_text,
|
|
511
|
+
health_check_interval: args.health_check_interval,
|
|
512
|
+
health_check_timeout: args.health_check_timeout,
|
|
513
|
+
health_check_retries: args.health_check_retries,
|
|
514
|
+
health_check_start_period: args.health_check_start_period,
|
|
446
515
|
custom_docker_run_options: args.custom_docker_run_options,
|
|
447
516
|
custom_labels: args.custom_labels,
|
|
448
517
|
instant_deploy: args.instant_deploy,
|
|
@@ -474,6 +543,21 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
474
543
|
description: args.description,
|
|
475
544
|
fqdn: args.fqdn,
|
|
476
545
|
domains: args.domains,
|
|
546
|
+
// Build-config fields (base_directory, install_command, etc.)
|
|
547
|
+
// are intentionally NOT forwarded: /applications/dockerimage is
|
|
548
|
+
// for pre-built registry images and has no build step.
|
|
549
|
+
health_check_enabled: args.health_check_enabled,
|
|
550
|
+
health_check_path: args.health_check_path,
|
|
551
|
+
health_check_port: args.health_check_port,
|
|
552
|
+
health_check_host: args.health_check_host,
|
|
553
|
+
health_check_method: args.health_check_method,
|
|
554
|
+
health_check_return_code: args.health_check_return_code,
|
|
555
|
+
health_check_scheme: args.health_check_scheme,
|
|
556
|
+
health_check_response_text: args.health_check_response_text,
|
|
557
|
+
health_check_interval: args.health_check_interval,
|
|
558
|
+
health_check_timeout: args.health_check_timeout,
|
|
559
|
+
health_check_retries: args.health_check_retries,
|
|
560
|
+
health_check_start_period: args.health_check_start_period,
|
|
477
561
|
custom_docker_run_options: args.custom_docker_run_options,
|
|
478
562
|
custom_labels: args.custom_labels,
|
|
479
563
|
instant_deploy: args.instant_deploy,
|
|
@@ -682,27 +766,39 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
682
766
|
// =========================================================================
|
|
683
767
|
// Environment Variables (1 tool - consolidated)
|
|
684
768
|
// =========================================================================
|
|
685
|
-
this.tool('env_vars',
|
|
769
|
+
this.tool('env_vars', "Manage env vars for app or service. Values are masked by default (returned as '***') to avoid leaking secrets to MCP clients; pass reveal=true on the list action when the caller explicitly needs the plaintext (e.g. 'what is FOO set to?'). Set is_buildtime=false (and/or is_runtime=true) for runtime-only vars to avoid Dockerfile ARG issues with multiline values like PEM keys.", {
|
|
686
770
|
resource: z.enum(['application', 'service']),
|
|
687
771
|
action: z.enum(['list', 'create', 'update', 'delete']),
|
|
688
772
|
uuid: z.string(),
|
|
689
773
|
key: z.string().optional(),
|
|
690
774
|
value: z.string().optional(),
|
|
691
775
|
env_uuid: z.string().optional(),
|
|
692
|
-
|
|
776
|
+
is_buildtime: z.boolean().optional(),
|
|
777
|
+
is_runtime: z.boolean().optional(),
|
|
778
|
+
reveal: z.boolean().optional(),
|
|
779
|
+
}, async ({ resource, action, uuid, key, value, env_uuid, is_buildtime, is_runtime, reveal, }) => {
|
|
693
780
|
if (resource === 'application') {
|
|
694
781
|
switch (action) {
|
|
695
782
|
case 'list':
|
|
696
|
-
return wrap(() => this.client.listApplicationEnvVars(uuid, { summary: true }));
|
|
783
|
+
return wrap(() => this.client.listApplicationEnvVars(uuid, { summary: true, reveal }));
|
|
697
784
|
case 'create':
|
|
698
785
|
if (!key || !value)
|
|
699
786
|
return { content: [{ type: 'text', text: 'Error: key, value required' }] };
|
|
700
|
-
|
|
701
|
-
|
|
787
|
+
return wrap(() => this.client.createApplicationEnvVar(uuid, {
|
|
788
|
+
key,
|
|
789
|
+
value,
|
|
790
|
+
is_buildtime,
|
|
791
|
+
is_runtime,
|
|
792
|
+
}));
|
|
702
793
|
case 'update':
|
|
703
794
|
if (!key || !value)
|
|
704
795
|
return { content: [{ type: 'text', text: 'Error: key, value required' }] };
|
|
705
|
-
return wrap(() => this.client.updateApplicationEnvVar(uuid, {
|
|
796
|
+
return wrap(() => this.client.updateApplicationEnvVar(uuid, {
|
|
797
|
+
key,
|
|
798
|
+
value,
|
|
799
|
+
is_buildtime,
|
|
800
|
+
is_runtime,
|
|
801
|
+
}));
|
|
706
802
|
case 'delete':
|
|
707
803
|
if (!env_uuid)
|
|
708
804
|
return { content: [{ type: 'text', text: 'Error: env_uuid required' }] };
|
|
@@ -712,11 +808,16 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
712
808
|
else {
|
|
713
809
|
switch (action) {
|
|
714
810
|
case 'list':
|
|
715
|
-
return wrap(() => this.client.listServiceEnvVars(uuid));
|
|
811
|
+
return wrap(() => this.client.listServiceEnvVars(uuid, { reveal }));
|
|
716
812
|
case 'create':
|
|
717
813
|
if (!key || !value)
|
|
718
814
|
return { content: [{ type: 'text', text: 'Error: key, value required' }] };
|
|
719
|
-
return wrap(() => this.client.createServiceEnvVar(uuid, {
|
|
815
|
+
return wrap(() => this.client.createServiceEnvVar(uuid, {
|
|
816
|
+
key,
|
|
817
|
+
value,
|
|
818
|
+
is_buildtime,
|
|
819
|
+
is_runtime,
|
|
820
|
+
}));
|
|
720
821
|
case 'update':
|
|
721
822
|
return {
|
|
722
823
|
content: [
|
|
@@ -1056,8 +1157,9 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
1056
1157
|
app_uuids: z.array(z.string()),
|
|
1057
1158
|
key: z.string(),
|
|
1058
1159
|
value: z.string(),
|
|
1059
|
-
|
|
1060
|
-
|
|
1160
|
+
is_buildtime: z.boolean().optional(),
|
|
1161
|
+
is_runtime: z.boolean().optional(),
|
|
1162
|
+
}, async ({ app_uuids, key, value, is_buildtime, is_runtime }) => wrap(() => this.client.bulkEnvUpdate(app_uuids, key, value, is_buildtime, is_runtime)));
|
|
1061
1163
|
this.tool('stop_all_apps', 'EMERGENCY: Stop all running apps', { confirm: z.literal(true) }, async ({ confirm }) => {
|
|
1062
1164
|
if (!confirm)
|
|
1063
1165
|
return { content: [{ type: 'text', text: 'Error: confirm=true required' }] };
|
package/dist/types/coolify.d.ts
CHANGED
|
@@ -224,6 +224,20 @@ export interface CreateApplicationPublicRequest {
|
|
|
224
224
|
install_command?: string;
|
|
225
225
|
build_command?: string;
|
|
226
226
|
start_command?: string;
|
|
227
|
+
dockerfile_location?: string;
|
|
228
|
+
watch_paths?: string;
|
|
229
|
+
health_check_enabled?: boolean;
|
|
230
|
+
health_check_path?: string;
|
|
231
|
+
health_check_port?: number;
|
|
232
|
+
health_check_host?: string;
|
|
233
|
+
health_check_method?: string;
|
|
234
|
+
health_check_return_code?: number;
|
|
235
|
+
health_check_scheme?: string;
|
|
236
|
+
health_check_response_text?: string;
|
|
237
|
+
health_check_interval?: number;
|
|
238
|
+
health_check_timeout?: number;
|
|
239
|
+
health_check_retries?: number;
|
|
240
|
+
health_check_start_period?: number;
|
|
227
241
|
custom_docker_run_options?: string;
|
|
228
242
|
custom_labels?: string;
|
|
229
243
|
instant_deploy?: boolean;
|
|
@@ -271,6 +285,18 @@ export interface CreateApplicationDockerImageRequest {
|
|
|
271
285
|
docker_registry_image_tag?: string;
|
|
272
286
|
ports_exposes: string;
|
|
273
287
|
ports_mappings?: string;
|
|
288
|
+
health_check_enabled?: boolean;
|
|
289
|
+
health_check_path?: string;
|
|
290
|
+
health_check_port?: number;
|
|
291
|
+
health_check_host?: string;
|
|
292
|
+
health_check_method?: string;
|
|
293
|
+
health_check_return_code?: number;
|
|
294
|
+
health_check_scheme?: string;
|
|
295
|
+
health_check_response_text?: string;
|
|
296
|
+
health_check_interval?: number;
|
|
297
|
+
health_check_timeout?: number;
|
|
298
|
+
health_check_retries?: number;
|
|
299
|
+
health_check_start_period?: number;
|
|
274
300
|
custom_docker_run_options?: string;
|
|
275
301
|
custom_labels?: string;
|
|
276
302
|
instant_deploy?: boolean;
|
|
@@ -316,6 +342,8 @@ export interface UpdateApplicationRequest {
|
|
|
316
342
|
install_command?: string;
|
|
317
343
|
build_command?: string;
|
|
318
344
|
start_command?: string;
|
|
345
|
+
dockerfile_target_build?: string;
|
|
346
|
+
watch_paths?: string;
|
|
319
347
|
health_check_enabled?: boolean;
|
|
320
348
|
health_check_path?: string;
|
|
321
349
|
health_check_port?: number;
|
|
@@ -344,7 +372,8 @@ export interface EnvironmentVariable {
|
|
|
344
372
|
uuid: string;
|
|
345
373
|
key: string;
|
|
346
374
|
value: string;
|
|
347
|
-
|
|
375
|
+
is_buildtime: boolean;
|
|
376
|
+
is_runtime: boolean;
|
|
348
377
|
is_literal: boolean;
|
|
349
378
|
is_multiline: boolean;
|
|
350
379
|
is_preview: boolean;
|
|
@@ -365,7 +394,8 @@ export interface CreateEnvVarRequest {
|
|
|
365
394
|
is_literal?: boolean;
|
|
366
395
|
is_multiline?: boolean;
|
|
367
396
|
is_shown_once?: boolean;
|
|
368
|
-
|
|
397
|
+
is_buildtime?: boolean;
|
|
398
|
+
is_runtime?: boolean;
|
|
369
399
|
}
|
|
370
400
|
export interface UpdateEnvVarRequest {
|
|
371
401
|
key: string;
|
|
@@ -374,7 +404,8 @@ export interface UpdateEnvVarRequest {
|
|
|
374
404
|
is_literal?: boolean;
|
|
375
405
|
is_multiline?: boolean;
|
|
376
406
|
is_shown_once?: boolean;
|
|
377
|
-
|
|
407
|
+
is_buildtime?: boolean;
|
|
408
|
+
is_runtime?: boolean;
|
|
378
409
|
}
|
|
379
410
|
export interface BulkUpdateEnvVarsRequest {
|
|
380
411
|
data: CreateEnvVarRequest[];
|
|
@@ -383,7 +414,8 @@ export interface EnvVarSummary {
|
|
|
383
414
|
uuid: string;
|
|
384
415
|
key: string;
|
|
385
416
|
value: string;
|
|
386
|
-
|
|
417
|
+
is_buildtime: boolean;
|
|
418
|
+
is_runtime: boolean;
|
|
387
419
|
}
|
|
388
420
|
export type DatabaseType = 'postgresql' | 'mysql' | 'mariadb' | 'mongodb' | 'redis' | 'keydb' | 'clickhouse' | 'dragonfly';
|
|
389
421
|
export interface DatabaseLimits {
|
|
@@ -844,7 +876,8 @@ export interface ApplicationDiagnostic {
|
|
|
844
876
|
count: number;
|
|
845
877
|
variables: Array<{
|
|
846
878
|
key: string;
|
|
847
|
-
|
|
879
|
+
is_buildtime: boolean;
|
|
880
|
+
is_runtime: boolean;
|
|
848
881
|
}>;
|
|
849
882
|
};
|
|
850
883
|
recent_deployments: Array<{
|
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.10.0",
|
|
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",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"globals": "^17.0.0",
|
|
76
76
|
"husky": "^9.0.11",
|
|
77
77
|
"jest": "^30.3.0",
|
|
78
|
-
"jest-junit": "^
|
|
79
|
-
"lint-staged": "^
|
|
78
|
+
"jest-junit": "^17.0.0",
|
|
79
|
+
"lint-staged": "^17.0.4",
|
|
80
80
|
"markdownlint-cli2": "^0.22.0",
|
|
81
81
|
"prettier": "^3.5.3",
|
|
82
82
|
"shx": "^0.4.0",
|