@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 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');
@@ -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;
@@ -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
+ }
@@ -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;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@masonator/coolify-mcp",
3
3
  "scope": "@masonator",
4
- "version": "2.8.0",
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",