@masonator/coolify-mcp 0.6.0 → 0.8.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 +17 -3
- package/dist/__tests__/coolify-client.test.js +627 -8
- package/dist/__tests__/integration/diagnostics.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/diagnostics.integration.test.js +140 -0
- package/dist/__tests__/mcp-server.test.js +166 -0
- package/dist/lib/coolify-client.d.ts +39 -3
- package/dist/lib/coolify-client.js +403 -15
- package/dist/lib/mcp-server.js +42 -1
- package/dist/types/coolify.d.ts +89 -0
- package/package.json +14 -4
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Complete HTTP client for the Coolify API v1
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Remove undefined values
|
|
7
|
-
*
|
|
6
|
+
* Remove undefined values from an object.
|
|
7
|
+
* Keeps explicit false values so features like HTTP Basic Auth can be disabled.
|
|
8
8
|
*/
|
|
9
9
|
function cleanRequestData(data) {
|
|
10
10
|
const cleaned = {};
|
|
11
11
|
for (const [key, value] of Object.entries(data)) {
|
|
12
|
-
if (value !== undefined
|
|
12
|
+
if (value !== undefined) {
|
|
13
13
|
cleaned[key] = value;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -406,18 +406,6 @@ export class CoolifyClient {
|
|
|
406
406
|
});
|
|
407
407
|
}
|
|
408
408
|
// ===========================================================================
|
|
409
|
-
// Database Backups
|
|
410
|
-
// ===========================================================================
|
|
411
|
-
async listDatabaseBackups(uuid) {
|
|
412
|
-
return this.request(`/databases/${uuid}/backups`);
|
|
413
|
-
}
|
|
414
|
-
async createDatabaseBackup(uuid, data) {
|
|
415
|
-
return this.request(`/databases/${uuid}/backups`, {
|
|
416
|
-
method: 'POST',
|
|
417
|
-
body: JSON.stringify(data),
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
// ===========================================================================
|
|
421
409
|
// Service endpoints
|
|
422
410
|
// ===========================================================================
|
|
423
411
|
async listServices(options) {
|
|
@@ -587,4 +575,404 @@ export class CoolifyClient {
|
|
|
587
575
|
async validateCloudToken(uuid) {
|
|
588
576
|
return this.request(`/cloud-tokens/${uuid}/validate`, { method: 'POST' });
|
|
589
577
|
}
|
|
578
|
+
// ===========================================================================
|
|
579
|
+
// Database Backup endpoints
|
|
580
|
+
// ===========================================================================
|
|
581
|
+
async listDatabaseBackups(databaseUuid) {
|
|
582
|
+
return this.request(`/databases/${databaseUuid}/backups`);
|
|
583
|
+
}
|
|
584
|
+
async getDatabaseBackup(databaseUuid, backupUuid) {
|
|
585
|
+
return this.request(`/databases/${databaseUuid}/backups/${backupUuid}`);
|
|
586
|
+
}
|
|
587
|
+
async listBackupExecutions(databaseUuid, backupUuid) {
|
|
588
|
+
return this.request(`/databases/${databaseUuid}/backups/${backupUuid}/executions`);
|
|
589
|
+
}
|
|
590
|
+
async getBackupExecution(databaseUuid, backupUuid, executionUuid) {
|
|
591
|
+
return this.request(`/databases/${databaseUuid}/backups/${backupUuid}/executions/${executionUuid}`);
|
|
592
|
+
}
|
|
593
|
+
// ===========================================================================
|
|
594
|
+
// Deployment Control endpoints
|
|
595
|
+
// ===========================================================================
|
|
596
|
+
async cancelDeployment(uuid) {
|
|
597
|
+
return this.request(`/deployments/${uuid}/cancel`, {
|
|
598
|
+
method: 'POST',
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
// ===========================================================================
|
|
602
|
+
// Smart Lookup Helpers
|
|
603
|
+
// ===========================================================================
|
|
604
|
+
/**
|
|
605
|
+
* Check if a string looks like a UUID (Coolify format or standard format).
|
|
606
|
+
* Coolify UUIDs are alphanumeric strings, typically 24 chars like "xs0sgs4gog044s4k4c88kgsc"
|
|
607
|
+
* Also accepts standard UUID format with hyphens like "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
608
|
+
*/
|
|
609
|
+
isLikelyUuid(query) {
|
|
610
|
+
// Coolify UUID format: alphanumeric, 20+ chars
|
|
611
|
+
if (/^[a-z0-9]{20,}$/i.test(query)) {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
// Standard UUID format with hyphens (8-4-4-4-12)
|
|
615
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(query)) {
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Find an application by UUID, name, or domain (FQDN).
|
|
622
|
+
* Returns the UUID if found, throws if not found or multiple matches.
|
|
623
|
+
*/
|
|
624
|
+
async resolveApplicationUuid(query) {
|
|
625
|
+
// If it looks like a UUID, use it directly
|
|
626
|
+
if (this.isLikelyUuid(query)) {
|
|
627
|
+
return query;
|
|
628
|
+
}
|
|
629
|
+
// Otherwise, search by name or domain
|
|
630
|
+
const apps = (await this.listApplications());
|
|
631
|
+
const queryLower = query.toLowerCase();
|
|
632
|
+
const matches = apps.filter((app) => {
|
|
633
|
+
const nameMatch = app.name?.toLowerCase().includes(queryLower);
|
|
634
|
+
const fqdnMatch = app.fqdn?.toLowerCase().includes(queryLower);
|
|
635
|
+
return nameMatch || fqdnMatch;
|
|
636
|
+
});
|
|
637
|
+
if (matches.length === 0) {
|
|
638
|
+
throw new Error(`No application found matching "${query}"`);
|
|
639
|
+
}
|
|
640
|
+
if (matches.length > 1) {
|
|
641
|
+
const matchList = matches.map((a) => `${a.name} (${a.fqdn || 'no domain'})`).join(', ');
|
|
642
|
+
throw new Error(`Multiple applications match "${query}": ${matchList}. Please be more specific or use a UUID.`);
|
|
643
|
+
}
|
|
644
|
+
return matches[0].uuid;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Find a server by UUID, name, or IP address.
|
|
648
|
+
* Returns the UUID if found, throws if not found or multiple matches.
|
|
649
|
+
*/
|
|
650
|
+
async resolveServerUuid(query) {
|
|
651
|
+
// If it looks like a UUID, use it directly
|
|
652
|
+
if (this.isLikelyUuid(query)) {
|
|
653
|
+
return query;
|
|
654
|
+
}
|
|
655
|
+
// Otherwise, search by name or IP
|
|
656
|
+
const servers = (await this.listServers());
|
|
657
|
+
const queryLower = query.toLowerCase();
|
|
658
|
+
const matches = servers.filter((server) => {
|
|
659
|
+
const nameMatch = server.name?.toLowerCase().includes(queryLower);
|
|
660
|
+
const ipMatch = server.ip?.toLowerCase().includes(queryLower);
|
|
661
|
+
return nameMatch || ipMatch;
|
|
662
|
+
});
|
|
663
|
+
if (matches.length === 0) {
|
|
664
|
+
throw new Error(`No server found matching "${query}"`);
|
|
665
|
+
}
|
|
666
|
+
if (matches.length > 1) {
|
|
667
|
+
const matchList = matches.map((s) => `${s.name} (${s.ip})`).join(', ');
|
|
668
|
+
throw new Error(`Multiple servers match "${query}": ${matchList}. Please be more specific or use a UUID.`);
|
|
669
|
+
}
|
|
670
|
+
return matches[0].uuid;
|
|
671
|
+
}
|
|
672
|
+
// ===========================================================================
|
|
673
|
+
// Diagnostic endpoints (composite tools)
|
|
674
|
+
// ===========================================================================
|
|
675
|
+
/**
|
|
676
|
+
* Get comprehensive diagnostic info for an application.
|
|
677
|
+
* Aggregates: application details, logs, env vars, recent deployments.
|
|
678
|
+
* @param query - Application UUID, name, or domain (FQDN)
|
|
679
|
+
*/
|
|
680
|
+
async diagnoseApplication(query) {
|
|
681
|
+
// Resolve query to UUID
|
|
682
|
+
let uuid;
|
|
683
|
+
try {
|
|
684
|
+
uuid = await this.resolveApplicationUuid(query);
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
688
|
+
return {
|
|
689
|
+
application: null,
|
|
690
|
+
health: { status: 'unknown', issues: [] },
|
|
691
|
+
logs: null,
|
|
692
|
+
environment_variables: { count: 0, variables: [] },
|
|
693
|
+
recent_deployments: [],
|
|
694
|
+
errors: [msg],
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const results = await Promise.allSettled([
|
|
698
|
+
this.getApplication(uuid),
|
|
699
|
+
this.getApplicationLogs(uuid, 50),
|
|
700
|
+
this.listApplicationEnvVars(uuid),
|
|
701
|
+
this.listApplicationDeployments(uuid),
|
|
702
|
+
]);
|
|
703
|
+
const errors = [];
|
|
704
|
+
const extract = (result, name) => {
|
|
705
|
+
if (result.status === 'fulfilled')
|
|
706
|
+
return result.value;
|
|
707
|
+
const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
708
|
+
errors.push(`${name}: ${msg}`);
|
|
709
|
+
return null;
|
|
710
|
+
};
|
|
711
|
+
const app = extract(results[0], 'application');
|
|
712
|
+
const logs = extract(results[1], 'logs');
|
|
713
|
+
const envVars = extract(results[2], 'environment_variables');
|
|
714
|
+
const deployments = extract(results[3], 'deployments');
|
|
715
|
+
// Determine health status and issues
|
|
716
|
+
const issues = [];
|
|
717
|
+
let healthStatus = 'unknown';
|
|
718
|
+
if (app) {
|
|
719
|
+
const status = app.status || '';
|
|
720
|
+
if (status.includes('running') && status.includes('healthy')) {
|
|
721
|
+
healthStatus = 'healthy';
|
|
722
|
+
}
|
|
723
|
+
else if (status.includes('exited') ||
|
|
724
|
+
status.includes('unhealthy') ||
|
|
725
|
+
status.includes('error')) {
|
|
726
|
+
healthStatus = 'unhealthy';
|
|
727
|
+
issues.push(`Status: ${status}`);
|
|
728
|
+
}
|
|
729
|
+
else if (status.includes('running')) {
|
|
730
|
+
healthStatus = 'healthy';
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
issues.push(`Status: ${status}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// Check for failed deployments
|
|
737
|
+
if (deployments) {
|
|
738
|
+
const recentFailed = deployments.slice(0, 5).filter((d) => d.status === 'failed');
|
|
739
|
+
if (recentFailed.length > 0) {
|
|
740
|
+
issues.push(`${recentFailed.length} failed deployment(s) in last 5`);
|
|
741
|
+
if (healthStatus === 'healthy')
|
|
742
|
+
healthStatus = 'unhealthy';
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
application: app
|
|
747
|
+
? {
|
|
748
|
+
uuid: app.uuid,
|
|
749
|
+
name: app.name,
|
|
750
|
+
status: app.status || 'unknown',
|
|
751
|
+
fqdn: app.fqdn || null,
|
|
752
|
+
git_repository: app.git_repository || null,
|
|
753
|
+
git_branch: app.git_branch || null,
|
|
754
|
+
}
|
|
755
|
+
: null,
|
|
756
|
+
health: {
|
|
757
|
+
status: healthStatus,
|
|
758
|
+
issues,
|
|
759
|
+
},
|
|
760
|
+
logs: typeof logs === 'string' ? logs : null,
|
|
761
|
+
environment_variables: {
|
|
762
|
+
count: envVars?.length || 0,
|
|
763
|
+
variables: (envVars || []).map((v) => ({
|
|
764
|
+
key: v.key,
|
|
765
|
+
is_build_time: v.is_build_time ?? false,
|
|
766
|
+
})),
|
|
767
|
+
},
|
|
768
|
+
recent_deployments: (deployments || []).slice(0, 5).map((d) => ({
|
|
769
|
+
uuid: d.uuid,
|
|
770
|
+
status: d.status,
|
|
771
|
+
created_at: d.created_at,
|
|
772
|
+
})),
|
|
773
|
+
...(errors.length > 0 && { errors }),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Get comprehensive diagnostic info for a server.
|
|
778
|
+
* Aggregates: server details, resources, domains, validation.
|
|
779
|
+
* @param query - Server UUID, name, or IP address
|
|
780
|
+
*/
|
|
781
|
+
async diagnoseServer(query) {
|
|
782
|
+
// Resolve query to UUID
|
|
783
|
+
let uuid;
|
|
784
|
+
try {
|
|
785
|
+
uuid = await this.resolveServerUuid(query);
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
789
|
+
return {
|
|
790
|
+
server: null,
|
|
791
|
+
health: { status: 'unknown', issues: [] },
|
|
792
|
+
resources: [],
|
|
793
|
+
domains: [],
|
|
794
|
+
validation: null,
|
|
795
|
+
errors: [msg],
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
const results = await Promise.allSettled([
|
|
799
|
+
this.getServer(uuid),
|
|
800
|
+
this.getServerResources(uuid),
|
|
801
|
+
this.getServerDomains(uuid),
|
|
802
|
+
this.validateServer(uuid),
|
|
803
|
+
]);
|
|
804
|
+
const errors = [];
|
|
805
|
+
const extract = (result, name) => {
|
|
806
|
+
if (result.status === 'fulfilled')
|
|
807
|
+
return result.value;
|
|
808
|
+
const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
809
|
+
errors.push(`${name}: ${msg}`);
|
|
810
|
+
return null;
|
|
811
|
+
};
|
|
812
|
+
const server = extract(results[0], 'server');
|
|
813
|
+
const resources = extract(results[1], 'resources');
|
|
814
|
+
const domains = extract(results[2], 'domains');
|
|
815
|
+
const validation = extract(results[3], 'validation');
|
|
816
|
+
// Determine health status and issues
|
|
817
|
+
const issues = [];
|
|
818
|
+
let healthStatus = 'unknown';
|
|
819
|
+
if (server) {
|
|
820
|
+
if (server.is_reachable === true) {
|
|
821
|
+
healthStatus = 'healthy';
|
|
822
|
+
}
|
|
823
|
+
else if (server.is_reachable === false) {
|
|
824
|
+
healthStatus = 'unhealthy';
|
|
825
|
+
issues.push('Server is not reachable');
|
|
826
|
+
}
|
|
827
|
+
if (server.is_usable === false) {
|
|
828
|
+
issues.push('Server is not usable');
|
|
829
|
+
healthStatus = 'unhealthy';
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Check for unhealthy resources
|
|
833
|
+
if (resources) {
|
|
834
|
+
const unhealthyResources = resources.filter((r) => r.status.includes('exited') ||
|
|
835
|
+
r.status.includes('unhealthy') ||
|
|
836
|
+
r.status.includes('error'));
|
|
837
|
+
if (unhealthyResources.length > 0) {
|
|
838
|
+
issues.push(`${unhealthyResources.length} unhealthy resource(s)`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
server: server
|
|
843
|
+
? {
|
|
844
|
+
uuid: server.uuid,
|
|
845
|
+
name: server.name,
|
|
846
|
+
ip: server.ip,
|
|
847
|
+
status: server.status || null,
|
|
848
|
+
is_reachable: server.is_reachable ?? null,
|
|
849
|
+
}
|
|
850
|
+
: null,
|
|
851
|
+
health: {
|
|
852
|
+
status: healthStatus,
|
|
853
|
+
issues,
|
|
854
|
+
},
|
|
855
|
+
resources: (resources || []).map((r) => ({
|
|
856
|
+
uuid: r.uuid,
|
|
857
|
+
name: r.name,
|
|
858
|
+
type: r.type,
|
|
859
|
+
status: r.status,
|
|
860
|
+
})),
|
|
861
|
+
domains: (domains || []).map((d) => ({
|
|
862
|
+
ip: d.ip,
|
|
863
|
+
domains: d.domains,
|
|
864
|
+
})),
|
|
865
|
+
validation: validation
|
|
866
|
+
? {
|
|
867
|
+
message: validation.message,
|
|
868
|
+
...(validation.validation_logs && { validation_logs: validation.validation_logs }),
|
|
869
|
+
}
|
|
870
|
+
: null,
|
|
871
|
+
...(errors.length > 0 && { errors }),
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Scan infrastructure for common issues.
|
|
876
|
+
* Finds: unreachable servers, unhealthy apps, exited databases, stopped services.
|
|
877
|
+
*/
|
|
878
|
+
async findInfrastructureIssues() {
|
|
879
|
+
const results = await Promise.allSettled([
|
|
880
|
+
this.listServers(),
|
|
881
|
+
this.listApplications(),
|
|
882
|
+
this.listDatabases(),
|
|
883
|
+
this.listServices(),
|
|
884
|
+
]);
|
|
885
|
+
const errors = [];
|
|
886
|
+
const issues = [];
|
|
887
|
+
const extract = (result, name) => {
|
|
888
|
+
if (result.status === 'fulfilled')
|
|
889
|
+
return result.value;
|
|
890
|
+
const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
891
|
+
errors.push(`${name}: ${msg}`);
|
|
892
|
+
return null;
|
|
893
|
+
};
|
|
894
|
+
const servers = extract(results[0], 'servers');
|
|
895
|
+
const applications = extract(results[1], 'applications');
|
|
896
|
+
const databases = extract(results[2], 'databases');
|
|
897
|
+
const services = extract(results[3], 'services');
|
|
898
|
+
// Check servers for unreachable
|
|
899
|
+
if (servers) {
|
|
900
|
+
for (const server of servers) {
|
|
901
|
+
if (server.is_reachable === false) {
|
|
902
|
+
issues.push({
|
|
903
|
+
type: 'server',
|
|
904
|
+
uuid: server.uuid,
|
|
905
|
+
name: server.name,
|
|
906
|
+
issue: 'Server is not reachable',
|
|
907
|
+
status: server.status || 'unreachable',
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// Check applications for unhealthy status
|
|
913
|
+
if (applications) {
|
|
914
|
+
for (const app of applications) {
|
|
915
|
+
const status = app.status || '';
|
|
916
|
+
if (status.includes('exited') ||
|
|
917
|
+
status.includes('unhealthy') ||
|
|
918
|
+
status.includes('error') ||
|
|
919
|
+
status === 'stopped') {
|
|
920
|
+
issues.push({
|
|
921
|
+
type: 'application',
|
|
922
|
+
uuid: app.uuid,
|
|
923
|
+
name: app.name,
|
|
924
|
+
issue: `Application status: ${status}`,
|
|
925
|
+
status,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// Check databases for unhealthy status
|
|
931
|
+
if (databases) {
|
|
932
|
+
for (const db of databases) {
|
|
933
|
+
const status = db.status || '';
|
|
934
|
+
if (status.includes('exited') ||
|
|
935
|
+
status.includes('unhealthy') ||
|
|
936
|
+
status.includes('error') ||
|
|
937
|
+
status === 'stopped') {
|
|
938
|
+
issues.push({
|
|
939
|
+
type: 'database',
|
|
940
|
+
uuid: db.uuid,
|
|
941
|
+
name: db.name,
|
|
942
|
+
issue: `Database status: ${status}`,
|
|
943
|
+
status,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// Check services for unhealthy status
|
|
949
|
+
if (services) {
|
|
950
|
+
for (const svc of services) {
|
|
951
|
+
const status = svc.status || '';
|
|
952
|
+
if (status.includes('exited') ||
|
|
953
|
+
status.includes('unhealthy') ||
|
|
954
|
+
status.includes('error') ||
|
|
955
|
+
status === 'stopped') {
|
|
956
|
+
issues.push({
|
|
957
|
+
type: 'service',
|
|
958
|
+
uuid: svc.uuid,
|
|
959
|
+
name: svc.name,
|
|
960
|
+
issue: `Service status: ${status}`,
|
|
961
|
+
status,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
summary: {
|
|
968
|
+
total_issues: issues.length,
|
|
969
|
+
unhealthy_applications: issues.filter((i) => i.type === 'application').length,
|
|
970
|
+
unhealthy_databases: issues.filter((i) => i.type === 'database').length,
|
|
971
|
+
unhealthy_services: issues.filter((i) => i.type === 'service').length,
|
|
972
|
+
unreachable_servers: issues.filter((i) => i.type === 'server').length,
|
|
973
|
+
},
|
|
974
|
+
issues,
|
|
975
|
+
...(errors.length > 0 && { errors }),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
590
978
|
}
|
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.8.0';
|
|
24
24
|
/** Wrap tool handler with consistent error handling */
|
|
25
25
|
function wrapHandler(fn) {
|
|
26
26
|
return fn()
|
|
@@ -306,5 +306,46 @@ export class CoolifyMcpServer extends McpServer {
|
|
|
306
306
|
force: z.boolean().optional().describe('Force rebuild'),
|
|
307
307
|
}, async ({ tag_or_uuid, force }) => wrapHandler(() => this.client.deployByTagOrUuid(tag_or_uuid, force)));
|
|
308
308
|
this.tool('list_application_deployments', 'List deployments for an application', { uuid: z.string().describe('Application UUID') }, async ({ uuid }) => wrapHandler(() => this.client.listApplicationDeployments(uuid)));
|
|
309
|
+
this.tool('cancel_deployment', 'Cancel a running deployment', { uuid: z.string().describe('Deployment UUID') }, async ({ uuid }) => wrapHandler(() => this.client.cancelDeployment(uuid)));
|
|
310
|
+
// =========================================================================
|
|
311
|
+
// Private Keys (5 tools)
|
|
312
|
+
// =========================================================================
|
|
313
|
+
this.tool('list_private_keys', 'List all private keys (SSH keys for deployments)', {}, async () => wrapHandler(() => this.client.listPrivateKeys()));
|
|
314
|
+
this.tool('get_private_key', 'Get private key details', { uuid: z.string().describe('Private key UUID') }, async ({ uuid }) => wrapHandler(() => this.client.getPrivateKey(uuid)));
|
|
315
|
+
this.tool('create_private_key', 'Create a new private key for deployments', {
|
|
316
|
+
private_key: z.string().describe('The private key content (PEM format)'),
|
|
317
|
+
name: z.string().optional().describe('Name for the key'),
|
|
318
|
+
description: z.string().optional().describe('Description'),
|
|
319
|
+
}, async (args) => wrapHandler(() => this.client.createPrivateKey(args)));
|
|
320
|
+
this.tool('update_private_key', 'Update a private key', {
|
|
321
|
+
uuid: z.string().describe('Private key UUID'),
|
|
322
|
+
name: z.string().optional().describe('Name for the key'),
|
|
323
|
+
description: z.string().optional().describe('Description'),
|
|
324
|
+
private_key: z.string().optional().describe('The private key content (PEM format)'),
|
|
325
|
+
}, async ({ uuid, ...data }) => wrapHandler(() => this.client.updatePrivateKey(uuid, data)));
|
|
326
|
+
this.tool('delete_private_key', 'Delete a private key', { uuid: z.string().describe('Private key UUID') }, async ({ uuid }) => wrapHandler(() => this.client.deletePrivateKey(uuid)));
|
|
327
|
+
// =========================================================================
|
|
328
|
+
// Database Backups (4 tools)
|
|
329
|
+
// =========================================================================
|
|
330
|
+
this.tool('list_database_backups', 'List scheduled backups for a database', { uuid: z.string().describe('Database UUID') }, async ({ uuid }) => wrapHandler(() => this.client.listDatabaseBackups(uuid)));
|
|
331
|
+
this.tool('get_database_backup', 'Get details of a scheduled backup', {
|
|
332
|
+
database_uuid: z.string().describe('Database UUID'),
|
|
333
|
+
backup_uuid: z.string().describe('Scheduled backup UUID'),
|
|
334
|
+
}, async ({ database_uuid, backup_uuid }) => wrapHandler(() => this.client.getDatabaseBackup(database_uuid, backup_uuid)));
|
|
335
|
+
this.tool('list_backup_executions', 'List execution history for a scheduled backup', {
|
|
336
|
+
database_uuid: z.string().describe('Database UUID'),
|
|
337
|
+
backup_uuid: z.string().describe('Scheduled backup UUID'),
|
|
338
|
+
}, async ({ database_uuid, backup_uuid }) => wrapHandler(() => this.client.listBackupExecutions(database_uuid, backup_uuid)));
|
|
339
|
+
this.tool('get_backup_execution', 'Get details of a specific backup execution', {
|
|
340
|
+
database_uuid: z.string().describe('Database UUID'),
|
|
341
|
+
backup_uuid: z.string().describe('Scheduled backup UUID'),
|
|
342
|
+
execution_uuid: z.string().describe('Backup execution UUID'),
|
|
343
|
+
}, async ({ database_uuid, backup_uuid, execution_uuid }) => wrapHandler(() => this.client.getBackupExecution(database_uuid, backup_uuid, execution_uuid)));
|
|
344
|
+
// =========================================================================
|
|
345
|
+
// Diagnostics (3 tools) - Composite tools for debugging
|
|
346
|
+
// =========================================================================
|
|
347
|
+
this.tool('diagnose_app', 'Get comprehensive diagnostic info for an application. Accepts UUID, name, or domain (e.g., "tidylinker.com" 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
|
+
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
|
+
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()));
|
|
309
350
|
}
|
|
310
351
|
}
|
package/dist/types/coolify.d.ts
CHANGED
|
@@ -300,6 +300,9 @@ export interface UpdateApplicationRequest {
|
|
|
300
300
|
limits_memory?: string;
|
|
301
301
|
limits_memory_swap?: string;
|
|
302
302
|
limits_cpus?: string;
|
|
303
|
+
is_http_basic_auth_enabled?: boolean;
|
|
304
|
+
http_basic_auth_username?: string;
|
|
305
|
+
http_basic_auth_password?: string;
|
|
303
306
|
}
|
|
304
307
|
export interface ApplicationActionResponse {
|
|
305
308
|
message: string;
|
|
@@ -470,6 +473,17 @@ export interface CreateDatabaseBackupRequest {
|
|
|
470
473
|
backup_retention?: number;
|
|
471
474
|
backup_retention_days?: number;
|
|
472
475
|
}
|
|
476
|
+
export interface BackupExecution {
|
|
477
|
+
id: number;
|
|
478
|
+
uuid: string;
|
|
479
|
+
scheduled_database_backup_id: number;
|
|
480
|
+
status: 'pending' | 'running' | 'success' | 'failed';
|
|
481
|
+
message?: string;
|
|
482
|
+
size?: number;
|
|
483
|
+
filename?: string;
|
|
484
|
+
created_at: string;
|
|
485
|
+
updated_at: string;
|
|
486
|
+
}
|
|
473
487
|
/**
|
|
474
488
|
* Available one-click service types in Coolify.
|
|
475
489
|
* This is a string type to avoid TypeScript memory issues with large const arrays.
|
|
@@ -639,3 +653,78 @@ export interface HealthCheck {
|
|
|
639
653
|
status: 'healthy' | 'unhealthy';
|
|
640
654
|
version?: string;
|
|
641
655
|
}
|
|
656
|
+
export type DiagnosticHealthStatus = 'healthy' | 'unhealthy' | 'unknown';
|
|
657
|
+
export interface ApplicationDiagnostic {
|
|
658
|
+
application: {
|
|
659
|
+
uuid: string;
|
|
660
|
+
name: string;
|
|
661
|
+
status: string;
|
|
662
|
+
fqdn: string | null;
|
|
663
|
+
git_repository: string | null;
|
|
664
|
+
git_branch: string | null;
|
|
665
|
+
} | null;
|
|
666
|
+
health: {
|
|
667
|
+
status: DiagnosticHealthStatus;
|
|
668
|
+
issues: string[];
|
|
669
|
+
};
|
|
670
|
+
logs: string | null;
|
|
671
|
+
environment_variables: {
|
|
672
|
+
count: number;
|
|
673
|
+
variables: Array<{
|
|
674
|
+
key: string;
|
|
675
|
+
is_build_time: boolean;
|
|
676
|
+
}>;
|
|
677
|
+
};
|
|
678
|
+
recent_deployments: Array<{
|
|
679
|
+
uuid: string;
|
|
680
|
+
status: string;
|
|
681
|
+
created_at: string;
|
|
682
|
+
}>;
|
|
683
|
+
errors?: string[];
|
|
684
|
+
}
|
|
685
|
+
export interface ServerDiagnostic {
|
|
686
|
+
server: {
|
|
687
|
+
uuid: string;
|
|
688
|
+
name: string;
|
|
689
|
+
ip: string;
|
|
690
|
+
status: string | null;
|
|
691
|
+
is_reachable: boolean | null;
|
|
692
|
+
} | null;
|
|
693
|
+
health: {
|
|
694
|
+
status: DiagnosticHealthStatus;
|
|
695
|
+
issues: string[];
|
|
696
|
+
};
|
|
697
|
+
resources: Array<{
|
|
698
|
+
uuid: string;
|
|
699
|
+
name: string;
|
|
700
|
+
type: string;
|
|
701
|
+
status: string;
|
|
702
|
+
}>;
|
|
703
|
+
domains: Array<{
|
|
704
|
+
ip: string;
|
|
705
|
+
domains: string[];
|
|
706
|
+
}>;
|
|
707
|
+
validation: {
|
|
708
|
+
message: string;
|
|
709
|
+
validation_logs?: string;
|
|
710
|
+
} | null;
|
|
711
|
+
errors?: string[];
|
|
712
|
+
}
|
|
713
|
+
export interface InfrastructureIssue {
|
|
714
|
+
type: 'application' | 'database' | 'service' | 'server';
|
|
715
|
+
uuid: string;
|
|
716
|
+
name: string;
|
|
717
|
+
issue: string;
|
|
718
|
+
status: string;
|
|
719
|
+
}
|
|
720
|
+
export interface InfrastructureIssuesReport {
|
|
721
|
+
summary: {
|
|
722
|
+
total_issues: number;
|
|
723
|
+
unhealthy_applications: number;
|
|
724
|
+
unhealthy_databases: number;
|
|
725
|
+
unhealthy_services: number;
|
|
726
|
+
unreachable_servers: number;
|
|
727
|
+
};
|
|
728
|
+
issues: InfrastructureIssue[];
|
|
729
|
+
errors?: string[];
|
|
730
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@masonator/coolify-mcp",
|
|
3
3
|
"scope": "@masonator",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "MCP server implementation for Coolify",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
23
23
|
"dev": "tsc --watch",
|
|
24
|
-
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
25
|
-
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
|
|
26
|
-
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
|
24
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=integration",
|
|
25
|
+
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch --testPathIgnorePatterns=integration",
|
|
26
|
+
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --testPathIgnorePatterns=integration",
|
|
27
|
+
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=integration --testTimeout=60000",
|
|
27
28
|
"lint": "eslint . --ext .ts",
|
|
28
29
|
"lint:fix": "eslint . --ext .ts --fix",
|
|
29
30
|
"format": "prettier --write .",
|
|
@@ -39,6 +40,10 @@
|
|
|
39
40
|
],
|
|
40
41
|
"author": "Stuart Mason",
|
|
41
42
|
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/StuMason/coolify-mcp.git"
|
|
46
|
+
},
|
|
42
47
|
"dependencies": {
|
|
43
48
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
44
49
|
"zod": "^3.24.2"
|
|
@@ -48,6 +53,7 @@
|
|
|
48
53
|
"@types/node": "^20.17.23",
|
|
49
54
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
50
55
|
"@typescript-eslint/parser": "^7.18.0",
|
|
56
|
+
"dotenv": "^16.4.5",
|
|
51
57
|
"eslint": "^8.56.0",
|
|
52
58
|
"eslint-config-prettier": "^9.1.0",
|
|
53
59
|
"husky": "^9.0.11",
|
|
@@ -61,5 +67,9 @@
|
|
|
61
67
|
},
|
|
62
68
|
"engines": {
|
|
63
69
|
"node": ">=18"
|
|
70
|
+
},
|
|
71
|
+
"lint-staged": {
|
|
72
|
+
"*.{ts,js,json,md,yaml,yml}": "prettier --write",
|
|
73
|
+
"*.ts": "eslint --fix"
|
|
64
74
|
}
|
|
65
75
|
}
|