mlgym-deploy 2.5.0 → 2.6.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 +396 -3
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -16,8 +16,8 @@ import crypto from 'crypto';
16
16
 
17
17
  const execAsync = promisify(exec);
18
18
 
19
- // Current version of this MCP server - INCREMENT FOR WORKFLOW COMPLIANCE
20
- const CURRENT_VERSION = '2.5.0';
19
+ // Current version of this MCP server - INCREMENT FOR PROJECT ANALYSIS
20
+ const CURRENT_VERSION = '2.6.0';
21
21
  const PACKAGE_NAME = 'mlgym-deploy';
22
22
 
23
23
  // Version check state
@@ -353,6 +353,140 @@ async function authenticate(args) {
353
353
  };
354
354
  }
355
355
 
356
+ // Analyze project to detect type, framework, and configuration
357
+ async function analyzeProject(local_path = '.') {
358
+ const absolutePath = path.resolve(local_path);
359
+ const dirName = path.basename(absolutePath);
360
+
361
+ const analysis = {
362
+ project_type: 'unknown',
363
+ detected_files: [],
364
+ suggested_name: dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
365
+ has_dockerfile: false,
366
+ has_git: false,
367
+ framework: null,
368
+ build_command: null,
369
+ start_command: null,
370
+ package_manager: null
371
+ };
372
+
373
+ try {
374
+ // Check for git
375
+ try {
376
+ await execAsync('git status', { cwd: absolutePath });
377
+ analysis.has_git = true;
378
+ } catch {}
379
+
380
+ // Check for Dockerfile
381
+ try {
382
+ await fs.access(path.join(absolutePath, 'Dockerfile'));
383
+ analysis.has_dockerfile = true;
384
+ analysis.detected_files.push('Dockerfile');
385
+ } catch {}
386
+
387
+ // Check for Node.js project
388
+ try {
389
+ const packageJsonPath = path.join(absolutePath, 'package.json');
390
+ await fs.access(packageJsonPath);
391
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
392
+
393
+ analysis.project_type = 'nodejs';
394
+ analysis.detected_files.push('package.json');
395
+ analysis.suggested_name = packageJson.name || analysis.suggested_name;
396
+
397
+ // Detect package manager
398
+ try {
399
+ await fs.access(path.join(absolutePath, 'package-lock.json'));
400
+ analysis.package_manager = 'npm';
401
+ analysis.detected_files.push('package-lock.json');
402
+ } catch {
403
+ try {
404
+ await fs.access(path.join(absolutePath, 'yarn.lock'));
405
+ analysis.package_manager = 'yarn';
406
+ analysis.detected_files.push('yarn.lock');
407
+ } catch {
408
+ analysis.package_manager = 'npm'; // default
409
+ }
410
+ }
411
+
412
+ // Detect framework
413
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
414
+ if (deps.next) {
415
+ analysis.framework = 'nextjs';
416
+ analysis.build_command = packageJson.scripts?.build || 'npm run build';
417
+ analysis.start_command = packageJson.scripts?.start || 'npm start';
418
+ } else if (deps.express) {
419
+ analysis.framework = 'express';
420
+ analysis.start_command = packageJson.scripts?.start || 'node index.js';
421
+ } else if (deps.react) {
422
+ analysis.framework = 'react';
423
+ analysis.build_command = packageJson.scripts?.build || 'npm run build';
424
+ } else if (deps.vue) {
425
+ analysis.framework = 'vue';
426
+ analysis.build_command = packageJson.scripts?.build || 'npm run build';
427
+ }
428
+
429
+ // Use package.json scripts as fallback
430
+ if (!analysis.build_command && packageJson.scripts?.build) {
431
+ analysis.build_command = 'npm run build';
432
+ }
433
+ if (!analysis.start_command && packageJson.scripts?.start) {
434
+ analysis.start_command = 'npm start';
435
+ }
436
+ } catch {}
437
+
438
+ // Check for Python project
439
+ if (analysis.project_type === 'unknown') {
440
+ try {
441
+ await fs.access(path.join(absolutePath, 'requirements.txt'));
442
+ analysis.project_type = 'python';
443
+ analysis.detected_files.push('requirements.txt');
444
+
445
+ // Check for specific Python files
446
+ try {
447
+ await fs.access(path.join(absolutePath, 'app.py'));
448
+ analysis.framework = 'flask';
449
+ analysis.start_command = 'python app.py';
450
+ analysis.detected_files.push('app.py');
451
+ } catch {
452
+ try {
453
+ await fs.access(path.join(absolutePath, 'main.py'));
454
+ analysis.framework = 'fastapi';
455
+ analysis.start_command = 'uvicorn main:app --host 0.0.0.0';
456
+ analysis.detected_files.push('main.py');
457
+ } catch {}
458
+ }
459
+ } catch {}
460
+ }
461
+
462
+ // Check for static HTML project
463
+ if (analysis.project_type === 'unknown') {
464
+ try {
465
+ await fs.access(path.join(absolutePath, 'index.html'));
466
+ analysis.project_type = 'static';
467
+ analysis.framework = 'html';
468
+ analysis.detected_files.push('index.html');
469
+ } catch {}
470
+ }
471
+
472
+ // Check for Go project
473
+ if (analysis.project_type === 'unknown') {
474
+ try {
475
+ await fs.access(path.join(absolutePath, 'go.mod'));
476
+ analysis.project_type = 'go';
477
+ analysis.detected_files.push('go.mod');
478
+ analysis.build_command = 'go build -o app';
479
+ analysis.start_command = './app';
480
+ } catch {}
481
+ }
482
+
483
+ } catch (error) {
484
+ console.error('Project analysis error:', error);
485
+ }
486
+
487
+ return analysis;
488
+ }
489
+
356
490
  // Check for existing MLGym project in current directory
357
491
  async function checkExistingProject(local_path = '.') {
358
492
  const absolutePath = path.resolve(local_path);
@@ -395,6 +529,203 @@ async function checkExistingProject(local_path = '.') {
395
529
  }
396
530
  }
397
531
 
532
+ // Generate appropriate Dockerfile based on project type
533
+ function generateDockerfile(projectType, framework, packageManager = 'npm') {
534
+ let dockerfile = '';
535
+
536
+ if (projectType === 'nodejs') {
537
+ if (framework === 'nextjs') {
538
+ dockerfile = `# Build stage
539
+ FROM node:18-alpine AS builder
540
+ WORKDIR /app
541
+ COPY package*.json ./
542
+ RUN ${packageManager} ${packageManager === 'npm' ? 'ci' : 'install --frozen-lockfile'}
543
+ COPY . .
544
+ RUN ${packageManager} run build
545
+
546
+ # Production stage
547
+ FROM node:18-alpine
548
+ WORKDIR /app
549
+ COPY --from=builder /app/.next ./.next
550
+ COPY --from=builder /app/node_modules ./node_modules
551
+ COPY --from=builder /app/package.json ./
552
+ COPY --from=builder /app/public ./public
553
+ EXPOSE 3000
554
+ CMD ["${packageManager}", "start"]`;
555
+ } else if (framework === 'express') {
556
+ dockerfile = `FROM node:18-alpine
557
+ WORKDIR /app
558
+ COPY package*.json ./
559
+ RUN ${packageManager} ${packageManager === 'npm' ? 'ci --only=production' : 'install --frozen-lockfile --production'}
560
+ COPY . .
561
+ EXPOSE 3000
562
+ CMD ["node", "index.js"]`;
563
+ } else if (framework === 'react' || framework === 'vue') {
564
+ dockerfile = `# Build stage
565
+ FROM node:18-alpine AS builder
566
+ WORKDIR /app
567
+ COPY package*.json ./
568
+ RUN ${packageManager} ${packageManager === 'npm' ? 'ci' : 'install --frozen-lockfile'}
569
+ COPY . .
570
+ RUN ${packageManager} run build
571
+
572
+ # Production stage
573
+ FROM nginx:alpine
574
+ COPY --from=builder /app/${framework === 'react' ? 'build' : 'dist'} /usr/share/nginx/html
575
+ EXPOSE 80
576
+ CMD ["nginx", "-g", "daemon off;"]`;
577
+ } else {
578
+ // Generic Node.js
579
+ dockerfile = `FROM node:18-alpine
580
+ WORKDIR /app
581
+ COPY package*.json ./
582
+ RUN ${packageManager} ${packageManager === 'npm' ? 'ci --only=production' : 'install --frozen-lockfile --production'}
583
+ COPY . .
584
+ EXPOSE 3000
585
+ CMD ["${packageManager}", "start"]`;
586
+ }
587
+ } else if (projectType === 'python') {
588
+ if (framework === 'flask') {
589
+ dockerfile = `FROM python:3.11-slim
590
+ WORKDIR /app
591
+ COPY requirements.txt .
592
+ RUN pip install --no-cache-dir -r requirements.txt
593
+ COPY . .
594
+ EXPOSE 5000
595
+ CMD ["python", "app.py"]`;
596
+ } else if (framework === 'fastapi') {
597
+ dockerfile = `FROM python:3.11-slim
598
+ WORKDIR /app
599
+ COPY requirements.txt .
600
+ RUN pip install --no-cache-dir -r requirements.txt
601
+ COPY . .
602
+ EXPOSE 8000
603
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]`;
604
+ } else {
605
+ // Generic Python
606
+ dockerfile = `FROM python:3.11-slim
607
+ WORKDIR /app
608
+ COPY requirements.txt .
609
+ RUN pip install --no-cache-dir -r requirements.txt
610
+ COPY . .
611
+ EXPOSE 8000
612
+ CMD ["python", "main.py"]`;
613
+ }
614
+ } else if (projectType === 'static') {
615
+ dockerfile = `FROM nginx:alpine
616
+ COPY . /usr/share/nginx/html
617
+ EXPOSE 80
618
+ CMD ["nginx", "-g", "daemon off;"]`;
619
+ } else if (projectType === 'go') {
620
+ dockerfile = `# Build stage
621
+ FROM golang:1.21-alpine AS builder
622
+ WORKDIR /app
623
+ COPY go.mod go.sum ./
624
+ RUN go mod download
625
+ COPY . .
626
+ RUN go build -o app
627
+
628
+ # Production stage
629
+ FROM alpine:latest
630
+ RUN apk --no-cache add ca-certificates
631
+ WORKDIR /root/
632
+ COPY --from=builder /app/app .
633
+ EXPOSE 8080
634
+ CMD ["./app"]`;
635
+ } else {
636
+ // Unknown type - basic Alpine with shell
637
+ dockerfile = `FROM alpine:latest
638
+ WORKDIR /app
639
+ COPY . .
640
+ RUN echo "Unknown project type - please configure manually"
641
+ CMD ["/bin/sh"]`;
642
+ }
643
+
644
+ return dockerfile;
645
+ }
646
+
647
+ // Prepare project for deployment
648
+ async function prepareProject(args) {
649
+ const { local_path = '.', project_type, framework, package_manager } = args;
650
+ const absolutePath = path.resolve(local_path);
651
+
652
+ const actions = [];
653
+
654
+ try {
655
+ // Check if Dockerfile exists
656
+ const dockerfilePath = path.join(absolutePath, 'Dockerfile');
657
+ let dockerfileExists = false;
658
+
659
+ try {
660
+ await fs.access(dockerfilePath);
661
+ dockerfileExists = true;
662
+ actions.push('Dockerfile already exists - skipping generation');
663
+ } catch {}
664
+
665
+ // Generate Dockerfile if missing
666
+ if (!dockerfileExists && project_type !== 'unknown') {
667
+ const dockerfile = generateDockerfile(project_type, framework, package_manager);
668
+ await fs.writeFile(dockerfilePath, dockerfile);
669
+ actions.push(`Generated Dockerfile for ${project_type}/${framework || 'generic'}`);
670
+ }
671
+
672
+ // Check/create .gitignore
673
+ const gitignorePath = path.join(absolutePath, '.gitignore');
674
+ let gitignoreExists = false;
675
+
676
+ try {
677
+ await fs.access(gitignorePath);
678
+ gitignoreExists = true;
679
+ } catch {}
680
+
681
+ if (!gitignoreExists) {
682
+ let gitignoreContent = '';
683
+
684
+ if (project_type === 'nodejs') {
685
+ gitignoreContent = `node_modules/
686
+ .env
687
+ .env.local
688
+ dist/
689
+ build/
690
+ .next/
691
+ *.log`;
692
+ } else if (project_type === 'python') {
693
+ gitignoreContent = `__pycache__/
694
+ *.py[cod]
695
+ *$py.class
696
+ .env
697
+ venv/
698
+ env/
699
+ .venv/`;
700
+ } else {
701
+ gitignoreContent = `.env
702
+ *.log
703
+ .DS_Store`;
704
+ }
705
+
706
+ await fs.writeFile(gitignorePath, gitignoreContent);
707
+ actions.push('Created .gitignore file');
708
+ }
709
+
710
+ return {
711
+ status: 'success',
712
+ message: 'Project prepared for deployment',
713
+ actions: actions,
714
+ dockerfile_created: !dockerfileExists && project_type !== 'unknown',
715
+ project_type: project_type,
716
+ framework: framework
717
+ };
718
+
719
+ } catch (error) {
720
+ return {
721
+ status: 'error',
722
+ message: 'Failed to prepare project',
723
+ error: error.message,
724
+ actions: actions
725
+ };
726
+ }
727
+ }
728
+
398
729
  // Check authentication status
399
730
  async function checkAuthStatus() {
400
731
  const auth = await loadAuth();
@@ -631,9 +962,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
631
962
  required: ['email', 'password']
632
963
  }
633
964
  },
965
+ {
966
+ name: 'mlgym_project_analyze',
967
+ description: 'PHASE 2: Analyze project to detect type, framework, and configuration. Call BEFORE creating project.',
968
+ inputSchema: {
969
+ type: 'object',
970
+ properties: {
971
+ local_path: {
972
+ type: 'string',
973
+ description: 'Local directory path (defaults to current directory)',
974
+ default: '.'
975
+ }
976
+ }
977
+ }
978
+ },
634
979
  {
635
980
  name: 'mlgym_project_status',
636
- description: 'PHASE 2 START: Check if MLGym project exists in current directory. Call this AFTER authentication succeeds.',
981
+ description: 'PHASE 2: Check if MLGym project exists in current directory.',
637
982
  inputSchema: {
638
983
  type: 'object',
639
984
  properties: {
@@ -682,6 +1027,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
682
1027
  },
683
1028
  required: ['name', 'description']
684
1029
  }
1030
+ },
1031
+ {
1032
+ name: 'mlgym_project_prepare',
1033
+ description: 'PHASE 2: Prepare project for deployment by generating Dockerfile and config files.',
1034
+ inputSchema: {
1035
+ type: 'object',
1036
+ properties: {
1037
+ local_path: {
1038
+ type: 'string',
1039
+ description: 'Local directory path (defaults to current directory)',
1040
+ default: '.'
1041
+ },
1042
+ project_type: {
1043
+ type: 'string',
1044
+ description: 'Project type from analysis',
1045
+ enum: ['nodejs', 'python', 'static', 'go', 'unknown']
1046
+ },
1047
+ framework: {
1048
+ type: 'string',
1049
+ description: 'Framework from analysis',
1050
+ enum: ['nextjs', 'express', 'react', 'vue', 'flask', 'fastapi', 'html', null]
1051
+ },
1052
+ package_manager: {
1053
+ type: 'string',
1054
+ description: 'Package manager for Node.js projects',
1055
+ enum: ['npm', 'yarn'],
1056
+ default: 'npm'
1057
+ }
1058
+ }
1059
+ }
685
1060
  }
686
1061
  ]
687
1062
  };
@@ -701,6 +1076,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
701
1076
  case 'mlgym_authenticate':
702
1077
  return await authenticate(args);
703
1078
 
1079
+ case 'mlgym_project_analyze':
1080
+ const analysis = await analyzeProject(args.local_path);
1081
+ return {
1082
+ content: [{
1083
+ type: 'text',
1084
+ text: JSON.stringify(analysis, null, 2)
1085
+ }]
1086
+ };
1087
+
704
1088
  case 'mlgym_project_status':
705
1089
  const projectStatus = await checkExistingProject(args.local_path);
706
1090
  return {
@@ -713,6 +1097,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
713
1097
  case 'mlgym_project_init':
714
1098
  return await initProject(args);
715
1099
 
1100
+ case 'mlgym_project_prepare':
1101
+ const prepResult = await prepareProject(args);
1102
+ return {
1103
+ content: [{
1104
+ type: 'text',
1105
+ text: JSON.stringify(prepResult, null, 2)
1106
+ }]
1107
+ };
1108
+
716
1109
  default:
717
1110
  throw new Error(`Unknown tool: ${name}`);
718
1111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlgym-deploy",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "MCP server for GitLab Backend - User creation and project deployment",
5
5
  "main": "index.js",
6
6
  "type": "module",