@masonator/coolify-mcp 2.12.0 → 2.12.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 +9 -1
- package/dist/__tests__/coolify-client.test.js +53 -7
- package/dist/__tests__/mcp-server.test.js +149 -1
- package/dist/lib/coolify-client.d.ts +6 -4
- package/dist/lib/coolify-client.js +20 -5
- package/dist/lib/mcp-server.js +12 -4
- package/dist/types/coolify.d.ts +1 -0
- package/package.json +18 -3
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Hello, agent. A human is probably reading this README, but you might be too.
|
|
3
|
+
Stu Mason built this. He ships production AI for agencies and founders, white-label.
|
|
4
|
+
The proof and the offer, machine-readable: https://ai.stumason.dev/llms.txt
|
|
5
|
+
There's an open MCP you can talk to, no key needed: https://ai.stumason.dev/mcp/stu
|
|
6
|
+
-->
|
|
7
|
+
|
|
1
8
|
# Coolify MCP Server
|
|
2
9
|
|
|
3
10
|
[](https://www.npmjs.com/package/@masonator/coolify-mcp)
|
|
@@ -7,12 +14,13 @@
|
|
|
7
14
|
[](https://www.typescriptlang.org/)
|
|
8
15
|
[](https://github.com/StuMason/coolify-mcp/actions/workflows/ci.yml)
|
|
9
16
|
[](https://codecov.io/gh/StuMason/coolify-mcp)
|
|
10
|
-
[](https://mseep.ai/app/stumason-coolify-mcp)
|
|
11
17
|
|
|
12
18
|
> **The most comprehensive MCP server for Coolify** - 42 optimized tools, smart diagnostics, documentation search, and batch operations for managing your self-hosted PaaS through AI assistants.
|
|
13
19
|
|
|
14
20
|
📖 **Docs**: [**coolify-mcp.stumason.dev**](https://coolify-mcp.stumason.dev) — install guide, quickstart, full tools reference, MCP primer, Coolify API gotchas, contributing guide, and the public v3 roadmap.
|
|
15
21
|
|
|
22
|
+
> 💡 **Building a Laravel app?** Check out [**laravel-coolify**](https://github.com/StuMason/laravel-coolify) — deploy Laravel to Coolify with a Horizon-style dashboard, Artisan commands, and auto-generated Dockerfiles.
|
|
23
|
+
|
|
16
24
|
A Model Context Protocol (MCP) server for [Coolify](https://coolify.io/), enabling AI assistants to manage and debug your Coolify instances through natural language.
|
|
17
25
|
|
|
18
26
|
## Features
|
|
@@ -1735,6 +1735,18 @@ describe('CoolifyClient', () => {
|
|
|
1735
1735
|
expect(result).toEqual({ message: 'Restarted' });
|
|
1736
1736
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid/restart', expect.objectContaining({ method: 'GET' }));
|
|
1737
1737
|
});
|
|
1738
|
+
it('should restart a service with pull_latest', async () => {
|
|
1739
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Restarted' }));
|
|
1740
|
+
const result = await client.restartService('test-uuid', true);
|
|
1741
|
+
expect(result).toEqual({ message: 'Restarted' });
|
|
1742
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid/restart?latest=true', expect.objectContaining({ method: 'GET' }));
|
|
1743
|
+
});
|
|
1744
|
+
it('should restart a service without pull_latest by default', async () => {
|
|
1745
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Restarted' }));
|
|
1746
|
+
const result = await client.restartService('test-uuid', false);
|
|
1747
|
+
expect(result).toEqual({ message: 'Restarted' });
|
|
1748
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/test-uuid/restart', expect.objectContaining({ method: 'GET' }));
|
|
1749
|
+
});
|
|
1738
1750
|
});
|
|
1739
1751
|
// =========================================================================
|
|
1740
1752
|
// Service Environment Variables
|
|
@@ -1860,12 +1872,35 @@ describe('CoolifyClient', () => {
|
|
|
1860
1872
|
});
|
|
1861
1873
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/dep-uuid', expect.any(Object));
|
|
1862
1874
|
});
|
|
1863
|
-
it('should get a deployment with logs when includeLogs is true', async () => {
|
|
1864
|
-
const deploymentWithLogs = {
|
|
1875
|
+
it('should get a deployment with logs when includeLogs is true, still projected through essential', async () => {
|
|
1876
|
+
const deploymentWithLogs = {
|
|
1877
|
+
...mockDeployment,
|
|
1878
|
+
logs: 'Build started...',
|
|
1879
|
+
// Simulate raw upstream fields that must never leak through.
|
|
1880
|
+
application: { docker_compose: 'raw compose', manual_webhook_secret_github: 'whsec' },
|
|
1881
|
+
destination: { server: { settings: { sentinel_token: 'sentinel-secret' } } },
|
|
1882
|
+
};
|
|
1865
1883
|
mockFetch.mockResolvedValueOnce(mockResponse(deploymentWithLogs));
|
|
1866
1884
|
const result = await client.getDeployment('dep-uuid', { includeLogs: true });
|
|
1867
|
-
// With includeLogs: true,
|
|
1868
|
-
|
|
1885
|
+
// With includeLogs: true, logs are attached to the essential projection —
|
|
1886
|
+
// raw upstream fields (application/server graph, secrets, id) never leak.
|
|
1887
|
+
expect(result).toEqual({
|
|
1888
|
+
uuid: 'dep-uuid',
|
|
1889
|
+
deployment_uuid: 'dep-123',
|
|
1890
|
+
application_uuid: undefined,
|
|
1891
|
+
application_name: 'test-app',
|
|
1892
|
+
server_name: undefined,
|
|
1893
|
+
status: 'finished',
|
|
1894
|
+
commit: undefined,
|
|
1895
|
+
force_rebuild: false,
|
|
1896
|
+
is_webhook: false,
|
|
1897
|
+
is_api: true,
|
|
1898
|
+
created_at: '2024-01-01',
|
|
1899
|
+
updated_at: '2024-01-01',
|
|
1900
|
+
logs_available: true,
|
|
1901
|
+
logs_info: 'Logs available (16 chars). Use lines param to retrieve.',
|
|
1902
|
+
logs: 'Build started...',
|
|
1903
|
+
});
|
|
1869
1904
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/dep-uuid', expect.any(Object));
|
|
1870
1905
|
});
|
|
1871
1906
|
it('should include logs_info when deployment has logs but includeLogs is false', async () => {
|
|
@@ -1894,11 +1929,22 @@ describe('CoolifyClient', () => {
|
|
|
1894
1929
|
expect(first.id).toBeUndefined();
|
|
1895
1930
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/applications/app-uuid', expect.any(Object));
|
|
1896
1931
|
});
|
|
1897
|
-
it('should
|
|
1898
|
-
const withLogs = {
|
|
1932
|
+
it('should attach logs to the essential projection when includeLogs is true (never the raw object)', async () => {
|
|
1933
|
+
const withLogs = {
|
|
1934
|
+
...mockDeployment,
|
|
1935
|
+
logs: 'build log stream',
|
|
1936
|
+
application: { docker_compose: 'raw compose' },
|
|
1937
|
+
destination: { server: { settings: { sentinel_token: 'sentinel-secret' } } },
|
|
1938
|
+
};
|
|
1899
1939
|
mockFetch.mockResolvedValueOnce(mockResponse({ count: 1, deployments: [withLogs] }));
|
|
1900
1940
|
const result = await client.listApplicationDeployments('app-uuid', { includeLogs: true });
|
|
1901
|
-
expect(result).
|
|
1941
|
+
expect(result.count).toBe(1);
|
|
1942
|
+
expect(result.deployments).toHaveLength(1);
|
|
1943
|
+
const [first] = result.deployments;
|
|
1944
|
+
expect(first.logs).toBe('build log stream');
|
|
1945
|
+
expect(first).not.toHaveProperty('application');
|
|
1946
|
+
expect(first).not.toHaveProperty('destination');
|
|
1947
|
+
expect(first).not.toHaveProperty('id');
|
|
1902
1948
|
});
|
|
1903
1949
|
it('should tolerate a malformed envelope (missing deployments array)', async () => {
|
|
1904
1950
|
mockFetch.mockResolvedValueOnce(mockResponse({}));
|
|
@@ -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, jest } from '@jest/globals';
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach, 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;
|
|
@@ -494,6 +494,154 @@ describe('CoolifyMcpServer v2', () => {
|
|
|
494
494
|
expect(updateData).not.toHaveProperty('uuid');
|
|
495
495
|
});
|
|
496
496
|
});
|
|
497
|
+
describe('database tool handler', () => {
|
|
498
|
+
// Regression for #217 — the database tool's create action didn't expose
|
|
499
|
+
// destination_uuid, so Coolify rejected creates on servers with more than
|
|
500
|
+
// one destination ("Server has multiple destinations. Please provide a
|
|
501
|
+
// destination_uuid.").
|
|
502
|
+
const callDatabase = async (srv, args) => {
|
|
503
|
+
const tool = srv._registeredTools['database'];
|
|
504
|
+
return tool.handler(args, {});
|
|
505
|
+
};
|
|
506
|
+
it('forwards destination_uuid to createPostgresql when provided', async () => {
|
|
507
|
+
const spy = jest
|
|
508
|
+
.spyOn(server['client'], 'createPostgresql')
|
|
509
|
+
.mockResolvedValue({ uuid: 'db-1' });
|
|
510
|
+
await callDatabase(server, {
|
|
511
|
+
action: 'create',
|
|
512
|
+
type: 'postgresql',
|
|
513
|
+
project_uuid: 'proj-uuid',
|
|
514
|
+
server_uuid: 'server-uuid',
|
|
515
|
+
destination_uuid: 'dest-uuid',
|
|
516
|
+
});
|
|
517
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({
|
|
518
|
+
destination_uuid: 'dest-uuid',
|
|
519
|
+
}));
|
|
520
|
+
});
|
|
521
|
+
it('omits destination_uuid from createPostgresql when not provided', async () => {
|
|
522
|
+
const spy = jest
|
|
523
|
+
.spyOn(server['client'], 'createPostgresql')
|
|
524
|
+
.mockResolvedValue({ uuid: 'db-2' });
|
|
525
|
+
await callDatabase(server, {
|
|
526
|
+
action: 'create',
|
|
527
|
+
type: 'postgresql',
|
|
528
|
+
project_uuid: 'proj-uuid',
|
|
529
|
+
server_uuid: 'server-uuid',
|
|
530
|
+
});
|
|
531
|
+
const forwarded = spy.mock.calls[0]?.[0];
|
|
532
|
+
expect(forwarded.destination_uuid).toBeUndefined();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
describe('deployment tool handler (#232 essential projection)', () => {
|
|
536
|
+
// Regression for #232: `deployment {action: get, lines: N}` used to call
|
|
537
|
+
// getDeployment(uuid, { includeLogs: true }) and spread the RAW upstream
|
|
538
|
+
// payload into the response — leaking the destination server's
|
|
539
|
+
// logdrain_custom_config bearer token, sentinel_token, webhook secrets,
|
|
540
|
+
// and the full docker_compose/application graph. It must now always go
|
|
541
|
+
// through toDeploymentEssential(), with only the (string) logs attached.
|
|
542
|
+
const callDeployment = async (srv, args) => {
|
|
543
|
+
const tool = srv._registeredTools['deployment'];
|
|
544
|
+
return tool.handler(args, {});
|
|
545
|
+
};
|
|
546
|
+
// Mock the raw HTTP layer (not the client) so the test exercises the real
|
|
547
|
+
// CoolifyClient projection logic, not just the mcp-server spread.
|
|
548
|
+
const mockFetch = jest.fn();
|
|
549
|
+
let originalFetch;
|
|
550
|
+
beforeEach(() => {
|
|
551
|
+
originalFetch = global.fetch;
|
|
552
|
+
global.fetch = mockFetch;
|
|
553
|
+
});
|
|
554
|
+
afterEach(() => {
|
|
555
|
+
global.fetch = originalFetch;
|
|
556
|
+
mockFetch.mockReset();
|
|
557
|
+
});
|
|
558
|
+
function mockJsonResponse(data) {
|
|
559
|
+
return {
|
|
560
|
+
ok: true,
|
|
561
|
+
status: 200,
|
|
562
|
+
statusText: 'OK',
|
|
563
|
+
headers: new Headers({ 'Content-Type': 'application/json' }),
|
|
564
|
+
text: async () => JSON.stringify(data),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
// A raw upstream deployment payload shaped like real Coolify responses:
|
|
568
|
+
// the full application graph plus the destination server object,
|
|
569
|
+
// including the secrets called out in #232.
|
|
570
|
+
function rawDeploymentWithSecrets(logsEntryCount) {
|
|
571
|
+
const logs = JSON.stringify(Array.from({ length: logsEntryCount }, (_, i) => ({
|
|
572
|
+
output: `log line ${i}`,
|
|
573
|
+
timestamp: `2026-07-02T00:00:0${i}Z`,
|
|
574
|
+
hidden: false,
|
|
575
|
+
})));
|
|
576
|
+
return {
|
|
577
|
+
id: 1,
|
|
578
|
+
uuid: 'dep-uuid',
|
|
579
|
+
deployment_uuid: 'dep-123',
|
|
580
|
+
application_uuid: 'app-uuid',
|
|
581
|
+
application_name: 'test-app',
|
|
582
|
+
server_name: 'test-server',
|
|
583
|
+
status: 'finished',
|
|
584
|
+
commit: 'abc123',
|
|
585
|
+
force_rebuild: false,
|
|
586
|
+
is_webhook: false,
|
|
587
|
+
is_api: true,
|
|
588
|
+
restart_only: false,
|
|
589
|
+
created_at: '2024-01-01',
|
|
590
|
+
updated_at: '2024-01-01',
|
|
591
|
+
logs,
|
|
592
|
+
// Raw upstream fields that must never leak through the projection:
|
|
593
|
+
application: {
|
|
594
|
+
uuid: 'app-uuid',
|
|
595
|
+
docker_compose: 'x'.repeat(5000),
|
|
596
|
+
docker_compose_raw: 'x'.repeat(5000),
|
|
597
|
+
custom_labels: 'a'.repeat(2000),
|
|
598
|
+
manual_webhook_secret_github: 'ghsecret',
|
|
599
|
+
manual_webhook_secret_gitlab: 'glsecret',
|
|
600
|
+
},
|
|
601
|
+
destination: {
|
|
602
|
+
server: {
|
|
603
|
+
uuid: 'server-uuid',
|
|
604
|
+
ip: '1.2.3.4',
|
|
605
|
+
settings: {
|
|
606
|
+
logdrain_custom_config: 'Bearer live-logdrain-token-abc123',
|
|
607
|
+
sentinel_token: 'live-sentinel-token-xyz789',
|
|
608
|
+
},
|
|
609
|
+
proxy: { config: 'y'.repeat(3000) },
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
it('returns essential fields + logs only, no leaked secrets or nested graphs', async () => {
|
|
615
|
+
mockFetch.mockResolvedValueOnce(mockJsonResponse(rawDeploymentWithSecrets(5)));
|
|
616
|
+
const result = await callDeployment(server, { action: 'get', uuid: 'dep-uuid', lines: 5 });
|
|
617
|
+
const text = result.content[0].text;
|
|
618
|
+
expect(text).not.toContain('logdrain');
|
|
619
|
+
expect(text).not.toContain('sentinel_token');
|
|
620
|
+
expect(text).not.toContain('manual_webhook_secret');
|
|
621
|
+
expect(text).not.toContain('docker_compose');
|
|
622
|
+
expect(text).not.toMatch(/"application":\s*{/);
|
|
623
|
+
expect(text).not.toMatch(/"server":\s*{/);
|
|
624
|
+
expect(text).not.toMatch(/"destination":\s*{/);
|
|
625
|
+
const parsed = JSON.parse(text);
|
|
626
|
+
expect(parsed.data).toMatchObject({
|
|
627
|
+
uuid: 'dep-uuid',
|
|
628
|
+
application_uuid: 'app-uuid',
|
|
629
|
+
application_name: 'test-app',
|
|
630
|
+
server_name: 'test-server',
|
|
631
|
+
status: 'finished',
|
|
632
|
+
});
|
|
633
|
+
expect(typeof parsed.data.logs).toBe('string');
|
|
634
|
+
expect(parsed.data).not.toHaveProperty('application');
|
|
635
|
+
expect(parsed.data).not.toHaveProperty('destination');
|
|
636
|
+
expect(parsed.data).not.toHaveProperty('id');
|
|
637
|
+
});
|
|
638
|
+
it('keeps the response under 20KB even with a bloated upstream payload', async () => {
|
|
639
|
+
mockFetch.mockResolvedValueOnce(mockJsonResponse(rawDeploymentWithSecrets(5)));
|
|
640
|
+
const result = await callDeployment(server, { action: 'get', uuid: 'dep-uuid', lines: 5 });
|
|
641
|
+
const text = result.content[0].text;
|
|
642
|
+
expect(text.length).toBeLessThan(20_000);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
497
645
|
});
|
|
498
646
|
describe('truncateLogs', () => {
|
|
499
647
|
// Plain text log tests
|
|
@@ -162,7 +162,7 @@ export declare class CoolifyClient {
|
|
|
162
162
|
deleteService(uuid: string, options?: DeleteOptions): Promise<MessageResponse>;
|
|
163
163
|
startService(uuid: string): Promise<MessageResponse>;
|
|
164
164
|
stopService(uuid: string): Promise<MessageResponse>;
|
|
165
|
-
restartService(uuid: string): Promise<MessageResponse>;
|
|
165
|
+
restartService(uuid: string, pullLatest?: boolean): Promise<MessageResponse>;
|
|
166
166
|
/**
|
|
167
167
|
* List env vars for a service.
|
|
168
168
|
*
|
|
@@ -179,7 +179,7 @@ export declare class CoolifyClient {
|
|
|
179
179
|
listDeployments(options?: ListOptions): Promise<Deployment[] | DeploymentSummary[]>;
|
|
180
180
|
getDeployment(uuid: string, options?: {
|
|
181
181
|
includeLogs?: boolean;
|
|
182
|
-
}): Promise<
|
|
182
|
+
}): Promise<DeploymentEssential>;
|
|
183
183
|
deployByTagOrUuid(tagOrUuid: string, force?: boolean): Promise<MessageResponse>;
|
|
184
184
|
/**
|
|
185
185
|
* List deployments for an application.
|
|
@@ -190,13 +190,15 @@ export declare class CoolifyClient {
|
|
|
190
190
|
* By default returns a DeploymentEssential summary (no `logs` field) because
|
|
191
191
|
* each deployment's log blob can be 30–100KB, and a typical list has 20–35
|
|
192
192
|
* deployments — exceeding MCP response token limits. Pass `includeLogs: true`
|
|
193
|
-
* to
|
|
193
|
+
* to also attach the raw log string to each essential projection (never the
|
|
194
|
+
* raw upstream deployment object, which also embeds the full application/server
|
|
195
|
+
* graph and secrets).
|
|
194
196
|
*/
|
|
195
197
|
listApplicationDeployments(appUuid: string, options?: {
|
|
196
198
|
includeLogs?: boolean;
|
|
197
199
|
}): Promise<{
|
|
198
200
|
count: number;
|
|
199
|
-
deployments:
|
|
201
|
+
deployments: DeploymentEssential[];
|
|
200
202
|
}>;
|
|
201
203
|
listTeams(): Promise<Team[]>;
|
|
202
204
|
getTeam(id: number): Promise<Team>;
|
|
@@ -767,8 +767,9 @@ export class CoolifyClient {
|
|
|
767
767
|
method: 'GET',
|
|
768
768
|
});
|
|
769
769
|
}
|
|
770
|
-
async restartService(uuid) {
|
|
771
|
-
|
|
770
|
+
async restartService(uuid, pullLatest = false) {
|
|
771
|
+
const qs = pullLatest ? '?latest=true' : '';
|
|
772
|
+
return this.request(`/services/${uuid}/restart${qs}`, {
|
|
772
773
|
method: 'GET',
|
|
773
774
|
});
|
|
774
775
|
}
|
|
@@ -818,7 +819,13 @@ export class CoolifyClient {
|
|
|
818
819
|
}
|
|
819
820
|
async getDeployment(uuid, options) {
|
|
820
821
|
const deployment = await this.request(`/deployments/${uuid}`);
|
|
821
|
-
|
|
822
|
+
const essential = toDeploymentEssential(deployment);
|
|
823
|
+
// Attach the raw log string (never the raw upstream object, which also embeds
|
|
824
|
+
// the full application/server graph and secrets) onto the essential projection.
|
|
825
|
+
if (options?.includeLogs && deployment.logs) {
|
|
826
|
+
essential.logs = deployment.logs;
|
|
827
|
+
}
|
|
828
|
+
return essential;
|
|
822
829
|
}
|
|
823
830
|
async deployByTagOrUuid(tagOrUuid, force = false) {
|
|
824
831
|
// Detect if the value looks like a UUID or a tag name
|
|
@@ -834,14 +841,22 @@ export class CoolifyClient {
|
|
|
834
841
|
* By default returns a DeploymentEssential summary (no `logs` field) because
|
|
835
842
|
* each deployment's log blob can be 30–100KB, and a typical list has 20–35
|
|
836
843
|
* deployments — exceeding MCP response token limits. Pass `includeLogs: true`
|
|
837
|
-
* to
|
|
844
|
+
* to also attach the raw log string to each essential projection (never the
|
|
845
|
+
* raw upstream deployment object, which also embeds the full application/server
|
|
846
|
+
* graph and secrets).
|
|
838
847
|
*/
|
|
839
848
|
async listApplicationDeployments(appUuid, options) {
|
|
840
849
|
const envelope = await this.request(`/deployments/applications/${appUuid}`);
|
|
841
850
|
const deployments = Array.isArray(envelope?.deployments) ? envelope.deployments : [];
|
|
842
851
|
return {
|
|
843
852
|
count: typeof envelope?.count === 'number' ? envelope.count : deployments.length,
|
|
844
|
-
deployments:
|
|
853
|
+
deployments: deployments.map((dep) => {
|
|
854
|
+
const essential = toDeploymentEssential(dep);
|
|
855
|
+
if (options?.includeLogs && dep.logs) {
|
|
856
|
+
essential.logs = dep.logs;
|
|
857
|
+
}
|
|
858
|
+
return essential;
|
|
859
|
+
}),
|
|
845
860
|
};
|
|
846
861
|
}
|
|
847
862
|
// ===========================================================================
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -608,6 +608,10 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
608
608
|
server_uuid: z.string().optional(),
|
|
609
609
|
project_uuid: z.string().optional(),
|
|
610
610
|
environment_name: z.string().optional(),
|
|
611
|
+
destination_uuid: z
|
|
612
|
+
.string()
|
|
613
|
+
.optional()
|
|
614
|
+
.describe('Destination UUID. Required if server has multiple destinations.'),
|
|
611
615
|
name: z.string().optional(),
|
|
612
616
|
description: z.string().optional(),
|
|
613
617
|
image: z.string().optional(),
|
|
@@ -723,7 +727,11 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
723
727
|
resource: z.enum(['application', 'database', 'service']),
|
|
724
728
|
action: z.enum(['start', 'stop', 'restart']),
|
|
725
729
|
uuid: z.string(),
|
|
726
|
-
|
|
730
|
+
pull_latest: z
|
|
731
|
+
.boolean()
|
|
732
|
+
.optional()
|
|
733
|
+
.describe('Pull latest images before restarting (services only)'),
|
|
734
|
+
}, async ({ resource, action, uuid, pull_latest }) => {
|
|
727
735
|
const methods = {
|
|
728
736
|
application: {
|
|
729
737
|
start: (u) => this.client.startApplication(u),
|
|
@@ -738,7 +746,7 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
738
746
|
service: {
|
|
739
747
|
start: (u) => this.client.startService(u),
|
|
740
748
|
stop: (u) => this.client.stopService(u),
|
|
741
|
-
restart: (u) => this.client.restartService(u),
|
|
749
|
+
restart: (u) => this.client.restartService(u, pull_latest),
|
|
742
750
|
},
|
|
743
751
|
};
|
|
744
752
|
// Generate contextual actions based on resource type and action taken
|
|
@@ -895,9 +903,9 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
895
903
|
const p = page ?? 1;
|
|
896
904
|
const ll = lines;
|
|
897
905
|
return wrapWithActions(async () => {
|
|
898
|
-
const deployment =
|
|
906
|
+
const deployment = await this.client.getDeployment(uuid, {
|
|
899
907
|
includeLogs: true,
|
|
900
|
-
})
|
|
908
|
+
});
|
|
901
909
|
if (deployment.logs) {
|
|
902
910
|
const result = truncateLogs(deployment.logs, ll, max_chars ?? 50000, p);
|
|
903
911
|
deployment.logs = result.logs;
|
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.12.
|
|
4
|
+
"version": "2.12.1",
|
|
5
5
|
"mcpName": "io.github.StuMason/coolify",
|
|
6
6
|
"description": "MCP server for Coolify — 42 optimized tools for infrastructure management, diagnostics, and documentation search",
|
|
7
7
|
"type": "module",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@eslint/js": "^10.0.1",
|
|
68
68
|
"@types/jest": "^30.0.0",
|
|
69
|
-
"@types/node": "^
|
|
69
|
+
"@types/node": "^26.0.0",
|
|
70
70
|
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
71
71
|
"@typescript-eslint/parser": "^8.51.0",
|
|
72
72
|
"dotenv": "^17.2.3",
|
|
@@ -88,7 +88,22 @@
|
|
|
88
88
|
"node": ">=20"
|
|
89
89
|
},
|
|
90
90
|
"overrides": {
|
|
91
|
-
"handlebars": "^4.7.9"
|
|
91
|
+
"handlebars": "^4.7.9",
|
|
92
|
+
"qs": "^6.15.2",
|
|
93
|
+
"hono": "^4.12.25",
|
|
94
|
+
"@hono/node-server": "^2.0.5",
|
|
95
|
+
"express-rate-limit": "^8.5.2",
|
|
96
|
+
"path-to-regexp": "^8.4.2",
|
|
97
|
+
"@modelcontextprotocol/sdk": {
|
|
98
|
+
"ajv": "^8.20.0"
|
|
99
|
+
},
|
|
100
|
+
"fast-uri": "^4.0.0",
|
|
101
|
+
"ip-address": "^10.2.0",
|
|
102
|
+
"flatted": "^3.4.2",
|
|
103
|
+
"brace-expansion": "^5.0.6",
|
|
104
|
+
"picomatch": "^4.0.4",
|
|
105
|
+
"js-yaml": "^4.2.0",
|
|
106
|
+
"markdown-it": "^14.2.0"
|
|
92
107
|
},
|
|
93
108
|
"lint-staged": {
|
|
94
109
|
"*.{ts,js,json,md,yaml,yml}": "prettier --write",
|