mlgym-deploy 3.2.0 → 3.2.2
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/index.js +213 -1
- package/mcp-docker-compose-patch.js +189 -0
- package/package.json +2 -2
- package/mlgym-deploy-2.10.0.tgz +0 -0
package/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import crypto from 'crypto';
|
|
|
17
17
|
const execAsync = promisify(exec);
|
|
18
18
|
|
|
19
19
|
// Current version of this MCP server - INCREMENT FOR WORKFLOW FIXES
|
|
20
|
-
const CURRENT_VERSION = '3.2.
|
|
20
|
+
const CURRENT_VERSION = '3.2.2'; // Added automatic validation and fixing for docker-compose and Dockerfile
|
|
21
21
|
const PACKAGE_NAME = 'mlgym-deploy';
|
|
22
22
|
|
|
23
23
|
// Debug logging configuration - ENABLED BY DEFAULT
|
|
@@ -1020,6 +1020,115 @@ async function detectDeploymentStrategy(projectPath) {
|
|
|
1020
1020
|
return { type: 'none', reason: 'No Dockerfile or docker-compose.yml found' };
|
|
1021
1021
|
}
|
|
1022
1022
|
|
|
1023
|
+
// Validate and potentially fix docker-compose.yml for Coolify compliance
|
|
1024
|
+
function validateAndFixDockerCompose(content) {
|
|
1025
|
+
const lines = content.split('\n');
|
|
1026
|
+
const issues = [];
|
|
1027
|
+
const fixes = [];
|
|
1028
|
+
let fixed = [...lines];
|
|
1029
|
+
let inServicesSection = false;
|
|
1030
|
+
let inPortsSection = false;
|
|
1031
|
+
let currentService = '';
|
|
1032
|
+
let currentIndent = '';
|
|
1033
|
+
|
|
1034
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1035
|
+
const line = lines[i];
|
|
1036
|
+
const trimmed = line.trim();
|
|
1037
|
+
|
|
1038
|
+
// Detect services section
|
|
1039
|
+
if (trimmed === 'services:') {
|
|
1040
|
+
inServicesSection = true;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (!inServicesSection) continue;
|
|
1045
|
+
|
|
1046
|
+
// Check for service definition
|
|
1047
|
+
if (trimmed && !line.startsWith(' ') && !line.startsWith('\t')) {
|
|
1048
|
+
inServicesSection = false;
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// New service
|
|
1053
|
+
if ((line.startsWith(' ') || line.startsWith('\t')) &&
|
|
1054
|
+
trimmed.endsWith(':') &&
|
|
1055
|
+
!trimmed.startsWith('-') &&
|
|
1056
|
+
!['ports:', 'expose:', 'environment:', 'volumes:', 'networks:'].includes(trimmed)) {
|
|
1057
|
+
currentService = trimmed.slice(0, -1);
|
|
1058
|
+
currentIndent = line.match(/^(\s*)/)[1];
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Check for ports section
|
|
1062
|
+
if (trimmed === 'ports:') {
|
|
1063
|
+
inPortsSection = true;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Process port entries
|
|
1068
|
+
if (inPortsSection && trimmed.startsWith('- ')) {
|
|
1069
|
+
const portEntry = trimmed.slice(2).replace(/['"]/g, '');
|
|
1070
|
+
|
|
1071
|
+
if (portEntry.includes(':')) {
|
|
1072
|
+
const [external, internal] = portEntry.split(':').map(p => p.trim());
|
|
1073
|
+
const webPorts = ['80', '8080', '3000', '4000', '5000', '8000', '9000'];
|
|
1074
|
+
|
|
1075
|
+
if (webPorts.includes(internal)) {
|
|
1076
|
+
issues.push({
|
|
1077
|
+
line: i + 1,
|
|
1078
|
+
service: currentService,
|
|
1079
|
+
issue: `Port mapping "${external}:${internal}" not allowed`,
|
|
1080
|
+
fix: 'Use port "80" only'
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// Fix the line
|
|
1084
|
+
fixed[i] = line.replace(trimmed, '- "80"');
|
|
1085
|
+
fixes.push(`Fixed ${currentService}: ${external}:${internal} → 80`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
} else if (inPortsSection && !trimmed.startsWith('-')) {
|
|
1089
|
+
inPortsSection = false;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return {
|
|
1094
|
+
isValid: issues.length === 0,
|
|
1095
|
+
issues,
|
|
1096
|
+
fixes,
|
|
1097
|
+
fixedContent: fixed.join('\n'),
|
|
1098
|
+
hasChanges: fixes.length > 0
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Validate Dockerfile for Coolify compliance
|
|
1103
|
+
function validateDockerfile(content) {
|
|
1104
|
+
const lines = content.split('\n');
|
|
1105
|
+
const issues = [];
|
|
1106
|
+
let hasExpose80 = false;
|
|
1107
|
+
|
|
1108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1109
|
+
const trimmed = lines[i].trim().toUpperCase();
|
|
1110
|
+
|
|
1111
|
+
if (trimmed.startsWith('EXPOSE')) {
|
|
1112
|
+
// Check if it exposes port 80
|
|
1113
|
+
if (trimmed.includes('80') && !trimmed.includes('8080') && !trimmed.includes('8443')) {
|
|
1114
|
+
hasExpose80 = true;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (!hasExpose80) {
|
|
1120
|
+
issues.push({
|
|
1121
|
+
issue: 'Dockerfile does not EXPOSE port 80',
|
|
1122
|
+
fix: 'Add "EXPOSE 80" to your Dockerfile'
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return {
|
|
1127
|
+
isValid: issues.length === 0,
|
|
1128
|
+
issues
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1023
1132
|
// Analyze docker-compose.yml to determine deployment strategy
|
|
1024
1133
|
async function analyzeComposeFile(composePath) {
|
|
1025
1134
|
const content = await fs.readFile(composePath, 'utf-8');
|
|
@@ -1186,6 +1295,32 @@ async function initProject(args) {
|
|
|
1186
1295
|
projectData.hostname = hostname;
|
|
1187
1296
|
projectData.local_path = local_path;
|
|
1188
1297
|
|
|
1298
|
+
// Read docker-compose content if using docker-compose strategy (v3.2.1+)
|
|
1299
|
+
if (strategy.type === 'docker-compose') {
|
|
1300
|
+
const composeFiles = ['docker-compose.yml', 'docker-compose.yaml'];
|
|
1301
|
+
let composeContent = null;
|
|
1302
|
+
|
|
1303
|
+
for (const filename of composeFiles) {
|
|
1304
|
+
const composePath = path.join(local_path, filename);
|
|
1305
|
+
if (fs.existsSync(composePath)) {
|
|
1306
|
+
try {
|
|
1307
|
+
composeContent = fs.readFileSync(composePath, 'utf8');
|
|
1308
|
+
log.info(`MCP >>> [initProject] Read ${filename}: ${composeContent.length} bytes`);
|
|
1309
|
+
break;
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
log.error(`MCP >>> [initProject] Error reading ${filename}:`, err.message);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (composeContent) {
|
|
1317
|
+
projectData.docker_compose_content = composeContent;
|
|
1318
|
+
log.info(`MCP >>> [initProject] Sending docker-compose content: ${composeContent.length} bytes`);
|
|
1319
|
+
} else {
|
|
1320
|
+
log.warn('MCP >>> [initProject] docker-compose strategy but no compose file found!');
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1189
1324
|
log.info(`MCP >>> [initProject] Sending deployment_type: ${strategy.type} to backend`);
|
|
1190
1325
|
}
|
|
1191
1326
|
|
|
@@ -1872,6 +2007,83 @@ async function deployProject(args) {
|
|
|
1872
2007
|
log.success('MCP >>> STEP 4: Project preparation complete');
|
|
1873
2008
|
log.debug('MCP >>> Preparation actions:', prepResult);
|
|
1874
2009
|
|
|
2010
|
+
// Step 4.5: Validate and fix deployment files for Coolify compliance
|
|
2011
|
+
log.info('MCP >>> STEP 4.5: Validating deployment configuration...');
|
|
2012
|
+
const strategy = detectDeploymentStrategy(local_path);
|
|
2013
|
+
|
|
2014
|
+
if (strategy.type === 'docker-compose') {
|
|
2015
|
+
const composePath = path.join(local_path, 'docker-compose.yml');
|
|
2016
|
+
const composePathYAML = path.join(local_path, 'docker-compose.yaml');
|
|
2017
|
+
const actualPath = fs.existsSync(composePath) ? composePath : composePathYAML;
|
|
2018
|
+
|
|
2019
|
+
if (fs.existsSync(actualPath)) {
|
|
2020
|
+
const content = fs.readFileSync(actualPath, 'utf8');
|
|
2021
|
+
const validation = validateAndFixDockerCompose(content);
|
|
2022
|
+
|
|
2023
|
+
if (!validation.isValid) {
|
|
2024
|
+
log.warning('MCP >>> Docker-compose validation issues found:');
|
|
2025
|
+
validation.issues.forEach(issue => {
|
|
2026
|
+
log.warning(` - Line ${issue.line}: ${issue.issue} (${issue.fix})`);
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
// Auto-fix the issues
|
|
2030
|
+
log.info('MCP >>> Auto-fixing docker-compose.yml port mappings...');
|
|
2031
|
+
fs.writeFileSync(actualPath, validation.fixedContent);
|
|
2032
|
+
|
|
2033
|
+
// Create backup
|
|
2034
|
+
const backupPath = actualPath + '.backup';
|
|
2035
|
+
fs.writeFileSync(backupPath, content);
|
|
2036
|
+
log.info(`MCP >>> Created backup at ${backupPath}`);
|
|
2037
|
+
|
|
2038
|
+
log.success('MCP >>> Fixed docker-compose.yml:');
|
|
2039
|
+
validation.fixes.forEach(fix => log.success(` - ${fix}`));
|
|
2040
|
+
} else {
|
|
2041
|
+
log.success('MCP >>> Docker-compose.yml is Coolify compliant');
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
} else if (strategy.type === 'dockerfile') {
|
|
2045
|
+
const dockerfilePath = path.join(local_path, 'Dockerfile');
|
|
2046
|
+
|
|
2047
|
+
if (fs.existsSync(dockerfilePath)) {
|
|
2048
|
+
const content = fs.readFileSync(dockerfilePath, 'utf8');
|
|
2049
|
+
const validation = validateDockerfile(content);
|
|
2050
|
+
|
|
2051
|
+
if (!validation.isValid) {
|
|
2052
|
+
log.warning('MCP >>> Dockerfile validation issues:');
|
|
2053
|
+
validation.issues.forEach(issue => {
|
|
2054
|
+
log.warning(` - ${issue.issue}`);
|
|
2055
|
+
log.info(` Fix: ${issue.fix}`);
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
// For Dockerfile, we'll add EXPOSE 80 if missing
|
|
2059
|
+
if (!content.includes('EXPOSE 80')) {
|
|
2060
|
+
log.info('MCP >>> Auto-fixing Dockerfile: adding EXPOSE 80...');
|
|
2061
|
+
const lines = content.split('\n');
|
|
2062
|
+
|
|
2063
|
+
// Find the last FROM or WORKDIR line to add EXPOSE after it
|
|
2064
|
+
let insertIndex = lines.length - 1;
|
|
2065
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
2066
|
+
if (lines[i].trim().startsWith('WORKDIR') || lines[i].trim().startsWith('FROM')) {
|
|
2067
|
+
insertIndex = i + 1;
|
|
2068
|
+
break;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
lines.splice(insertIndex, 0, '', 'EXPOSE 80');
|
|
2073
|
+
const fixedContent = lines.join('\n');
|
|
2074
|
+
|
|
2075
|
+
// Create backup
|
|
2076
|
+
fs.writeFileSync(dockerfilePath + '.backup', content);
|
|
2077
|
+
fs.writeFileSync(dockerfilePath, fixedContent);
|
|
2078
|
+
|
|
2079
|
+
log.success('MCP >>> Fixed Dockerfile: added EXPOSE 80');
|
|
2080
|
+
}
|
|
2081
|
+
} else {
|
|
2082
|
+
log.success('MCP >>> Dockerfile is Coolify compliant');
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1875
2087
|
// Step 5: Create GitLab project and deploy
|
|
1876
2088
|
log.info('MCP >>> STEP 5: Creating GitLab project and deploying...');
|
|
1877
2089
|
log.debug('MCP >>> Project params:', { name: project_name, hostname: hostname || project_name, local_path });
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// MCP Patch: Add docker-compose.yml content reading (matching CLI v2.0.14+)
|
|
2
|
+
//
|
|
3
|
+
// This patch ensures MCP reads docker-compose.yml content and sends it to the backend
|
|
4
|
+
// just like the CLI does, so the backend can store it in Coolify's database.
|
|
5
|
+
|
|
6
|
+
// Add this function after detectDeploymentStrategy function:
|
|
7
|
+
function readDockerComposeContent(projectPath) {
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
// Check both .yml and .yaml extensions
|
|
12
|
+
const composeFiles = ['docker-compose.yml', 'docker-compose.yaml'];
|
|
13
|
+
|
|
14
|
+
for (const filename of composeFiles) {
|
|
15
|
+
const composePath = path.join(projectPath, filename);
|
|
16
|
+
if (fs.existsSync(composePath)) {
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync(composePath, 'utf8');
|
|
19
|
+
console.log(`[MCP] Read ${filename}: ${content.length} bytes`);
|
|
20
|
+
return content;
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(`[MCP] Error reading ${filename}:`, err.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MODIFICATION NEEDED in initProject function (around line 1180-1190):
|
|
31
|
+
//
|
|
32
|
+
// FIND THIS BLOCK:
|
|
33
|
+
// projectData.deployment_type = strategy.type; // ← NEW: Send detected type!
|
|
34
|
+
// log.info(`MCP >>> [initProject] Sending deployment_type: ${strategy.type} to backend`);
|
|
35
|
+
//
|
|
36
|
+
// ADD AFTER IT:
|
|
37
|
+
// // Read docker-compose content if using docker-compose strategy
|
|
38
|
+
// if (strategy.type === 'docker-compose') {
|
|
39
|
+
// const composeContent = readDockerComposeContent(projectPath);
|
|
40
|
+
// if (composeContent) {
|
|
41
|
+
// projectData.docker_compose_content = composeContent;
|
|
42
|
+
// log.info(`MCP >>> [initProject] Sending docker-compose content: ${composeContent.length} bytes`);
|
|
43
|
+
// } else {
|
|
44
|
+
// log.warn('MCP >>> [initProject] docker-compose strategy but no compose file found!');
|
|
45
|
+
// }
|
|
46
|
+
// }
|
|
47
|
+
|
|
48
|
+
// The complete section should look like:
|
|
49
|
+
/*
|
|
50
|
+
// CRITICAL: Send deployment_type so backend doesn't try to detect from empty repo
|
|
51
|
+
log.info(`MCP >>> [initProject] Using strategy: ${JSON.stringify(strategy)}`);
|
|
52
|
+
projectData.deployment_type = strategy.type; // ← NEW: Send detected type!
|
|
53
|
+
|
|
54
|
+
// Read docker-compose content if using docker-compose strategy (v3.2.1+)
|
|
55
|
+
if (strategy.type === 'docker-compose') {
|
|
56
|
+
const composeContent = readDockerComposeContent(projectPath);
|
|
57
|
+
if (composeContent) {
|
|
58
|
+
projectData.docker_compose_content = composeContent;
|
|
59
|
+
log.info(`MCP >>> [initProject] Sending docker-compose content: ${composeContent.length} bytes`);
|
|
60
|
+
} else {
|
|
61
|
+
log.warn('MCP >>> [initProject] docker-compose strategy but no compose file found!');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
log.info(`MCP >>> [initProject] Sending deployment_type: ${strategy.type} to backend`);
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
// Also add validation function:
|
|
69
|
+
function validateDockerCompose(content) {
|
|
70
|
+
const issues = [];
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Parse YAML to check structure
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
|
|
76
|
+
// Check for 'ports' vs 'expose'
|
|
77
|
+
const hasPortsMapping = content.includes('ports:');
|
|
78
|
+
const hasExposeOnly = content.includes('expose:') && !hasPortsMapping;
|
|
79
|
+
|
|
80
|
+
if (!hasPortsMapping) {
|
|
81
|
+
issues.push({
|
|
82
|
+
level: 'ERROR',
|
|
83
|
+
message: 'No service exposes ports for web traffic',
|
|
84
|
+
suggestion: 'At least one service must have "ports:" configuration for Coolify/Traefik routing'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (hasExposeOnly) {
|
|
89
|
+
issues.push({
|
|
90
|
+
level: 'WARNING',
|
|
91
|
+
message: 'Service uses "expose" instead of "ports"',
|
|
92
|
+
suggestion: 'Change "expose" to "ports" for proper Traefik routing'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check for hardcoded passwords
|
|
97
|
+
const passwordPatterns = [
|
|
98
|
+
/password:\s*["'](?!.*\$\{).*["']/gi,
|
|
99
|
+
/PASSWORD=(?!.*\$\{)[^$\s]+/gi,
|
|
100
|
+
/secret:\s*["'](?!.*\$\{).*["']/gi
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const pattern of passwordPatterns) {
|
|
104
|
+
if (pattern.test(content)) {
|
|
105
|
+
issues.push({
|
|
106
|
+
level: 'WARNING',
|
|
107
|
+
message: 'Hardcoded password detected',
|
|
108
|
+
suggestion: 'Use environment variables: PASSWORD=${PASSWORD}'
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check for absolute volume paths
|
|
115
|
+
if (/volumes:\s*\n\s*-\s*\//m.test(content)) {
|
|
116
|
+
issues.push({
|
|
117
|
+
level: 'WARNING',
|
|
118
|
+
message: 'Absolute host path in volume mount',
|
|
119
|
+
suggestion: 'Use named volumes instead of absolute paths'
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
} catch (err) {
|
|
124
|
+
issues.push({
|
|
125
|
+
level: 'ERROR',
|
|
126
|
+
message: `Invalid docker-compose format: ${err.message}`,
|
|
127
|
+
suggestion: 'Fix YAML syntax errors'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return issues;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateDockerfile(projectPath) {
|
|
135
|
+
const fs = require('fs');
|
|
136
|
+
const path = require('path');
|
|
137
|
+
const dockerfilePath = path.join(projectPath, 'Dockerfile');
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const issues = [];
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const content = fs.readFileSync(dockerfilePath, 'utf8');
|
|
147
|
+
|
|
148
|
+
// Check for EXPOSE directive
|
|
149
|
+
if (!content.includes('EXPOSE ')) {
|
|
150
|
+
issues.push({
|
|
151
|
+
level: 'WARNING',
|
|
152
|
+
message: 'Dockerfile has no EXPOSE directive',
|
|
153
|
+
suggestion: 'Add EXPOSE 80 or appropriate port'
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check for USER directive (security)
|
|
158
|
+
if (!content.includes('USER ') || content.includes('USER root')) {
|
|
159
|
+
issues.push({
|
|
160
|
+
level: 'WARNING',
|
|
161
|
+
message: 'Dockerfile runs as root user',
|
|
162
|
+
suggestion: 'Add non-root USER for better security'
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check for WORKDIR
|
|
167
|
+
if (!content.includes('WORKDIR ')) {
|
|
168
|
+
issues.push({
|
|
169
|
+
level: 'INFO',
|
|
170
|
+
message: 'No WORKDIR set in Dockerfile',
|
|
171
|
+
suggestion: 'Consider setting WORKDIR for organization'
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// Ignore read errors
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return issues;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Export for testing
|
|
183
|
+
if (typeof module !== 'undefined') {
|
|
184
|
+
module.exports = {
|
|
185
|
+
readDockerComposeContent,
|
|
186
|
+
validateDockerCompose,
|
|
187
|
+
validateDockerfile
|
|
188
|
+
};
|
|
189
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mlgym-deploy",
|
|
3
|
-
"version": "3.2.
|
|
4
|
-
"description": "MCP server for MLGym -
|
|
3
|
+
"version": "3.2.2",
|
|
4
|
+
"description": "MCP server for MLGym - Auto-validates and fixes docker-compose/Dockerfile for Coolify",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
package/mlgym-deploy-2.10.0.tgz
DELETED
|
Binary file
|