mlgym-deploy 2.10.0 → 3.0.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 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 = '2.10.0'; // Fixed SSH key handling: generate and include in user creation request
20
+ const CURRENT_VERSION = '3.0.0'; // Monolithic workflow: reduced AI complexity, server-side orchestration
21
21
  const PACKAGE_NAME = 'mlgym-deploy';
22
22
 
23
23
  // Version check state
@@ -1107,6 +1107,342 @@ async function initProject(args) {
1107
1107
  };
1108
1108
  }
1109
1109
 
1110
+ // ============================================================================
1111
+ // MONOLITHIC DEPLOYMENT WORKFLOW CLASS (v3.0.0)
1112
+ // ============================================================================
1113
+ class DeploymentWorkflow {
1114
+ constructor() {
1115
+ this.currentStep = null;
1116
+ this.authToken = null;
1117
+ this.projectAnalysis = null;
1118
+ this.gitlabProject = null;
1119
+ this.steps = [];
1120
+ }
1121
+
1122
+ addStep(stepName, status = 'pending', result = null) {
1123
+ this.steps.push({ step: stepName, status, result, timestamp: new Date().toISOString() });
1124
+ }
1125
+
1126
+ updateLastStep(status, result = null) {
1127
+ if (this.steps.length > 0) {
1128
+ this.steps[this.steps.length - 1].status = status;
1129
+ if (result) this.steps[this.steps.length - 1].result = result;
1130
+ }
1131
+ }
1132
+
1133
+ async ensureAuth(email, password) {
1134
+ this.currentStep = 'authentication';
1135
+ this.addStep('authentication', 'running');
1136
+
1137
+ // Check if already authenticated
1138
+ const auth = await loadAuth();
1139
+ if (auth.token) {
1140
+ this.authToken = auth.token;
1141
+ this.updateLastStep('completed', 'Using cached authentication');
1142
+ return { authenticated: true, cached: true };
1143
+ }
1144
+
1145
+ // Need credentials
1146
+ if (!email || !password) {
1147
+ this.updateLastStep('failed', 'Email and password required');
1148
+ throw new Error('Authentication required. Please provide email and password.');
1149
+ }
1150
+
1151
+ // Authenticate using existing function
1152
+ const authResult = await authenticate({ email, password, create_if_not_exists: false });
1153
+
1154
+ // Parse the result
1155
+ const resultData = JSON.parse(authResult.content[0].text);
1156
+
1157
+ if (resultData.status === 'success' || resultData.authenticated) {
1158
+ this.authToken = (await loadAuth()).token;
1159
+ this.updateLastStep('completed', 'Authentication successful');
1160
+ return { authenticated: true, cached: false };
1161
+ } else {
1162
+ this.updateLastStep('failed', resultData.message);
1163
+ throw new Error(`Authentication failed: ${resultData.message}`);
1164
+ }
1165
+ }
1166
+
1167
+ async analyzeProject(local_path) {
1168
+ this.currentStep = 'analysis';
1169
+ this.addStep('project_analysis', 'running');
1170
+
1171
+ try {
1172
+ this.projectAnalysis = await analyzeProject(local_path);
1173
+ this.updateLastStep('completed', {
1174
+ type: this.projectAnalysis.project_type,
1175
+ framework: this.projectAnalysis.framework
1176
+ });
1177
+ return this.projectAnalysis;
1178
+ } catch (error) {
1179
+ this.updateLastStep('failed', error.message);
1180
+ throw error;
1181
+ }
1182
+ }
1183
+
1184
+ async checkExisting(local_path) {
1185
+ this.currentStep = 'check_existing';
1186
+ this.addStep('check_existing_project', 'running');
1187
+
1188
+ try {
1189
+ const existing = await checkExistingProject(local_path);
1190
+
1191
+ if (existing.exists) {
1192
+ this.updateLastStep('completed', 'Project already configured');
1193
+ return existing;
1194
+ } else {
1195
+ this.updateLastStep('completed', 'No existing project found');
1196
+ return null;
1197
+ }
1198
+ } catch (error) {
1199
+ this.updateLastStep('failed', error.message);
1200
+ throw error;
1201
+ }
1202
+ }
1203
+
1204
+ async prepareProject(projectType, framework, packageManager) {
1205
+ this.currentStep = 'preparation';
1206
+ this.addStep('prepare_project', 'running');
1207
+
1208
+ try {
1209
+ // Check if Dockerfile exists
1210
+ const analysis = this.projectAnalysis || await analyzeProject('.');
1211
+
1212
+ if (analysis.has_dockerfile) {
1213
+ this.updateLastStep('skipped', 'Dockerfile already exists');
1214
+ return { skipped: true, reason: 'Dockerfile already exists' };
1215
+ }
1216
+
1217
+ // Generate Dockerfile
1218
+ const prepResult = await prepareProject({
1219
+ local_path: '.',
1220
+ project_type: projectType || analysis.project_type,
1221
+ framework: framework || analysis.framework,
1222
+ package_manager: packageManager || 'npm'
1223
+ });
1224
+
1225
+ this.updateLastStep('completed', prepResult.actions);
1226
+ return prepResult;
1227
+ } catch (error) {
1228
+ this.updateLastStep('failed', error.message);
1229
+ throw error;
1230
+ }
1231
+ }
1232
+
1233
+ async createAndDeployProject(params) {
1234
+ this.currentStep = 'project_creation';
1235
+ this.addStep('create_gitlab_project', 'running');
1236
+
1237
+ const { name, description, hostname, local_path } = params;
1238
+
1239
+ try {
1240
+ // Create project using existing function
1241
+ const initResult = await initProject({
1242
+ name,
1243
+ description,
1244
+ enable_deployment: true,
1245
+ hostname: hostname || name,
1246
+ local_path: local_path || '.'
1247
+ });
1248
+
1249
+ // Parse result
1250
+ const resultData = JSON.parse(initResult.content[0].text);
1251
+
1252
+ if (resultData.status === 'success') {
1253
+ this.gitlabProject = resultData.project;
1254
+ this.updateLastStep('completed', resultData.project);
1255
+ return resultData;
1256
+ } else {
1257
+ this.updateLastStep('failed', resultData.error || resultData.message);
1258
+ throw new Error(resultData.error || resultData.message);
1259
+ }
1260
+ } catch (error) {
1261
+ this.updateLastStep('failed', error.message);
1262
+ throw error;
1263
+ }
1264
+ }
1265
+
1266
+ getRecoverySuggestion(error) {
1267
+ const errorMsg = error.message.toLowerCase();
1268
+
1269
+ if (errorMsg.includes('authentication') || errorMsg.includes('token')) {
1270
+ return 'Please provide valid email and password credentials';
1271
+ }
1272
+ if (errorMsg.includes('project') && errorMsg.includes('exists')) {
1273
+ return 'Project already exists. Use mlgym_status to check existing configuration';
1274
+ }
1275
+ if (errorMsg.includes('hostname')) {
1276
+ return 'Please provide a valid hostname (lowercase letters, numbers, and hyphens only)';
1277
+ }
1278
+ if (errorMsg.includes('ssh')) {
1279
+ return 'SSH key generation failed. Check SSH configuration';
1280
+ }
1281
+
1282
+ return 'Check the error message and try again with corrected parameters';
1283
+ }
1284
+ }
1285
+
1286
+ // ============================================================================
1287
+ // MONOLITHIC DEPLOY FUNCTION
1288
+ // ============================================================================
1289
+ async function deployProject(args) {
1290
+ const workflow = new DeploymentWorkflow();
1291
+
1292
+ const {
1293
+ email,
1294
+ password,
1295
+ project_name,
1296
+ project_description,
1297
+ hostname,
1298
+ local_path = '.',
1299
+ project_type,
1300
+ framework,
1301
+ package_manager = 'npm'
1302
+ } = args;
1303
+
1304
+ try {
1305
+ // Step 1: Ensure authenticated
1306
+ await workflow.ensureAuth(email, password);
1307
+
1308
+ // Step 2: Check if project already exists
1309
+ const existing = await workflow.checkExisting(local_path);
1310
+ if (existing && existing.exists) {
1311
+ return {
1312
+ content: [{
1313
+ type: 'text',
1314
+ text: JSON.stringify({
1315
+ status: 'already_exists',
1316
+ message: 'Project already configured in this directory',
1317
+ project: {
1318
+ name: existing.name,
1319
+ namespace: existing.namespace,
1320
+ git_remote: `git@git.mlgym.io:${existing.namespace}/${existing.name}.git`
1321
+ },
1322
+ next_steps: [
1323
+ 'Project is already set up',
1324
+ 'To update: git push mlgym main',
1325
+ 'To check status: use mlgym_status tool'
1326
+ ],
1327
+ workflow_steps: workflow.steps
1328
+ }, null, 2)
1329
+ }]
1330
+ };
1331
+ }
1332
+
1333
+ // Step 3: Analyze project (auto-detect type and framework)
1334
+ const analysis = await workflow.analyzeProject(local_path);
1335
+
1336
+ // Step 4: Prepare project (generate Dockerfile if needed)
1337
+ await workflow.prepareProject(project_type, framework, package_manager);
1338
+
1339
+ // Step 5: Create GitLab project and deploy
1340
+ const deployResult = await workflow.createAndDeployProject({
1341
+ name: project_name,
1342
+ description: project_description,
1343
+ hostname: hostname || project_name,
1344
+ local_path
1345
+ });
1346
+
1347
+ // Success response
1348
+ return {
1349
+ content: [{
1350
+ type: 'text',
1351
+ text: JSON.stringify({
1352
+ status: 'success',
1353
+ message: 'Project deployed successfully',
1354
+ project: deployResult.project,
1355
+ deployment: deployResult.deployment,
1356
+ url: deployResult.project.deployment_url,
1357
+ analysis: {
1358
+ detected_type: analysis.project_type,
1359
+ detected_framework: analysis.framework
1360
+ },
1361
+ next_steps: deployResult.next_steps,
1362
+ workflow_steps: workflow.steps
1363
+ }, null, 2)
1364
+ }]
1365
+ };
1366
+
1367
+ } catch (error) {
1368
+ console.error('Deployment workflow failed:', error);
1369
+
1370
+ return {
1371
+ content: [{
1372
+ type: 'text',
1373
+ text: JSON.stringify({
1374
+ status: 'error',
1375
+ message: 'Deployment failed',
1376
+ error: error.message,
1377
+ failed_at_step: workflow.currentStep,
1378
+ recovery_suggestion: workflow.getRecoverySuggestion(error),
1379
+ workflow_steps: workflow.steps
1380
+ }, null, 2)
1381
+ }]
1382
+ };
1383
+ }
1384
+ }
1385
+
1386
+ // ============================================================================
1387
+ // SIMPLIFIED STATUS CHECK
1388
+ // ============================================================================
1389
+ async function getStatus(args) {
1390
+ const local_path = args.local_path || '.';
1391
+
1392
+ try {
1393
+ // Check authentication
1394
+ const auth = await loadAuth();
1395
+ const authenticated = !!auth.token;
1396
+
1397
+ // Check existing project
1398
+ const projectStatus = await checkExistingProject(local_path);
1399
+
1400
+ // Analyze project if directory exists
1401
+ let analysis = null;
1402
+ try {
1403
+ analysis = await analyzeProject(local_path);
1404
+ } catch (e) {
1405
+ analysis = { error: 'Could not analyze project' };
1406
+ }
1407
+
1408
+ return {
1409
+ content: [{
1410
+ type: 'text',
1411
+ text: JSON.stringify({
1412
+ status: 'ok',
1413
+ authentication: {
1414
+ authenticated,
1415
+ email: auth.email || null
1416
+ },
1417
+ project: projectStatus.exists ? {
1418
+ configured: true,
1419
+ name: projectStatus.name,
1420
+ namespace: projectStatus.namespace,
1421
+ git_remote: `git@git.mlgym.io:${projectStatus.namespace}/${projectStatus.name}.git`
1422
+ } : {
1423
+ configured: false
1424
+ },
1425
+ analysis: analysis.project_type ? {
1426
+ type: analysis.project_type,
1427
+ framework: analysis.framework,
1428
+ has_dockerfile: analysis.has_dockerfile
1429
+ } : null
1430
+ }, null, 2)
1431
+ }]
1432
+ };
1433
+ } catch (error) {
1434
+ return {
1435
+ content: [{
1436
+ type: 'text',
1437
+ text: JSON.stringify({
1438
+ status: 'error',
1439
+ error: error.message
1440
+ }, null, 2)
1441
+ }]
1442
+ };
1443
+ }
1444
+ }
1445
+
1110
1446
  // Create the MCP server
1111
1447
  const server = new Server(
1112
1448
  {
@@ -1124,154 +1460,75 @@ const server = new Server(
1124
1460
  server.setRequestHandler(ListToolsRequestSchema, async () => {
1125
1461
  return {
1126
1462
  tools: [
1463
+ // ========== NEW MONOLITHIC TOOLS (v3.0.0) ==========
1127
1464
  {
1128
- name: 'mlgym_auth_status',
1129
- description: 'ALWAYS CALL THIS FIRST! Check authentication status before any other operation.',
1130
- inputSchema: {
1131
- type: 'object',
1132
- properties: {}
1133
- }
1134
- },
1135
- {
1136
- name: 'mlgym_authenticate',
1137
- description: 'PHASE 1: Authentication ONLY. Get email, password, and existing account status in ONE interaction. Never ask for project details here!',
1138
- inputSchema: {
1139
- type: 'object',
1140
- properties: {
1141
- email: {
1142
- type: 'string',
1143
- description: 'Email address',
1144
- pattern: '^[^@]+@[^@]+\\.[^@]+$'
1145
- },
1146
- password: {
1147
- type: 'string',
1148
- description: 'Password (min 8 characters)',
1149
- minLength: 8
1150
- },
1151
- create_if_not_exists: {
1152
- type: 'boolean',
1153
- description: 'If true and login fails, attempt to create new account',
1154
- default: false
1155
- },
1156
- full_name: {
1157
- type: 'string',
1158
- description: 'Full name (required only for new account creation)',
1159
- minLength: 2
1160
- },
1161
- accept_terms: {
1162
- type: 'boolean',
1163
- description: 'Accept terms and conditions (required only for new account creation)',
1164
- default: false
1165
- }
1166
- },
1167
- required: ['email', 'password']
1168
- }
1169
- },
1170
- {
1171
- name: 'mlgym_project_analyze',
1172
- description: 'PHASE 2: Analyze project to detect type, framework, and configuration. Call BEFORE creating project.',
1173
- inputSchema: {
1174
- type: 'object',
1175
- properties: {
1176
- local_path: {
1177
- type: 'string',
1178
- description: 'Local directory path (defaults to current directory)',
1179
- default: '.'
1180
- }
1181
- }
1182
- }
1183
- },
1184
- {
1185
- name: 'mlgym_project_status',
1186
- description: 'PHASE 2: Check if MLGym project exists in current directory.',
1187
- inputSchema: {
1188
- type: 'object',
1189
- properties: {
1190
- local_path: {
1191
- type: 'string',
1192
- description: 'Local directory path (defaults to current directory)',
1193
- default: '.'
1194
- }
1195
- }
1196
- }
1197
- },
1198
- {
1199
- name: 'mlgym_project_init',
1200
- description: 'PHASE 2: Create project ONLY after checking project status. Never ask for email/password here - only project details!',
1465
+ name: 'mlgym_deploy',
1466
+ description: 'RECOMMENDED: Complete deployment workflow in one call. Automatically authenticates (uses cached token if available), analyzes project, creates GitLab repo, and deploys to Coolify. Just provide project details - the server handles everything else internally.',
1201
1467
  inputSchema: {
1202
1468
  type: 'object',
1203
1469
  properties: {
1204
- name: {
1470
+ project_name: {
1205
1471
  type: 'string',
1206
- description: 'Project name (lowercase alphanumeric with hyphens)',
1472
+ description: 'Project name (lowercase alphanumeric with hyphens, e.g., "my-app")',
1207
1473
  pattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$',
1208
1474
  minLength: 3
1209
1475
  },
1210
- description: {
1476
+ project_description: {
1211
1477
  type: 'string',
1212
- description: 'Project description',
1478
+ description: 'Brief project description (min 10 characters)',
1213
1479
  minLength: 10
1214
1480
  },
1215
- enable_deployment: {
1216
- type: 'boolean',
1217
- description: 'Enable automatic deployment via Coolify',
1218
- default: true
1481
+ email: {
1482
+ type: 'string',
1483
+ description: 'Email for authentication (optional if already authenticated)',
1484
+ pattern: '^[^@]+@[^@]+\\.[^@]+$'
1485
+ },
1486
+ password: {
1487
+ type: 'string',
1488
+ description: 'Password (optional if already authenticated, min 8 characters)',
1489
+ minLength: 8
1219
1490
  },
1220
1491
  hostname: {
1221
1492
  type: 'string',
1222
- description: 'Hostname for deployment (required if deployment enabled, will be subdomain)',
1493
+ description: 'Deployment hostname/subdomain (optional, defaults to project_name). Will be accessible at https://<hostname>.ezb.net',
1223
1494
  pattern: '^[a-z][a-z0-9-]*[a-z0-9]$',
1224
1495
  minLength: 3,
1225
1496
  maxLength: 63
1226
1497
  },
1227
1498
  local_path: {
1228
1499
  type: 'string',
1229
- description: 'Local directory path (defaults to current directory)',
1230
- default: '.'
1231
- }
1232
- },
1233
- required: ['name', 'description']
1234
- }
1235
- },
1236
- {
1237
- name: 'mlgym_project_prepare',
1238
- description: 'PHASE 2: Prepare project for deployment by generating Dockerfile and config files.',
1239
- inputSchema: {
1240
- type: 'object',
1241
- properties: {
1242
- local_path: {
1243
- type: 'string',
1244
- description: 'Local directory path (defaults to current directory)',
1500
+ description: 'Local project directory path (optional, defaults to current directory)',
1245
1501
  default: '.'
1246
1502
  },
1247
1503
  project_type: {
1248
1504
  type: 'string',
1249
- description: 'Project type from analysis',
1250
- enum: ['nodejs', 'python', 'static', 'go', 'unknown']
1505
+ description: 'Override auto-detected project type (optional)',
1506
+ enum: ['nodejs', 'python', 'static', 'go']
1251
1507
  },
1252
1508
  framework: {
1253
1509
  type: 'string',
1254
- description: 'Framework from analysis',
1255
- enum: ['nextjs', 'express', 'react', 'vue', 'flask', 'fastapi', 'html', null]
1510
+ description: 'Override auto-detected framework (optional)',
1511
+ enum: ['nextjs', 'express', 'react', 'vue', 'flask', 'fastapi', 'html']
1256
1512
  },
1257
1513
  package_manager: {
1258
1514
  type: 'string',
1259
- description: 'Package manager for Node.js projects',
1515
+ description: 'Package manager for Node.js projects (optional, defaults to npm)',
1260
1516
  enum: ['npm', 'yarn'],
1261
1517
  default: 'npm'
1262
1518
  }
1263
- }
1519
+ },
1520
+ required: ['project_name', 'project_description']
1264
1521
  }
1265
1522
  },
1266
1523
  {
1267
- name: 'mlgym_smart_deploy',
1268
- description: 'RECOMMENDED: Smart deployment workflow that automatically analyzes, prepares, and guides you through the entire deployment process. Use this for new projects!',
1524
+ name: 'mlgym_status',
1525
+ description: 'Check current authentication status and project configuration. Read-only operation that shows what is already set up.',
1269
1526
  inputSchema: {
1270
1527
  type: 'object',
1271
1528
  properties: {
1272
1529
  local_path: {
1273
1530
  type: 'string',
1274
- description: 'Local directory path (defaults to current directory)',
1531
+ description: 'Local directory path to check (defaults to current directory)',
1275
1532
  default: '.'
1276
1533
  }
1277
1534
  }
@@ -1285,51 +1542,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1285
1542
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1286
1543
  const { name, arguments: args } = request.params;
1287
1544
 
1288
- console.error(`Tool called: ${name}`);
1545
+ console.error(`Tool called: ${name} (MCP v${CURRENT_VERSION})`);
1289
1546
 
1290
1547
  try {
1291
1548
  switch (name) {
1292
- case 'mlgym_auth_status':
1293
- return await checkAuthStatus();
1294
-
1295
- case 'mlgym_authenticate':
1296
- return await authenticate(args);
1297
-
1298
- case 'mlgym_project_analyze':
1299
- const analysis = await analyzeProject(args.local_path);
1300
- return {
1301
- content: [{
1302
- type: 'text',
1303
- text: JSON.stringify(analysis, null, 2)
1304
- }]
1305
- };
1306
-
1307
- case 'mlgym_project_status':
1308
- const projectStatus = await checkExistingProject(args.local_path);
1309
- return {
1310
- content: [{
1311
- type: 'text',
1312
- text: JSON.stringify(projectStatus, null, 2)
1313
- }]
1314
- };
1315
-
1316
- case 'mlgym_project_init':
1317
- return await initProject(args);
1318
-
1319
- case 'mlgym_project_prepare':
1320
- const prepResult = await prepareProject(args);
1321
- return {
1322
- content: [{
1323
- type: 'text',
1324
- text: JSON.stringify(prepResult, null, 2)
1325
- }]
1326
- };
1549
+ case 'mlgym_deploy':
1550
+ return await deployProject(args);
1327
1551
 
1328
- case 'mlgym_smart_deploy':
1329
- return await smartDeploy(args);
1552
+ case 'mlgym_status':
1553
+ return await getStatus(args);
1330
1554
 
1331
1555
  default:
1332
- throw new Error(`Unknown tool: ${name}`);
1556
+ throw new Error(`Unknown tool: ${name}. Available tools: mlgym_deploy, mlgym_status`);
1333
1557
  }
1334
1558
  } catch (error) {
1335
1559
  console.error(`Tool execution failed:`, error);