mlgym-deploy 2.0.0 → 2.1.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 (2) hide show
  1. package/index.js +741 -70
  2. package/package.json +1 -1
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;
@@ -1212,9 +1774,12 @@ async function createUser(args) {
1212
1774
  // First generate SSH key pair
1213
1775
  console.error('Generating SSH key pair...');
1214
1776
  const { publicKey, privateKeyPath } = await generateSSHKeyPair(email);
1777
+ const publicKeyPath = privateKeyPath + '.pub';
1215
1778
  console.error(`SSH key generated: ${privateKeyPath}`);
1216
1779
 
1217
1780
  // Create user via backend API with SSH key
1781
+ trackOperation('user_create', true, null);
1782
+
1218
1783
  const result = await apiRequest('POST', '/api/v1/users', {
1219
1784
  email,
1220
1785
  name,
@@ -1223,6 +1788,7 @@ async function createUser(args) {
1223
1788
  }, false);
1224
1789
 
1225
1790
  if (!result.success) {
1791
+ trackOperation('user_create', false, new Error(result.error || 'User creation failed'));
1226
1792
  return {
1227
1793
  content: [{
1228
1794
  type: 'text',
@@ -1238,6 +1804,28 @@ async function createUser(args) {
1238
1804
 
1239
1805
  // Format successful response with actual fields from backend
1240
1806
  const userData = result.data.user || {};
1807
+
1808
+ // Check SSH key upload status
1809
+ const sshKeyStatus = result.data.ssh_key_status || 'Unknown';
1810
+ const sshKeyUploaded = sshKeyStatus === 'SSH key uploaded successfully';
1811
+
1812
+ // Adjust next steps based on SSH key upload status
1813
+ const nextSteps = [
1814
+ passwordGenerated ? `⚠️ SAVE THIS PASSWORD: ${password}` : 'Using your provided password',
1815
+ `SSH key generated locally at: ${privateKeyPath}`
1816
+ ];
1817
+
1818
+ if (sshKeyUploaded) {
1819
+ nextSteps.push(`✅ SSH key uploaded to GitLab (ID: ${result.data.ssh_key_id || 'unknown'})`);
1820
+ nextSteps.push(`Add to SSH agent: ssh-add "${privateKeyPath}"`);
1821
+ nextSteps.push('You can now push code to GitLab repositories');
1822
+ } else {
1823
+ nextSteps.push(`⚠️ SSH key upload failed: ${sshKeyStatus}`);
1824
+ nextSteps.push('You need to manually add the SSH key to GitLab:');
1825
+ nextSteps.push(`cat ${publicKeyPath} # Copy this key`);
1826
+ nextSteps.push('Then add it at: https://git.mlgym.io/-/user_settings/ssh_keys');
1827
+ }
1828
+
1241
1829
  const response = {
1242
1830
  user_id: userData.user_id || userData.UserID,
1243
1831
  email: userData.email || email,
@@ -1246,14 +1834,11 @@ async function createUser(args) {
1246
1834
  password: passwordGenerated ? password : '[Your provided password]',
1247
1835
  password_generated: passwordGenerated,
1248
1836
  ssh_key_path: privateKeyPath,
1837
+ ssh_key_status: sshKeyStatus,
1838
+ ssh_key_uploaded: sshKeyUploaded,
1249
1839
  token: result.data.token,
1250
- message: result.data.message || 'User created successfully with GitLab, Coolify, and SSH key',
1251
- next_steps: [
1252
- passwordGenerated ? `⚠️ SAVE THIS PASSWORD: ${password}` : 'Using your provided password',
1253
- `SSH key created at: ${privateKeyPath}`,
1254
- `Add this to your SSH agent: ssh-add "${privateKeyPath}"`,
1255
- 'You can now push code to GitLab repositories'
1256
- ]
1840
+ message: result.data.message || 'User created successfully',
1841
+ next_steps: nextSteps
1257
1842
  };
1258
1843
 
1259
1844
  return {
@@ -1333,22 +1918,46 @@ async function detectProjectName(localPath) {
1333
1918
 
1334
1919
  // Tool implementation: Initialize Project
1335
1920
  async function initProject(args) {
1336
- 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()}`;
1337
1925
 
1338
1926
  // Validate required fields are provided
1339
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 });
1340
1938
  return {
1341
1939
  content: [{
1342
1940
  type: 'text',
1343
- text: JSON.stringify({
1344
- status: 'error',
1345
- message: 'Project name and description are required and must be provided by user',
1346
- required_fields: {
1347
- name: name ? '✓ provided' : '✗ missing - must be provided by user',
1348
- description: description ? '✓ provided' : '✗ missing - must be provided by user'
1349
- },
1350
- note: 'Auto-detection is disabled. User must explicitly provide project details.'
1351
- }, 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)
1352
1961
  }]
1353
1962
  };
1354
1963
  }
@@ -1358,10 +1967,12 @@ async function initProject(args) {
1358
1967
  // Check authentication
1359
1968
  const auth = await loadAuth();
1360
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 });
1361
1972
  return {
1362
1973
  content: [{
1363
1974
  type: 'text',
1364
- text: 'Error: Not authenticated. Please create a user first with mlgym_user_create'
1975
+ text: `Error: ${errorMsg}`
1365
1976
  }]
1366
1977
  };
1367
1978
  }
@@ -1375,16 +1986,19 @@ async function initProject(args) {
1375
1986
  description,
1376
1987
  visibility: 'private',
1377
1988
  enable_deployment: enable_deployment,
1378
- webhook_secret: webhookSecret
1989
+ webhook_secret: webhookSecret,
1990
+ hostname: hostname || null
1379
1991
  };
1380
1992
 
1381
1993
  const result = await apiRequest('POST', '/api/v1/projects', projectData);
1382
1994
 
1383
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 });
1384
1998
  return {
1385
1999
  content: [{
1386
2000
  type: 'text',
1387
- text: `Failed to create project: ${result.error}`
2001
+ text: errorMsg
1388
2002
  }]
1389
2003
  };
1390
2004
  }
@@ -1423,7 +2037,13 @@ async function initProject(args) {
1423
2037
  }
1424
2038
 
1425
2039
  // Add git commands for local setup
2040
+ // First ensure git is configured
1426
2041
  response.next_steps = [
2042
+ '# First, ensure git is configured (check with: git config --list | grep user)',
2043
+ '# If not configured, run:',
2044
+ `git config --global user.email "${auth.email}"`,
2045
+ `git config --global user.name "${auth.email.split('@')[0]}"`,
2046
+ '# Then initialize and push the repository:',
1427
2047
  `cd ${local_path}`,
1428
2048
  'git init',
1429
2049
  `git remote add origin ${project.ssh_url_to_repo}`,
@@ -1432,6 +2052,11 @@ async function initProject(args) {
1432
2052
  'git push -u origin main'
1433
2053
  ];
1434
2054
 
2055
+ 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.';
2056
+
2057
+ // Track successful operation
2058
+ trackOperation('mlgym_project_init', operationId, 'success', `Project ${name} created successfully`, { args, response });
2059
+
1435
2060
  return {
1436
2061
  content: [{
1437
2062
  type: 'text',
@@ -1454,6 +2079,9 @@ function generateWebhookSecret() {
1454
2079
  async function recoverUser(args) {
1455
2080
  const { email, pin, new_password } = args;
1456
2081
 
2082
+ // Start tracking this operation
2083
+ const operationId = `recover-user-${Date.now()}`;
2084
+
1457
2085
  // Clean up any expired PINs first
1458
2086
  await cleanupExpiredPINs();
1459
2087
 
@@ -1472,6 +2100,9 @@ async function recoverUser(args) {
1472
2100
 
1473
2101
  console.error(`Recovery PIN generated for ${email}: ${generatedPIN}`);
1474
2102
 
2103
+ // Track PIN generation
2104
+ trackOperation('mlgym_user_recover', operationId, 'success', `Recovery PIN generated for ${email}`, { args: { email }, stage: 'pin_generation' });
2105
+
1475
2106
  // In production, this would send an actual email
1476
2107
  // For now, we'll include it in the response for testing
1477
2108
  return {
@@ -1493,13 +2124,15 @@ async function recoverUser(args) {
1493
2124
  const stored = await loadPIN(email);
1494
2125
 
1495
2126
  if (!stored) {
2127
+ const error = {
2128
+ status: 'error',
2129
+ message: 'No recovery request found. Please request a new PIN.'
2130
+ };
2131
+ trackOperation('mlgym_user_recover', operationId, 'failed', error.message, { args: { email }, stage: 'pin_verification' });
1496
2132
  return {
1497
2133
  content: [{
1498
2134
  type: 'text',
1499
- text: JSON.stringify({
1500
- status: 'error',
1501
- message: 'No recovery request found. Please request a new PIN.'
1502
- }, null, 2)
2135
+ text: JSON.stringify(error, null, 2)
1503
2136
  }]
1504
2137
  };
1505
2138
  }
@@ -1507,13 +2140,15 @@ async function recoverUser(args) {
1507
2140
  // Check if PIN expired (10 minutes)
1508
2141
  if (Date.now() - stored.timestamp > 10 * 60 * 1000) {
1509
2142
  await deletePIN(email);
2143
+ const error = {
2144
+ status: 'error',
2145
+ message: 'PIN has expired. Please request a new one.'
2146
+ };
2147
+ trackOperation('mlgym_user_recover', operationId, 'failed', error.message, { args: { email }, stage: 'pin_verification' });
1510
2148
  return {
1511
2149
  content: [{
1512
2150
  type: 'text',
1513
- text: JSON.stringify({
1514
- status: 'error',
1515
- message: 'PIN has expired. Please request a new one.'
1516
- }, null, 2)
2151
+ text: JSON.stringify(error, null, 2)
1517
2152
  }]
1518
2153
  };
1519
2154
  }
@@ -1521,13 +2156,15 @@ async function recoverUser(args) {
1521
2156
  // Check attempts
1522
2157
  if (stored.attempts >= 3) {
1523
2158
  await deletePIN(email);
2159
+ const error = {
2160
+ status: 'error',
2161
+ message: 'Too many failed attempts. Please request a new PIN.'
2162
+ };
2163
+ trackOperation('mlgym_user_recover', operationId, 'failed', error.message, { args: { email }, stage: 'pin_verification' });
1524
2164
  return {
1525
2165
  content: [{
1526
2166
  type: 'text',
1527
- text: JSON.stringify({
1528
- status: 'error',
1529
- message: 'Too many failed attempts. Please request a new PIN.'
1530
- }, null, 2)
2167
+ text: JSON.stringify(error, null, 2)
1531
2168
  }]
1532
2169
  };
1533
2170
  }
@@ -4087,14 +4724,19 @@ async function getDeploymentStatus(args) {
4087
4724
  async function triggerDeployment(args) {
4088
4725
  const { project_id, environment = 'production' } = args;
4089
4726
 
4727
+ // Start tracking this operation
4728
+ const operationId = `trigger-deployment-${Date.now()}`;
4729
+
4090
4730
  if (!project_id) {
4731
+ const error = {
4732
+ status: 'error',
4733
+ message: 'project_id is required'
4734
+ };
4735
+ trackOperation('mlgym_deployments_trigger', operationId, 'failed', error.message, { args });
4091
4736
  return {
4092
4737
  content: [{
4093
4738
  type: 'text',
4094
- text: JSON.stringify({
4095
- status: 'error',
4096
- message: 'project_id is required'
4097
- }, null, 2)
4739
+ text: JSON.stringify(error, null, 2)
4098
4740
  }]
4099
4741
  };
4100
4742
  }
@@ -4104,33 +4746,40 @@ async function triggerDeployment(args) {
4104
4746
  // Check authentication
4105
4747
  const auth = await loadAuth();
4106
4748
  if (!auth.token) {
4749
+ const error = {
4750
+ status: 'error',
4751
+ message: 'Not authenticated. Please login first with mlgym_auth_login'
4752
+ };
4753
+ trackOperation('mlgym_deployments_trigger', operationId, 'failed', error.message, { args });
4107
4754
  return {
4108
4755
  content: [{
4109
4756
  type: 'text',
4110
- text: JSON.stringify({
4111
- status: 'error',
4112
- message: 'Not authenticated. Please login first with mlgym_auth_login'
4113
- }, null, 2)
4757
+ text: JSON.stringify(error, null, 2)
4114
4758
  }]
4115
4759
  };
4116
4760
  }
4117
4761
 
4118
4762
  try {
4119
4763
  // Trigger deployment via backend API
4120
- const result = await apiRequest('POST', '/api/v1/deployments/trigger', {
4121
- project_id,
4122
- environment
4123
- });
4764
+ // Backend expects project_id (integer) and optional trigger field
4765
+ const deploymentData = {
4766
+ project_id: parseInt(project_id), // Ensure it's an integer
4767
+ trigger: 'initial' // Use 'initial' like the CLI does, not 'environment'
4768
+ };
4769
+
4770
+ const result = await apiRequest('POST', '/api/v1/deployments/trigger', deploymentData);
4124
4771
 
4125
4772
  if (!result.success) {
4773
+ const error = {
4774
+ status: 'error',
4775
+ message: result.error || 'Failed to trigger deployment',
4776
+ details: 'Project may not have deployment enabled or webhook configured'
4777
+ };
4778
+ trackOperation('mlgym_deployments_trigger', operationId, 'failed', error.message, { args, deploymentData, response: result });
4126
4779
  return {
4127
4780
  content: [{
4128
4781
  type: 'text',
4129
- text: JSON.stringify({
4130
- status: 'error',
4131
- message: result.error || 'Failed to trigger deployment',
4132
- details: 'Project may not have deployment enabled or webhook configured'
4133
- }, null, 2)
4782
+ text: JSON.stringify(error, null, 2)
4134
4783
  }]
4135
4784
  };
4136
4785
  }
@@ -4151,6 +4800,9 @@ async function triggerDeployment(args) {
4151
4800
  ]
4152
4801
  };
4153
4802
 
4803
+ // Track successful operation
4804
+ trackOperation('mlgym_deployments_trigger', operationId, 'success', `Deployment triggered for project ${project_id}`, { args, response });
4805
+
4154
4806
  return {
4155
4807
  content: [{
4156
4808
  type: 'text',
@@ -4158,14 +4810,16 @@ async function triggerDeployment(args) {
4158
4810
  }]
4159
4811
  };
4160
4812
  } catch (error) {
4813
+ const errorDetails = {
4814
+ status: 'error',
4815
+ message: 'Failed to trigger deployment',
4816
+ error: error.message
4817
+ };
4818
+ trackOperation('mlgym_deployments_trigger', operationId, 'failed', error.message, { args, error: errorDetails });
4161
4819
  return {
4162
4820
  content: [{
4163
4821
  type: 'text',
4164
- text: JSON.stringify({
4165
- status: 'error',
4166
- message: 'Failed to trigger deployment',
4167
- error: error.message
4168
- }, null, 2)
4822
+ text: JSON.stringify(errorDetails, null, 2)
4169
4823
  }]
4170
4824
  };
4171
4825
  }
@@ -4247,14 +4901,19 @@ async function listProjects(args) {
4247
4901
  async function loginUser(args) {
4248
4902
  const { email, password } = args;
4249
4903
 
4904
+ // Start tracking this operation
4905
+ const operationId = `login-user-${Date.now()}`;
4906
+
4250
4907
  if (!email || !password) {
4908
+ const error = {
4909
+ status: 'error',
4910
+ message: 'Email and password are required'
4911
+ };
4912
+ trackOperation('mlgym_auth_login', operationId, 'failed', error.message, { args: { email } }); // Don't log password
4251
4913
  return {
4252
4914
  content: [{
4253
4915
  type: 'text',
4254
- text: JSON.stringify({
4255
- status: 'error',
4256
- message: 'Email and password are required'
4257
- }, null, 2)
4916
+ text: JSON.stringify(error, null, 2)
4258
4917
  }]
4259
4918
  };
4260
4919
  }
@@ -4269,14 +4928,16 @@ async function loginUser(args) {
4269
4928
  }, false);
4270
4929
 
4271
4930
  if (!result.success) {
4931
+ const error = {
4932
+ status: 'error',
4933
+ message: result.error || 'Login failed',
4934
+ details: 'Invalid email or password'
4935
+ };
4936
+ trackOperation('mlgym_auth_login', operationId, 'failed', error.message, { args: { email }, response: result });
4272
4937
  return {
4273
4938
  content: [{
4274
4939
  type: 'text',
4275
- text: JSON.stringify({
4276
- status: 'error',
4277
- message: result.error || 'Login failed',
4278
- details: 'Invalid email or password'
4279
- }, null, 2)
4940
+ text: JSON.stringify(error, null, 2)
4280
4941
  }]
4281
4942
  };
4282
4943
  }
@@ -4306,6 +4967,9 @@ async function loginUser(args) {
4306
4967
  ]
4307
4968
  };
4308
4969
 
4970
+ // Track successful operation
4971
+ trackOperation('mlgym_auth_login', operationId, 'success', `User ${email} logged in successfully`, { args: { email }, response });
4972
+
4309
4973
  return {
4310
4974
  content: [{
4311
4975
  type: 'text',
@@ -4313,14 +4977,16 @@ async function loginUser(args) {
4313
4977
  }]
4314
4978
  };
4315
4979
  } catch (error) {
4980
+ const errorDetails = {
4981
+ status: 'error',
4982
+ message: 'Login failed',
4983
+ error: error.message
4984
+ };
4985
+ trackOperation('mlgym_auth_login', operationId, 'failed', error.message, { args: { email }, error: errorDetails });
4316
4986
  return {
4317
4987
  content: [{
4318
4988
  type: 'text',
4319
- text: JSON.stringify({
4320
- status: 'error',
4321
- message: 'Login failed',
4322
- error: error.message
4323
- }, null, 2)
4989
+ text: JSON.stringify(errorDetails, null, 2)
4324
4990
  }]
4325
4991
  };
4326
4992
  }
@@ -5229,7 +5895,12 @@ async function replicateProject(args) {
5229
5895
  async function main() {
5230
5896
  const transport = new StdioServerTransport();
5231
5897
  await server.connect(transport);
5232
- console.error('GitLab Backend MCP Server v2.0.0 started');
5898
+ console.error(`GitLab Backend MCP Server v${CURRENT_VERSION} started`);
5899
+
5900
+ // Check for updates in the background (don't block startup)
5901
+ setTimeout(async () => {
5902
+ await checkForUpdates();
5903
+ }, 2000); // Check after 2 seconds
5233
5904
  }
5234
5905
 
5235
5906
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlgym-deploy",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "MCP server for GitLab Backend - User creation and project deployment",
5
5
  "main": "index.js",
6
6
  "type": "module",