@masonator/coolify-mcp 0.8.0 → 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 +48 -25
- package/dist/__tests__/coolify-client.test.js +316 -0
- package/dist/lib/coolify-client.d.ts +32 -2
- package/dist/lib/coolify-client.js +124 -2
- package/dist/lib/mcp-server.js +29 -3
- package/dist/types/coolify.d.ts +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
[](https://mseep.ai/app/stumason-coolify-mcp)
|
|
2
|
-
|
|
3
1
|
# Coolify MCP Server
|
|
4
2
|
|
|
3
|
+
[](https://mseep.ai/app/stumason-coolify-mcp)
|
|
4
|
+
|
|
5
5
|
A Model Context Protocol (MCP) server for [Coolify](https://coolify.io/), enabling AI assistants to manage and debug your Coolify instances through natural language.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
This MCP server provides **
|
|
10
|
-
|
|
11
|
-
| Category
|
|
12
|
-
|
|
|
13
|
-
| **Infrastructure**
|
|
14
|
-
| **
|
|
15
|
-
| **
|
|
16
|
-
| **
|
|
17
|
-
| **
|
|
18
|
-
| **
|
|
19
|
-
| **
|
|
20
|
-
| **
|
|
21
|
-
| **
|
|
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 |
|
|
22
24
|
|
|
23
25
|
## Installation
|
|
24
26
|
|
|
@@ -82,11 +84,12 @@ The Coolify API returns extremely verbose responses - a single application can c
|
|
|
82
84
|
|
|
83
85
|
### Response Size Comparison
|
|
84
86
|
|
|
85
|
-
| Endpoint
|
|
86
|
-
|
|
|
87
|
-
| list_applications
|
|
88
|
-
| list_services
|
|
89
|
-
| list_servers
|
|
87
|
+
| Endpoint | Full Response | Summary Response | Reduction |
|
|
88
|
+
| --------------------- | ------------- | ---------------- | --------- |
|
|
89
|
+
| list_applications | ~170KB | ~4.4KB | **97%** |
|
|
90
|
+
| list_services | ~367KB | ~1.2KB | **99%** |
|
|
91
|
+
| list_servers | ~4KB | ~0.4KB | **90%** |
|
|
92
|
+
| list_application_envs | ~3KB/var | ~0.1KB/var | **97%** |
|
|
90
93
|
|
|
91
94
|
### Recommended Workflow
|
|
92
95
|
|
|
@@ -108,7 +111,7 @@ list_applications(page=2, per_page=10)
|
|
|
108
111
|
|
|
109
112
|
### Getting Started
|
|
110
113
|
|
|
111
|
-
```
|
|
114
|
+
```text
|
|
112
115
|
Give me an overview of my infrastructure
|
|
113
116
|
Show me all my applications
|
|
114
117
|
What's running on my servers?
|
|
@@ -116,9 +119,12 @@ What's running on my servers?
|
|
|
116
119
|
|
|
117
120
|
### Debugging & Monitoring
|
|
118
121
|
|
|
119
|
-
```
|
|
122
|
+
```text
|
|
123
|
+
Diagnose my stuartmason.co.uk app
|
|
124
|
+
What's wrong with my-api application?
|
|
125
|
+
Check the status of server 192.168.1.100
|
|
126
|
+
Find any issues in my infrastructure
|
|
120
127
|
Get the logs for application {uuid}
|
|
121
|
-
What's the status of application {uuid}?
|
|
122
128
|
What environment variables are set for application {uuid}?
|
|
123
129
|
Show me recent deployments for application {uuid}
|
|
124
130
|
What resources are running on server {uuid}?
|
|
@@ -126,7 +132,7 @@ What resources are running on server {uuid}?
|
|
|
126
132
|
|
|
127
133
|
### Application Management
|
|
128
134
|
|
|
129
|
-
```
|
|
135
|
+
```text
|
|
130
136
|
Restart application {uuid}
|
|
131
137
|
Stop the database {uuid}
|
|
132
138
|
Start service {uuid}
|
|
@@ -136,7 +142,7 @@ Update the DATABASE_URL env var for application {uuid}
|
|
|
136
142
|
|
|
137
143
|
### Project Setup
|
|
138
144
|
|
|
139
|
-
```
|
|
145
|
+
```text
|
|
140
146
|
Create a new project called "my-app"
|
|
141
147
|
Create a staging environment in project {uuid}
|
|
142
148
|
Deploy my app from private GitHub repo org/repo on branch main
|
|
@@ -176,6 +182,14 @@ node dist/index.js
|
|
|
176
182
|
- `get_version` - Get Coolify API version
|
|
177
183
|
- `get_infrastructure_overview` - Get a high-level overview of all infrastructure (servers, projects, applications, databases, services)
|
|
178
184
|
|
|
185
|
+
### Diagnostics (Smart Lookup)
|
|
186
|
+
|
|
187
|
+
These tools accept human-friendly identifiers instead of just UUIDs:
|
|
188
|
+
|
|
189
|
+
- `diagnose_app` - Get comprehensive app diagnostics (status, logs, env vars, deployments). Accepts UUID, name, or domain (e.g., "stuartmason.co.uk" or "my-app")
|
|
190
|
+
- `diagnose_server` - Get server diagnostics (status, resources, domains, validation). Accepts UUID, name, or IP address (e.g., "coolify-apps" or "192.168.1.100")
|
|
191
|
+
- `find_issues` - Scan entire infrastructure for unhealthy apps, databases, services, and unreachable servers
|
|
192
|
+
|
|
179
193
|
### Servers
|
|
180
194
|
|
|
181
195
|
- `list_servers` - List all servers (returns summary)
|
|
@@ -258,6 +272,15 @@ node dist/index.js
|
|
|
258
272
|
- `update_private_key` - Update a private key
|
|
259
273
|
- `delete_private_key` - Delete a private key
|
|
260
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
|
+
|
|
261
284
|
## Contributing
|
|
262
285
|
|
|
263
286
|
Contributions welcome! Please open an issue first to discuss major changes.
|
|
@@ -649,6 +649,34 @@ describe('CoolifyClient', () => {
|
|
|
649
649
|
expect(result).toEqual([mockEnvVar]);
|
|
650
650
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/envs', expect.any(Object));
|
|
651
651
|
});
|
|
652
|
+
it('should list application env vars with summary', async () => {
|
|
653
|
+
const fullEnvVar = {
|
|
654
|
+
id: 1,
|
|
655
|
+
uuid: 'env-var-uuid',
|
|
656
|
+
key: 'API_KEY',
|
|
657
|
+
value: 'secret123',
|
|
658
|
+
is_build_time: false,
|
|
659
|
+
is_literal: true,
|
|
660
|
+
is_multiline: false,
|
|
661
|
+
is_preview: false,
|
|
662
|
+
is_shared: false,
|
|
663
|
+
is_shown_once: false,
|
|
664
|
+
application_id: 1,
|
|
665
|
+
created_at: '2024-01-01',
|
|
666
|
+
updated_at: '2024-01-01',
|
|
667
|
+
};
|
|
668
|
+
mockFetch.mockResolvedValueOnce(mockResponse([fullEnvVar]));
|
|
669
|
+
const result = await client.listApplicationEnvVars('app-uuid', { summary: true });
|
|
670
|
+
// Summary should only include uuid, key, value, is_build_time
|
|
671
|
+
expect(result).toEqual([
|
|
672
|
+
{
|
|
673
|
+
uuid: 'env-var-uuid',
|
|
674
|
+
key: 'API_KEY',
|
|
675
|
+
value: 'secret123',
|
|
676
|
+
is_build_time: false,
|
|
677
|
+
},
|
|
678
|
+
]);
|
|
679
|
+
});
|
|
652
680
|
it('should create application env var', async () => {
|
|
653
681
|
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'new-env-uuid' }));
|
|
654
682
|
const result = await client.createApplicationEnvVar('app-uuid', {
|
|
@@ -1641,4 +1669,292 @@ describe('CoolifyClient', () => {
|
|
|
1641
1669
|
});
|
|
1642
1670
|
});
|
|
1643
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
|
+
});
|
|
1644
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, 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;
|
|
@@ -100,7 +100,9 @@ export declare class CoolifyClient {
|
|
|
100
100
|
}): Promise<ApplicationActionResponse>;
|
|
101
101
|
stopApplication(uuid: string): Promise<ApplicationActionResponse>;
|
|
102
102
|
restartApplication(uuid: string): Promise<ApplicationActionResponse>;
|
|
103
|
-
listApplicationEnvVars(uuid: string
|
|
103
|
+
listApplicationEnvVars(uuid: string, options?: {
|
|
104
|
+
summary?: boolean;
|
|
105
|
+
}): Promise<EnvironmentVariable[] | EnvVarSummary[]>;
|
|
104
106
|
createApplicationEnvVar(uuid: string, data: CreateEnvVarRequest): Promise<UuidResponse>;
|
|
105
107
|
updateApplicationEnvVar(uuid: string, data: UpdateEnvVarRequest): Promise<MessageResponse>;
|
|
106
108
|
bulkUpdateApplicationEnvVars(uuid: string, data: BulkUpdateEnvVarsRequest): Promise<MessageResponse>;
|
|
@@ -182,4 +184,32 @@ export declare class CoolifyClient {
|
|
|
182
184
|
* Finds: unreachable servers, unhealthy apps, exited databases, stopped services.
|
|
183
185
|
*/
|
|
184
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>;
|
|
185
215
|
}
|
|
@@ -71,6 +71,14 @@ function toProjectSummary(proj) {
|
|
|
71
71
|
description: proj.description,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
function toEnvVarSummary(envVar) {
|
|
75
|
+
return {
|
|
76
|
+
uuid: envVar.uuid,
|
|
77
|
+
key: envVar.key,
|
|
78
|
+
value: envVar.value,
|
|
79
|
+
is_build_time: envVar.is_build_time,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
74
82
|
/**
|
|
75
83
|
* HTTP client for the Coolify API
|
|
76
84
|
*/
|
|
@@ -333,8 +341,9 @@ export class CoolifyClient {
|
|
|
333
341
|
// ===========================================================================
|
|
334
342
|
// Application Environment Variables
|
|
335
343
|
// ===========================================================================
|
|
336
|
-
async listApplicationEnvVars(uuid) {
|
|
337
|
-
|
|
344
|
+
async listApplicationEnvVars(uuid, options) {
|
|
345
|
+
const envVars = await this.request(`/applications/${uuid}/envs`);
|
|
346
|
+
return options?.summary ? envVars.map(toEnvVarSummary) : envVars;
|
|
338
347
|
}
|
|
339
348
|
async createApplicationEnvVar(uuid, data) {
|
|
340
349
|
return this.request(`/applications/${uuid}/envs`, {
|
|
@@ -975,4 +984,117 @@ export class CoolifyClient {
|
|
|
975
984
|
...(errors.length > 0 && { errors }),
|
|
976
985
|
};
|
|
977
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
|
+
}
|
|
978
1100
|
}
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -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.
|
|
23
|
+
const VERSION = '0.9.0';
|
|
24
24
|
/** Wrap tool handler with consistent error handling */
|
|
25
25
|
function wrapHandler(fn) {
|
|
26
26
|
return fn()
|
|
@@ -193,7 +193,7 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
193
193
|
lines: z.number().optional().describe('Number of lines'),
|
|
194
194
|
}, async ({ uuid, lines }) => wrapHandler(() => this.client.getApplicationLogs(uuid, lines)));
|
|
195
195
|
// Application env vars
|
|
196
|
-
this.tool('list_application_envs', 'List application environment variables', { uuid: z.string().describe('Application UUID') }, async ({ uuid }) => wrapHandler(() => this.client.listApplicationEnvVars(uuid)));
|
|
196
|
+
this.tool('list_application_envs', 'List application environment variables (returns summary: uuid, key, value, is_build_time)', { uuid: z.string().describe('Application UUID') }, async ({ uuid }) => wrapHandler(() => this.client.listApplicationEnvVars(uuid, { summary: true })));
|
|
197
197
|
this.tool('create_application_env', 'Create application environment variable', {
|
|
198
198
|
uuid: z.string().describe('Application UUID'),
|
|
199
199
|
key: z.string().describe('Variable key'),
|
|
@@ -344,8 +344,34 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
344
344
|
// =========================================================================
|
|
345
345
|
// Diagnostics (3 tools) - Composite tools for debugging
|
|
346
346
|
// =========================================================================
|
|
347
|
-
this.tool('diagnose_app', 'Get comprehensive diagnostic info for an application. Accepts UUID, name, or domain (e.g., "
|
|
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
|
}
|
package/dist/types/coolify.d.ts
CHANGED
|
@@ -348,6 +348,12 @@ export interface UpdateEnvVarRequest {
|
|
|
348
348
|
export interface BulkUpdateEnvVarsRequest {
|
|
349
349
|
data: CreateEnvVarRequest[];
|
|
350
350
|
}
|
|
351
|
+
export interface EnvVarSummary {
|
|
352
|
+
uuid: string;
|
|
353
|
+
key: string;
|
|
354
|
+
value: string;
|
|
355
|
+
is_build_time: boolean;
|
|
356
|
+
}
|
|
351
357
|
export type DatabaseType = 'postgresql' | 'mysql' | 'mariadb' | 'mongodb' | 'redis' | 'keydb' | 'clickhouse' | 'dragonfly';
|
|
352
358
|
export interface DatabaseLimits {
|
|
353
359
|
memory?: string;
|
|
@@ -728,3 +734,19 @@ export interface InfrastructureIssuesReport {
|
|
|
728
734
|
issues: InfrastructureIssue[];
|
|
729
735
|
errors?: string[];
|
|
730
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
|
+
}
|