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