mlgym-deploy 2.0.1 → 2.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.
Files changed (3) hide show
  1. package/index-v2.js +24 -2
  2. package/index.js +928 -65
  3. package/package.json +1 -1
package/index-v2.js CHANGED
@@ -964,25 +964,44 @@ async function deployProject(sessionId, args) {
964
964
  description: `Deployed via MLGym MCP from ${session.framework} project`,
965
965
  visibility: 'private',
966
966
  enable_deployment: true,
967
- deployment_region: region,
968
967
  webhook_secret: crypto.randomBytes(16).toString('hex')
968
+ // Note: deployment_region is not used by backend, it uses random server selection
969
969
  };
970
970
 
971
+ console.error(`Creating project with deployment enabled for ${session.projectName} in region ${region}`);
971
972
  const result = await apiRequest('POST', '/api/v1/projects', projectData);
972
973
 
973
974
  if (!result.success) {
975
+ console.error(`Project creation failed: ${result.error}`);
976
+ // Provide more detailed error information
974
977
  return {
975
978
  content: [{
976
979
  type: 'text',
977
980
  text: JSON.stringify({
978
981
  status: 'error',
979
- message: `Deployment failed: ${result.error}`
982
+ message: `Deployment failed: ${result.error}`,
983
+ details: {
984
+ project_name: session.projectName,
985
+ region: region,
986
+ framework: session.framework,
987
+ suggestion: 'Please ensure the backend and Coolify services are running properly'
988
+ }
980
989
  }, null, 2)
981
990
  }]
982
991
  };
983
992
  }
984
993
 
985
994
  const project = result.data;
995
+ console.error(`Project created successfully: ${project.name} (ID: ${project.id})`);
996
+ console.error(`SSH URL: ${project.ssh_url_to_repo}`);
997
+
998
+ // Check if deployment was actually created
999
+ const deploymentCreated = project.deployment_status || project.coolify_resource_id;
1000
+ if (deploymentCreated) {
1001
+ console.error(`Deployment resource created in Coolify`);
1002
+ } else {
1003
+ console.error(`Warning: Project created but deployment resource might not be set up`);
1004
+ }
986
1005
 
987
1006
  // Initialize git repository
988
1007
  const gitCommands = [
@@ -990,6 +1009,8 @@ async function deployProject(sessionId, args) {
990
1009
  `git remote add origin ${project.ssh_url_to_repo}`,
991
1010
  'git add .',
992
1011
  'git commit -m "Initial deployment via MLGym"',
1012
+ '# Wait 5-10 seconds for SSH key to propagate in GitLab',
1013
+ 'sleep 10',
993
1014
  'git push -u origin main'
994
1015
  ];
995
1016
 
@@ -1014,6 +1035,7 @@ async function deployProject(sessionId, args) {
1014
1035
  ssl: 'auto-provisioned',
1015
1036
  cdn: 'enabled'
1016
1037
  },
1038
+ important_note: '⚠️ SSH key propagation: Please wait 10 seconds before pushing to allow GitLab to activate your SSH key',
1017
1039
  next_steps: {
1018
1040
  message: 'Run these commands in your project directory:',
1019
1041
  commands: gitCommands
package/index.js CHANGED
@@ -16,6 +16,65 @@ import crypto from 'crypto';
16
16
 
17
17
  const execAsync = promisify(exec);
18
18
 
19
+ // Current version of this MCP server
20
+ const CURRENT_VERSION = '2.1.0';
21
+ const PACKAGE_NAME = 'mlgym-deploy';
22
+
23
+ // Version check state
24
+ let versionCheckResult = null;
25
+ let lastVersionCheck = 0;
26
+ const VERSION_CHECK_INTERVAL = 3600000; // Check once per hour
27
+
28
+ // Helper to check for updates
29
+ async function checkForUpdates() {
30
+ try {
31
+ // Check if we've recently checked
32
+ if (Date.now() - lastVersionCheck < VERSION_CHECK_INTERVAL && versionCheckResult) {
33
+ return versionCheckResult;
34
+ }
35
+
36
+ // Get latest version from npm
37
+ const { stdout } = await execAsync(`npm view ${PACKAGE_NAME} version`);
38
+ const latestVersion = stdout.trim();
39
+
40
+ // Compare versions
41
+ const isUpdateAvailable = latestVersion !== CURRENT_VERSION;
42
+
43
+ versionCheckResult = {
44
+ current: CURRENT_VERSION,
45
+ latest: latestVersion,
46
+ updateAvailable: isUpdateAvailable,
47
+ lastChecked: new Date().toISOString()
48
+ };
49
+
50
+ lastVersionCheck = Date.now();
51
+
52
+ // Log to stderr if update available (visible in Cursor logs)
53
+ if (isUpdateAvailable) {
54
+ console.error(`\n╔════════════════════════════════════════════════════════════════╗`);
55
+ console.error(`║ MLGym MCP Server Update Available! ║`);
56
+ console.error(`║ ║`);
57
+ console.error(`║ Current version: ${CURRENT_VERSION.padEnd(49)} ║`);
58
+ console.error(`║ Latest version: ${latestVersion.padEnd(49)} ║`);
59
+ console.error(`║ ║`);
60
+ console.error(`║ To update, run: npm install -g ${PACKAGE_NAME}@latest ║`);
61
+ console.error(`╚════════════════════════════════════════════════════════════════╝\n`);
62
+ }
63
+
64
+ return versionCheckResult;
65
+ } catch (error) {
66
+ // Silent fail - don't disrupt normal operation
67
+ console.error('Version check failed (network may be offline):', error.message);
68
+ return {
69
+ current: CURRENT_VERSION,
70
+ latest: 'unknown',
71
+ updateAvailable: false,
72
+ error: error.message,
73
+ lastChecked: new Date().toISOString()
74
+ };
75
+ }
76
+ }
77
+
19
78
  // Helper to generate random password
20
79
  function generateRandomPassword() {
21
80
  const chars = 'ABCDEFGHJKLMNPQRSTWXYZabcdefghjkmnpqrstwxyz23456789!@#$%^&*';
@@ -195,6 +254,35 @@ async function apiRequest(method, endpoint, data = null, useAuth = true) {
195
254
  }
196
255
  }
197
256
 
257
+ // Global error and status tracking
258
+ let lastError = null;
259
+ let lastOperation = null;
260
+ let operationHistory = [];
261
+ const MAX_HISTORY = 20;
262
+
263
+ // Helper to track operations
264
+ function trackOperation(operation, success = true, error = null) {
265
+ const entry = {
266
+ operation,
267
+ success,
268
+ timestamp: new Date().toISOString(),
269
+ error: error ? error.message || error : null
270
+ };
271
+
272
+ lastOperation = entry;
273
+ if (!success && error) {
274
+ lastError = {
275
+ ...entry,
276
+ stack: error.stack || null
277
+ };
278
+ }
279
+
280
+ operationHistory.unshift(entry);
281
+ if (operationHistory.length > MAX_HISTORY) {
282
+ operationHistory = operationHistory.slice(0, MAX_HISTORY);
283
+ }
284
+ }
285
+
198
286
  // Create MCP server
199
287
  const server = new Server(
200
288
  {
@@ -210,6 +298,119 @@ const server = new Server(
210
298
 
211
299
  // Tool definitions
212
300
  const TOOLS = [
301
+ {
302
+ name: 'mlgym_git_configure',
303
+ description: 'Check and configure git user settings (required before making commits)',
304
+ inputSchema: {
305
+ type: 'object',
306
+ properties: {
307
+ email: {
308
+ type: 'string',
309
+ description: 'Email to use for git commits (defaults to MLGym user email if logged in)',
310
+ pattern: '^[^@]+@[^@]+\\.[^@]+$'
311
+ },
312
+ name: {
313
+ type: 'string',
314
+ description: 'Name to use for git commits (defaults to email username if not provided)'
315
+ },
316
+ check_only: {
317
+ type: 'boolean',
318
+ description: 'If true, only check current configuration without changing it',
319
+ default: false
320
+ }
321
+ }
322
+ }
323
+ },
324
+ {
325
+ name: 'mlgym_debug_last_error',
326
+ description: 'Get detailed error information from the last failed operation',
327
+ inputSchema: {
328
+ type: 'object',
329
+ properties: {
330
+ include_stack: {
331
+ type: 'boolean',
332
+ description: 'Include stack trace if available',
333
+ default: true
334
+ }
335
+ }
336
+ }
337
+ },
338
+ {
339
+ name: 'mlgym_debug_status',
340
+ description: 'Get current system status and recent operations',
341
+ inputSchema: {
342
+ type: 'object',
343
+ properties: {
344
+ verbose: {
345
+ type: 'boolean',
346
+ description: 'Include detailed status information',
347
+ default: false
348
+ }
349
+ }
350
+ }
351
+ },
352
+ {
353
+ name: 'mlgym_debug_logs',
354
+ description: 'View recent logs from backend, User Agent, or Coolify',
355
+ inputSchema: {
356
+ type: 'object',
357
+ properties: {
358
+ source: {
359
+ type: 'string',
360
+ enum: ['backend', 'user-agent', 'coolify', 'all'],
361
+ description: 'Which service logs to retrieve',
362
+ default: 'all'
363
+ },
364
+ lines: {
365
+ type: 'integer',
366
+ description: 'Number of log lines to retrieve',
367
+ default: 50,
368
+ minimum: 10,
369
+ maximum: 500
370
+ },
371
+ filter: {
372
+ type: 'string',
373
+ description: 'Filter logs by keyword (e.g., "error", "deployment", "user")'
374
+ }
375
+ }
376
+ }
377
+ },
378
+ {
379
+ name: 'mlgym_debug_deployment',
380
+ description: 'Check detailed deployment status and troubleshoot deployment issues',
381
+ inputSchema: {
382
+ type: 'object',
383
+ properties: {
384
+ project_id: {
385
+ type: 'integer',
386
+ description: 'Project ID to check deployment status for'
387
+ },
388
+ deployment_uuid: {
389
+ type: 'string',
390
+ description: 'Specific deployment UUID to investigate'
391
+ },
392
+ check_queue: {
393
+ type: 'boolean',
394
+ description: 'Check Coolify deployment queue status',
395
+ default: true
396
+ }
397
+ }
398
+ }
399
+ },
400
+ {
401
+ name: 'mlgym_version_check',
402
+ description: 'Check for MCP server updates and get version information',
403
+ inputSchema: {
404
+ type: 'object',
405
+ properties: {
406
+ check_now: {
407
+ type: 'boolean',
408
+ description: 'Force immediate version check (bypasses cache)',
409
+ default: false
410
+ }
411
+ }
412
+ }
413
+ },
213
414
  {
214
415
  name: 'mlgym_user_create',
215
416
  description: 'Create a new user with GitLab, Coolify, and SSH key setup. IMPORTANT: You must explicitly ask the user for their email, full name, password, and terms acceptance.',
@@ -241,7 +442,7 @@ const TOOLS = [
241
442
  },
242
443
  {
243
444
  name: 'mlgym_project_init',
244
- description: 'Initialize and deploy a project with GitLab repository and Coolify deployment. IMPORTANT: You must explicitly ask the user for project name and description.',
445
+ description: 'Initialize and deploy a project with GitLab repository and Coolify deployment. IMPORTANT: You must explicitly ask the user for project name, description, and deployment URL if deployment is enabled.',
245
446
  inputSchema: {
246
447
  type: 'object',
247
448
  properties: {
@@ -261,6 +462,13 @@ const TOOLS = [
261
462
  description: 'Enable Coolify deployment with webhooks (ask user yes/no)',
262
463
  default: true
263
464
  },
465
+ hostname: {
466
+ type: 'string',
467
+ description: 'Custom hostname for deployment (REQUIRED if enable_deployment is true: ask user for their preferred hostname, will be used as subdomain)',
468
+ pattern: '^[a-z][a-z0-9-]*[a-z0-9]$',
469
+ minLength: 3,
470
+ maxLength: 63
471
+ },
264
472
  local_path: {
265
473
  type: 'string',
266
474
  description: 'Local directory path to initialize as git repository',
@@ -955,6 +1163,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
955
1163
  const { name, arguments: args } = request.params;
956
1164
 
957
1165
  switch (name) {
1166
+ case 'mlgym_git_configure':
1167
+ return await configureGit(args);
1168
+
1169
+ case 'mlgym_debug_last_error':
1170
+ return await debugLastError(args);
1171
+
1172
+ case 'mlgym_debug_status':
1173
+ return await debugStatus(args);
1174
+
1175
+ case 'mlgym_debug_logs':
1176
+ return await debugLogs(args);
1177
+
1178
+ case 'mlgym_debug_deployment':
1179
+ return await debugDeployment(args);
1180
+
1181
+ case 'mlgym_version_check':
1182
+ return await versionCheck(args);
1183
+
958
1184
  case 'mlgym_user_create':
959
1185
  return await createUser(args);
960
1186
 
@@ -1138,6 +1364,342 @@ async function generateSSHKeyPair(email) {
1138
1364
  }
1139
1365
  }
1140
1366
 
1367
+ // Tool implementation: Debug Last Error
1368
+ async function debugLastError(args) {
1369
+ const { include_stack = true } = args;
1370
+
1371
+ if (!lastError) {
1372
+ return {
1373
+ content: [{
1374
+ type: 'text',
1375
+ text: JSON.stringify({
1376
+ status: 'info',
1377
+ message: 'No errors recorded',
1378
+ last_operation: lastOperation || 'None'
1379
+ }, null, 2)
1380
+ }]
1381
+ };
1382
+ }
1383
+
1384
+ const response = {
1385
+ status: 'error',
1386
+ error: {
1387
+ operation: lastError.operation,
1388
+ message: lastError.error,
1389
+ timestamp: lastError.timestamp
1390
+ }
1391
+ };
1392
+
1393
+ if (include_stack && lastError.stack) {
1394
+ response.error.stack = lastError.stack;
1395
+ }
1396
+
1397
+ return {
1398
+ content: [{
1399
+ type: 'text',
1400
+ text: JSON.stringify(response, null, 2)
1401
+ }]
1402
+ };
1403
+ }
1404
+
1405
+ // Tool implementation: Debug Status
1406
+ async function debugStatus(args) {
1407
+ const { verbose = false } = args;
1408
+
1409
+ // Check authentication status
1410
+ const auth = await loadAuth();
1411
+ const isAuthenticated = !!auth.token;
1412
+
1413
+ const response = {
1414
+ status: 'info',
1415
+ authentication: {
1416
+ logged_in: isAuthenticated,
1417
+ user: isAuthenticated ? auth.email : null
1418
+ },
1419
+ last_operation: lastOperation || { message: 'No operations recorded' },
1420
+ recent_operations: operationHistory.slice(0, verbose ? 10 : 5).map(op => ({
1421
+ operation: op.operation,
1422
+ success: op.success,
1423
+ timestamp: op.timestamp,
1424
+ error: op.error
1425
+ }))
1426
+ };
1427
+
1428
+ if (verbose) {
1429
+ response.backend_url = process.env.GITLAB_BACKEND_URL || 'https://backend.eu.ezb.net';
1430
+ response.gitlab_url = process.env.GITLAB_URL || 'https://git.mlgym.io';
1431
+ response.coolify_url = process.env.COOLIFY_URL || 'https://coolify.eu.ezb.net';
1432
+ response.error_count = operationHistory.filter(op => !op.success).length;
1433
+ response.success_count = operationHistory.filter(op => op.success).length;
1434
+ }
1435
+
1436
+ return {
1437
+ content: [{
1438
+ type: 'text',
1439
+ text: JSON.stringify(response, null, 2)
1440
+ }]
1441
+ };
1442
+ }
1443
+
1444
+ // Tool implementation: Debug Logs
1445
+ async function debugLogs(args) {
1446
+ const { source = 'all', lines = 50, filter } = args;
1447
+
1448
+ // Since we're in an MCP server, we can't directly access remote logs
1449
+ // But we can provide instructions on how to check them
1450
+ const instructions = {
1451
+ status: 'info',
1452
+ message: 'Log viewing instructions',
1453
+ sources: {}
1454
+ };
1455
+
1456
+ if (source === 'all' || source === 'backend') {
1457
+ instructions.sources.backend = {
1458
+ description: 'GitLab Backend Server logs',
1459
+ commands: [
1460
+ 'ssh -i ~/.ssh/id_rsa_chka05 root@backend.eu.ezb.net',
1461
+ 'docker logs gitlab-admin-server --tail ' + lines,
1462
+ filter ? `docker logs gitlab-admin-server 2>&1 | grep -i "${filter}" | tail -${lines}` : null
1463
+ ].filter(Boolean)
1464
+ };
1465
+ }
1466
+
1467
+ if (source === 'all' || source === 'user-agent') {
1468
+ instructions.sources.user_agent = {
1469
+ description: 'Coolify User Agent logs',
1470
+ commands: [
1471
+ 'ssh -i ~/.ssh/id_rsa_chka05 root@coolify.eu.ezb.net',
1472
+ 'docker logs coolify-user-agent --tail ' + lines,
1473
+ filter ? `docker logs coolify-user-agent 2>&1 | grep -i "${filter}" | tail -${lines}` : null
1474
+ ].filter(Boolean)
1475
+ };
1476
+ }
1477
+
1478
+ if (source === 'all' || source === 'coolify') {
1479
+ instructions.sources.coolify = {
1480
+ description: 'Coolify platform logs',
1481
+ commands: [
1482
+ 'ssh -i ~/.ssh/id_rsa_chka05 root@coolify.eu.ezb.net',
1483
+ 'docker logs coolify --tail ' + lines,
1484
+ filter ? `docker logs coolify 2>&1 | grep -i "${filter}" | tail -${lines}` : null
1485
+ ].filter(Boolean)
1486
+ };
1487
+ }
1488
+
1489
+ instructions.note = 'The AI assistant should execute these commands to retrieve logs for debugging';
1490
+
1491
+ return {
1492
+ content: [{
1493
+ type: 'text',
1494
+ text: JSON.stringify(instructions, null, 2)
1495
+ }]
1496
+ };
1497
+ }
1498
+
1499
+ // Tool implementation: Debug Deployment
1500
+ async function debugDeployment(args) {
1501
+ const { project_id, deployment_uuid, check_queue = true } = args;
1502
+
1503
+ const response = {
1504
+ status: 'info',
1505
+ deployment_debugging: {}
1506
+ };
1507
+
1508
+ // Get authentication
1509
+ const auth = await loadAuth();
1510
+ if (!auth.token) {
1511
+ return {
1512
+ content: [{
1513
+ type: 'text',
1514
+ text: JSON.stringify({
1515
+ status: 'error',
1516
+ message: 'Not authenticated. Login required to check deployments.'
1517
+ }, null, 2)
1518
+ }]
1519
+ };
1520
+ }
1521
+
1522
+ if (project_id) {
1523
+ // Get project deployment status
1524
+ try {
1525
+ const result = await apiRequest('GET', `/api/v1/projects/${project_id}/deployments?limit=5`);
1526
+ response.deployment_debugging.project_deployments = result.success ? result.data : { error: result.error };
1527
+ } catch (error) {
1528
+ response.deployment_debugging.project_deployments = { error: error.message };
1529
+ }
1530
+ }
1531
+
1532
+ if (deployment_uuid) {
1533
+ response.deployment_debugging.specific_deployment = {
1534
+ uuid: deployment_uuid,
1535
+ check_commands: [
1536
+ 'Check deployment status in Coolify database:',
1537
+ `ssh -i ~/.ssh/id_rsa_chka05 root@coolify.eu.ezb.net`,
1538
+ `docker exec coolify-db psql -U coolify -d coolify -c "SELECT * FROM application_deployment_queues WHERE deployment_uuid = '${deployment_uuid}';"`,
1539
+ '',
1540
+ 'Check if deployment is stuck in queue:',
1541
+ `docker exec coolify sh -c "cd /var/www/html && php artisan check:deployment-queue"`
1542
+ ]
1543
+ };
1544
+ }
1545
+
1546
+ if (check_queue) {
1547
+ response.deployment_debugging.queue_status = {
1548
+ description: 'Commands to check Coolify deployment queue',
1549
+ commands: [
1550
+ 'Check recent deployments:',
1551
+ `ssh -i ~/.ssh/id_rsa_chka05 root@coolify.eu.ezb.net`,
1552
+ `docker exec coolify-db psql -U coolify -d coolify -c "SELECT id, application_id, deployment_uuid, status, created_at FROM application_deployment_queues ORDER BY created_at DESC LIMIT 10;"`,
1553
+ '',
1554
+ 'Check if Horizon queue processor is running:',
1555
+ `docker exec coolify sh -c "cd /var/www/html && php artisan horizon:status"`,
1556
+ '',
1557
+ 'Try to manually process queue:',
1558
+ `docker exec coolify sh -c "cd /var/www/html && php artisan queue:work --once"`
1559
+ ]
1560
+ };
1561
+ }
1562
+
1563
+ response.common_issues = [
1564
+ 'Deployment stuck in "queued" status: Coolify queue processor not picking up jobs',
1565
+ 'Authentication errors: SSH keys not properly configured',
1566
+ 'Build failures: Check Dockerfile or build configuration',
1567
+ 'Network issues: Verify GitLab repository is accessible'
1568
+ ];
1569
+
1570
+ return {
1571
+ content: [{
1572
+ type: 'text',
1573
+ text: JSON.stringify(response, null, 2)
1574
+ }]
1575
+ };
1576
+ }
1577
+
1578
+ // Tool implementation: Version Check
1579
+ async function versionCheck(args) {
1580
+ const { check_now = false } = args;
1581
+
1582
+ // Force check if requested
1583
+ if (check_now) {
1584
+ lastVersionCheck = 0;
1585
+ }
1586
+
1587
+ // Check for updates
1588
+ const versionInfo = await checkForUpdates();
1589
+
1590
+ const response = {
1591
+ status: 'success',
1592
+ version: {
1593
+ current: versionInfo.current,
1594
+ latest: versionInfo.latest,
1595
+ update_available: versionInfo.updateAvailable,
1596
+ last_checked: versionInfo.lastChecked
1597
+ }
1598
+ };
1599
+
1600
+ if (versionInfo.updateAvailable) {
1601
+ response.update_instructions = {
1602
+ message: '🔄 A new version of MLGym MCP Server is available!',
1603
+ current_version: versionInfo.current,
1604
+ latest_version: versionInfo.latest,
1605
+ commands: [
1606
+ 'To update globally:',
1607
+ `npm install -g ${PACKAGE_NAME}@latest`,
1608
+ '',
1609
+ 'Or update to a specific version:',
1610
+ `npm install -g ${PACKAGE_NAME}@${versionInfo.latest}`,
1611
+ '',
1612
+ 'After updating, restart Cursor IDE to load the new version.'
1613
+ ],
1614
+ changelog_url: `https://www.npmjs.com/package/${PACKAGE_NAME}?activeTab=versions`
1615
+ };
1616
+ } else {
1617
+ response.message = '✅ You are running the latest version of MLGym MCP Server';
1618
+ }
1619
+
1620
+ if (versionInfo.error) {
1621
+ response.note = `Version check had issues: ${versionInfo.error}`;
1622
+ }
1623
+
1624
+ return {
1625
+ content: [{
1626
+ type: 'text',
1627
+ text: JSON.stringify(response, null, 2)
1628
+ }]
1629
+ };
1630
+ }
1631
+
1632
+ // Tool implementation: Configure Git
1633
+ async function configureGit(args) {
1634
+ const { email, name, check_only = false } = args;
1635
+
1636
+ // Try to get email from auth if not provided
1637
+ let gitEmail = email;
1638
+ let gitName = name;
1639
+
1640
+ if (!gitEmail && !check_only) {
1641
+ // Try to get from authenticated user
1642
+ const auth = await loadAuth();
1643
+ if (auth.token && auth.email) {
1644
+ gitEmail = auth.email;
1645
+ gitName = gitName || auth.email.split('@')[0];
1646
+ }
1647
+ }
1648
+
1649
+ // If just checking, return current configuration
1650
+ if (check_only) {
1651
+ return {
1652
+ content: [{
1653
+ type: 'text',
1654
+ text: JSON.stringify({
1655
+ status: 'info',
1656
+ message: 'Git configuration check',
1657
+ instructions: [
1658
+ 'Run: git config --global user.email',
1659
+ 'Run: git config --global user.name',
1660
+ 'If not set, configure with:',
1661
+ `git config --global user.email "${gitEmail || 'your-email@example.com'}"`,
1662
+ `git config --global user.name "${gitName || 'Your Name'}"`,
1663
+ '',
1664
+ 'IMPORTANT: Git must be configured before making any commits.'
1665
+ ]
1666
+ }, null, 2)
1667
+ }]
1668
+ };
1669
+ }
1670
+
1671
+ // Return configuration instructions
1672
+ if (!gitEmail) {
1673
+ return {
1674
+ content: [{
1675
+ type: 'text',
1676
+ text: JSON.stringify({
1677
+ status: 'error',
1678
+ message: 'Email is required for git configuration',
1679
+ solution: 'Either provide email parameter or login with mlgym_auth_login first'
1680
+ }, null, 2)
1681
+ }]
1682
+ };
1683
+ }
1684
+
1685
+ gitName = gitName || gitEmail.split('@')[0];
1686
+
1687
+ return {
1688
+ content: [{
1689
+ type: 'text',
1690
+ text: JSON.stringify({
1691
+ status: 'success',
1692
+ message: 'Git configuration instructions',
1693
+ commands: [
1694
+ `git config --global user.email "${gitEmail}"`,
1695
+ `git config --global user.name "${gitName}"`
1696
+ ],
1697
+ note: 'The AI assistant should execute these commands to configure git before making any commits'
1698
+ }, null, 2)
1699
+ }]
1700
+ };
1701
+ }
1702
+
1141
1703
  // Tool implementation: Create User
1142
1704
  async function createUser(args) {
1143
1705
  let { email, name, password, accept_terms } = args;
@@ -1216,6 +1778,8 @@ async function createUser(args) {
1216
1778
  console.error(`SSH key generated: ${privateKeyPath}`);
1217
1779
 
1218
1780
  // Create user via backend API with SSH key
1781
+ trackOperation('user_create', true, null);
1782
+
1219
1783
  const result = await apiRequest('POST', '/api/v1/users', {
1220
1784
  email,
1221
1785
  name,
@@ -1224,6 +1788,7 @@ async function createUser(args) {
1224
1788
  }, false);
1225
1789
 
1226
1790
  if (!result.success) {
1791
+ trackOperation('user_create', false, new Error(result.error || 'User creation failed'));
1227
1792
  return {
1228
1793
  content: [{
1229
1794
  type: 'text',
@@ -1353,22 +1918,46 @@ async function detectProjectName(localPath) {
1353
1918
 
1354
1919
  // Tool implementation: Initialize Project
1355
1920
  async function initProject(args) {
1356
- let { name, description, enable_deployment = true, local_path = '.' } = args;
1921
+ let { name, description, enable_deployment = true, hostname, local_path = '.' } = args;
1922
+
1923
+ // Start tracking this operation
1924
+ const operationId = `init-project-${Date.now()}`;
1357
1925
 
1358
1926
  // Validate required fields are provided
1359
1927
  if (!name || !description) {
1928
+ const error = {
1929
+ status: 'error',
1930
+ message: 'Project name and description are required and must be provided by user',
1931
+ required_fields: {
1932
+ name: name ? '✓ provided' : '✗ missing - must be provided by user',
1933
+ description: description ? '✓ provided' : '✗ missing - must be provided by user'
1934
+ },
1935
+ note: 'Auto-detection is disabled. User must explicitly provide project details.'
1936
+ };
1937
+ trackOperation('mlgym_project_init', operationId, 'failed', error.message, { args, error });
1360
1938
  return {
1361
1939
  content: [{
1362
1940
  type: 'text',
1363
- text: JSON.stringify({
1364
- status: 'error',
1365
- message: 'Project name and description are required and must be provided by user',
1366
- required_fields: {
1367
- name: name ? '✓ provided' : '✗ missing - must be provided by user',
1368
- description: description ? '✓ provided' : '✗ missing - must be provided by user'
1369
- },
1370
- note: 'Auto-detection is disabled. User must explicitly provide project details.'
1371
- }, null, 2)
1941
+ text: JSON.stringify(error, null, 2)
1942
+ }]
1943
+ };
1944
+ }
1945
+
1946
+ // Validate hostname if deployment is enabled
1947
+ if (enable_deployment && !hostname) {
1948
+ const error = {
1949
+ status: 'error',
1950
+ message: 'Hostname is required when deployment is enabled',
1951
+ required_fields: {
1952
+ hostname: '✗ missing - must be provided by user (will be used as subdomain)'
1953
+ },
1954
+ note: 'Please provide a unique hostname for your deployment (e.g., "myapp" for myapp.ezb.net)'
1955
+ };
1956
+ trackOperation('mlgym_project_init', operationId, 'failed', error.message, { args, error });
1957
+ return {
1958
+ content: [{
1959
+ type: 'text',
1960
+ text: JSON.stringify(error, null, 2)
1372
1961
  }]
1373
1962
  };
1374
1963
  }
@@ -1378,10 +1967,12 @@ async function initProject(args) {
1378
1967
  // Check authentication
1379
1968
  const auth = await loadAuth();
1380
1969
  if (!auth.token) {
1970
+ const errorMsg = 'Not authenticated. Please create a user first with mlgym_user_create';
1971
+ trackOperation('mlgym_project_init', operationId, 'failed', errorMsg, { args });
1381
1972
  return {
1382
1973
  content: [{
1383
1974
  type: 'text',
1384
- text: 'Error: Not authenticated. Please create a user first with mlgym_user_create'
1975
+ text: `Error: ${errorMsg}`
1385
1976
  }]
1386
1977
  };
1387
1978
  }
@@ -1395,16 +1986,19 @@ async function initProject(args) {
1395
1986
  description,
1396
1987
  visibility: 'private',
1397
1988
  enable_deployment: enable_deployment,
1398
- webhook_secret: webhookSecret
1989
+ webhook_secret: webhookSecret,
1990
+ hostname: hostname || null
1399
1991
  };
1400
1992
 
1401
1993
  const result = await apiRequest('POST', '/api/v1/projects', projectData);
1402
1994
 
1403
1995
  if (!result.success) {
1996
+ const errorMsg = `Failed to create project: ${result.error}`;
1997
+ trackOperation('mlgym_project_init', operationId, 'failed', errorMsg, { args, projectData, response: result });
1404
1998
  return {
1405
1999
  content: [{
1406
2000
  type: 'text',
1407
- text: `Failed to create project: ${result.error}`
2001
+ text: errorMsg
1408
2002
  }]
1409
2003
  };
1410
2004
  }
@@ -1425,10 +2019,47 @@ async function initProject(args) {
1425
2019
  namespace: project.namespace?.path || project.namespace || auth.email.split('@')[0]
1426
2020
  };
1427
2021
 
1428
- // If deployment was enabled, the backend has already created webhook and Coolify resources
2022
+ // If deployment was enabled, create Dockerfile if not exists
1429
2023
  if (enable_deployment) {
1430
2024
  console.error('Deployment enabled - backend has set up webhook and Coolify resources automatically');
1431
2025
 
2026
+ // Check if Dockerfile already exists
2027
+ const dockerfilePath = path.join(local_path, 'Dockerfile');
2028
+ let dockerfileCreated = false;
2029
+
2030
+ try {
2031
+ await fs.access(dockerfilePath);
2032
+ console.error('Dockerfile already exists in project');
2033
+ } catch {
2034
+ // Dockerfile doesn't exist, create one based on detected framework
2035
+ console.error('No Dockerfile found, generating one...');
2036
+
2037
+ try {
2038
+ const framework = await detectFramework(local_path);
2039
+ console.error(`Detected framework: ${framework}`);
2040
+
2041
+ const dockerfileContent = generateDockerfile(framework, name);
2042
+ await fs.writeFile(dockerfilePath, dockerfileContent, 'utf8');
2043
+ dockerfileCreated = true;
2044
+
2045
+ console.error(`Created Dockerfile for ${framework} framework`);
2046
+
2047
+ // If React app, also create nginx.conf
2048
+ if (framework === 'react') {
2049
+ const nginxPath = path.join(local_path, 'nginx.conf');
2050
+ try {
2051
+ await fs.access(nginxPath);
2052
+ } catch {
2053
+ const nginxContent = generateNginxConf();
2054
+ await fs.writeFile(nginxPath, nginxContent, 'utf8');
2055
+ console.error('Created nginx.conf for React app');
2056
+ }
2057
+ }
2058
+ } catch (err) {
2059
+ console.error(`Failed to create Dockerfile: ${err.message}`);
2060
+ }
2061
+ }
2062
+
1432
2063
  response.webhook = {
1433
2064
  secret: webhookSecret,
1434
2065
  status: 'created',
@@ -1438,12 +2069,19 @@ async function initProject(args) {
1438
2069
  response.deployment = {
1439
2070
  status: 'initialized',
1440
2071
  domain: `${name}.eu-central.mlgym.app`,
1441
- message: 'Coolify deployment ready - push to main branch to trigger deployment'
2072
+ message: 'Coolify deployment ready - push to main branch to trigger deployment',
2073
+ dockerfile: dockerfileCreated ? 'Generated automatically based on framework detection' : 'Using existing Dockerfile'
1442
2074
  };
1443
2075
  }
1444
2076
 
1445
2077
  // Add git commands for local setup
2078
+ // First ensure git is configured
1446
2079
  response.next_steps = [
2080
+ '# First, ensure git is configured (check with: git config --list | grep user)',
2081
+ '# If not configured, run:',
2082
+ `git config --global user.email "${auth.email}"`,
2083
+ `git config --global user.name "${auth.email.split('@')[0]}"`,
2084
+ '# Then initialize and push the repository:',
1447
2085
  `cd ${local_path}`,
1448
2086
  'git init',
1449
2087
  `git remote add origin ${project.ssh_url_to_repo}`,
@@ -1452,6 +2090,11 @@ async function initProject(args) {
1452
2090
  'git push -u origin main'
1453
2091
  ];
1454
2092
 
2093
+ response.important_note = 'IMPORTANT: Ensure git is configured with user.email and user.name before committing. The AI should check this first and configure if needed.';
2094
+
2095
+ // Track successful operation
2096
+ trackOperation('mlgym_project_init', operationId, 'success', `Project ${name} created successfully`, { args, response });
2097
+
1455
2098
  return {
1456
2099
  content: [{
1457
2100
  type: 'text',
@@ -1470,10 +2113,187 @@ function generateWebhookSecret() {
1470
2113
  return secret;
1471
2114
  }
1472
2115
 
2116
+ // Generate Dockerfile based on detected framework
2117
+ function generateDockerfile(framework, projectName) {
2118
+ const dockerfiles = {
2119
+ nextjs: `# Next.js Production Dockerfile
2120
+ FROM node:20-alpine AS builder
2121
+ WORKDIR /app
2122
+ COPY package*.json ./
2123
+ RUN npm ci
2124
+ COPY . .
2125
+ RUN npm run build
2126
+
2127
+ FROM node:20-alpine
2128
+ WORKDIR /app
2129
+ ENV NODE_ENV=production
2130
+ COPY --from=builder /app/public ./public
2131
+ COPY --from=builder /app/.next ./.next
2132
+ COPY --from=builder /app/node_modules ./node_modules
2133
+ COPY --from=builder /app/package.json ./package.json
2134
+ EXPOSE 3000
2135
+ ENV PORT=3000
2136
+ CMD ["npm", "start"]`,
2137
+
2138
+ react: `# React Production Dockerfile
2139
+ FROM node:20-alpine AS builder
2140
+ WORKDIR /app
2141
+ COPY package*.json ./
2142
+ RUN npm ci
2143
+ COPY . .
2144
+ RUN npm run build
2145
+
2146
+ FROM nginx:alpine
2147
+ COPY --from=builder /app/build /usr/share/nginx/html
2148
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
2149
+ EXPOSE 80
2150
+ CMD ["nginx", "-g", "daemon off;"]`,
2151
+
2152
+ express: `# Express.js Dockerfile
2153
+ FROM node:20-alpine
2154
+ WORKDIR /app
2155
+ COPY package*.json ./
2156
+ RUN npm ci
2157
+ COPY . .
2158
+ EXPOSE 3000
2159
+ ENV PORT=3000
2160
+ CMD ["node", "index.js"]`,
2161
+
2162
+ django: `# Django Dockerfile
2163
+ FROM python:3.11-slim
2164
+ WORKDIR /app
2165
+ ENV PYTHONUNBUFFERED=1
2166
+ COPY requirements.txt .
2167
+ RUN pip install --no-cache-dir -r requirements.txt
2168
+ COPY . .
2169
+ RUN python manage.py collectstatic --noinput
2170
+ EXPOSE 8000
2171
+ CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]`,
2172
+
2173
+ php: `# PHP Dockerfile
2174
+ FROM php:8.2-apache
2175
+ RUN docker-php-ext-install pdo pdo_mysql
2176
+ COPY . /var/www/html/
2177
+ RUN a2enmod rewrite
2178
+ EXPOSE 80`,
2179
+
2180
+ static: `# Static Site Dockerfile
2181
+ FROM nginx:alpine
2182
+ COPY . /usr/share/nginx/html
2183
+ EXPOSE 80
2184
+ CMD ["nginx", "-g", "daemon off;"]`,
2185
+
2186
+ go: `# Go Dockerfile
2187
+ FROM golang:1.21-alpine AS builder
2188
+ WORKDIR /app
2189
+ COPY go.* ./
2190
+ RUN go mod download
2191
+ COPY . .
2192
+ RUN go build -o main .
2193
+
2194
+ FROM alpine:latest
2195
+ RUN apk --no-cache add ca-certificates
2196
+ WORKDIR /root/
2197
+ COPY --from=builder /app/main .
2198
+ EXPOSE 8080
2199
+ CMD ["./main"]`,
2200
+
2201
+ rust: `# Rust Dockerfile
2202
+ FROM rust:1.75 AS builder
2203
+ WORKDIR /app
2204
+ COPY Cargo.* ./
2205
+ COPY src ./src
2206
+ RUN cargo build --release
2207
+
2208
+ FROM debian:bookworm-slim
2209
+ WORKDIR /app
2210
+ COPY --from=builder /app/target/release/${projectName} .
2211
+ EXPOSE 8080
2212
+ CMD ["./${projectName}"]`
2213
+ };
2214
+
2215
+ return dockerfiles[framework] || dockerfiles.static;
2216
+ }
2217
+
2218
+ // Generate nginx.conf for React apps
2219
+ function generateNginxConf() {
2220
+ return `server {
2221
+ listen 80;
2222
+ location / {
2223
+ root /usr/share/nginx/html;
2224
+ index index.html index.htm;
2225
+ try_files $uri $uri/ /index.html;
2226
+ }
2227
+ }`;
2228
+ }
2229
+
2230
+ // Detect project framework by analyzing files
2231
+ async function detectFramework(localPath) {
2232
+ try {
2233
+ // Check for package.json
2234
+ const packagePath = path.join(localPath, 'package.json');
2235
+ const packageData = await fs.readFile(packagePath, 'utf8');
2236
+ const packageJson = JSON.parse(packageData);
2237
+
2238
+ // Check dependencies
2239
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
2240
+
2241
+ if (deps.next) return 'nextjs';
2242
+ if (deps.react && !deps.next) return 'react';
2243
+ if (deps.express) return 'express';
2244
+ if (deps.vue) return 'vue';
2245
+ if (deps.angular) return 'angular';
2246
+
2247
+ // If has package.json but no recognized framework
2248
+ return 'node';
2249
+ } catch {
2250
+ // No package.json, check for other files
2251
+ }
2252
+
2253
+ // Check for Python
2254
+ try {
2255
+ await fs.access(path.join(localPath, 'requirements.txt'));
2256
+ const content = await fs.readFile(path.join(localPath, 'requirements.txt'), 'utf8');
2257
+ if (content.includes('django')) return 'django';
2258
+ if (content.includes('flask')) return 'flask';
2259
+ return 'python';
2260
+ } catch {}
2261
+
2262
+ // Check for Go
2263
+ try {
2264
+ await fs.access(path.join(localPath, 'go.mod'));
2265
+ return 'go';
2266
+ } catch {}
2267
+
2268
+ // Check for Rust
2269
+ try {
2270
+ await fs.access(path.join(localPath, 'Cargo.toml'));
2271
+ return 'rust';
2272
+ } catch {}
2273
+
2274
+ // Check for PHP
2275
+ try {
2276
+ await fs.access(path.join(localPath, 'composer.json'));
2277
+ return 'php';
2278
+ } catch {}
2279
+
2280
+ // Check for index.php
2281
+ try {
2282
+ await fs.access(path.join(localPath, 'index.php'));
2283
+ return 'php';
2284
+ } catch {}
2285
+
2286
+ // Default to static site
2287
+ return 'static';
2288
+ }
2289
+
1473
2290
  // Tool implementation: Recover User
1474
2291
  async function recoverUser(args) {
1475
2292
  const { email, pin, new_password } = args;
1476
2293
 
2294
+ // Start tracking this operation
2295
+ const operationId = `recover-user-${Date.now()}`;
2296
+
1477
2297
  // Clean up any expired PINs first
1478
2298
  await cleanupExpiredPINs();
1479
2299
 
@@ -1492,6 +2312,9 @@ async function recoverUser(args) {
1492
2312
 
1493
2313
  console.error(`Recovery PIN generated for ${email}: ${generatedPIN}`);
1494
2314
 
2315
+ // Track PIN generation
2316
+ trackOperation('mlgym_user_recover', operationId, 'success', `Recovery PIN generated for ${email}`, { args: { email }, stage: 'pin_generation' });
2317
+
1495
2318
  // In production, this would send an actual email
1496
2319
  // For now, we'll include it in the response for testing
1497
2320
  return {
@@ -1513,13 +2336,15 @@ async function recoverUser(args) {
1513
2336
  const stored = await loadPIN(email);
1514
2337
 
1515
2338
  if (!stored) {
2339
+ const error = {
2340
+ status: 'error',
2341
+ message: 'No recovery request found. Please request a new PIN.'
2342
+ };
2343
+ trackOperation('mlgym_user_recover', operationId, 'failed', error.message, { args: { email }, stage: 'pin_verification' });
1516
2344
  return {
1517
2345
  content: [{
1518
2346
  type: 'text',
1519
- text: JSON.stringify({
1520
- status: 'error',
1521
- message: 'No recovery request found. Please request a new PIN.'
1522
- }, null, 2)
2347
+ text: JSON.stringify(error, null, 2)
1523
2348
  }]
1524
2349
  };
1525
2350
  }
@@ -1527,13 +2352,15 @@ async function recoverUser(args) {
1527
2352
  // Check if PIN expired (10 minutes)
1528
2353
  if (Date.now() - stored.timestamp > 10 * 60 * 1000) {
1529
2354
  await deletePIN(email);
2355
+ const error = {
2356
+ status: 'error',
2357
+ message: 'PIN has expired. Please request a new one.'
2358
+ };
2359
+ trackOperation('mlgym_user_recover', operationId, 'failed', error.message, { args: { email }, stage: 'pin_verification' });
1530
2360
  return {
1531
2361
  content: [{
1532
2362
  type: 'text',
1533
- text: JSON.stringify({
1534
- status: 'error',
1535
- message: 'PIN has expired. Please request a new one.'
1536
- }, null, 2)
2363
+ text: JSON.stringify(error, null, 2)
1537
2364
  }]
1538
2365
  };
1539
2366
  }
@@ -1541,13 +2368,15 @@ async function recoverUser(args) {
1541
2368
  // Check attempts
1542
2369
  if (stored.attempts >= 3) {
1543
2370
  await deletePIN(email);
2371
+ const error = {
2372
+ status: 'error',
2373
+ message: 'Too many failed attempts. Please request a new PIN.'
2374
+ };
2375
+ trackOperation('mlgym_user_recover', operationId, 'failed', error.message, { args: { email }, stage: 'pin_verification' });
1544
2376
  return {
1545
2377
  content: [{
1546
2378
  type: 'text',
1547
- text: JSON.stringify({
1548
- status: 'error',
1549
- message: 'Too many failed attempts. Please request a new PIN.'
1550
- }, null, 2)
2379
+ text: JSON.stringify(error, null, 2)
1551
2380
  }]
1552
2381
  };
1553
2382
  }
@@ -4107,14 +4936,19 @@ async function getDeploymentStatus(args) {
4107
4936
  async function triggerDeployment(args) {
4108
4937
  const { project_id, environment = 'production' } = args;
4109
4938
 
4939
+ // Start tracking this operation
4940
+ const operationId = `trigger-deployment-${Date.now()}`;
4941
+
4110
4942
  if (!project_id) {
4943
+ const error = {
4944
+ status: 'error',
4945
+ message: 'project_id is required'
4946
+ };
4947
+ trackOperation('mlgym_deployments_trigger', operationId, 'failed', error.message, { args });
4111
4948
  return {
4112
4949
  content: [{
4113
4950
  type: 'text',
4114
- text: JSON.stringify({
4115
- status: 'error',
4116
- message: 'project_id is required'
4117
- }, null, 2)
4951
+ text: JSON.stringify(error, null, 2)
4118
4952
  }]
4119
4953
  };
4120
4954
  }
@@ -4124,33 +4958,40 @@ async function triggerDeployment(args) {
4124
4958
  // Check authentication
4125
4959
  const auth = await loadAuth();
4126
4960
  if (!auth.token) {
4961
+ const error = {
4962
+ status: 'error',
4963
+ message: 'Not authenticated. Please login first with mlgym_auth_login'
4964
+ };
4965
+ trackOperation('mlgym_deployments_trigger', operationId, 'failed', error.message, { args });
4127
4966
  return {
4128
4967
  content: [{
4129
4968
  type: 'text',
4130
- text: JSON.stringify({
4131
- status: 'error',
4132
- message: 'Not authenticated. Please login first with mlgym_auth_login'
4133
- }, null, 2)
4969
+ text: JSON.stringify(error, null, 2)
4134
4970
  }]
4135
4971
  };
4136
4972
  }
4137
4973
 
4138
4974
  try {
4139
4975
  // Trigger deployment via backend API
4140
- const result = await apiRequest('POST', '/api/v1/deployments/trigger', {
4141
- project_id,
4142
- environment
4143
- });
4976
+ // Backend expects project_id (integer) and optional trigger field
4977
+ const deploymentData = {
4978
+ project_id: parseInt(project_id), // Ensure it's an integer
4979
+ trigger: 'initial' // Use 'initial' like the CLI does, not 'environment'
4980
+ };
4981
+
4982
+ const result = await apiRequest('POST', '/api/v1/deployments/trigger', deploymentData);
4144
4983
 
4145
4984
  if (!result.success) {
4985
+ const error = {
4986
+ status: 'error',
4987
+ message: result.error || 'Failed to trigger deployment',
4988
+ details: 'Project may not have deployment enabled or webhook configured'
4989
+ };
4990
+ trackOperation('mlgym_deployments_trigger', operationId, 'failed', error.message, { args, deploymentData, response: result });
4146
4991
  return {
4147
4992
  content: [{
4148
4993
  type: 'text',
4149
- text: JSON.stringify({
4150
- status: 'error',
4151
- message: result.error || 'Failed to trigger deployment',
4152
- details: 'Project may not have deployment enabled or webhook configured'
4153
- }, null, 2)
4994
+ text: JSON.stringify(error, null, 2)
4154
4995
  }]
4155
4996
  };
4156
4997
  }
@@ -4171,6 +5012,9 @@ async function triggerDeployment(args) {
4171
5012
  ]
4172
5013
  };
4173
5014
 
5015
+ // Track successful operation
5016
+ trackOperation('mlgym_deployments_trigger', operationId, 'success', `Deployment triggered for project ${project_id}`, { args, response });
5017
+
4174
5018
  return {
4175
5019
  content: [{
4176
5020
  type: 'text',
@@ -4178,14 +5022,16 @@ async function triggerDeployment(args) {
4178
5022
  }]
4179
5023
  };
4180
5024
  } catch (error) {
5025
+ const errorDetails = {
5026
+ status: 'error',
5027
+ message: 'Failed to trigger deployment',
5028
+ error: error.message
5029
+ };
5030
+ trackOperation('mlgym_deployments_trigger', operationId, 'failed', error.message, { args, error: errorDetails });
4181
5031
  return {
4182
5032
  content: [{
4183
5033
  type: 'text',
4184
- text: JSON.stringify({
4185
- status: 'error',
4186
- message: 'Failed to trigger deployment',
4187
- error: error.message
4188
- }, null, 2)
5034
+ text: JSON.stringify(errorDetails, null, 2)
4189
5035
  }]
4190
5036
  };
4191
5037
  }
@@ -4267,14 +5113,19 @@ async function listProjects(args) {
4267
5113
  async function loginUser(args) {
4268
5114
  const { email, password } = args;
4269
5115
 
5116
+ // Start tracking this operation
5117
+ const operationId = `login-user-${Date.now()}`;
5118
+
4270
5119
  if (!email || !password) {
5120
+ const error = {
5121
+ status: 'error',
5122
+ message: 'Email and password are required'
5123
+ };
5124
+ trackOperation('mlgym_auth_login', operationId, 'failed', error.message, { args: { email } }); // Don't log password
4271
5125
  return {
4272
5126
  content: [{
4273
5127
  type: 'text',
4274
- text: JSON.stringify({
4275
- status: 'error',
4276
- message: 'Email and password are required'
4277
- }, null, 2)
5128
+ text: JSON.stringify(error, null, 2)
4278
5129
  }]
4279
5130
  };
4280
5131
  }
@@ -4289,14 +5140,16 @@ async function loginUser(args) {
4289
5140
  }, false);
4290
5141
 
4291
5142
  if (!result.success) {
5143
+ const error = {
5144
+ status: 'error',
5145
+ message: result.error || 'Login failed',
5146
+ details: 'Invalid email or password'
5147
+ };
5148
+ trackOperation('mlgym_auth_login', operationId, 'failed', error.message, { args: { email }, response: result });
4292
5149
  return {
4293
5150
  content: [{
4294
5151
  type: 'text',
4295
- text: JSON.stringify({
4296
- status: 'error',
4297
- message: result.error || 'Login failed',
4298
- details: 'Invalid email or password'
4299
- }, null, 2)
5152
+ text: JSON.stringify(error, null, 2)
4300
5153
  }]
4301
5154
  };
4302
5155
  }
@@ -4326,6 +5179,9 @@ async function loginUser(args) {
4326
5179
  ]
4327
5180
  };
4328
5181
 
5182
+ // Track successful operation
5183
+ trackOperation('mlgym_auth_login', operationId, 'success', `User ${email} logged in successfully`, { args: { email }, response });
5184
+
4329
5185
  return {
4330
5186
  content: [{
4331
5187
  type: 'text',
@@ -4333,14 +5189,16 @@ async function loginUser(args) {
4333
5189
  }]
4334
5190
  };
4335
5191
  } catch (error) {
5192
+ const errorDetails = {
5193
+ status: 'error',
5194
+ message: 'Login failed',
5195
+ error: error.message
5196
+ };
5197
+ trackOperation('mlgym_auth_login', operationId, 'failed', error.message, { args: { email }, error: errorDetails });
4336
5198
  return {
4337
5199
  content: [{
4338
5200
  type: 'text',
4339
- text: JSON.stringify({
4340
- status: 'error',
4341
- message: 'Login failed',
4342
- error: error.message
4343
- }, null, 2)
5201
+ text: JSON.stringify(errorDetails, null, 2)
4344
5202
  }]
4345
5203
  };
4346
5204
  }
@@ -5249,7 +6107,12 @@ async function replicateProject(args) {
5249
6107
  async function main() {
5250
6108
  const transport = new StdioServerTransport();
5251
6109
  await server.connect(transport);
5252
- console.error('GitLab Backend MCP Server v2.0.0 started');
6110
+ console.error(`GitLab Backend MCP Server v${CURRENT_VERSION} started`);
6111
+
6112
+ // Check for updates in the background (don't block startup)
6113
+ setTimeout(async () => {
6114
+ await checkForUpdates();
6115
+ }, 2000); // Check after 2 seconds
5253
6116
  }
5254
6117
 
5255
6118
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlgym-deploy",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server for GitLab Backend - User creation and project deployment",
5
5
  "main": "index.js",
6
6
  "type": "module",