mlgym-deploy 2.3.5 → 2.4.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 DELETED
@@ -1,1062 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema
8
- } from '@modelcontextprotocol/sdk/types.js';
9
- import axios from 'axios';
10
- import fs from 'fs/promises';
11
- import path from 'path';
12
- import os from 'os';
13
- import crypto from 'crypto';
14
-
15
- // Configuration
16
- const CONFIG = {
17
- backend_url: process.env.GITLAB_BACKEND_URL || 'https://backend.eu.ezb.net',
18
- gitlab_url: process.env.GITLAB_URL || 'https://git.mlgym.io',
19
- coolify_url: process.env.COOLIFY_URL || 'https://coolify.eu.ezb.net',
20
- config_file: path.join(os.homedir(), '.mlgym', 'config.json'),
21
- mlgym_ini: path.join(os.homedir(), '.mlgym', 'mlgym.ini'),
22
- terms_url: 'https://mlgym.io/terms',
23
- privacy_url: 'https://mlgym.io/privacy',
24
- docs_url: 'https://docs.mlgym.io'
25
- };
26
-
27
- // Available regions for deployment
28
- const REGIONS = {
29
- 'eu-central': {
30
- name: 'Europe (Frankfurt)',
31
- coolify_url: 'https://coolify.eu.ezb.net',
32
- latency: 'Low for EU users'
33
- },
34
- 'us-east': {
35
- name: 'US East (Virginia)',
36
- coolify_url: 'https://coolify.us-east.mlgym.io',
37
- latency: 'Low for US East Coast'
38
- },
39
- 'asia-pacific': {
40
- name: 'Asia Pacific (Singapore)',
41
- coolify_url: 'https://coolify.ap.mlgym.io',
42
- latency: 'Low for APAC users'
43
- }
44
- };
45
-
46
- // Session storage for multi-step flows
47
- const sessions = new Map();
48
-
49
- // Helper to check if file exists
50
- async function fileExists(filePath) {
51
- try {
52
- await fs.access(filePath);
53
- return true;
54
- } catch {
55
- return false;
56
- }
57
- }
58
-
59
- // Helper to check if directory exists
60
- async function dirExists(dirPath) {
61
- try {
62
- const stats = await fs.stat(dirPath);
63
- return stats.isDirectory();
64
- } catch {
65
- return false;
66
- }
67
- }
68
-
69
- // Helper to load user configuration
70
- async function loadUserConfig() {
71
- try {
72
- // First check for mlgym.ini (legacy format)
73
- if (await fileExists(CONFIG.mlgym_ini)) {
74
- const iniContent = await fs.readFile(CONFIG.mlgym_ini, 'utf8');
75
- // Parse simple INI format
76
- const config = {};
77
- iniContent.split('\n').forEach(line => {
78
- const [key, value] = line.split('=');
79
- if (key && value) {
80
- config[key.trim()] = value.trim();
81
- }
82
- });
83
- return config;
84
- }
85
-
86
- // Check for JSON config
87
- if (await fileExists(CONFIG.config_file)) {
88
- const data = await fs.readFile(CONFIG.config_file, 'utf8');
89
- return JSON.parse(data);
90
- }
91
-
92
- return null;
93
- } catch {
94
- return null;
95
- }
96
- }
97
-
98
- // Helper to save user configuration
99
- async function saveUserConfig(config) {
100
- const dir = path.dirname(CONFIG.config_file);
101
- await fs.mkdir(dir, { recursive: true });
102
-
103
- // Save as JSON
104
- await fs.writeFile(
105
- CONFIG.config_file,
106
- JSON.stringify(config, null, 2)
107
- );
108
-
109
- // Also save as INI for backwards compatibility
110
- const iniContent = Object.entries(config)
111
- .map(([key, value]) => `${key}=${value}`)
112
- .join('\n');
113
- await fs.writeFile(CONFIG.mlgym_ini, iniContent);
114
- }
115
-
116
- // Detect framework from project files
117
- async function detectFramework(projectPath) {
118
- const checks = [
119
- {
120
- file: 'package.json',
121
- detect: async (content) => {
122
- const pkg = JSON.parse(content);
123
- if (pkg.dependencies?.next || pkg.devDependencies?.next) return 'nextjs';
124
- if (pkg.dependencies?.react || pkg.devDependencies?.react) return 'react';
125
- if (pkg.dependencies?.vue || pkg.devDependencies?.vue) return 'vue';
126
- if (pkg.dependencies?.express) return 'express';
127
- if (pkg.dependencies?.fastify) return 'fastify';
128
- return 'node';
129
- }
130
- },
131
- {
132
- file: 'composer.json',
133
- detect: async (content) => {
134
- const composer = JSON.parse(content);
135
- if (composer.require?.['laravel/framework']) return 'laravel';
136
- if (composer.require?.['symfony/framework-bundle']) return 'symfony';
137
- return 'php';
138
- }
139
- },
140
- {
141
- file: 'requirements.txt',
142
- detect: async (content) => {
143
- if (content.includes('django')) return 'django';
144
- if (content.includes('flask')) return 'flask';
145
- if (content.includes('fastapi')) return 'fastapi';
146
- return 'python';
147
- }
148
- },
149
- {
150
- file: 'Cargo.toml',
151
- detect: () => 'rust'
152
- },
153
- {
154
- file: 'go.mod',
155
- detect: () => 'go'
156
- }
157
- ];
158
-
159
- for (const check of checks) {
160
- const filePath = path.join(projectPath, check.file);
161
- if (await fileExists(filePath)) {
162
- if (check.detect) {
163
- const content = await fs.readFile(filePath, 'utf8');
164
- return await check.detect(content);
165
- }
166
- }
167
- }
168
-
169
- return 'static'; // Default for static HTML sites
170
- }
171
-
172
- // Generate Dockerfile based on framework
173
- function generateDockerfile(framework, projectName) {
174
- const dockerfiles = {
175
- nextjs: `# Next.js Production Dockerfile
176
- FROM node:20-alpine AS builder
177
- WORKDIR /app
178
- COPY package*.json ./
179
- RUN npm ci
180
- COPY . .
181
- RUN npm run build
182
-
183
- FROM node:20-alpine
184
- WORKDIR /app
185
- ENV NODE_ENV=production
186
- COPY --from=builder /app/public ./public
187
- COPY --from=builder /app/.next ./.next
188
- COPY --from=builder /app/node_modules ./node_modules
189
- COPY --from=builder /app/package.json ./package.json
190
- EXPOSE 3000
191
- ENV PORT=3000
192
- CMD ["npm", "start"]`,
193
-
194
- react: `# React Production Dockerfile
195
- FROM node:20-alpine AS builder
196
- WORKDIR /app
197
- COPY package*.json ./
198
- RUN npm ci
199
- COPY . .
200
- RUN npm run build
201
-
202
- FROM nginx:alpine
203
- COPY --from=builder /app/build /usr/share/nginx/html
204
- COPY nginx.conf /etc/nginx/conf.d/default.conf
205
- EXPOSE 80
206
- CMD ["nginx", "-g", "daemon off;"]`,
207
-
208
- express: `# Express.js Dockerfile
209
- FROM node:20-alpine
210
- WORKDIR /app
211
- COPY package*.json ./
212
- RUN npm ci
213
- COPY . .
214
- EXPOSE 3000
215
- ENV PORT=3000
216
- CMD ["node", "index.js"]`,
217
-
218
- django: `# Django Dockerfile
219
- FROM python:3.11-slim
220
- WORKDIR /app
221
- ENV PYTHONUNBUFFERED=1
222
- COPY requirements.txt .
223
- RUN pip install --no-cache-dir -r requirements.txt
224
- COPY . .
225
- RUN python manage.py collectstatic --noinput
226
- EXPOSE 8000
227
- CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]`,
228
-
229
- php: `# PHP Dockerfile
230
- FROM php:8.2-apache
231
- RUN docker-php-ext-install pdo pdo_mysql
232
- COPY . /var/www/html/
233
- RUN a2enmod rewrite
234
- EXPOSE 80`,
235
-
236
- static: `# Static Site Dockerfile
237
- FROM nginx:alpine
238
- COPY . /usr/share/nginx/html
239
- EXPOSE 80
240
- CMD ["nginx", "-g", "daemon off;"]`,
241
-
242
- go: `# Go Dockerfile
243
- FROM golang:1.21-alpine AS builder
244
- WORKDIR /app
245
- COPY go.* ./
246
- RUN go mod download
247
- COPY . .
248
- RUN go build -o main .
249
-
250
- FROM alpine:latest
251
- RUN apk --no-cache add ca-certificates
252
- WORKDIR /root/
253
- COPY --from=builder /app/main .
254
- EXPOSE 8080
255
- CMD ["./main"]`,
256
-
257
- rust: `# Rust Dockerfile
258
- FROM rust:1.75 AS builder
259
- WORKDIR /app
260
- COPY Cargo.* ./
261
- COPY src ./src
262
- RUN cargo build --release
263
-
264
- FROM debian:bookworm-slim
265
- WORKDIR /app
266
- COPY --from=builder /app/target/release/${projectName} .
267
- EXPOSE 8080
268
- CMD ["./${projectName}"]`
269
- };
270
-
271
- return dockerfiles[framework] || dockerfiles.static;
272
- }
273
-
274
- // Generate nginx.conf for React apps
275
- function generateNginxConf() {
276
- return `server {
277
- listen 80;
278
- location / {
279
- root /usr/share/nginx/html;
280
- index index.html index.htm;
281
- try_files $uri $uri/ /index.html;
282
- }
283
- }`;
284
- }
285
-
286
- // Detect project name from various sources
287
- async function detectProjectName(localPath) {
288
- // Try package.json first
289
- try {
290
- const packagePath = path.join(localPath, 'package.json');
291
- const packageData = await fs.readFile(packagePath, 'utf8');
292
- const packageJson = JSON.parse(packageData);
293
- if (packageJson.name) {
294
- return packageJson.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '');
295
- }
296
- } catch {}
297
-
298
- // Try composer.json
299
- try {
300
- const composerPath = path.join(localPath, 'composer.json');
301
- const composerData = await fs.readFile(composerPath, 'utf8');
302
- const composerJson = JSON.parse(composerData);
303
- if (composerJson.name) {
304
- const parts = composerJson.name.split('/');
305
- return parts[parts.length - 1].toLowerCase().replace(/[^a-z0-9-]/g, '-');
306
- }
307
- } catch {}
308
-
309
- // Fall back to directory name
310
- const absolutePath = path.resolve(localPath);
311
- return path.basename(absolutePath).toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '');
312
- }
313
-
314
- // API client helper
315
- async function apiRequest(method, endpoint, data = null, useAuth = true) {
316
- const config = {
317
- method,
318
- url: `${CONFIG.backend_url}${endpoint}`,
319
- headers: {
320
- 'Content-Type': 'application/json'
321
- }
322
- };
323
-
324
- if (useAuth) {
325
- const userConfig = await loadUserConfig();
326
- // Handle both 'token' and 'auth_token' field names
327
- const authToken = userConfig?.token || userConfig?.auth_token;
328
- if (authToken) {
329
- config.headers['Authorization'] = `Bearer ${authToken}`;
330
- }
331
- }
332
-
333
- if (data) {
334
- config.data = data;
335
- }
336
-
337
- try {
338
- const response = await axios(config);
339
- return { success: true, data: response.data };
340
- } catch (error) {
341
- return {
342
- success: false,
343
- error: error.response?.data?.message || error.message,
344
- status: error.response?.status
345
- };
346
- }
347
- }
348
-
349
- // Create MCP server
350
- const server = new Server(
351
- {
352
- name: 'gitlab-backend-mcp',
353
- version: '3.0.0',
354
- },
355
- {
356
- capabilities: {
357
- tools: {}
358
- }
359
- }
360
- );
361
-
362
- // Tool definitions
363
- const TOOLS = [
364
- {
365
- name: 'mlgym_get_details',
366
- description: 'Get current MLGym user details, authentication status, and configuration. Use this tool when user asks about their MLGym account, login status, or configuration details.',
367
- inputSchema: {
368
- type: 'object',
369
- properties: {
370
- verbose: {
371
- type: 'boolean',
372
- description: 'Include detailed information about projects and resources',
373
- default: false
374
- }
375
- }
376
- }
377
- },
378
- {
379
- name: 'mlgym_deploy',
380
- description: 'Deploy this project to the cloud. Use this tool when user wants to deploy, publish, or host their project online. Handles all deployment tasks including Docker setup, GitLab integration, and cloud hosting on MLGym platform.',
381
- inputSchema: {
382
- type: 'object',
383
- properties: {
384
- action: {
385
- type: 'string',
386
- enum: ['analyze', 'register', 'configure', 'deploy'],
387
- description: 'Action to perform: analyze (scan project), register (create account), configure (set up project), deploy (final deployment)'
388
- },
389
- session_id: {
390
- type: 'string',
391
- description: 'Session ID for multi-step flow (provided after initial analysis)'
392
- },
393
- local_path: {
394
- type: 'string',
395
- description: 'Local directory path of the project',
396
- default: '.'
397
- },
398
- // For registration step
399
- user_email: {
400
- type: 'string',
401
- description: 'Email for new account registration'
402
- },
403
- user_name: {
404
- type: 'string',
405
- description: 'Full name for new account registration'
406
- },
407
- accept_terms: {
408
- type: 'boolean',
409
- description: 'Accept MLGym terms and conditions'
410
- },
411
- // For configuration step
412
- confirm_dockerfile: {
413
- type: 'boolean',
414
- description: 'Confirm generated Dockerfile'
415
- },
416
- custom_dockerfile: {
417
- type: 'string',
418
- description: 'Custom Dockerfile content if not using generated one'
419
- },
420
- // For deployment step
421
- region: {
422
- type: 'string',
423
- enum: ['eu-central', 'us-east', 'asia-pacific'],
424
- description: 'Deployment region'
425
- },
426
- confirm_deployment: {
427
- type: 'boolean',
428
- description: 'Final confirmation to deploy'
429
- }
430
- },
431
- required: ['action']
432
- }
433
- }
434
- ];
435
-
436
- // Handle list tools request
437
- server.setRequestHandler(ListToolsRequestSchema, async () => {
438
- return {
439
- tools: TOOLS
440
- };
441
- });
442
-
443
- // Handle tool execution
444
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
445
- const { name, arguments: args } = request.params;
446
-
447
- switch (name) {
448
- case 'mlgym_get_details':
449
- return await getUserDetails(args);
450
- case 'mlgym_deploy':
451
- return await smartDeploy(args);
452
- default:
453
- throw new Error(`Unknown tool: ${name}`);
454
- }
455
- });
456
-
457
- // Get user details function
458
- async function getUserDetails(args = {}) {
459
- const { verbose = false } = args;
460
-
461
- try {
462
- // Check for MLGym configuration
463
- const config = await loadUserConfig();
464
-
465
- // Handle both 'token' and 'auth_token' field names
466
- const authToken = config?.auth_token || config?.token;
467
-
468
- if (!config || !authToken) {
469
- return {
470
- content: [{
471
- type: 'text',
472
- text: `❌ No MLGym account found
473
-
474
- You are not currently logged into MLGym. To get started:
475
-
476
- 1. Deploy a project using the deploy button
477
- 2. Or check your ~/.mlgym/config.json file
478
-
479
- MLGym provides:
480
- - Free GitLab repository hosting
481
- - Automated CI/CD pipelines
482
- - Cloud deployment with custom domains
483
- - SSL certificates
484
- - Container orchestration
485
-
486
- Visit https://mlgym.io to learn more.`
487
- }]
488
- };
489
- }
490
-
491
- // Get user information from backend
492
- try {
493
- const response = await axios.get(`${CONFIG.backend_url}/api/v1/user`, {
494
- headers: {
495
- 'Authorization': `Bearer ${authToken}`
496
- }
497
- });
498
-
499
- const user = response.data;
500
-
501
- let details = `✅ MLGym Account Details
502
-
503
- 👤 User Information:
504
- - Email: ${user.email}
505
- - Name: ${user.name}
506
- - User ID: ${user.id}
507
- - GitLab ID: ${user.gitlab_id || 'Not set'}
508
- - Admin: ${user.is_admin ? 'Yes' : 'No'}
509
- - Created: ${new Date(user.created_at).toLocaleDateString()}`;
510
-
511
- if (verbose) {
512
- // Get projects
513
- try {
514
- const projectsResponse = await axios.get(`${CONFIG.backend_url}/api/v1/projects`, {
515
- headers: {
516
- 'Authorization': `Bearer ${authToken}`
517
- }
518
- });
519
-
520
- // API returns array directly, not wrapped in an object
521
- const projects = Array.isArray(projectsResponse.data) ? projectsResponse.data :
522
- (projectsResponse.data.projects || []);
523
-
524
- if (projects.length > 0) {
525
- details += `\n\n📦 Projects (${projects.length}):`;
526
- projects.forEach(project => {
527
- details += `\n- ${project.name}`;
528
- if (project.deployment_url) {
529
- details += ` → ${project.deployment_url}`;
530
- }
531
- });
532
- } else {
533
- details += `\n\n📦 No projects yet`;
534
- }
535
- } catch (err) {
536
- // Projects endpoint might not be available
537
- console.error('Could not fetch projects:', err.message);
538
- }
539
- }
540
-
541
- details += `\n\n🔗 Configuration:
542
- - Config file: ${CONFIG.config_file}
543
- - Backend: ${CONFIG.backend_url}
544
- - GitLab: ${CONFIG.gitlab_url}`;
545
-
546
- return {
547
- content: [{
548
- type: 'text',
549
- text: details
550
- }]
551
- };
552
-
553
- } catch (apiError) {
554
- if (apiError.response?.status === 401) {
555
- return {
556
- content: [{
557
- type: 'text',
558
- text: `⚠️ Authentication expired
559
-
560
- Your MLGym session has expired. Please:
561
- 1. Deploy a new project to refresh your authentication
562
- 2. Or manually update your token in ~/.mlgym/config.json
563
-
564
- Error: ${apiError.message}`
565
- }]
566
- };
567
- }
568
-
569
- return {
570
- content: [{
571
- type: 'text',
572
- text: `❌ Error fetching user details
573
-
574
- Could not connect to MLGym backend.
575
- - Backend URL: ${CONFIG.backend_url}
576
- - Error: ${apiError.message}
577
-
578
- Your config file exists at: ${CONFIG.config_file}`
579
- }]
580
- };
581
- }
582
-
583
- } catch (error) {
584
- return {
585
- content: [{
586
- type: 'text',
587
- text: `❌ Error reading MLGym configuration
588
-
589
- ${error.message}
590
-
591
- Please check:
592
- 1. ~/.mlgym/config.json exists
593
- 2. You have proper permissions
594
- 3. The file contains valid JSON`
595
- }]
596
- };
597
- }
598
- }
599
-
600
- // Smart deployment flow
601
- async function smartDeploy(args) {
602
- const { action, session_id, local_path = '.' } = args;
603
-
604
- switch (action) {
605
- case 'analyze':
606
- return await analyzeProject(local_path);
607
-
608
- case 'register':
609
- return await registerUser(args);
610
-
611
- case 'configure':
612
- return await configureProject(session_id, args);
613
-
614
- case 'deploy':
615
- return await deployProject(session_id, args);
616
-
617
- default:
618
- return {
619
- content: [{
620
- type: 'text',
621
- text: JSON.stringify({
622
- error: 'Invalid action. Use: analyze, register, configure, or deploy'
623
- }, null, 2)
624
- }]
625
- };
626
- }
627
- }
628
-
629
- // Step 1: Analyze project
630
- async function analyzeProject(localPath) {
631
- console.error(`Analyzing project at: ${localPath}`);
632
-
633
- const sessionId = crypto.randomBytes(16).toString('hex');
634
-
635
- // Detect project details
636
- const projectName = await detectProjectName(localPath);
637
- const framework = await detectFramework(localPath);
638
- const hasDockerfile = await fileExists(path.join(localPath, 'Dockerfile'));
639
- const hasTests = await dirExists(path.join(localPath, 'tests')) ||
640
- await dirExists(path.join(localPath, '__tests__')) ||
641
- await fileExists(path.join(localPath, 'test'));
642
-
643
- // Check if user is already registered
644
- const userConfig = await loadUserConfig();
645
- // Handle both 'token' and 'auth_token' field names
646
- const hasAccount = !!(userConfig?.token || userConfig?.auth_token);
647
-
648
- // Store session data
649
- const sessionData = {
650
- sessionId,
651
- localPath,
652
- projectName,
653
- framework,
654
- hasDockerfile,
655
- hasTests,
656
- hasAccount,
657
- userEmail: userConfig?.email
658
- };
659
- sessions.set(sessionId, sessionData);
660
-
661
- // Prepare response
662
- const response = {
663
- status: 'analysis_complete',
664
- session_id: sessionId,
665
- project: {
666
- name: projectName,
667
- path: localPath,
668
- framework,
669
- has_dockerfile: hasDockerfile,
670
- has_tests: hasTests
671
- }
672
- };
673
-
674
- if (!hasAccount) {
675
- response.next_step = 'registration_required';
676
- response.mlgym_info = {
677
- description: 'MLGym is a cloud deployment platform that provides:',
678
- features: [
679
- '✅ Automatic CI/CD with GitLab integration',
680
- '✅ Container-based deployments with Coolify',
681
- '✅ Global CDN and SSL certificates',
682
- '✅ Automatic scaling and monitoring',
683
- '✅ One-click deployments from your IDE'
684
- ],
685
- pricing: 'Free tier available with 1 project and 1GB storage',
686
- terms_url: CONFIG.terms_url,
687
- privacy_url: CONFIG.privacy_url,
688
- docs_url: CONFIG.docs_url
689
- };
690
- response.action_required = {
691
- message: 'To continue, you need to create a free MLGym account.',
692
- next_action: 'mlgym_deploy with action: "register"',
693
- required_fields: ['user_email', 'user_name', 'accept_terms']
694
- };
695
- } else {
696
- response.next_step = 'configuration';
697
- response.user = {
698
- email: userConfig.email,
699
- authenticated: true
700
- };
701
-
702
- if (!hasDockerfile) {
703
- const dockerfile = generateDockerfile(framework, projectName);
704
- response.suggested_dockerfile = {
705
- content: dockerfile,
706
- framework_detected: framework,
707
- message: 'No Dockerfile found. Generated one based on detected framework.'
708
- };
709
- }
710
-
711
- response.action_required = {
712
- message: hasDockerfile ?
713
- 'Project has Dockerfile. Ready to configure deployment.' :
714
- 'Review the generated Dockerfile above.',
715
- next_action: 'mlgym_deploy with action: "configure"',
716
- required_fields: hasDockerfile ? [] : ['confirm_dockerfile']
717
- };
718
- }
719
-
720
- return {
721
- content: [{
722
- type: 'text',
723
- text: JSON.stringify(response, null, 2)
724
- }]
725
- };
726
- }
727
-
728
- // Step 2: Register user
729
- async function registerUser(args) {
730
- const { user_email, user_name, accept_terms, session_id } = args;
731
-
732
- if (!accept_terms) {
733
- return {
734
- content: [{
735
- type: 'text',
736
- text: JSON.stringify({
737
- status: 'error',
738
- message: 'You must accept the terms and conditions to continue.',
739
- terms_url: CONFIG.terms_url
740
- }, null, 2)
741
- }]
742
- };
743
- }
744
-
745
- if (!user_email || !user_name) {
746
- return {
747
- content: [{
748
- type: 'text',
749
- text: JSON.stringify({
750
- status: 'error',
751
- message: 'Email and name are required for registration.'
752
- }, null, 2)
753
- }]
754
- };
755
- }
756
-
757
- console.error(`Registering user: ${user_email}`);
758
-
759
- // Generate secure password
760
- const password = crypto.randomBytes(12).toString('base64').replace(/[^a-zA-Z0-9]/g, '') + '!Aa1';
761
-
762
- // Create user via API
763
- const result = await apiRequest('POST', '/api/v1/users', {
764
- email: user_email,
765
- name: user_name,
766
- password
767
- }, false);
768
-
769
- if (!result.success) {
770
- return {
771
- content: [{
772
- type: 'text',
773
- text: JSON.stringify({
774
- status: 'error',
775
- message: `Registration failed: ${result.error}`
776
- }, null, 2)
777
- }]
778
- };
779
- }
780
-
781
- // Save user configuration
782
- const userConfig = {
783
- email: user_email,
784
- name: user_name,
785
- token: result.data.token,
786
- user_id: result.data.id,
787
- gitlab_id: result.data.gitlab_id,
788
- created: new Date().toISOString()
789
- };
790
- await saveUserConfig(userConfig);
791
-
792
- // Update session
793
- if (session_id && sessions.has(session_id)) {
794
- const session = sessions.get(session_id);
795
- session.hasAccount = true;
796
- session.userEmail = user_email;
797
- }
798
-
799
- // Generate Dockerfile if we have session context
800
- let dockerfileSection = {};
801
- if (session_id && sessions.has(session_id)) {
802
- const session = sessions.get(session_id);
803
- if (!session.hasDockerfile) {
804
- const dockerfile = generateDockerfile(session.framework, session.projectName);
805
- dockerfileSection = {
806
- suggested_dockerfile: {
807
- content: dockerfile,
808
- framework_detected: session.framework,
809
- message: 'Generated Dockerfile for your project.'
810
- }
811
- };
812
- }
813
- }
814
-
815
- return {
816
- content: [{
817
- type: 'text',
818
- text: JSON.stringify({
819
- status: 'registration_successful',
820
- message: 'Welcome to MLGym! Your account has been created.',
821
- user: {
822
- email: user_email,
823
- name: user_name,
824
- gitlab_id: result.data.gitlab_id
825
- },
826
- credentials: {
827
- note: 'Save these credentials securely:',
828
- email: user_email,
829
- password,
830
- gitlab_url: CONFIG.gitlab_url
831
- },
832
- ...dockerfileSection,
833
- next_step: 'configuration',
834
- action_required: {
835
- message: 'Account created. Now configure your project for deployment.',
836
- next_action: 'mlgym_deploy with action: "configure"',
837
- session_id,
838
- required_fields: dockerfileSection.suggested_dockerfile ? ['confirm_dockerfile'] : []
839
- }
840
- }, null, 2)
841
- }]
842
- };
843
- }
844
-
845
- // Step 3: Configure project
846
- async function configureProject(sessionId, args) {
847
- if (!sessionId || !sessions.has(sessionId)) {
848
- return {
849
- content: [{
850
- type: 'text',
851
- text: JSON.stringify({
852
- status: 'error',
853
- message: 'Invalid session. Please start with action: "analyze"'
854
- }, null, 2)
855
- }]
856
- };
857
- }
858
-
859
- const session = sessions.get(sessionId);
860
- const { confirm_dockerfile, custom_dockerfile } = args;
861
-
862
- // Create/update Dockerfile if needed
863
- if (!session.hasDockerfile) {
864
- if (!confirm_dockerfile && !custom_dockerfile) {
865
- return {
866
- content: [{
867
- type: 'text',
868
- text: JSON.stringify({
869
- status: 'error',
870
- message: 'Please confirm the generated Dockerfile or provide a custom one.'
871
- }, null, 2)
872
- }]
873
- };
874
- }
875
-
876
- const dockerfilePath = path.join(session.localPath, 'Dockerfile');
877
- const dockerfileContent = custom_dockerfile || generateDockerfile(session.framework, session.projectName);
878
-
879
- await fs.writeFile(dockerfilePath, dockerfileContent);
880
- console.error('Created Dockerfile at:', dockerfilePath);
881
-
882
- // Create nginx.conf for React apps
883
- if (session.framework === 'react' && !custom_dockerfile) {
884
- const nginxPath = path.join(session.localPath, 'nginx.conf');
885
- await fs.writeFile(nginxPath, generateNginxConf());
886
- console.error('Created nginx.conf for React app');
887
- }
888
- }
889
-
890
- // Update session
891
- session.configured = true;
892
- session.dockerfileCreated = !session.hasDockerfile;
893
-
894
- return {
895
- content: [{
896
- type: 'text',
897
- text: JSON.stringify({
898
- status: 'configuration_complete',
899
- message: 'Project configured successfully.',
900
- project: {
901
- name: session.projectName,
902
- framework: session.framework,
903
- dockerfile: session.dockerfileCreated ? 'Created' : 'Existing'
904
- },
905
- deployment_regions: REGIONS,
906
- next_step: 'deployment',
907
- action_required: {
908
- message: 'Choose deployment region and confirm deployment.',
909
- next_action: 'mlgym_deploy with action: "deploy"',
910
- session_id: sessionId,
911
- required_fields: ['region', 'confirm_deployment']
912
- }
913
- }, null, 2)
914
- }]
915
- };
916
- }
917
-
918
- // Step 4: Deploy project
919
- async function deployProject(sessionId, args) {
920
- if (!sessionId || !sessions.has(sessionId)) {
921
- return {
922
- content: [{
923
- type: 'text',
924
- text: JSON.stringify({
925
- status: 'error',
926
- message: 'Invalid session. Please start with action: "analyze"'
927
- }, null, 2)
928
- }]
929
- };
930
- }
931
-
932
- const session = sessions.get(sessionId);
933
- const { region, confirm_deployment } = args;
934
-
935
- if (!confirm_deployment) {
936
- return {
937
- content: [{
938
- type: 'text',
939
- text: JSON.stringify({
940
- status: 'error',
941
- message: 'Deployment cancelled. Set confirm_deployment: true to proceed.'
942
- }, null, 2)
943
- }]
944
- };
945
- }
946
-
947
- if (!region || !REGIONS[region]) {
948
- return {
949
- content: [{
950
- type: 'text',
951
- text: JSON.stringify({
952
- status: 'error',
953
- message: 'Invalid region. Choose from: ' + Object.keys(REGIONS).join(', ')
954
- }, null, 2)
955
- }]
956
- };
957
- }
958
-
959
- console.error(`Deploying project ${session.projectName} to ${region}`);
960
-
961
- // Create project via API
962
- const projectData = {
963
- name: session.projectName,
964
- description: `Deployed via MLGym MCP from ${session.framework} project`,
965
- visibility: 'private',
966
- enable_deployment: true,
967
- webhook_secret: crypto.randomBytes(16).toString('hex')
968
- // Note: deployment_region is not used by backend, it uses random server selection
969
- };
970
-
971
- console.error(`Creating project with deployment enabled for ${session.projectName} in region ${region}`);
972
- const result = await apiRequest('POST', '/api/v1/projects', projectData);
973
-
974
- if (!result.success) {
975
- console.error(`Project creation failed: ${result.error}`);
976
- // Provide more detailed error information
977
- return {
978
- content: [{
979
- type: 'text',
980
- text: JSON.stringify({
981
- status: '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
- }
989
- }, null, 2)
990
- }]
991
- };
992
- }
993
-
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
- }
1005
-
1006
- // Initialize git repository
1007
- const gitCommands = [
1008
- 'git init',
1009
- `git remote add origin ${project.ssh_url_to_repo}`,
1010
- 'git add .',
1011
- 'git commit -m "Initial deployment via MLGym"',
1012
- '# Wait 5-10 seconds for SSH key to propagate in GitLab',
1013
- 'sleep 10',
1014
- 'git push -u origin main'
1015
- ];
1016
-
1017
- // Clear session
1018
- sessions.delete(sessionId);
1019
-
1020
- return {
1021
- content: [{
1022
- type: 'text',
1023
- text: JSON.stringify({
1024
- status: 'deployment_successful',
1025
- message: '🚀 Project deployed successfully!',
1026
- project: {
1027
- name: project.name,
1028
- url: project.web_url,
1029
- ssh_url: project.ssh_url_to_repo,
1030
- region: REGIONS[region].name
1031
- },
1032
- deployment: {
1033
- domain: `${session.projectName}.${region}.mlgym.app`,
1034
- status: 'initializing',
1035
- ssl: 'auto-provisioned',
1036
- cdn: 'enabled'
1037
- },
1038
- important_note: '⚠️ SSH key propagation: Please wait 10 seconds before pushing to allow GitLab to activate your SSH key',
1039
- next_steps: {
1040
- message: 'Run these commands in your project directory:',
1041
- commands: gitCommands
1042
- },
1043
- monitoring: {
1044
- dashboard: `${CONFIG.backend_url}/projects/${project.id}`,
1045
- logs: `${CONFIG.coolify_url}/projects/${project.id}/logs`
1046
- }
1047
- }, null, 2)
1048
- }]
1049
- };
1050
- }
1051
-
1052
- // Start the server
1053
- async function main() {
1054
- const transport = new StdioServerTransport();
1055
- await server.connect(transport);
1056
- console.error('MLGym Smart Deploy MCP Server v3.0.0 started');
1057
- }
1058
-
1059
- main().catch((error) => {
1060
- console.error('Server error:', error);
1061
- process.exit(1);
1062
- });