mlgym-deploy 3.0.7 → 3.2.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/index.js +363 -5
- package/package.json +2 -2
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.0
|
|
20
|
+
const CURRENT_VERSION = '3.2.0'; // Added mlgym_deploy_logs tool for viewing deployment history
|
|
21
21
|
const PACKAGE_NAME = 'mlgym-deploy';
|
|
22
22
|
|
|
23
23
|
// Debug logging configuration - ENABLED BY DEFAULT
|
|
@@ -705,25 +705,38 @@ CMD ["/bin/sh"]`;
|
|
|
705
705
|
async function prepareProject(args) {
|
|
706
706
|
const { local_path = '.', project_type, framework, package_manager } = args;
|
|
707
707
|
const absolutePath = path.resolve(local_path);
|
|
708
|
+
log.info('MCP >>> [prepareProject-func] Called with local_path:', local_path);
|
|
709
|
+
log.debug('MCP >>> [prepareProject-func] Resolved absolutePath:', absolutePath);
|
|
710
|
+
log.debug('MCP >>> [prepareProject-func] project_type:', project_type, 'framework:', framework);
|
|
708
711
|
|
|
709
712
|
const actions = [];
|
|
710
713
|
|
|
711
714
|
try {
|
|
712
715
|
// Check if Dockerfile exists
|
|
713
716
|
const dockerfilePath = path.join(absolutePath, 'Dockerfile');
|
|
717
|
+
log.debug('MCP >>> [prepareProject-func] Dockerfile path:', dockerfilePath);
|
|
714
718
|
let dockerfileExists = false;
|
|
715
719
|
|
|
716
720
|
try {
|
|
717
721
|
await fs.access(dockerfilePath);
|
|
718
722
|
dockerfileExists = true;
|
|
723
|
+
log.info('MCP >>> [prepareProject-func] Dockerfile already exists at', dockerfilePath);
|
|
719
724
|
actions.push('Dockerfile already exists - skipping generation');
|
|
720
|
-
} catch {
|
|
725
|
+
} catch {
|
|
726
|
+
log.info('MCP >>> [prepareProject-func] Dockerfile does not exist, will generate');
|
|
727
|
+
}
|
|
721
728
|
|
|
722
729
|
// Generate Dockerfile if missing
|
|
723
730
|
if (!dockerfileExists && project_type !== 'unknown') {
|
|
731
|
+
log.info('MCP >>> [prepareProject-func] Generating Dockerfile...');
|
|
724
732
|
const dockerfile = generateDockerfile(project_type, framework, package_manager);
|
|
733
|
+
log.debug('MCP >>> [prepareProject-func] Generated Dockerfile length:', dockerfile.length, 'bytes');
|
|
734
|
+
log.info('MCP >>> [prepareProject-func] Writing Dockerfile to:', dockerfilePath);
|
|
725
735
|
await fs.writeFile(dockerfilePath, dockerfile);
|
|
736
|
+
log.success('MCP >>> [prepareProject-func] ✅ Dockerfile written successfully!');
|
|
726
737
|
actions.push(`Generated Dockerfile for ${project_type}/${framework || 'generic'}`);
|
|
738
|
+
} else if (project_type === 'unknown') {
|
|
739
|
+
log.warning('MCP >>> [prepareProject-func] Project type is unknown, skipping Dockerfile generation');
|
|
727
740
|
}
|
|
728
741
|
|
|
729
742
|
// Check/create .gitignore
|
|
@@ -950,6 +963,145 @@ async function smartDeploy(args) {
|
|
|
950
963
|
}
|
|
951
964
|
}
|
|
952
965
|
|
|
966
|
+
// Deployment Strategy Detection (matches CLI logic)
|
|
967
|
+
async function detectDeploymentStrategy(projectPath) {
|
|
968
|
+
const dockerfilePath = path.join(projectPath, 'Dockerfile');
|
|
969
|
+
const composePathYML = path.join(projectPath, 'docker-compose.yml');
|
|
970
|
+
const composePathYAML = path.join(projectPath, 'docker-compose.yaml');
|
|
971
|
+
|
|
972
|
+
const hasDockerfile = await fileExists(dockerfilePath);
|
|
973
|
+
const hasComposeYML = await fileExists(composePathYML);
|
|
974
|
+
const hasComposeYAML = await fileExists(composePathYAML);
|
|
975
|
+
const hasCompose = hasComposeYML || hasComposeYAML;
|
|
976
|
+
const composePath = hasComposeYML ? composePathYML : composePathYAML;
|
|
977
|
+
|
|
978
|
+
log.info(`MCP >>> [detectDeploymentStrategy] Dockerfile: ${hasDockerfile}, docker-compose: ${hasCompose}`);
|
|
979
|
+
|
|
980
|
+
// Case 1: Only Dockerfile
|
|
981
|
+
if (hasDockerfile && !hasCompose) {
|
|
982
|
+
log.info('MCP >>> [detectDeploymentStrategy] Strategy: dockerfile (Dockerfile only)');
|
|
983
|
+
return { type: 'dockerfile', reason: 'Dockerfile only' };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Case 2: Only docker-compose
|
|
987
|
+
if (!hasDockerfile && hasCompose) {
|
|
988
|
+
log.info('MCP >>> [detectDeploymentStrategy] Strategy: docker-compose (compose only)');
|
|
989
|
+
return { type: 'docker-compose', reason: 'docker-compose.yml only' };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Case 3: Both exist - analyze docker-compose to decide
|
|
993
|
+
if (hasDockerfile && hasCompose) {
|
|
994
|
+
log.info('MCP >>> [detectDeploymentStrategy] Both files found, analyzing compose...');
|
|
995
|
+
|
|
996
|
+
try {
|
|
997
|
+
const { webServiceCount, usesLocalDockerfile, totalServiceCount } = await analyzeComposeFile(composePath);
|
|
998
|
+
|
|
999
|
+
log.info(`MCP >>> [analyzeCompose] Total services: ${totalServiceCount}, Web services: ${webServiceCount}, Uses local Dockerfile: ${usesLocalDockerfile}`);
|
|
1000
|
+
|
|
1001
|
+
// SIMPLE: Only 1 web service, no other services, uses local Dockerfile
|
|
1002
|
+
// → docker-compose is just convenience wrapper
|
|
1003
|
+
if (totalServiceCount === 1 && webServiceCount === 1 && usesLocalDockerfile) {
|
|
1004
|
+
log.info('MCP >>> [detectDeploymentStrategy] Strategy: dockerfile (single web service)');
|
|
1005
|
+
return { type: 'dockerfile', reason: 'docker-compose.yml only has web service (convenience wrapper for Dockerfile)' };
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// COMPLEX: Multiple services (web + database, etc.)
|
|
1009
|
+
// → MUST use docker-compose for orchestration
|
|
1010
|
+
log.info('MCP >>> [detectDeploymentStrategy] Strategy: docker-compose (multi-service)');
|
|
1011
|
+
return { type: 'docker-compose', reason: 'Application requires multiple services (web + database/cache/etc)' };
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
log.error('MCP >>> [detectDeploymentStrategy] Analysis failed, defaulting to docker-compose:', err.message);
|
|
1014
|
+
return { type: 'docker-compose', reason: 'Multiple deployment files found' };
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Case 4: No deployment files
|
|
1019
|
+
log.info('MCP >>> [detectDeploymentStrategy] Strategy: none (no deployment files)');
|
|
1020
|
+
return { type: 'none', reason: 'No Dockerfile or docker-compose.yml found' };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Analyze docker-compose.yml to determine deployment strategy
|
|
1024
|
+
async function analyzeComposeFile(composePath) {
|
|
1025
|
+
const content = await fs.readFile(composePath, 'utf-8');
|
|
1026
|
+
const lines = content.split('\n');
|
|
1027
|
+
|
|
1028
|
+
let webServiceCount = 0;
|
|
1029
|
+
let totalServiceCount = 0;
|
|
1030
|
+
let usesLocalDockerfile = false;
|
|
1031
|
+
let inServicesSection = false;
|
|
1032
|
+
let currentService = '';
|
|
1033
|
+
let currentServiceIsWeb = false;
|
|
1034
|
+
|
|
1035
|
+
for (const line of lines) {
|
|
1036
|
+
const trimmed = line.trim();
|
|
1037
|
+
|
|
1038
|
+
// Detect services section
|
|
1039
|
+
if (trimmed.startsWith('services:')) {
|
|
1040
|
+
inServicesSection = true;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (!inServicesSection) continue;
|
|
1045
|
+
|
|
1046
|
+
// End of services section
|
|
1047
|
+
if (trimmed && !line.startsWith(' ') && !line.startsWith('\t')) {
|
|
1048
|
+
inServicesSection = false;
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// New service definition (at 2-space or 1-tab indentation level)
|
|
1053
|
+
if ((line.startsWith(' ') || line.startsWith('\t')) &&
|
|
1054
|
+
trimmed.includes(':') &&
|
|
1055
|
+
!trimmed.startsWith('-') &&
|
|
1056
|
+
!trimmed.includes('image:') &&
|
|
1057
|
+
!trimmed.includes('build:') &&
|
|
1058
|
+
!trimmed.includes('ports:') &&
|
|
1059
|
+
!trimmed.includes('expose:')) {
|
|
1060
|
+
|
|
1061
|
+
if (currentService) {
|
|
1062
|
+
totalServiceCount++;
|
|
1063
|
+
if (currentServiceIsWeb) {
|
|
1064
|
+
webServiceCount++;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
currentService = trimmed.split(':')[0].trim();
|
|
1069
|
+
currentServiceIsWeb = false;
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Check if current service exposes web ports
|
|
1074
|
+
if (trimmed.startsWith('ports:') || trimmed.startsWith('expose:')) {
|
|
1075
|
+
currentServiceIsWeb = true;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Check if service uses local Dockerfile
|
|
1079
|
+
if (trimmed === 'build: .' || trimmed === 'build: ./') {
|
|
1080
|
+
usesLocalDockerfile = true;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Count last service
|
|
1085
|
+
if (currentService) {
|
|
1086
|
+
totalServiceCount++;
|
|
1087
|
+
if (currentServiceIsWeb) {
|
|
1088
|
+
webServiceCount++;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return { webServiceCount, usesLocalDockerfile, totalServiceCount };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Helper to check if file exists
|
|
1096
|
+
async function fileExists(filePath) {
|
|
1097
|
+
try {
|
|
1098
|
+
await fs.access(filePath);
|
|
1099
|
+
return true;
|
|
1100
|
+
} catch {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
953
1105
|
// Initialize Project (requires authentication)
|
|
954
1106
|
async function initProject(args) {
|
|
955
1107
|
let { name, description, enable_deployment = true, hostname, local_path = '.' } = args;
|
|
@@ -1009,6 +1161,17 @@ async function initProject(args) {
|
|
|
1009
1161
|
};
|
|
1010
1162
|
|
|
1011
1163
|
if (enable_deployment) {
|
|
1164
|
+
// Detect deployment strategy from local directory (like CLI does)
|
|
1165
|
+
const absolutePath = path.resolve(local_path);
|
|
1166
|
+
log.info(`MCP >>> [initProject] Detecting deployment strategy in: ${absolutePath}`);
|
|
1167
|
+
|
|
1168
|
+
const strategy = await detectDeploymentStrategy(absolutePath);
|
|
1169
|
+
log.info(`MCP >>> [initProject] Detected strategy: ${strategy.type} - ${strategy.reason}`);
|
|
1170
|
+
console.error(`🔍 Detected deployment strategy: ${strategy.type}`);
|
|
1171
|
+
if (strategy.reason) {
|
|
1172
|
+
console.error(` Reason: ${strategy.reason}`);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1012
1175
|
// Generate a secure webhook secret for deployments
|
|
1013
1176
|
const webhookSecret = Array.from(
|
|
1014
1177
|
crypto.getRandomValues(new Uint8Array(32)),
|
|
@@ -1016,10 +1179,14 @@ async function initProject(args) {
|
|
|
1016
1179
|
).join('');
|
|
1017
1180
|
|
|
1018
1181
|
// Use FLAT structure exactly like CLI does - no nested deployment_info
|
|
1182
|
+
// CRITICAL: Send deployment_type so backend doesn't try to detect from empty repo
|
|
1019
1183
|
projectData.enable_deployment = true;
|
|
1184
|
+
projectData.deployment_type = strategy.type; // ← NEW: Send detected type!
|
|
1020
1185
|
projectData.webhook_secret = webhookSecret;
|
|
1021
1186
|
projectData.hostname = hostname;
|
|
1022
1187
|
projectData.local_path = local_path;
|
|
1188
|
+
|
|
1189
|
+
log.info(`MCP >>> [initProject] Sending deployment_type: ${strategy.type} to backend`);
|
|
1023
1190
|
}
|
|
1024
1191
|
|
|
1025
1192
|
const result = await apiRequest('POST', '/api/v1/projects', projectData, true);
|
|
@@ -1121,44 +1288,59 @@ async function initProject(args) {
|
|
|
1121
1288
|
}
|
|
1122
1289
|
|
|
1123
1290
|
// Create initial commit and push (like CLI does)
|
|
1291
|
+
log.info('MCP >>> [initProject] Starting git commit and push...');
|
|
1292
|
+
log.debug('MCP >>> [initProject] Working directory:', absolutePath);
|
|
1124
1293
|
const gitSteps = [];
|
|
1125
1294
|
let pushSucceeded = false;
|
|
1126
1295
|
|
|
1127
1296
|
try {
|
|
1128
1297
|
// Check if there are any files to commit
|
|
1298
|
+
log.info('MCP >>> [initProject] Checking git status...');
|
|
1129
1299
|
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: absolutePath });
|
|
1300
|
+
log.debug('MCP >>> [initProject] Git status output:', statusOutput.trim() || '(clean)');
|
|
1130
1301
|
|
|
1131
1302
|
if (statusOutput.trim()) {
|
|
1132
1303
|
// There are uncommitted changes
|
|
1304
|
+
log.info('MCP >>> [initProject] Uncommitted changes detected, staging files...');
|
|
1133
1305
|
gitSteps.push('Adding files to git');
|
|
1134
1306
|
await execAsync('git add .', { cwd: absolutePath });
|
|
1307
|
+
log.success('MCP >>> [initProject] Files staged (git add .)');
|
|
1135
1308
|
|
|
1309
|
+
log.info('MCP >>> [initProject] Creating commit...');
|
|
1136
1310
|
gitSteps.push('Creating initial commit');
|
|
1137
1311
|
await execAsync('git commit -m "Initial MLGym deployment"', { cwd: absolutePath });
|
|
1312
|
+
log.success('MCP >>> [initProject] Commit created');
|
|
1138
1313
|
} else {
|
|
1139
1314
|
// Check if there are any commits at all
|
|
1315
|
+
log.info('MCP >>> [initProject] No uncommitted changes, checking for existing commits...');
|
|
1140
1316
|
try {
|
|
1141
1317
|
await execAsync('git rev-parse HEAD', { cwd: absolutePath });
|
|
1318
|
+
log.info('MCP >>> [initProject] Repository already has commits');
|
|
1142
1319
|
gitSteps.push('Repository already has commits');
|
|
1143
1320
|
} catch {
|
|
1144
1321
|
// No commits yet, create an initial one
|
|
1322
|
+
log.warning('MCP >>> [initProject] No commits found, creating initial README...');
|
|
1145
1323
|
gitSteps.push('Creating README for initial commit');
|
|
1146
1324
|
const readmePath = path.join(absolutePath, 'README.md');
|
|
1147
1325
|
if (!await fs.access(readmePath).then(() => true).catch(() => false)) {
|
|
1148
1326
|
await fs.writeFile(readmePath, `# ${name}\n\n${description}\n\nDeployed with MLGym\n`);
|
|
1327
|
+
log.info('MCP >>> [initProject] README.md created');
|
|
1149
1328
|
}
|
|
1150
1329
|
await execAsync('git add .', { cwd: absolutePath });
|
|
1151
1330
|
await execAsync('git commit -m "Initial MLGym deployment"', { cwd: absolutePath });
|
|
1331
|
+
log.success('MCP >>> [initProject] Initial commit created with README');
|
|
1152
1332
|
}
|
|
1153
1333
|
}
|
|
1154
1334
|
|
|
1155
1335
|
// Push to trigger webhook and deployment
|
|
1336
|
+
log.info('MCP >>> [initProject] Pushing to GitLab...');
|
|
1337
|
+
log.debug('MCP >>> [initProject] Git remote URL:', gitUrl);
|
|
1156
1338
|
gitSteps.push('Pushing to GitLab to trigger deployment');
|
|
1157
1339
|
console.error('Executing: git push -u mlgym main');
|
|
1158
1340
|
await execAsync('git push -u mlgym main', { cwd: absolutePath });
|
|
1159
1341
|
gitSteps.push('✅ Successfully pushed to GitLab');
|
|
1160
1342
|
pushSucceeded = true;
|
|
1161
|
-
|
|
1343
|
+
log.success('MCP >>> [initProject] ✅ Git push completed successfully!');
|
|
1162
1344
|
|
|
1163
1345
|
} catch (pushError) {
|
|
1164
1346
|
console.error('❌ Git push failed:', pushError.message);
|
|
@@ -1534,27 +1716,35 @@ class DeploymentWorkflow {
|
|
|
1534
1716
|
async prepareProject(localPath, projectType, framework, packageManager) {
|
|
1535
1717
|
this.currentStep = 'preparation';
|
|
1536
1718
|
this.addStep('prepare_project', 'running');
|
|
1719
|
+
log.info('MCP >>> [prepareProject] Starting with localPath:', localPath);
|
|
1537
1720
|
|
|
1538
1721
|
try {
|
|
1539
1722
|
// Check if Dockerfile exists
|
|
1540
1723
|
const analysis = this.projectAnalysis || await analyzeProject(localPath);
|
|
1724
|
+
log.debug('MCP >>> [prepareProject] has_dockerfile:', analysis.has_dockerfile);
|
|
1541
1725
|
|
|
1542
1726
|
if (analysis.has_dockerfile) {
|
|
1727
|
+
log.info('MCP >>> [prepareProject] Dockerfile already exists, skipping generation');
|
|
1543
1728
|
this.updateLastStep('skipped', 'Dockerfile already exists');
|
|
1544
1729
|
return { skipped: true, reason: 'Dockerfile already exists' };
|
|
1545
1730
|
}
|
|
1546
1731
|
|
|
1547
1732
|
// Generate Dockerfile
|
|
1733
|
+
log.info('MCP >>> [prepareProject] Generating Dockerfile...');
|
|
1734
|
+
log.debug('MCP >>> [prepareProject] Type:', projectType || analysis.project_type, 'Framework:', framework || analysis.framework);
|
|
1548
1735
|
const prepResult = await prepareProject({
|
|
1549
1736
|
local_path: localPath,
|
|
1550
1737
|
project_type: projectType || analysis.project_type,
|
|
1551
1738
|
framework: framework || analysis.framework,
|
|
1552
1739
|
package_manager: packageManager || 'npm'
|
|
1553
1740
|
});
|
|
1741
|
+
log.success('MCP >>> [prepareProject] Dockerfile generated successfully');
|
|
1742
|
+
log.debug('MCP >>> [prepareProject] Actions:', prepResult.actions);
|
|
1554
1743
|
|
|
1555
1744
|
this.updateLastStep('completed', prepResult.actions);
|
|
1556
1745
|
return prepResult;
|
|
1557
1746
|
} catch (error) {
|
|
1747
|
+
log.error('MCP >>> [prepareProject] FAILED:', error.message);
|
|
1558
1748
|
this.updateLastStep('failed', error.message);
|
|
1559
1749
|
throw error;
|
|
1560
1750
|
}
|
|
@@ -1631,13 +1821,21 @@ async function deployProject(args) {
|
|
|
1631
1821
|
package_manager = 'npm'
|
|
1632
1822
|
} = args;
|
|
1633
1823
|
|
|
1824
|
+
log.info('MCP >>> Starting deployment workflow');
|
|
1825
|
+
log.debug('MCP >>> Arguments:', { project_name, hostname, local_path, project_type, framework, package_manager });
|
|
1826
|
+
|
|
1634
1827
|
try {
|
|
1635
1828
|
// Step 1: Ensure authenticated
|
|
1829
|
+
log.info('MCP >>> STEP 1: Authenticating...');
|
|
1636
1830
|
await workflow.ensureAuth(email, password);
|
|
1831
|
+
log.success('MCP >>> STEP 1: Authentication complete');
|
|
1637
1832
|
|
|
1638
1833
|
// Step 2: Check if project already exists
|
|
1834
|
+
log.info('MCP >>> STEP 2: Checking for existing project in', local_path);
|
|
1639
1835
|
const existing = await workflow.checkExisting(local_path);
|
|
1640
1836
|
if (existing && existing.exists) {
|
|
1837
|
+
log.warning('MCP >>> Project already exists:', existing.name);
|
|
1838
|
+
|
|
1641
1839
|
return {
|
|
1642
1840
|
content: [{
|
|
1643
1841
|
type: 'text',
|
|
@@ -1659,22 +1857,35 @@ async function deployProject(args) {
|
|
|
1659
1857
|
}]
|
|
1660
1858
|
};
|
|
1661
1859
|
}
|
|
1860
|
+
log.success('MCP >>> STEP 2: No existing project found, proceeding with deployment');
|
|
1662
1861
|
|
|
1663
1862
|
// Step 3: Analyze project (auto-detect type and framework)
|
|
1863
|
+
log.info('MCP >>> STEP 3: Analyzing project...');
|
|
1664
1864
|
const analysis = await workflow.analyzeProject(local_path);
|
|
1865
|
+
log.success('MCP >>> STEP 3: Analysis complete -', analysis.project_type, '/', analysis.framework);
|
|
1866
|
+
log.debug('MCP >>> Detected files:', analysis.detected_files);
|
|
1665
1867
|
|
|
1666
1868
|
// Step 4: Prepare project (generate Dockerfile if needed)
|
|
1667
|
-
|
|
1869
|
+
log.info('MCP >>> STEP 4: Preparing project (Dockerfile generation)...');
|
|
1870
|
+
log.debug('MCP >>> Using local_path:', local_path);
|
|
1871
|
+
const prepResult = await workflow.prepareProject(local_path, project_type, framework, package_manager);
|
|
1872
|
+
log.success('MCP >>> STEP 4: Project preparation complete');
|
|
1873
|
+
log.debug('MCP >>> Preparation actions:', prepResult);
|
|
1668
1874
|
|
|
1669
1875
|
// Step 5: Create GitLab project and deploy
|
|
1876
|
+
log.info('MCP >>> STEP 5: Creating GitLab project and deploying...');
|
|
1877
|
+
log.debug('MCP >>> Project params:', { name: project_name, hostname: hostname || project_name, local_path });
|
|
1670
1878
|
const deployResult = await workflow.createAndDeployProject({
|
|
1671
1879
|
name: project_name,
|
|
1672
1880
|
description: project_description,
|
|
1673
1881
|
hostname: hostname || project_name,
|
|
1674
1882
|
local_path
|
|
1675
1883
|
});
|
|
1884
|
+
log.success('MCP >>> STEP 5: Deployment complete!');
|
|
1885
|
+
log.debug('MCP >>> Deploy result:', { project_id: deployResult.project?.id, deployment_url: deployResult.project?.deployment_url });
|
|
1676
1886
|
|
|
1677
1887
|
// Success response
|
|
1888
|
+
log.success('MCP >>> ✅ Full deployment workflow completed successfully!');
|
|
1678
1889
|
return {
|
|
1679
1890
|
content: [{
|
|
1680
1891
|
type: 'text',
|
|
@@ -1773,6 +1984,121 @@ async function getStatus(args) {
|
|
|
1773
1984
|
}
|
|
1774
1985
|
}
|
|
1775
1986
|
|
|
1987
|
+
// Get deployment logs for a project
|
|
1988
|
+
async function getDeploymentLogs(args) {
|
|
1989
|
+
const local_path = args.local_path || '.';
|
|
1990
|
+
const depth = args.depth || 1;
|
|
1991
|
+
const project_name = args.project_name;
|
|
1992
|
+
|
|
1993
|
+
log.info(`MCP >>> [getDeploymentLogs] Getting logs for project: ${project_name || 'auto-detect'}, depth: ${depth}`);
|
|
1994
|
+
|
|
1995
|
+
try {
|
|
1996
|
+
// Check authentication
|
|
1997
|
+
const auth = await loadAuth();
|
|
1998
|
+
if (!auth.token) {
|
|
1999
|
+
throw new Error('Authentication required. Please deploy or login first.');
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Determine project name
|
|
2003
|
+
let projectName = project_name;
|
|
2004
|
+
if (!projectName) {
|
|
2005
|
+
// Try to get from git remote
|
|
2006
|
+
const projectStatus = await checkExistingProject(local_path);
|
|
2007
|
+
if (!projectStatus.exists) {
|
|
2008
|
+
throw new Error('Project not found. Please specify project_name or run from a project directory with git remote.');
|
|
2009
|
+
}
|
|
2010
|
+
projectName = projectStatus.name;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
log.info(`MCP >>> [getDeploymentLogs] Detected project name: ${projectName}`);
|
|
2014
|
+
|
|
2015
|
+
// Get project details from backend to get Coolify app UUID
|
|
2016
|
+
const projectsResult = await apiRequest('GET', '/api/v1/projects', null, true);
|
|
2017
|
+
if (!projectsResult.success) {
|
|
2018
|
+
throw new Error(`Failed to get projects: ${projectsResult.error}`);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const project = projectsResult.data.projects?.find(p => p.name === projectName);
|
|
2022
|
+
if (!project) {
|
|
2023
|
+
throw new Error(`Project "${projectName}" not found in your account`);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
if (!project.coolify_app_uuid) {
|
|
2027
|
+
throw new Error(`Project "${projectName}" does not have deployment enabled`);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
const appUUID = project.coolify_app_uuid;
|
|
2031
|
+
log.info(`MCP >>> [getDeploymentLogs] Found Coolify app UUID: ${appUUID}`);
|
|
2032
|
+
|
|
2033
|
+
// Call User Agent API to get deployment logs
|
|
2034
|
+
const userAgentURL = 'http://coolify.eu.ezb.net:9000';
|
|
2035
|
+
const endpoint = `${userAgentURL}/applications/${appUUID}/deployment-logs?depth=${depth}`;
|
|
2036
|
+
|
|
2037
|
+
log.info(`MCP >>> [getDeploymentLogs] Calling User Agent: ${endpoint}`);
|
|
2038
|
+
|
|
2039
|
+
const response = await axios.get(endpoint, {
|
|
2040
|
+
headers: {
|
|
2041
|
+
'Authorization': 'Bearer 1|Jkztb5qPptwRKgtocfbT2TLjp8WGJG1SkPE4DzLt4c5d600f',
|
|
2042
|
+
'Content-Type': 'application/json'
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
const logsData = response.data;
|
|
2047
|
+
log.info(`MCP >>> [getDeploymentLogs] Retrieved ${logsData.count} deployment(s)`);
|
|
2048
|
+
|
|
2049
|
+
// Format the response
|
|
2050
|
+
if (logsData.count === 0) {
|
|
2051
|
+
return {
|
|
2052
|
+
content: [{
|
|
2053
|
+
type: 'text',
|
|
2054
|
+
text: JSON.stringify({
|
|
2055
|
+
status: 'ok',
|
|
2056
|
+
project_name: projectName,
|
|
2057
|
+
app_uuid: appUUID,
|
|
2058
|
+
count: 0,
|
|
2059
|
+
message: 'No deployment logs found for this application. To create a deployment, push code to GitLab.'
|
|
2060
|
+
}, null, 2)
|
|
2061
|
+
}]
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// Format deployments with readable timestamps
|
|
2066
|
+
const formattedDeployments = logsData.deployments.map(dep => ({
|
|
2067
|
+
id: dep.id,
|
|
2068
|
+
status: dep.status,
|
|
2069
|
+
created_at: new Date(dep.created_at).toISOString().replace('T', ' ').substring(0, 19),
|
|
2070
|
+
updated_at: new Date(dep.updated_at).toISOString().replace('T', ' ').substring(0, 19),
|
|
2071
|
+
output: dep.output || 'No logs available'
|
|
2072
|
+
}));
|
|
2073
|
+
|
|
2074
|
+
return {
|
|
2075
|
+
content: [{
|
|
2076
|
+
type: 'text',
|
|
2077
|
+
text: JSON.stringify({
|
|
2078
|
+
status: 'ok',
|
|
2079
|
+
project_name: projectName,
|
|
2080
|
+
app_uuid: appUUID,
|
|
2081
|
+
count: logsData.count,
|
|
2082
|
+
deployments: formattedDeployments
|
|
2083
|
+
}, null, 2)
|
|
2084
|
+
}]
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
log.error(`MCP >>> [getDeploymentLogs] Error: ${error.message}`);
|
|
2089
|
+
return {
|
|
2090
|
+
content: [{
|
|
2091
|
+
type: 'text',
|
|
2092
|
+
text: JSON.stringify({
|
|
2093
|
+
status: 'error',
|
|
2094
|
+
error: error.message,
|
|
2095
|
+
hint: 'Make sure the project exists and has deployment enabled'
|
|
2096
|
+
}, null, 2)
|
|
2097
|
+
}]
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
1776
2102
|
// Create the MCP server
|
|
1777
2103
|
const server = new Server(
|
|
1778
2104
|
{
|
|
@@ -1864,6 +2190,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1864
2190
|
}
|
|
1865
2191
|
}
|
|
1866
2192
|
},
|
|
2193
|
+
{
|
|
2194
|
+
name: 'mlgym_deploy_logs',
|
|
2195
|
+
description: 'View deployment history and logs for a project. Shows the last N deployments with their status, timestamps, and build logs.',
|
|
2196
|
+
inputSchema: {
|
|
2197
|
+
type: 'object',
|
|
2198
|
+
properties: {
|
|
2199
|
+
project_name: {
|
|
2200
|
+
type: 'string',
|
|
2201
|
+
description: 'Project name (optional if in project directory with git remote)',
|
|
2202
|
+
pattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$',
|
|
2203
|
+
minLength: 3
|
|
2204
|
+
},
|
|
2205
|
+
depth: {
|
|
2206
|
+
type: 'number',
|
|
2207
|
+
description: 'Number of deployments to retrieve (1-10, default: 1)',
|
|
2208
|
+
minimum: 1,
|
|
2209
|
+
maximum: 10,
|
|
2210
|
+
default: 1
|
|
2211
|
+
},
|
|
2212
|
+
local_path: {
|
|
2213
|
+
type: 'string',
|
|
2214
|
+
description: 'Local project directory path (optional, defaults to current directory)',
|
|
2215
|
+
default: '.'
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
},
|
|
1867
2220
|
// ========== LEGACY TOOLS (Re-enabled for specific use cases) ==========
|
|
1868
2221
|
{
|
|
1869
2222
|
name: 'mlgym_user_create',
|
|
@@ -1941,6 +2294,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1941
2294
|
result = await getStatus(args);
|
|
1942
2295
|
break;
|
|
1943
2296
|
|
|
2297
|
+
case 'mlgym_deploy_logs':
|
|
2298
|
+
log.info(`Retrieving deployment logs...`);
|
|
2299
|
+
result = await getDeploymentLogs(args);
|
|
2300
|
+
break;
|
|
2301
|
+
|
|
1944
2302
|
case 'mlgym_user_create':
|
|
1945
2303
|
log.info(`Creating user account...`);
|
|
1946
2304
|
result = await createUser(args);
|
|
@@ -1952,7 +2310,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1952
2310
|
break;
|
|
1953
2311
|
|
|
1954
2312
|
default:
|
|
1955
|
-
throw new Error(`Unknown tool: ${name}. Available tools: mlgym_deploy, mlgym_status, mlgym_user_create, mlgym_auth_login`);
|
|
2313
|
+
throw new Error(`Unknown tool: ${name}. Available tools: mlgym_deploy, mlgym_status, mlgym_deploy_logs, mlgym_user_create, mlgym_auth_login`);
|
|
1956
2314
|
}
|
|
1957
2315
|
|
|
1958
2316
|
const duration = Date.now() - startTime;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mlgym-deploy",
|
|
3
|
-
"version": "3.0
|
|
4
|
-
"description": "MCP server for MLGym -
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "MCP server for MLGym - Added deployment logs viewer tool",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|