mlgym-deploy 2.0.1 → 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 +714 -63
  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;
@@ -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
  }
@@ -1443,7 +2037,13 @@ async function initProject(args) {
1443
2037
  }
1444
2038
 
1445
2039
  // Add git commands for local setup
2040
+ // First ensure git is configured
1446
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:',
1447
2047
  `cd ${local_path}`,
1448
2048
  'git init',
1449
2049
  `git remote add origin ${project.ssh_url_to_repo}`,
@@ -1452,6 +2052,11 @@ async function initProject(args) {
1452
2052
  'git push -u origin main'
1453
2053
  ];
1454
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
+
1455
2060
  return {
1456
2061
  content: [{
1457
2062
  type: 'text',
@@ -1474,6 +2079,9 @@ function generateWebhookSecret() {
1474
2079
  async function recoverUser(args) {
1475
2080
  const { email, pin, new_password } = args;
1476
2081
 
2082
+ // Start tracking this operation
2083
+ const operationId = `recover-user-${Date.now()}`;
2084
+
1477
2085
  // Clean up any expired PINs first
1478
2086
  await cleanupExpiredPINs();
1479
2087
 
@@ -1492,6 +2100,9 @@ async function recoverUser(args) {
1492
2100
 
1493
2101
  console.error(`Recovery PIN generated for ${email}: ${generatedPIN}`);
1494
2102
 
2103
+ // Track PIN generation
2104
+ trackOperation('mlgym_user_recover', operationId, 'success', `Recovery PIN generated for ${email}`, { args: { email }, stage: 'pin_generation' });
2105
+
1495
2106
  // In production, this would send an actual email
1496
2107
  // For now, we'll include it in the response for testing
1497
2108
  return {
@@ -1513,13 +2124,15 @@ async function recoverUser(args) {
1513
2124
  const stored = await loadPIN(email);
1514
2125
 
1515
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' });
1516
2132
  return {
1517
2133
  content: [{
1518
2134
  type: 'text',
1519
- text: JSON.stringify({
1520
- status: 'error',
1521
- message: 'No recovery request found. Please request a new PIN.'
1522
- }, null, 2)
2135
+ text: JSON.stringify(error, null, 2)
1523
2136
  }]
1524
2137
  };
1525
2138
  }
@@ -1527,13 +2140,15 @@ async function recoverUser(args) {
1527
2140
  // Check if PIN expired (10 minutes)
1528
2141
  if (Date.now() - stored.timestamp > 10 * 60 * 1000) {
1529
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' });
1530
2148
  return {
1531
2149
  content: [{
1532
2150
  type: 'text',
1533
- text: JSON.stringify({
1534
- status: 'error',
1535
- message: 'PIN has expired. Please request a new one.'
1536
- }, null, 2)
2151
+ text: JSON.stringify(error, null, 2)
1537
2152
  }]
1538
2153
  };
1539
2154
  }
@@ -1541,13 +2156,15 @@ async function recoverUser(args) {
1541
2156
  // Check attempts
1542
2157
  if (stored.attempts >= 3) {
1543
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' });
1544
2164
  return {
1545
2165
  content: [{
1546
2166
  type: 'text',
1547
- text: JSON.stringify({
1548
- status: 'error',
1549
- message: 'Too many failed attempts. Please request a new PIN.'
1550
- }, null, 2)
2167
+ text: JSON.stringify(error, null, 2)
1551
2168
  }]
1552
2169
  };
1553
2170
  }
@@ -4107,14 +4724,19 @@ async function getDeploymentStatus(args) {
4107
4724
  async function triggerDeployment(args) {
4108
4725
  const { project_id, environment = 'production' } = args;
4109
4726
 
4727
+ // Start tracking this operation
4728
+ const operationId = `trigger-deployment-${Date.now()}`;
4729
+
4110
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 });
4111
4736
  return {
4112
4737
  content: [{
4113
4738
  type: 'text',
4114
- text: JSON.stringify({
4115
- status: 'error',
4116
- message: 'project_id is required'
4117
- }, null, 2)
4739
+ text: JSON.stringify(error, null, 2)
4118
4740
  }]
4119
4741
  };
4120
4742
  }
@@ -4124,33 +4746,40 @@ async function triggerDeployment(args) {
4124
4746
  // Check authentication
4125
4747
  const auth = await loadAuth();
4126
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 });
4127
4754
  return {
4128
4755
  content: [{
4129
4756
  type: 'text',
4130
- text: JSON.stringify({
4131
- status: 'error',
4132
- message: 'Not authenticated. Please login first with mlgym_auth_login'
4133
- }, null, 2)
4757
+ text: JSON.stringify(error, null, 2)
4134
4758
  }]
4135
4759
  };
4136
4760
  }
4137
4761
 
4138
4762
  try {
4139
4763
  // Trigger deployment via backend API
4140
- const result = await apiRequest('POST', '/api/v1/deployments/trigger', {
4141
- project_id,
4142
- environment
4143
- });
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);
4144
4771
 
4145
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 });
4146
4779
  return {
4147
4780
  content: [{
4148
4781
  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)
4782
+ text: JSON.stringify(error, null, 2)
4154
4783
  }]
4155
4784
  };
4156
4785
  }
@@ -4171,6 +4800,9 @@ async function triggerDeployment(args) {
4171
4800
  ]
4172
4801
  };
4173
4802
 
4803
+ // Track successful operation
4804
+ trackOperation('mlgym_deployments_trigger', operationId, 'success', `Deployment triggered for project ${project_id}`, { args, response });
4805
+
4174
4806
  return {
4175
4807
  content: [{
4176
4808
  type: 'text',
@@ -4178,14 +4810,16 @@ async function triggerDeployment(args) {
4178
4810
  }]
4179
4811
  };
4180
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 });
4181
4819
  return {
4182
4820
  content: [{
4183
4821
  type: 'text',
4184
- text: JSON.stringify({
4185
- status: 'error',
4186
- message: 'Failed to trigger deployment',
4187
- error: error.message
4188
- }, null, 2)
4822
+ text: JSON.stringify(errorDetails, null, 2)
4189
4823
  }]
4190
4824
  };
4191
4825
  }
@@ -4267,14 +4901,19 @@ async function listProjects(args) {
4267
4901
  async function loginUser(args) {
4268
4902
  const { email, password } = args;
4269
4903
 
4904
+ // Start tracking this operation
4905
+ const operationId = `login-user-${Date.now()}`;
4906
+
4270
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
4271
4913
  return {
4272
4914
  content: [{
4273
4915
  type: 'text',
4274
- text: JSON.stringify({
4275
- status: 'error',
4276
- message: 'Email and password are required'
4277
- }, null, 2)
4916
+ text: JSON.stringify(error, null, 2)
4278
4917
  }]
4279
4918
  };
4280
4919
  }
@@ -4289,14 +4928,16 @@ async function loginUser(args) {
4289
4928
  }, false);
4290
4929
 
4291
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 });
4292
4937
  return {
4293
4938
  content: [{
4294
4939
  type: 'text',
4295
- text: JSON.stringify({
4296
- status: 'error',
4297
- message: result.error || 'Login failed',
4298
- details: 'Invalid email or password'
4299
- }, null, 2)
4940
+ text: JSON.stringify(error, null, 2)
4300
4941
  }]
4301
4942
  };
4302
4943
  }
@@ -4326,6 +4967,9 @@ async function loginUser(args) {
4326
4967
  ]
4327
4968
  };
4328
4969
 
4970
+ // Track successful operation
4971
+ trackOperation('mlgym_auth_login', operationId, 'success', `User ${email} logged in successfully`, { args: { email }, response });
4972
+
4329
4973
  return {
4330
4974
  content: [{
4331
4975
  type: 'text',
@@ -4333,14 +4977,16 @@ async function loginUser(args) {
4333
4977
  }]
4334
4978
  };
4335
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 });
4336
4986
  return {
4337
4987
  content: [{
4338
4988
  type: 'text',
4339
- text: JSON.stringify({
4340
- status: 'error',
4341
- message: 'Login failed',
4342
- error: error.message
4343
- }, null, 2)
4989
+ text: JSON.stringify(errorDetails, null, 2)
4344
4990
  }]
4345
4991
  };
4346
4992
  }
@@ -5249,7 +5895,12 @@ async function replicateProject(args) {
5249
5895
  async function main() {
5250
5896
  const transport = new StdioServerTransport();
5251
5897
  await server.connect(transport);
5252
- 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
5253
5904
  }
5254
5905
 
5255
5906
  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.1.0",
4
4
  "description": "MCP server for GitLab Backend - User creation and project deployment",
5
5
  "main": "index.js",
6
6
  "type": "module",