@masonator/coolify-mcp 2.8.0 → 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 +28 -0
- package/dist/__tests__/coolify-client.test.js +36 -0
- 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 +1 -0
- package/dist/lib/coolify-client.js +15 -0
- package/dist/lib/parse-headers.d.ts +1 -0
- package/dist/lib/parse-headers.js +14 -0
- package/dist/types/coolify.d.ts +1 -0
- package/package.json +1 -1
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
|
|
@@ -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 () => {
|
|
@@ -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');
|
|
@@ -151,6 +151,7 @@ function toEnvVarSummary(envVar) {
|
|
|
151
151
|
export class CoolifyClient {
|
|
152
152
|
baseUrl;
|
|
153
153
|
accessToken;
|
|
154
|
+
customHeaders;
|
|
154
155
|
cachedVersion = null;
|
|
155
156
|
constructor(config) {
|
|
156
157
|
if (!config.baseUrl) {
|
|
@@ -161,6 +162,18 @@ export class CoolifyClient {
|
|
|
161
162
|
}
|
|
162
163
|
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
163
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;
|
|
164
177
|
}
|
|
165
178
|
// ===========================================================================
|
|
166
179
|
// Private HTTP methods
|
|
@@ -173,6 +186,7 @@ export class CoolifyClient {
|
|
|
173
186
|
headers: {
|
|
174
187
|
'Content-Type': 'application/json',
|
|
175
188
|
Authorization: `Bearer ${this.accessToken}`,
|
|
189
|
+
...this.customHeaders,
|
|
176
190
|
...options.headers,
|
|
177
191
|
},
|
|
178
192
|
});
|
|
@@ -222,6 +236,7 @@ export class CoolifyClient {
|
|
|
222
236
|
const response = await fetch(url, {
|
|
223
237
|
headers: {
|
|
224
238
|
Authorization: `Bearer ${this.accessToken}`,
|
|
239
|
+
...this.customHeaders,
|
|
225
240
|
},
|
|
226
241
|
});
|
|
227
242
|
if (!response.ok) {
|
|
@@ -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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@masonator/coolify-mcp",
|
|
3
3
|
"scope": "@masonator",
|
|
4
|
-
"version": "2.8.
|
|
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",
|