@masonator/coolify-mcp 0.8.1 → 0.9.0

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
@@ -6,20 +6,21 @@ A Model Context Protocol (MCP) server for [Coolify](https://coolify.io/), enabli
6
6
 
7
7
  ## Features
8
8
 
9
- This MCP server provides **61 tools** focused on **debugging, management, and deployment**:
10
-
11
- | Category | Tools |
12
- | ------------------ | -------------------------------------------------------------------------------------------------------- |
13
- | **Infrastructure** | overview (all resources at once) |
14
- | **Diagnostics** | diagnose_app, diagnose_server, find_issues (smart lookup by name/domain/IP) |
15
- | **Servers** | list, get, validate, resources, domains |
16
- | **Projects** | list, get, create, update, delete |
17
- | **Environments** | list, get, create, delete |
18
- | **Applications** | list, get, update, delete, start, stop, restart, logs, env vars (CRUD), create (private-gh, private-key) |
19
- | **Databases** | list, get, start, stop, restart, backups (list, get), backup executions (list, get) |
20
- | **Services** | list, get, create, update, delete, start, stop, restart, env vars (list, create, delete) |
21
- | **Deployments** | list, get, deploy, cancel, list by application |
22
- | **Private Keys** | list, get, create, update, delete |
9
+ This MCP server provides **65 tools** focused on **debugging, management, and deployment**:
10
+
11
+ | Category | Tools |
12
+ | -------------------- | -------------------------------------------------------------------------------------------------------- |
13
+ | **Infrastructure** | overview (all resources at once) |
14
+ | **Diagnostics** | diagnose_app, diagnose_server, find_issues (smart lookup by name/domain/IP) |
15
+ | **Batch Operations** | restart_project_apps, bulk_env_update, stop_all_apps, redeploy_project |
16
+ | **Servers** | list, get, validate, resources, domains |
17
+ | **Projects** | list, get, create, update, delete |
18
+ | **Environments** | list, get, create, delete |
19
+ | **Applications** | list, get, update, delete, start, stop, restart, logs, env vars (CRUD), create (private-gh, private-key) |
20
+ | **Databases** | list, get, start, stop, restart, backups (list, get), backup executions (list, get) |
21
+ | **Services** | list, get, create, update, delete, start, stop, restart, env vars (list, create, delete) |
22
+ | **Deployments** | list, get, deploy, cancel, list by application |
23
+ | **Private Keys** | list, get, create, update, delete |
23
24
 
24
25
  ## Installation
25
26
 
@@ -271,6 +272,15 @@ These tools accept human-friendly identifiers instead of just UUIDs:
271
272
  - `update_private_key` - Update a private key
272
273
  - `delete_private_key` - Delete a private key
273
274
 
275
+ ### Batch Operations
276
+
277
+ Power user tools for operating on multiple resources at once:
278
+
279
+ - `restart_project_apps` - Restart all applications in a project
280
+ - `bulk_env_update` - Update or create an environment variable across multiple applications (upsert behavior)
281
+ - `stop_all_apps` - Emergency stop all running applications (requires confirmation)
282
+ - `redeploy_project` - Redeploy all applications in a project with force rebuild
283
+
274
284
  ## Contributing
275
285
 
276
286
  Contributions welcome! Please open an issue first to discuss major changes.
@@ -1669,4 +1669,292 @@ describe('CoolifyClient', () => {
1669
1669
  });
1670
1670
  });
1671
1671
  });
1672
+ // ===========================================================================
1673
+ // Batch Operations Tests
1674
+ // ===========================================================================
1675
+ describe('Batch Operations', () => {
1676
+ describe('restartProjectApps', () => {
1677
+ const mockApps = [
1678
+ {
1679
+ id: 1,
1680
+ uuid: 'app-1',
1681
+ name: 'app-one',
1682
+ project_uuid: 'proj-1',
1683
+ status: 'running',
1684
+ created_at: '2024-01-01',
1685
+ updated_at: '2024-01-01',
1686
+ },
1687
+ {
1688
+ id: 2,
1689
+ uuid: 'app-2',
1690
+ name: 'app-two',
1691
+ project_uuid: 'proj-1',
1692
+ status: 'running',
1693
+ created_at: '2024-01-01',
1694
+ updated_at: '2024-01-01',
1695
+ },
1696
+ {
1697
+ id: 3,
1698
+ uuid: 'app-3',
1699
+ name: 'app-three',
1700
+ project_uuid: 'proj-2', // Different project
1701
+ status: 'running',
1702
+ created_at: '2024-01-01',
1703
+ updated_at: '2024-01-01',
1704
+ },
1705
+ ];
1706
+ it('should restart all apps in a project', async () => {
1707
+ mockFetch
1708
+ .mockResolvedValueOnce(mockResponse(mockApps))
1709
+ .mockResolvedValueOnce(mockResponse({ message: 'Restarted' })) // app-1
1710
+ .mockResolvedValueOnce(mockResponse({ message: 'Restarted' })); // app-2
1711
+ const result = await client.restartProjectApps('proj-1');
1712
+ expect(result.summary.total).toBe(2);
1713
+ expect(result.summary.succeeded).toBe(2);
1714
+ expect(result.summary.failed).toBe(0);
1715
+ expect(result.succeeded).toEqual([
1716
+ { uuid: 'app-1', name: 'app-one' },
1717
+ { uuid: 'app-2', name: 'app-two' },
1718
+ ]);
1719
+ expect(result.failed).toEqual([]);
1720
+ });
1721
+ it('should handle partial failures gracefully', async () => {
1722
+ mockFetch
1723
+ .mockResolvedValueOnce(mockResponse(mockApps))
1724
+ .mockResolvedValueOnce(mockResponse({ message: 'Restarted' }))
1725
+ .mockRejectedValueOnce(new Error('App not running'));
1726
+ const result = await client.restartProjectApps('proj-1');
1727
+ expect(result.summary.succeeded).toBe(1);
1728
+ expect(result.summary.failed).toBe(1);
1729
+ expect(result.succeeded).toHaveLength(1);
1730
+ expect(result.failed).toHaveLength(1);
1731
+ expect(result.failed[0].error).toBe('App not running');
1732
+ });
1733
+ it('should return empty result for empty project', async () => {
1734
+ mockFetch.mockResolvedValueOnce(mockResponse([]));
1735
+ const result = await client.restartProjectApps('empty-project');
1736
+ expect(result.summary.total).toBe(0);
1737
+ expect(result.summary.succeeded).toBe(0);
1738
+ expect(result.summary.failed).toBe(0);
1739
+ });
1740
+ it('should return empty result for project with no apps', async () => {
1741
+ mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
1742
+ const result = await client.restartProjectApps('nonexistent-project');
1743
+ expect(result.summary.total).toBe(0);
1744
+ });
1745
+ });
1746
+ describe('bulkEnvUpdate', () => {
1747
+ const mockApps = [
1748
+ {
1749
+ id: 1,
1750
+ uuid: 'app-1',
1751
+ name: 'app-one',
1752
+ status: 'running',
1753
+ created_at: '2024-01-01',
1754
+ updated_at: '2024-01-01',
1755
+ },
1756
+ {
1757
+ id: 2,
1758
+ uuid: 'app-2',
1759
+ name: 'app-two',
1760
+ status: 'running',
1761
+ created_at: '2024-01-01',
1762
+ updated_at: '2024-01-01',
1763
+ },
1764
+ {
1765
+ id: 3,
1766
+ uuid: 'app-3',
1767
+ name: 'app-three',
1768
+ status: 'running',
1769
+ created_at: '2024-01-01',
1770
+ updated_at: '2024-01-01',
1771
+ },
1772
+ ];
1773
+ it('should update env var across multiple apps', async () => {
1774
+ mockFetch
1775
+ .mockResolvedValueOnce(mockResponse(mockApps)) // listApplications
1776
+ .mockResolvedValueOnce(mockResponse({ message: 'Updated' })) // app-1
1777
+ .mockResolvedValueOnce(mockResponse({ message: 'Updated' })); // app-2
1778
+ const result = await client.bulkEnvUpdate(['app-1', 'app-2'], 'API_KEY', 'new-value');
1779
+ expect(result.summary.total).toBe(2);
1780
+ expect(result.summary.succeeded).toBe(2);
1781
+ expect(result.summary.failed).toBe(0);
1782
+ expect(result.succeeded).toEqual([
1783
+ { uuid: 'app-1', name: 'app-one' },
1784
+ { uuid: 'app-2', name: 'app-two' },
1785
+ ]);
1786
+ });
1787
+ it('should handle partial failures', async () => {
1788
+ mockFetch
1789
+ .mockResolvedValueOnce(mockResponse(mockApps))
1790
+ .mockResolvedValueOnce(mockResponse({ message: 'Updated' }))
1791
+ .mockRejectedValueOnce(new Error('App not found'));
1792
+ const result = await client.bulkEnvUpdate(['app-1', 'app-2'], 'API_KEY', 'new-value');
1793
+ expect(result.summary.succeeded).toBe(1);
1794
+ expect(result.summary.failed).toBe(1);
1795
+ expect(result.failed[0].error).toBe('App not found');
1796
+ });
1797
+ it('should handle unknown app UUIDs gracefully', async () => {
1798
+ mockFetch
1799
+ .mockResolvedValueOnce(mockResponse(mockApps))
1800
+ .mockResolvedValueOnce(mockResponse({ message: 'Updated' }))
1801
+ .mockRejectedValueOnce(new Error('Application not found'));
1802
+ const result = await client.bulkEnvUpdate(['app-1', 'unknown-app'], 'API_KEY', 'new-value');
1803
+ expect(result.summary.total).toBe(2);
1804
+ expect(result.summary.succeeded).toBe(1);
1805
+ expect(result.summary.failed).toBe(1);
1806
+ expect(result.succeeded[0].uuid).toBe('app-1');
1807
+ expect(result.failed[0].uuid).toBe('unknown-app');
1808
+ expect(result.failed[0].error).toBe('Application not found');
1809
+ });
1810
+ it('should return empty result for empty app UUIDs array', async () => {
1811
+ const result = await client.bulkEnvUpdate([], 'API_KEY', 'new-value');
1812
+ expect(result.summary.total).toBe(0);
1813
+ expect(result.summary.succeeded).toBe(0);
1814
+ expect(result.summary.failed).toBe(0);
1815
+ // No API calls should be made
1816
+ expect(mockFetch).not.toHaveBeenCalled();
1817
+ });
1818
+ it('should send build time flag when specified', async () => {
1819
+ mockFetch
1820
+ .mockResolvedValueOnce(mockResponse(mockApps))
1821
+ .mockResolvedValueOnce(mockResponse({ message: 'Updated' }));
1822
+ await client.bulkEnvUpdate(['app-1'], 'BUILD_VAR', 'value', true);
1823
+ // Verify the PATCH call was made with is_build_time
1824
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-1/envs', expect.objectContaining({
1825
+ method: 'PATCH',
1826
+ body: JSON.stringify({ key: 'BUILD_VAR', value: 'value', is_build_time: true }),
1827
+ }));
1828
+ });
1829
+ });
1830
+ describe('stopAllApps', () => {
1831
+ const mockApps = [
1832
+ {
1833
+ id: 1,
1834
+ uuid: 'app-1',
1835
+ name: 'running-app',
1836
+ status: 'running:healthy',
1837
+ created_at: '2024-01-01',
1838
+ updated_at: '2024-01-01',
1839
+ },
1840
+ {
1841
+ id: 2,
1842
+ uuid: 'app-2',
1843
+ name: 'healthy-app',
1844
+ status: 'healthy',
1845
+ created_at: '2024-01-01',
1846
+ updated_at: '2024-01-01',
1847
+ },
1848
+ {
1849
+ id: 3,
1850
+ uuid: 'app-3',
1851
+ name: 'stopped-app',
1852
+ status: 'exited',
1853
+ created_at: '2024-01-01',
1854
+ updated_at: '2024-01-01',
1855
+ },
1856
+ ];
1857
+ it('should stop all running apps', async () => {
1858
+ mockFetch
1859
+ .mockResolvedValueOnce(mockResponse(mockApps))
1860
+ .mockResolvedValueOnce(mockResponse({ message: 'Stopped' })) // app-1
1861
+ .mockResolvedValueOnce(mockResponse({ message: 'Stopped' })); // app-2
1862
+ const result = await client.stopAllApps();
1863
+ // Only 2 apps are running (app-1 and app-2), app-3 is already stopped
1864
+ expect(result.summary.total).toBe(2);
1865
+ expect(result.summary.succeeded).toBe(2);
1866
+ expect(result.summary.failed).toBe(0);
1867
+ });
1868
+ it('should handle partial failures', async () => {
1869
+ mockFetch
1870
+ .mockResolvedValueOnce(mockResponse(mockApps))
1871
+ .mockResolvedValueOnce(mockResponse({ message: 'Stopped' }))
1872
+ .mockRejectedValueOnce(new Error('Failed to stop'));
1873
+ const result = await client.stopAllApps();
1874
+ expect(result.summary.succeeded).toBe(1);
1875
+ expect(result.summary.failed).toBe(1);
1876
+ });
1877
+ it('should return empty result when no running apps', async () => {
1878
+ const stoppedApps = [
1879
+ { ...mockApps[2] }, // Only the stopped app
1880
+ ];
1881
+ mockFetch.mockResolvedValueOnce(mockResponse(stoppedApps));
1882
+ const result = await client.stopAllApps();
1883
+ expect(result.summary.total).toBe(0);
1884
+ });
1885
+ });
1886
+ describe('redeployProjectApps', () => {
1887
+ const mockApps = [
1888
+ {
1889
+ id: 1,
1890
+ uuid: 'app-1',
1891
+ name: 'app-one',
1892
+ project_uuid: 'proj-1',
1893
+ status: 'running',
1894
+ created_at: '2024-01-01',
1895
+ updated_at: '2024-01-01',
1896
+ },
1897
+ {
1898
+ id: 2,
1899
+ uuid: 'app-2',
1900
+ name: 'app-two',
1901
+ project_uuid: 'proj-1',
1902
+ status: 'running',
1903
+ created_at: '2024-01-01',
1904
+ updated_at: '2024-01-01',
1905
+ },
1906
+ {
1907
+ id: 3,
1908
+ uuid: 'app-3',
1909
+ name: 'app-three',
1910
+ project_uuid: 'proj-2', // Different project
1911
+ status: 'running',
1912
+ created_at: '2024-01-01',
1913
+ updated_at: '2024-01-01',
1914
+ },
1915
+ ];
1916
+ it('should redeploy all apps in a project', async () => {
1917
+ mockFetch
1918
+ .mockResolvedValueOnce(mockResponse(mockApps))
1919
+ .mockResolvedValueOnce(mockResponse({ message: 'Deployed' })) // app-1
1920
+ .mockResolvedValueOnce(mockResponse({ message: 'Deployed' })); // app-2
1921
+ const result = await client.redeployProjectApps('proj-1');
1922
+ expect(result.summary.total).toBe(2);
1923
+ expect(result.summary.succeeded).toBe(2);
1924
+ expect(result.summary.failed).toBe(0);
1925
+ });
1926
+ it('should use force=true by default', async () => {
1927
+ mockFetch
1928
+ .mockResolvedValueOnce(mockResponse(mockApps))
1929
+ .mockResolvedValueOnce(mockResponse({ message: 'Deployed' }))
1930
+ .mockResolvedValueOnce(mockResponse({ message: 'Deployed' }));
1931
+ await client.redeployProjectApps('proj-1');
1932
+ // Verify deploy calls use force=true
1933
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?tag=app-1&force=true', expect.any(Object));
1934
+ });
1935
+ it('should support force=false', async () => {
1936
+ mockFetch
1937
+ .mockResolvedValueOnce(mockResponse(mockApps))
1938
+ .mockResolvedValueOnce(mockResponse({ message: 'Deployed' }))
1939
+ .mockResolvedValueOnce(mockResponse({ message: 'Deployed' }));
1940
+ await client.redeployProjectApps('proj-1', false);
1941
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?tag=app-1&force=false', expect.any(Object));
1942
+ });
1943
+ it('should handle partial failures', async () => {
1944
+ mockFetch
1945
+ .mockResolvedValueOnce(mockResponse(mockApps))
1946
+ .mockResolvedValueOnce(mockResponse({ message: 'Deployed' }))
1947
+ .mockRejectedValueOnce(new Error('Build failed'));
1948
+ const result = await client.redeployProjectApps('proj-1');
1949
+ expect(result.summary.succeeded).toBe(1);
1950
+ expect(result.summary.failed).toBe(1);
1951
+ expect(result.failed[0].error).toBe('Build failed');
1952
+ });
1953
+ it('should return empty result for empty project', async () => {
1954
+ mockFetch.mockResolvedValueOnce(mockResponse([]));
1955
+ const result = await client.redeployProjectApps('empty-project');
1956
+ expect(result.summary.total).toBe(0);
1957
+ });
1958
+ });
1959
+ });
1672
1960
  });
@@ -2,7 +2,7 @@
2
2
  * Coolify API Client
3
3
  * Complete HTTP client for the Coolify API v1
4
4
  */
5
- import type { CoolifyConfig, DeleteOptions, MessageResponse, UuidResponse, Server, ServerResource, ServerDomain, ServerValidation, CreateServerRequest, UpdateServerRequest, Project, CreateProjectRequest, UpdateProjectRequest, Environment, CreateEnvironmentRequest, Application, CreateApplicationPublicRequest, CreateApplicationPrivateGHRequest, CreateApplicationPrivateKeyRequest, CreateApplicationDockerfileRequest, CreateApplicationDockerImageRequest, CreateApplicationDockerComposeRequest, UpdateApplicationRequest, ApplicationActionResponse, EnvironmentVariable, EnvVarSummary, CreateEnvVarRequest, UpdateEnvVarRequest, BulkUpdateEnvVarsRequest, Database, UpdateDatabaseRequest, DatabaseBackup, BackupExecution, Service, CreateServiceRequest, UpdateServiceRequest, ServiceCreateResponse, Deployment, Team, TeamMember, PrivateKey, CreatePrivateKeyRequest, UpdatePrivateKeyRequest, CloudToken, CreateCloudTokenRequest, UpdateCloudTokenRequest, CloudTokenValidation, Version, ApplicationDiagnostic, ServerDiagnostic, InfrastructureIssuesReport } from '../types/coolify.js';
5
+ import type { CoolifyConfig, DeleteOptions, MessageResponse, UuidResponse, Server, ServerResource, ServerDomain, ServerValidation, CreateServerRequest, UpdateServerRequest, Project, CreateProjectRequest, UpdateProjectRequest, Environment, CreateEnvironmentRequest, Application, CreateApplicationPublicRequest, CreateApplicationPrivateGHRequest, CreateApplicationPrivateKeyRequest, CreateApplicationDockerfileRequest, CreateApplicationDockerImageRequest, CreateApplicationDockerComposeRequest, UpdateApplicationRequest, ApplicationActionResponse, EnvironmentVariable, EnvVarSummary, CreateEnvVarRequest, UpdateEnvVarRequest, BulkUpdateEnvVarsRequest, Database, UpdateDatabaseRequest, DatabaseBackup, BackupExecution, Service, CreateServiceRequest, UpdateServiceRequest, ServiceCreateResponse, Deployment, Team, TeamMember, PrivateKey, CreatePrivateKeyRequest, UpdatePrivateKeyRequest, CloudToken, CreateCloudTokenRequest, UpdateCloudTokenRequest, CloudTokenValidation, Version, ApplicationDiagnostic, ServerDiagnostic, InfrastructureIssuesReport, BatchOperationResult } from '../types/coolify.js';
6
6
  export interface ListOptions {
7
7
  page?: number;
8
8
  per_page?: number;
@@ -184,4 +184,32 @@ export declare class CoolifyClient {
184
184
  * Finds: unreachable servers, unhealthy apps, exited databases, stopped services.
185
185
  */
186
186
  findInfrastructureIssues(): Promise<InfrastructureIssuesReport>;
187
+ /**
188
+ * Aggregate results from Promise.allSettled into a BatchOperationResult.
189
+ */
190
+ private aggregateBatchResults;
191
+ /**
192
+ * Restart all applications in a project.
193
+ * @param projectUuid - Project UUID
194
+ */
195
+ restartProjectApps(projectUuid: string): Promise<BatchOperationResult>;
196
+ /**
197
+ * Update or create an environment variable across multiple applications.
198
+ * Uses upsert behavior: creates if not exists, updates if exists.
199
+ * @param appUuids - Array of application UUIDs
200
+ * @param key - Environment variable key
201
+ * @param value - Environment variable value
202
+ * @param isBuildTime - Whether this is a build-time variable (default: false)
203
+ */
204
+ bulkEnvUpdate(appUuids: string[], key: string, value: string, isBuildTime?: boolean): Promise<BatchOperationResult>;
205
+ /**
206
+ * Emergency stop all running applications across entire infrastructure.
207
+ */
208
+ stopAllApps(): Promise<BatchOperationResult>;
209
+ /**
210
+ * Redeploy all applications in a project.
211
+ * @param projectUuid - Project UUID
212
+ * @param force - Force rebuild (default: true)
213
+ */
214
+ redeployProjectApps(projectUuid: string, force?: boolean): Promise<BatchOperationResult>;
187
215
  }
@@ -984,4 +984,117 @@ export class CoolifyClient {
984
984
  ...(errors.length > 0 && { errors }),
985
985
  };
986
986
  }
987
+ // ===========================================================================
988
+ // Batch Operations
989
+ // ===========================================================================
990
+ /**
991
+ * Aggregate results from Promise.allSettled into a BatchOperationResult.
992
+ */
993
+ aggregateBatchResults(resources, results) {
994
+ const succeeded = [];
995
+ const failed = [];
996
+ results.forEach((result, index) => {
997
+ const resource = resources[index];
998
+ const name = resource.name || resource.uuid;
999
+ if (result.status === 'fulfilled') {
1000
+ succeeded.push({ uuid: resource.uuid, name });
1001
+ }
1002
+ else {
1003
+ const error = result.reason instanceof Error ? result.reason.message : String(result.reason);
1004
+ failed.push({ uuid: resource.uuid, name, error });
1005
+ }
1006
+ });
1007
+ return {
1008
+ summary: {
1009
+ total: resources.length,
1010
+ succeeded: succeeded.length,
1011
+ failed: failed.length,
1012
+ },
1013
+ succeeded,
1014
+ failed,
1015
+ };
1016
+ }
1017
+ /**
1018
+ * Restart all applications in a project.
1019
+ * @param projectUuid - Project UUID
1020
+ */
1021
+ async restartProjectApps(projectUuid) {
1022
+ const allApps = (await this.listApplications());
1023
+ const projectApps = allApps.filter((app) => app.project_uuid === projectUuid);
1024
+ if (projectApps.length === 0) {
1025
+ return {
1026
+ summary: { total: 0, succeeded: 0, failed: 0 },
1027
+ succeeded: [],
1028
+ failed: [],
1029
+ };
1030
+ }
1031
+ const results = await Promise.allSettled(projectApps.map((app) => this.restartApplication(app.uuid)));
1032
+ return this.aggregateBatchResults(projectApps, results);
1033
+ }
1034
+ /**
1035
+ * Update or create an environment variable across multiple applications.
1036
+ * Uses upsert behavior: creates if not exists, updates if exists.
1037
+ * @param appUuids - Array of application UUIDs
1038
+ * @param key - Environment variable key
1039
+ * @param value - Environment variable value
1040
+ * @param isBuildTime - Whether this is a build-time variable (default: false)
1041
+ */
1042
+ async bulkEnvUpdate(appUuids, key, value, isBuildTime = false) {
1043
+ // Early return for empty array - avoid unnecessary API call
1044
+ if (appUuids.length === 0) {
1045
+ return {
1046
+ summary: { total: 0, succeeded: 0, failed: 0 },
1047
+ succeeded: [],
1048
+ failed: [],
1049
+ };
1050
+ }
1051
+ // Get app names first for better response
1052
+ const allApps = (await this.listApplications());
1053
+ const appMap = new Map(allApps.map((a) => [a.uuid, a.name || a.uuid]));
1054
+ // Build the resource list with names
1055
+ const resources = appUuids.map((uuid) => ({
1056
+ uuid,
1057
+ name: appMap.get(uuid) || uuid,
1058
+ }));
1059
+ const results = await Promise.allSettled(appUuids.map((uuid) => this.updateApplicationEnvVar(uuid, { key, value, is_build_time: isBuildTime })));
1060
+ return this.aggregateBatchResults(resources, results);
1061
+ }
1062
+ /**
1063
+ * Emergency stop all running applications across entire infrastructure.
1064
+ */
1065
+ async stopAllApps() {
1066
+ const allApps = (await this.listApplications());
1067
+ // Only stop running apps
1068
+ const runningApps = allApps.filter((app) => {
1069
+ const status = app.status || '';
1070
+ return status.includes('running') || status.includes('healthy');
1071
+ });
1072
+ if (runningApps.length === 0) {
1073
+ return {
1074
+ summary: { total: 0, succeeded: 0, failed: 0 },
1075
+ succeeded: [],
1076
+ failed: [],
1077
+ };
1078
+ }
1079
+ const results = await Promise.allSettled(runningApps.map((app) => this.stopApplication(app.uuid)));
1080
+ return this.aggregateBatchResults(runningApps, results);
1081
+ }
1082
+ /**
1083
+ * Redeploy all applications in a project.
1084
+ * @param projectUuid - Project UUID
1085
+ * @param force - Force rebuild (default: true)
1086
+ */
1087
+ async redeployProjectApps(projectUuid, force = true) {
1088
+ const allApps = (await this.listApplications());
1089
+ const projectApps = allApps.filter((app) => app.project_uuid === projectUuid);
1090
+ if (projectApps.length === 0) {
1091
+ return {
1092
+ summary: { total: 0, succeeded: 0, failed: 0 },
1093
+ succeeded: [],
1094
+ failed: [],
1095
+ };
1096
+ }
1097
+ const results = await Promise.allSettled(projectApps.map((app) => this.deployByTagOrUuid(app.uuid, force)));
1098
+ return this.aggregateBatchResults(projectApps, results);
1099
+ }
987
1100
  }
@@ -20,7 +20,7 @@
20
20
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
21
21
  import { z } from 'zod';
22
22
  import { CoolifyClient, } from './coolify-client.js';
23
- const VERSION = '0.8.1';
23
+ const VERSION = '0.9.0';
24
24
  /** Wrap tool handler with consistent error handling */
25
25
  function wrapHandler(fn) {
26
26
  return fn()
@@ -347,5 +347,31 @@ export class CoolifyMcpServer extends McpServer {
347
347
  this.tool('diagnose_app', 'Get comprehensive diagnostic info for an application. Accepts UUID, name, or domain (e.g., "stuartmason.co.uk" or "my-app"). Aggregates: status, health assessment, logs (last 50 lines), environment variables (keys only, values hidden), and recent deployments. Use this for debugging application issues.', { query: z.string().describe('Application UUID, name, or domain (FQDN)') }, async ({ query }) => wrapHandler(() => this.client.diagnoseApplication(query)));
348
348
  this.tool('diagnose_server', 'Get comprehensive diagnostic info for a server. Accepts UUID, name, or IP address (e.g., "coolify-apps" or "192.168.1.100"). Aggregates: server status, health assessment, running resources, configured domains, and connection validation. Use this for debugging server issues.', { query: z.string().describe('Server UUID, name, or IP address') }, async ({ query }) => wrapHandler(() => this.client.diagnoseServer(query)));
349
349
  this.tool('find_issues', 'Scan entire infrastructure for common issues. Finds: unreachable servers, unhealthy/stopped applications, exited databases, and stopped services. Returns a summary with issue counts and detailed list of problems.', {}, async () => wrapHandler(() => this.client.findInfrastructureIssues()));
350
+ // =========================================================================
351
+ // Batch Operations (4 tools) - Operate on multiple resources at once
352
+ // =========================================================================
353
+ this.tool('restart_project_apps', 'Restart all applications in a project. Returns a summary of succeeded/failed restarts with details.', { project_uuid: z.string().describe('Project UUID') }, async ({ project_uuid }) => wrapHandler(() => this.client.restartProjectApps(project_uuid)));
354
+ this.tool('bulk_env_update', 'Update or create an environment variable across multiple applications (upsert behavior). Returns summary of succeeded/failed updates.', {
355
+ app_uuids: z.array(z.string()).describe('Array of application UUIDs'),
356
+ key: z.string().describe('Environment variable key'),
357
+ value: z.string().describe('Environment variable value'),
358
+ is_build_time: z.boolean().optional().describe('Build-time variable (default: false)'),
359
+ }, async ({ app_uuids, key, value, is_build_time }) => wrapHandler(() => this.client.bulkEnvUpdate(app_uuids, key, value, is_build_time)));
360
+ this.tool('stop_all_apps', 'EMERGENCY: Stop ALL running applications across entire infrastructure. Only stops apps that are currently running or healthy. Use with caution!', {
361
+ confirm: z.literal(true).describe('Must be true to confirm this dangerous operation'),
362
+ }, async ({ confirm }) => {
363
+ if (!confirm) {
364
+ return {
365
+ content: [
366
+ { type: 'text', text: 'Error: Must set confirm=true to stop all apps' },
367
+ ],
368
+ };
369
+ }
370
+ return wrapHandler(() => this.client.stopAllApps());
371
+ });
372
+ this.tool('redeploy_project', 'Redeploy all applications in a project with force rebuild. Returns summary of succeeded/failed deployments.', {
373
+ project_uuid: z.string().describe('Project UUID'),
374
+ force: z.boolean().optional().describe('Force rebuild (default: true)'),
375
+ }, async ({ project_uuid, force }) => wrapHandler(() => this.client.redeployProjectApps(project_uuid, force ?? true)));
350
376
  }
351
377
  }
@@ -734,3 +734,19 @@ export interface InfrastructureIssuesReport {
734
734
  issues: InfrastructureIssue[];
735
735
  errors?: string[];
736
736
  }
737
+ export interface BatchOperationResult {
738
+ summary: {
739
+ total: number;
740
+ succeeded: number;
741
+ failed: number;
742
+ };
743
+ succeeded: Array<{
744
+ uuid: string;
745
+ name: string;
746
+ }>;
747
+ failed: Array<{
748
+ uuid: string;
749
+ name: string;
750
+ error: string;
751
+ }>;
752
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@masonator/coolify-mcp",
3
3
  "scope": "@masonator",
4
- "version": "0.8.1",
4
+ "version": "0.9.0",
5
5
  "description": "MCP server implementation for Coolify",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",