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.
- package/index-v2.js +24 -2
- package/index.js +928 -65
- 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
|
|
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
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
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(
|
|
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) => {
|