@fndchagas/coolify-mcp 0.1.1 → 0.1.3

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
@@ -1,20 +1,20 @@
1
1
  # coolify-mcp
2
2
 
3
- MCP server for Coolify, pinned to a specific Coolify OpenAPI version.
3
+ MCP server for Coolify API.
4
4
 
5
- ## Pinned Coolify version
5
+ ## Pinned Coolify Version
6
6
 
7
- This repo is pinned to:
7
+ Version is defined in `src/coolify/constants.ts`. To update:
8
8
 
9
- - `v4.0.0-beta.460`
10
- - OpenAPI file: `openapi/coolify/v4.0.0-beta.460.json`
9
+ 1. Edit `COOLIFY_VERSION` in `src/coolify/constants.ts`
10
+ 2. Run `npm run update`
11
11
 
12
12
  ## Requirements
13
13
 
14
14
  - Node 18+
15
15
  - A Coolify API token
16
16
 
17
- ## Install (package)
17
+ ## Install
18
18
 
19
19
  ```bash
20
20
  npm install -g @fndchagas/coolify-mcp
@@ -22,7 +22,7 @@ npm install -g @fndchagas/coolify-mcp
22
22
  npx -y @fndchagas/coolify-mcp
23
23
  ```
24
24
 
25
- ## Use with Claude Code CLI (stdio)
25
+ ## Use with Claude Code CLI
26
26
 
27
27
  ```bash
28
28
  claude mcp add coolify \
@@ -31,13 +31,13 @@ claude mcp add coolify \
31
31
  -- npx -y @fndchagas/coolify-mcp
32
32
  ```
33
33
 
34
- Optional: disable write tools (deploy/upsert) by adding:
34
+ Disable write tools (deploy/env mutations):
35
35
 
36
36
  ```bash
37
37
  --env COOLIFY_ALLOW_WRITE=false
38
38
  ```
39
39
 
40
- ## Use with OpenAI Codex CLI (stdio)
40
+ ## Use with OpenAI Codex CLI
41
41
 
42
42
  ```bash
43
43
  codex mcp add coolify \
@@ -55,70 +55,51 @@ args = ["-y", "@fndchagas/coolify-mcp"]
55
55
  env = { COOLIFY_BASE_URL = "https://coolify.example.com", COOLIFY_TOKEN = "<token>" }
56
56
  ```
57
57
 
58
- ## Install (dev)
58
+ ## Development
59
59
 
60
60
  ```bash
61
61
  npm install
62
- ```
63
-
64
- ## Generate types (if OpenAPI changes)
65
-
66
- ```bash
67
- npm run generate:openapi
68
- ```
69
-
70
- ## Run (stdio)
71
-
72
- ```bash
73
- COOLIFY_BASE_URL="https://coolify.example.com" \
74
- COOLIFY_TOKEN="<token>" \
75
- MCP_TRANSPORT=stdio \
76
62
  npm run dev
77
63
  ```
78
64
 
79
- ## Run (HTTP)
65
+ ## Scripts
80
66
 
81
67
  ```bash
82
- COOLIFY_BASE_URL="https://coolify.example.com" \
83
- COOLIFY_TOKEN="<token>" \
84
- MCP_TRANSPORT=http \
85
- PORT=7331 \
86
- npm run dev
68
+ npm run dev # Run in development mode
69
+ npm run build # Build TypeScript
70
+ npm run generate # Regenerate types from OpenAPI
71
+ npm run fetch:openapi # Fetch latest OpenAPI spec
72
+ npm run update # Fetch + regenerate
87
73
  ```
88
74
 
89
- Endpoint: `POST http://localhost:7331/mcp`
75
+ ## Environment Variables
90
76
 
91
- ## Run (both)
92
-
93
- ```bash
94
- MCP_TRANSPORT=both npm run dev
95
- ```
96
-
97
- ## Environment variables
98
-
99
- - `COOLIFY_BASE_URL` (required)
100
- - `COOLIFY_TOKEN` (required)
101
- - `COOLIFY_OPENAPI_REF` (default: `v4.0.0-beta.460`)
102
- - `COOLIFY_STRICT_VERSION` (default: `false`)
103
- - `COOLIFY_ALLOW_WRITE` (default: `true`)
104
- - `MCP_TRANSPORT` (`stdio`, `http`, `both`)
105
- - `PORT` (HTTP port, default `7331`)
77
+ | Variable | Default | Description |
78
+ |----------|---------|-------------|
79
+ | `COOLIFY_BASE_URL` | required | Coolify API URL |
80
+ | `COOLIFY_TOKEN` | required | API token |
81
+ | `COOLIFY_STRICT_VERSION` | `false` | Fail on version mismatch |
82
+ | `COOLIFY_ALLOW_WRITE` | `true` | Enable write operations |
83
+ | `MCP_TRANSPORT` | `stdio` | Transport: `stdio`, `http`, `both` |
84
+ | `PORT` | `7331` | HTTP port |
106
85
 
107
86
  ## Tools
108
87
 
109
88
  - `coolify.listResources`
89
+ - `coolify.listApplications`
110
90
  - `coolify.getApplication`
91
+ - `coolify.getLogs`
111
92
  - `coolify.listEnvs`
112
- - `coolify.upsertEnv`
93
+ - `coolify.createEnv`
94
+ - `coolify.updateEnv`
113
95
  - `coolify.deploy`
114
96
  - `coolify.getDeployment`
115
- - `coolify.getLogs`
116
97
  - `coolify.listDatabases`
117
98
  - `coolify.getDatabase`
118
99
 
119
- ## MCP usage examples
100
+ ## MCP Usage Examples
120
101
 
121
- ### HTTP client (Streamable HTTP)
102
+ ### HTTP Client
122
103
 
123
104
  ```ts
124
105
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -131,31 +112,21 @@ const transport = new StreamableHTTPClientTransport(
131
112
 
132
113
  await client.connect(transport);
133
114
 
134
- const tools = await client.listTools();
135
- console.log(tools.tools.map((t) => t.name));
136
-
137
115
  const resources = await client.callTool({
138
116
  name: 'coolify.listResources',
139
117
  arguments: {},
140
118
  });
141
119
  console.log(resources.structuredContent);
142
120
 
143
- const logs = await client.callTool({
144
- name: 'coolify.getLogs',
145
- arguments: { appUuid: 'nwggo800g800oosow8ks4c88' },
146
- });
147
- console.log(logs.structuredContent);
148
-
149
121
  await client.close();
150
122
  ```
151
123
 
152
- ### Stdio client (spawn server)
124
+ ### Stdio Client
153
125
 
154
126
  ```ts
155
127
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
156
128
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
157
129
 
158
- // Ensure COOLIFY_BASE_URL and COOLIFY_TOKEN are set in the environment
159
130
  const client = new Client({ name: 'coolify-client', version: '1.0.0' });
160
131
  const transport = new StdioClientTransport({
161
132
  command: 'node',
@@ -166,7 +137,7 @@ await client.connect(transport);
166
137
 
167
138
  const result = await client.callTool({
168
139
  name: 'coolify.getApplication',
169
- arguments: { uuid: 'nwggo800g800oosow8ks4c88' },
140
+ arguments: { uuid: 'your-app-uuid' },
170
141
  });
171
142
  console.log(result.structuredContent);
172
143
 
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { COOLIFY_VERSION } from './coolify/constants.js';
1
2
  export const COOLIFY_BASE_URL = process.env.COOLIFY_BASE_URL;
2
3
  export const COOLIFY_TOKEN = process.env.COOLIFY_TOKEN;
3
- export const COOLIFY_OPENAPI_REF = process.env.COOLIFY_OPENAPI_REF ?? 'v4.0.0-beta.460';
4
+ export const COOLIFY_OPENAPI_REF = process.env.COOLIFY_OPENAPI_REF ?? COOLIFY_VERSION;
4
5
  export const MCP_TRANSPORT = process.env.MCP_TRANSPORT ?? 'stdio';
5
6
  export const MCP_HTTP_PORT = Number(process.env.PORT ?? '7331');
6
7
  export const COOLIFY_STRICT_VERSION = process.env.COOLIFY_STRICT_VERSION === 'true';
@@ -1,53 +1,16 @@
1
1
  import { COOLIFY_BASE_URL, COOLIFY_TOKEN } from '../config.js';
2
- function requireEnv(value, name) {
3
- if (!value) {
4
- throw new Error(`${name} is required`);
2
+ import { client } from '../generated/client.gen.js';
3
+ export function initializeClient() {
4
+ if (!COOLIFY_BASE_URL) {
5
+ throw new Error('COOLIFY_BASE_URL is required');
5
6
  }
6
- return value;
7
- }
8
- function buildUrl(path, query) {
9
- const base = requireEnv(COOLIFY_BASE_URL, 'COOLIFY_BASE_URL');
10
- const url = new URL(path, base);
11
- if (query) {
12
- for (const [key, value] of Object.entries(query)) {
13
- if (value !== undefined) {
14
- url.searchParams.set(key, value);
15
- }
16
- }
17
- }
18
- return url.toString();
19
- }
20
- export async function request(method, path, options) {
21
- const token = requireEnv(COOLIFY_TOKEN, 'COOLIFY_TOKEN');
22
- const url = buildUrl(path, options?.query);
23
- const headers = {
24
- Authorization: `Bearer ${token}`,
25
- Accept: 'application/json',
26
- };
27
- let body;
28
- if (options?.body !== undefined) {
29
- headers['Content-Type'] = 'application/json';
30
- body = JSON.stringify(options.body);
31
- }
32
- const response = await fetch(url, { method, headers, body });
33
- const contentType = response.headers.get('content-type') ?? '';
34
- if (!response.ok) {
35
- const text = await response.text();
36
- throw new Error(`Coolify API error ${response.status}: ${text || response.statusText}`);
37
- }
38
- if (contentType.includes('application/json')) {
39
- return (await response.json());
40
- }
41
- return (await response.text());
42
- }
43
- export async function getVersion() {
44
- const data = await request('GET', '/api/v1/version');
45
- if (typeof data === 'string') {
46
- return data.trim();
47
- }
48
- if (data && typeof data === 'object' && 'version' in data) {
49
- const version = data.version;
50
- return version ?? 'unknown';
7
+ if (!COOLIFY_TOKEN) {
8
+ throw new Error('COOLIFY_TOKEN is required');
51
9
  }
52
- return 'unknown';
10
+ client.setConfig({
11
+ baseUrl: COOLIFY_BASE_URL,
12
+ headers: {
13
+ Authorization: `Bearer ${COOLIFY_TOKEN}`,
14
+ },
15
+ });
53
16
  }
@@ -0,0 +1,3 @@
1
+ export const COOLIFY_VERSION = "v4.0.0-beta.460";
2
+ const COOLIFY_REPO = "coollabsio/coolify";
3
+ export const COOLIFY_OPENAPI_RAW_URL = `https://raw.githubusercontent.com/${COOLIFY_REPO}/${COOLIFY_VERSION}/openapi.json`;
@@ -0,0 +1,229 @@
1
+ // This file is auto-generated by @hey-api/openapi-ts
2
+ import { createSseClient } from '../core/serverSentEvents.gen.js';
3
+ import { getValidRequestBody } from '../core/utils.gen.js';
4
+ import { buildUrl, createConfig, createInterceptors, getParseAs, mergeConfigs, mergeHeaders, setAuthParams, } from './utils.gen.js';
5
+ export const createClient = (config = {}) => {
6
+ let _config = mergeConfigs(createConfig(), config);
7
+ const getConfig = () => ({ ..._config });
8
+ const setConfig = (config) => {
9
+ _config = mergeConfigs(_config, config);
10
+ return getConfig();
11
+ };
12
+ const interceptors = createInterceptors();
13
+ const beforeRequest = async (options) => {
14
+ const opts = {
15
+ ..._config,
16
+ ...options,
17
+ fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
18
+ headers: mergeHeaders(_config.headers, options.headers),
19
+ serializedBody: undefined,
20
+ };
21
+ if (opts.security) {
22
+ await setAuthParams({
23
+ ...opts,
24
+ security: opts.security,
25
+ });
26
+ }
27
+ if (opts.requestValidator) {
28
+ await opts.requestValidator(opts);
29
+ }
30
+ if (opts.body !== undefined && opts.bodySerializer) {
31
+ opts.serializedBody = opts.bodySerializer(opts.body);
32
+ }
33
+ // remove Content-Type header if body is empty to avoid sending invalid requests
34
+ if (opts.body === undefined || opts.serializedBody === '') {
35
+ opts.headers.delete('Content-Type');
36
+ }
37
+ const url = buildUrl(opts);
38
+ return { opts, url };
39
+ };
40
+ const request = async (options) => {
41
+ // @ts-expect-error
42
+ const { opts, url } = await beforeRequest(options);
43
+ const requestInit = {
44
+ redirect: 'follow',
45
+ ...opts,
46
+ body: getValidRequestBody(opts),
47
+ };
48
+ let request = new Request(url, requestInit);
49
+ for (const fn of interceptors.request.fns) {
50
+ if (fn) {
51
+ request = await fn(request, opts);
52
+ }
53
+ }
54
+ // fetch must be assigned here, otherwise it would throw the error:
55
+ // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
56
+ const _fetch = opts.fetch;
57
+ let response;
58
+ try {
59
+ response = await _fetch(request);
60
+ }
61
+ catch (error) {
62
+ // Handle fetch exceptions (AbortError, network errors, etc.)
63
+ let finalError = error;
64
+ for (const fn of interceptors.error.fns) {
65
+ if (fn) {
66
+ finalError = (await fn(error, undefined, request, opts));
67
+ }
68
+ }
69
+ finalError = finalError || {};
70
+ if (opts.throwOnError) {
71
+ throw finalError;
72
+ }
73
+ // Return error response
74
+ return opts.responseStyle === 'data'
75
+ ? undefined
76
+ : {
77
+ error: finalError,
78
+ request,
79
+ response: undefined,
80
+ };
81
+ }
82
+ for (const fn of interceptors.response.fns) {
83
+ if (fn) {
84
+ response = await fn(response, request, opts);
85
+ }
86
+ }
87
+ const result = {
88
+ request,
89
+ response,
90
+ };
91
+ if (response.ok) {
92
+ const parseAs = (opts.parseAs === 'auto'
93
+ ? getParseAs(response.headers.get('Content-Type'))
94
+ : opts.parseAs) ?? 'json';
95
+ if (response.status === 204 ||
96
+ response.headers.get('Content-Length') === '0') {
97
+ let emptyData;
98
+ switch (parseAs) {
99
+ case 'arrayBuffer':
100
+ case 'blob':
101
+ case 'text':
102
+ emptyData = await response[parseAs]();
103
+ break;
104
+ case 'formData':
105
+ emptyData = new FormData();
106
+ break;
107
+ case 'stream':
108
+ emptyData = response.body;
109
+ break;
110
+ case 'json':
111
+ default:
112
+ emptyData = {};
113
+ break;
114
+ }
115
+ return opts.responseStyle === 'data'
116
+ ? emptyData
117
+ : {
118
+ data: emptyData,
119
+ ...result,
120
+ };
121
+ }
122
+ let data;
123
+ switch (parseAs) {
124
+ case 'arrayBuffer':
125
+ case 'blob':
126
+ case 'formData':
127
+ case 'json':
128
+ case 'text':
129
+ data = await response[parseAs]();
130
+ break;
131
+ case 'stream':
132
+ return opts.responseStyle === 'data'
133
+ ? response.body
134
+ : {
135
+ data: response.body,
136
+ ...result,
137
+ };
138
+ }
139
+ if (parseAs === 'json') {
140
+ if (opts.responseValidator) {
141
+ await opts.responseValidator(data);
142
+ }
143
+ if (opts.responseTransformer) {
144
+ data = await opts.responseTransformer(data);
145
+ }
146
+ }
147
+ return opts.responseStyle === 'data'
148
+ ? data
149
+ : {
150
+ data,
151
+ ...result,
152
+ };
153
+ }
154
+ const textError = await response.text();
155
+ let jsonError;
156
+ try {
157
+ jsonError = JSON.parse(textError);
158
+ }
159
+ catch {
160
+ // noop
161
+ }
162
+ const error = jsonError ?? textError;
163
+ let finalError = error;
164
+ for (const fn of interceptors.error.fns) {
165
+ if (fn) {
166
+ finalError = (await fn(error, response, request, opts));
167
+ }
168
+ }
169
+ finalError = finalError || {};
170
+ if (opts.throwOnError) {
171
+ throw finalError;
172
+ }
173
+ // TODO: we probably want to return error and improve types
174
+ return opts.responseStyle === 'data'
175
+ ? undefined
176
+ : {
177
+ error: finalError,
178
+ ...result,
179
+ };
180
+ };
181
+ const makeMethodFn = (method) => (options) => request({ ...options, method });
182
+ const makeSseFn = (method) => async (options) => {
183
+ const { opts, url } = await beforeRequest(options);
184
+ return createSseClient({
185
+ ...opts,
186
+ body: opts.body,
187
+ headers: opts.headers,
188
+ method,
189
+ onRequest: async (url, init) => {
190
+ let request = new Request(url, init);
191
+ for (const fn of interceptors.request.fns) {
192
+ if (fn) {
193
+ request = await fn(request, opts);
194
+ }
195
+ }
196
+ return request;
197
+ },
198
+ serializedBody: getValidRequestBody(opts),
199
+ url,
200
+ });
201
+ };
202
+ return {
203
+ buildUrl,
204
+ connect: makeMethodFn('CONNECT'),
205
+ delete: makeMethodFn('DELETE'),
206
+ get: makeMethodFn('GET'),
207
+ getConfig,
208
+ head: makeMethodFn('HEAD'),
209
+ interceptors,
210
+ options: makeMethodFn('OPTIONS'),
211
+ patch: makeMethodFn('PATCH'),
212
+ post: makeMethodFn('POST'),
213
+ put: makeMethodFn('PUT'),
214
+ request,
215
+ setConfig,
216
+ sse: {
217
+ connect: makeSseFn('CONNECT'),
218
+ delete: makeSseFn('DELETE'),
219
+ get: makeSseFn('GET'),
220
+ head: makeSseFn('HEAD'),
221
+ options: makeSseFn('OPTIONS'),
222
+ patch: makeSseFn('PATCH'),
223
+ post: makeSseFn('POST'),
224
+ put: makeSseFn('PUT'),
225
+ trace: makeSseFn('TRACE'),
226
+ },
227
+ trace: makeMethodFn('TRACE'),
228
+ };
229
+ };
@@ -0,0 +1,6 @@
1
+ // This file is auto-generated by @hey-api/openapi-ts
2
+ export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen.js';
3
+ export { buildClientParams } from '../core/params.gen.js';
4
+ export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen.js';
5
+ export { createClient } from './client.gen.js';
6
+ export { createConfig, mergeHeaders } from './utils.gen.js';
@@ -0,0 +1,2 @@
1
+ // This file is auto-generated by @hey-api/openapi-ts
2
+ export {};