mlgym-deploy 3.0.8 → 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 +303 -2
- 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
|
|
@@ -963,6 +963,145 @@ async function smartDeploy(args) {
|
|
|
963
963
|
}
|
|
964
964
|
}
|
|
965
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
|
+
|
|
966
1105
|
// Initialize Project (requires authentication)
|
|
967
1106
|
async function initProject(args) {
|
|
968
1107
|
let { name, description, enable_deployment = true, hostname, local_path = '.' } = args;
|
|
@@ -1022,6 +1161,17 @@ async function initProject(args) {
|
|
|
1022
1161
|
};
|
|
1023
1162
|
|
|
1024
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
|
+
|
|
1025
1175
|
// Generate a secure webhook secret for deployments
|
|
1026
1176
|
const webhookSecret = Array.from(
|
|
1027
1177
|
crypto.getRandomValues(new Uint8Array(32)),
|
|
@@ -1029,10 +1179,14 @@ async function initProject(args) {
|
|
|
1029
1179
|
).join('');
|
|
1030
1180
|
|
|
1031
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
|
|
1032
1183
|
projectData.enable_deployment = true;
|
|
1184
|
+
projectData.deployment_type = strategy.type; // ← NEW: Send detected type!
|
|
1033
1185
|
projectData.webhook_secret = webhookSecret;
|
|
1034
1186
|
projectData.hostname = hostname;
|
|
1035
1187
|
projectData.local_path = local_path;
|
|
1188
|
+
|
|
1189
|
+
log.info(`MCP >>> [initProject] Sending deployment_type: ${strategy.type} to backend`);
|
|
1036
1190
|
}
|
|
1037
1191
|
|
|
1038
1192
|
const result = await apiRequest('POST', '/api/v1/projects', projectData, true);
|
|
@@ -1830,6 +1984,121 @@ async function getStatus(args) {
|
|
|
1830
1984
|
}
|
|
1831
1985
|
}
|
|
1832
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
|
+
|
|
1833
2102
|
// Create the MCP server
|
|
1834
2103
|
const server = new Server(
|
|
1835
2104
|
{
|
|
@@ -1921,6 +2190,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1921
2190
|
}
|
|
1922
2191
|
}
|
|
1923
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
|
+
},
|
|
1924
2220
|
// ========== LEGACY TOOLS (Re-enabled for specific use cases) ==========
|
|
1925
2221
|
{
|
|
1926
2222
|
name: 'mlgym_user_create',
|
|
@@ -1998,6 +2294,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1998
2294
|
result = await getStatus(args);
|
|
1999
2295
|
break;
|
|
2000
2296
|
|
|
2297
|
+
case 'mlgym_deploy_logs':
|
|
2298
|
+
log.info(`Retrieving deployment logs...`);
|
|
2299
|
+
result = await getDeploymentLogs(args);
|
|
2300
|
+
break;
|
|
2301
|
+
|
|
2001
2302
|
case 'mlgym_user_create':
|
|
2002
2303
|
log.info(`Creating user account...`);
|
|
2003
2304
|
result = await createUser(args);
|
|
@@ -2009,7 +2310,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2009
2310
|
break;
|
|
2010
2311
|
|
|
2011
2312
|
default:
|
|
2012
|
-
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`);
|
|
2013
2314
|
}
|
|
2014
2315
|
|
|
2015
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 - Added
|
|
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": {
|