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.
- package/index.js +396 -3
- 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
|
|
20
|
-
const CURRENT_VERSION = '2.
|
|
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
|
|
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
|
}
|