mlgym-deploy
Version: 
MCP server for GitLab Backend - User creation and project deployment
1,279 lines (1,156 loc) • 38.1 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { exec } from 'child_process';
import { promisify } from 'util';
import crypto from 'crypto';
const execAsync = promisify(exec);
// Current version of this MCP server - INCREMENT FOR WORKFLOW FIXES
const CURRENT_VERSION = '2.7.1';
const PACKAGE_NAME = 'mlgym-deploy';
// Version check state
let versionCheckResult = null;
let lastVersionCheck = 0;
const VERSION_CHECK_INTERVAL = 3600000; // Check once per hour
// Configuration
const CONFIG = {
  backend_url: process.env.MLGYM_API_ENDPOINT || 'https://backend.eu.ezb.net',
  gitlab_url: 'https://git.mlgym.io',
  coolify_url: 'https://coolify.eu.ezb.net',
  config_file: path.join(os.homedir(), '.mlgym', 'mcp_config.json')
};
// Helper to load/save authentication
async function loadAuth() {
  try {
    const data = await fs.readFile(CONFIG.config_file, 'utf8');
    return JSON.parse(data);
  } catch {
    return { token: null, email: null };
  }
}
async function saveAuth(email, token) {
  const dir = path.dirname(CONFIG.config_file);
  await fs.mkdir(dir, { recursive: true });
  await fs.writeFile(
    CONFIG.config_file,
    JSON.stringify({ email, token, timestamp: new Date().toISOString() }, null, 2)
  );
}
// API client helper
async function apiRequest(method, endpoint, data = null, useAuth = true) {
  const config = {
    method,
    url: `${CONFIG.backend_url}${endpoint}`,
    headers: {
      'Content-Type': 'application/json'
    }
  };
  if (useAuth) {
    const auth = await loadAuth();
    if (auth.token) {
      config.headers['Authorization'] = `Bearer ${auth.token}`;
    }
  }
  if (data) {
    config.data = data;
  }
  try {
    const response = await axios(config);
    return { success: true, data: response.data };
  } catch (error) {
    const errorData = error.response?.data || { error: error.message };
    return { success: false, error: errorData.error || errorData.message || 'Request failed' };
  }
}
// Helper to generate random password
function generateRandomPassword() {
  const chars = 'ABCDEFGHJKLMNPQRSTWXYZabcdefghjkmnpqrstwxyz23456789!@#$%^&*';
  let password = '';
  for (let i = 0; i < 16; i++) {
    password += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return password;
}
// SSH key generation
async function generateSSHKeyPair(email) {
  const sshDir = path.join(os.homedir(), '.ssh');
  await fs.mkdir(sshDir, { recursive: true, mode: 0o700 });
  const sanitizedEmail = email.replace('@', '_at_').replace(/[^a-zA-Z0-9_-]/g, '_');
  const keyPath = path.join(sshDir, `mlgym_${sanitizedEmail}`);
  try {
    await fs.access(keyPath);
    console.error(`SSH key already exists at ${keyPath}, using existing key`);
    const publicKey = await fs.readFile(`${keyPath}.pub`, 'utf8');
    return { publicKey: publicKey.trim(), privateKeyPath: keyPath };
  } catch {
    // Key doesn't exist, generate new one
  }
  const { stdout, stderr } = await execAsync(
    `ssh-keygen -t ed25519 -f "${keyPath}" -N "" -C "${email}"`,
    { timeout: 10000 }
  );
  if (stderr && !stderr.includes('Generating public/private')) {
    throw new Error(`SSH key generation failed: ${stderr}`);
  }
  await execAsync(`chmod 600 "${keyPath}"`);
  await execAsync(`chmod 644 "${keyPath}.pub"`);
  const publicKey = await fs.readFile(`${keyPath}.pub`, 'utf8');
  // Add to SSH config
  const configPath = path.join(sshDir, 'config');
  const configEntry = `
# MLGym GitLab (added by mlgym-deploy)
Host git.mlgym.io
    User git
    Port 22
    IdentityFile ${keyPath}
    StrictHostKeyChecking no
`;
  try {
    const existingConfig = await fs.readFile(configPath, 'utf8');
    if (!existingConfig.includes('Host git.mlgym.io')) {
      await fs.appendFile(configPath, configEntry);
    }
  } catch {
    await fs.writeFile(configPath, configEntry, { mode: 0o600 });
  }
  return { publicKey: publicKey.trim(), privateKeyPath: keyPath };
}
// SECURE CONSOLIDATED AUTHENTICATION FUNCTION
async function authenticate(args) {
  const { email, password, create_if_not_exists = false, full_name, accept_terms } = args;
  // Validate required fields
  if (!email || !password) {
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'error',
          message: 'Email and password are required',
          required_fields: {
            email: email ? '✓ provided' : '✗ missing',
            password: password ? '✓ provided' : '✗ missing'
          }
        }, null, 2)
      }]
    };
  }
  console.error(`Attempting authentication for: ${email}`);
  // Step 1: Always try login first (never reveal if account exists)
  try {
    const loginResult = await apiRequest('POST', '/api/v1/auth/login', {
      email,
      password
    }, false);
    if (loginResult.success && loginResult.data.token) {
      // Login successful - save token and setup SSH if needed
      await saveAuth(email, loginResult.data.token);
      // Check for SSH keys
      const keysResult = await apiRequest('GET', '/api/v1/keys', null, true);
      const sshKeys = keysResult.success ? keysResult.data : [];
      // If no SSH keys, generate and add one
      if (sshKeys.length === 0) {
        console.error('No SSH keys found, generating new key pair...');
        const { publicKey, privateKeyPath } = await generateSSHKeyPair(email);
        const keyTitle = `mlgym-${new Date().toISOString().split('T')[0]}`;
        const keyResult = await apiRequest('POST', '/api/v1/keys', {
          title: keyTitle,
          key: publicKey
        }, true);
        if (keyResult.success) {
          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                status: 'authenticated',
                message: 'Successfully logged in and SSH key configured',
                email: email,
                user_id: loginResult.data.user?.user_id,
                gitlab_username: loginResult.data.user?.gitlab_username,
                ssh_key_added: true,
                ssh_key_path: privateKeyPath,
                next_steps: [
                  'Authentication successful',
                  `SSH key generated at: ${privateKeyPath}`,
                  'You can now create projects and deploy'
                ]
              }, null, 2)
            }]
          };
        }
      }
      // User has SSH keys already
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            status: 'authenticated',
            message: 'Successfully logged in',
            email: email,
            user_id: loginResult.data.user?.user_id,
            gitlab_username: loginResult.data.user?.gitlab_username,
            ssh_keys_count: sshKeys.length,
            next_steps: [
              'Authentication successful',
              'You can now create projects and deploy'
            ]
          }, null, 2)
        }]
      };
    }
  } catch (error) {
    // Login failed - continue to next step
    console.error('Login attempt failed:', error.message);
  }
  // Step 2: Login failed - check if we should create account
  if (create_if_not_exists) {
    // Validate required fields for account creation
    if (!full_name) {
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            status: 'error',
            message: 'Account creation requires full_name',
            note: 'Login failed. To create a new account, provide full_name and accept_terms=true'
          }, null, 2)
        }]
      };
    }
    if (!accept_terms) {
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            status: 'error',
            message: 'You must accept the terms and conditions to create an account',
            note: 'Set accept_terms=true to proceed with account creation'
          }, null, 2)
        }]
      };
    }
    console.error(`Creating new account for: ${email}`);
    // Try to create account
    try {
      const createResult = await apiRequest('POST', '/api/v1/users', {
        email,
        name: full_name,
        password
      }, false);
      if (createResult.success) {
        // Account created, now login
        const loginResult = await apiRequest('POST', '/api/v1/auth/login', {
          email,
          password
        }, false);
        if (loginResult.success && loginResult.data.token) {
          await saveAuth(email, loginResult.data.token);
          // Generate and add SSH key for new user
          console.error('Generating SSH key for new user...');
          const { publicKey, privateKeyPath } = await generateSSHKeyPair(email);
          const keyTitle = `mlgym-${new Date().toISOString().split('T')[0]}`;
          await apiRequest('POST', '/api/v1/keys', {
            title: keyTitle,
            key: publicKey
          }, true);
          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                status: 'authenticated',
                message: 'Account created successfully and logged in',
                email: email,
                user_id: loginResult.data.user?.user_id,
                gitlab_username: loginResult.data.user?.gitlab_username,
                ssh_key_path: privateKeyPath,
                next_steps: [
                  'New account created',
                  `SSH key generated at: ${privateKeyPath}`,
                  'You can now create projects and deploy'
                ]
              }, null, 2)
            }]
          };
        }
      }
    } catch (error) {
      // Account creation failed - generic error
      console.error('Account creation failed:', error.message);
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            status: 'error',
            message: 'Account creation failed. The email may already be in use or password requirements not met.',
            requirements: {
              password: 'Minimum 8 characters with mixed case, numbers, and special characters',
              email: 'Valid email address not already registered'
            }
          }, null, 2)
        }]
      };
    }
  }
  // Step 3: Generic authentication failure (never reveal if account exists)
  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        status: 'error',
        message: 'Authentication failed. Invalid credentials.',
        hint: 'If you need to create a new account, set create_if_not_exists=true and provide full_name and accept_terms=true'
      }, null, 2)
    }]
  };
}
// Analyze project to detect type, framework, and configuration
async function analyzeProject(local_path = '.') {
  const absolutePath = path.resolve(local_path);
  const dirName = path.basename(absolutePath);
  const analysis = {
    project_type: 'unknown',
    detected_files: [],
    suggested_name: dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
    has_dockerfile: false,
    has_git: false,
    framework: null,
    build_command: null,
    start_command: null,
    package_manager: null
  };
  try {
    // Check for git
    try {
      await execAsync('git status', { cwd: absolutePath });
      analysis.has_git = true;
    } catch {}
    // Check for Dockerfile
    try {
      await fs.access(path.join(absolutePath, 'Dockerfile'));
      analysis.has_dockerfile = true;
      analysis.detected_files.push('Dockerfile');
    } catch {}
    // Check for Node.js project
    try {
      const packageJsonPath = path.join(absolutePath, 'package.json');
      await fs.access(packageJsonPath);
      const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
      analysis.project_type = 'nodejs';
      analysis.detected_files.push('package.json');
      analysis.suggested_name = packageJson.name || analysis.suggested_name;
      // Detect package manager
      try {
        await fs.access(path.join(absolutePath, 'package-lock.json'));
        analysis.package_manager = 'npm';
        analysis.detected_files.push('package-lock.json');
      } catch {
        try {
          await fs.access(path.join(absolutePath, 'yarn.lock'));
          analysis.package_manager = 'yarn';
          analysis.detected_files.push('yarn.lock');
        } catch {
          analysis.package_manager = 'npm'; // default
        }
      }
      // Detect framework
      const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
      if (deps.next) {
        analysis.framework = 'nextjs';
        analysis.build_command = packageJson.scripts?.build || 'npm run build';
        analysis.start_command = packageJson.scripts?.start || 'npm start';
      } else if (deps.express) {
        analysis.framework = 'express';
        analysis.start_command = packageJson.scripts?.start || 'node index.js';
      } else if (deps.react) {
        analysis.framework = 'react';
        analysis.build_command = packageJson.scripts?.build || 'npm run build';
      } else if (deps.vue) {
        analysis.framework = 'vue';
        analysis.build_command = packageJson.scripts?.build || 'npm run build';
      }
      // Use package.json scripts as fallback
      if (!analysis.build_command && packageJson.scripts?.build) {
        analysis.build_command = 'npm run build';
      }
      if (!analysis.start_command && packageJson.scripts?.start) {
        analysis.start_command = 'npm start';
      }
    } catch {}
    // Check for Python project
    if (analysis.project_type === 'unknown') {
      try {
        await fs.access(path.join(absolutePath, 'requirements.txt'));
        analysis.project_type = 'python';
        analysis.detected_files.push('requirements.txt');
        // Check for specific Python files
        try {
          await fs.access(path.join(absolutePath, 'app.py'));
          analysis.framework = 'flask';
          analysis.start_command = 'python app.py';
          analysis.detected_files.push('app.py');
        } catch {
          try {
            await fs.access(path.join(absolutePath, 'main.py'));
            analysis.framework = 'fastapi';
            analysis.start_command = 'uvicorn main:app --host 0.0.0.0';
            analysis.detected_files.push('main.py');
          } catch {}
        }
      } catch {}
    }
    // Check for static HTML project
    if (analysis.project_type === 'unknown') {
      try {
        await fs.access(path.join(absolutePath, 'index.html'));
        analysis.project_type = 'static';
        analysis.framework = 'html';
        analysis.detected_files.push('index.html');
      } catch {}
    }
    // Check for Go project
    if (analysis.project_type === 'unknown') {
      try {
        await fs.access(path.join(absolutePath, 'go.mod'));
        analysis.project_type = 'go';
        analysis.detected_files.push('go.mod');
        analysis.build_command = 'go build -o app';
        analysis.start_command = './app';
      } catch {}
    }
  } catch (error) {
    console.error('Project analysis error:', error);
  }
  return analysis;
}
// Check for existing MLGym project in current directory
async function checkExistingProject(local_path = '.') {
  const absolutePath = path.resolve(local_path);
  // Check for git repository
  try {
    const { stdout: remotes } = await execAsync('git remote -v', { cwd: absolutePath });
    // Check for mlgym remote
    if (remotes.includes('mlgym') && remotes.includes('git.mlgym.io')) {
      // Extract project info from remote URL
      const match = remotes.match(/mlgym\s+git@git\.mlgym\.io:([^\/]+)\/([^\.]+)\.git/);
      if (match) {
        const [, namespace, projectName] = match;
        return {
          exists: true,
          name: projectName,
          namespace: namespace,
          configured: true,
          message: `Project '${projectName}' already configured for MLGym deployment`
        };
      }
    }
    // Git repo exists but no mlgym remote
    return {
      exists: false,
      has_git: true,
      configured: false,
      message: 'Git repository exists but no MLGym remote configured'
    };
  } catch {
    // No git repository
    return {
      exists: false,
      has_git: false,
      configured: false,
      message: 'No git repository found in current directory'
    };
  }
}
// Generate appropriate Dockerfile based on project type
function generateDockerfile(projectType, framework, packageManager = 'npm') {
  let dockerfile = '';
  if (projectType === 'nodejs') {
    if (framework === 'nextjs') {
      dockerfile = `# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN ${packageManager} ${packageManager === 'npm' ? 'ci' : 'install --frozen-lockfile'}
COPY . .
RUN ${packageManager} run build
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["${packageManager}", "start"]`;
    } else if (framework === 'express') {
      dockerfile = `FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN ${packageManager} ${packageManager === 'npm' ? 'ci --only=production' : 'install --frozen-lockfile --production'}
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]`;
    } else if (framework === 'react' || framework === 'vue') {
      dockerfile = `# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN ${packageManager} ${packageManager === 'npm' ? 'ci' : 'install --frozen-lockfile'}
COPY . .
RUN ${packageManager} run build
# Production stage
FROM nginx:alpine
COPY --from=builder /app/${framework === 'react' ? 'build' : 'dist'} /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]`;
    } else {
      // Generic Node.js
      dockerfile = `FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN ${packageManager} ${packageManager === 'npm' ? 'ci --only=production' : 'install --frozen-lockfile --production'}
COPY . .
EXPOSE 3000
CMD ["${packageManager}", "start"]`;
    }
  } else if (projectType === 'python') {
    if (framework === 'flask') {
      dockerfile = `FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]`;
    } else if (framework === 'fastapi') {
      dockerfile = `FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]`;
    } else {
      // Generic Python
      dockerfile = `FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "main.py"]`;
    }
  } else if (projectType === 'static') {
    dockerfile = `FROM nginx:alpine
COPY . /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]`;
  } else if (projectType === 'go') {
    dockerfile = `# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o app
# Production stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/app .
EXPOSE 8080
CMD ["./app"]`;
  } else {
    // Unknown type - basic Alpine with shell
    dockerfile = `FROM alpine:latest
WORKDIR /app
COPY . .
RUN echo "Unknown project type - please configure manually"
CMD ["/bin/sh"]`;
  }
  return dockerfile;
}
// Prepare project for deployment
async function prepareProject(args) {
  const { local_path = '.', project_type, framework, package_manager } = args;
  const absolutePath = path.resolve(local_path);
  const actions = [];
  try {
    // Check if Dockerfile exists
    const dockerfilePath = path.join(absolutePath, 'Dockerfile');
    let dockerfileExists = false;
    try {
      await fs.access(dockerfilePath);
      dockerfileExists = true;
      actions.push('Dockerfile already exists - skipping generation');
    } catch {}
    // Generate Dockerfile if missing
    if (!dockerfileExists && project_type !== 'unknown') {
      const dockerfile = generateDockerfile(project_type, framework, package_manager);
      await fs.writeFile(dockerfilePath, dockerfile);
      actions.push(`Generated Dockerfile for ${project_type}/${framework || 'generic'}`);
    }
    // Check/create .gitignore
    const gitignorePath = path.join(absolutePath, '.gitignore');
    let gitignoreExists = false;
    try {
      await fs.access(gitignorePath);
      gitignoreExists = true;
    } catch {}
    if (!gitignoreExists) {
      let gitignoreContent = '';
      if (project_type === 'nodejs') {
        gitignoreContent = `node_modules/
.env
.env.local
dist/
build/
.next/
*.log`;
      } else if (project_type === 'python') {
        gitignoreContent = `__pycache__/
*.py[cod]
*$py.class
.env
venv/
env/
.venv/`;
      } else {
        gitignoreContent = `.env
*.log
.DS_Store`;
      }
      await fs.writeFile(gitignorePath, gitignoreContent);
      actions.push('Created .gitignore file');
    }
    return {
      status: 'success',
      message: 'Project prepared for deployment',
      actions: actions,
      dockerfile_created: !dockerfileExists && project_type !== 'unknown',
      project_type: project_type,
      framework: framework
    };
  } catch (error) {
    return {
      status: 'error',
      message: 'Failed to prepare project',
      error: error.message,
      actions: actions
    };
  }
}
// Check authentication status
async function checkAuthStatus() {
  const auth = await loadAuth();
  if (!auth.token) {
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'not_authenticated',
          message: 'No authentication found. Please use mlgym_authenticate first.',
          next_step: 'Call mlgym_authenticate with email and password'
        }, null, 2)
      }]
    };
  }
  // Verify token is still valid
  try {
    const result = await apiRequest('GET', '/api/v1/user', null, true);
    if (result.success) {
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            status: 'authenticated',
            email: auth.email,
            message: 'Authentication valid',
            user: result.data,
            next_step: 'You can now use mlgym_project_init to create a project'
          }, null, 2)
        }]
      };
    }
  } catch (error) {
    // Token might be expired
  }
  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        status: 'token_expired',
        message: 'Authentication token expired. Please authenticate again.',
        next_step: 'Call mlgym_authenticate with email and password'
      }, null, 2)
    }]
  };
}
// Smart deployment initialization that follows the correct workflow
async function smartDeploy(args) {
  const { local_path = '.' } = args;
  const absolutePath = path.resolve(local_path);
  const steps = [];
  try {
    // Step 1: Check authentication
    steps.push({ step: 'auth_check', status: 'running' });
    const auth = await loadAuth();
    if (!auth.token) {
      steps[steps.length - 1].status = 'failed';
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            status: 'error',
            message: 'Not authenticated. Please use mlgym_authenticate first',
            workflow_steps: steps
          }, null, 2)
        }]
      };
    }
    steps[steps.length - 1].status = 'completed';
    // Step 2: Analyze project
    steps.push({ step: 'project_analysis', status: 'running' });
    const analysis = await analyzeProject(local_path);
    steps[steps.length - 1].status = 'completed';
    steps[steps.length - 1].result = {
      type: analysis.project_type,
      framework: analysis.framework,
      suggested_name: analysis.suggested_name
    };
    // Step 3: Check existing project
    steps.push({ step: 'check_existing', status: 'running' });
    const projectStatus = await checkExistingProject(local_path);
    steps[steps.length - 1].status = 'completed';
    if (projectStatus.configured) {
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            status: 'info',
            message: projectStatus.message,
            project_name: projectStatus.name,
            git_remote: `git@git.mlgym.io:${projectStatus.namespace}/${projectStatus.name}.git`,
            next_steps: [
              'Project already configured',
              'Run: git push mlgym main'
            ],
            workflow_steps: steps
          }, null, 2)
        }]
      };
    }
    // Step 4: Prepare project (generate Dockerfile if needed)
    steps.push({ step: 'prepare_project', status: 'running' });
    if (!analysis.has_dockerfile && analysis.project_type !== 'unknown') {
      const prepResult = await prepareProject({
        local_path,
        project_type: analysis.project_type,
        framework: analysis.framework,
        package_manager: analysis.package_manager
      });
      steps[steps.length - 1].status = 'completed';
      steps[steps.length - 1].result = prepResult.actions;
    } else {
      steps[steps.length - 1].status = 'skipped';
      steps[steps.length - 1].result = 'Dockerfile already exists or project type unknown';
    }
    // Return analysis and next steps
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'ready',
          message: 'Project analyzed and prepared. Ready for MLGym initialization.',
          analysis: {
            project_type: analysis.project_type,
            framework: analysis.framework,
            suggested_name: analysis.suggested_name,
            has_dockerfile: analysis.has_dockerfile || true
          },
          next_step: 'Use mlgym_project_init with project details to create MLGym project',
          suggested_params: {
            name: analysis.suggested_name,
            description: `${analysis.framework || analysis.project_type} application`,
            enable_deployment: true,
            hostname: analysis.suggested_name
          },
          workflow_steps: steps
        }, null, 2)
      }]
    };
  } catch (error) {
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'error',
          message: 'Smart deploy failed',
          error: error.message,
          workflow_steps: steps
        }, null, 2)
      }]
    };
  }
}
// Initialize Project (requires authentication)
async function initProject(args) {
  let { name, description, enable_deployment = true, hostname, local_path = '.' } = args;
  // Validate required fields
  if (!name || !description) {
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'error',
          message: 'Project name and description are required',
          required_fields: {
            name: name ? '✓ provided' : '✗ missing',
            description: description ? '✓ provided' : '✗ missing'
          }
        }, null, 2)
      }]
    };
  }
  if (enable_deployment && !hostname) {
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'error',
          message: 'Hostname is required when deployment is enabled',
          required_fields: {
            hostname: '✗ missing - will be used as subdomain (e.g., "myapp" for myapp.ezb.net)'
          }
        }, null, 2)
      }]
    };
  }
  // Check authentication
  const auth = await loadAuth();
  if (!auth.token) {
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'error',
          message: 'Not authenticated. Please use mlgym_authenticate first'
        }, null, 2)
      }]
    };
  }
  console.error(`Creating project: ${name}`);
  // Create project via backend API
  const projectData = {
    name: name,
    description: description,
    enable_deployment: enable_deployment
  };
  if (enable_deployment) {
    // Generate a secure webhook secret for deployments
    const webhookSecret = Array.from(
      crypto.getRandomValues(new Uint8Array(32)),
      byte => byte.toString(16).padStart(2, '0')
    ).join('');
    projectData.webhook_secret = webhookSecret;
    if (hostname) {
      projectData.hostname = hostname;
    }
  }
  const result = await apiRequest('POST', '/api/v1/projects', projectData, true);
  if (!result.success) {
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'error',
          message: 'Failed to create project',
          error: result.error
        }, null, 2)
      }]
    };
  }
  const project = result.data;
  // Initialize local git repository if needed
  const absolutePath = path.resolve(local_path);
  try {
    await execAsync('git status', { cwd: absolutePath });
    console.error('Directory is already a git repository');
  } catch {
    console.error('Initializing git repository...');
    await execAsync('git init', { cwd: absolutePath });
    await execAsync('git branch -M main', { cwd: absolutePath });
  }
  // Add GitLab remote
  const gitUrl = project.ssh_url || `git@git.mlgym.io:${project.namespace}/${project.name}.git`;
  try {
    await execAsync('git remote remove mlgym', { cwd: absolutePath });
  } catch {}
  await execAsync(`git remote add mlgym ${gitUrl}`, { cwd: absolutePath });
  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        status: 'success',
        message: 'Project created successfully',
        project: {
          id: project.id,
          name: project.name,
          description: project.description,
          git_url: gitUrl,
          deployment_enabled: enable_deployment,
          deployment_url: hostname ? `https://${hostname}.ezb.net` : null
        },
        next_steps: [
          'Add your code files',
          'git add .',
          `git commit -m "Initial commit"`,
          'git push mlgym main',
          enable_deployment ? 'Your app will be automatically deployed after push' : null
        ].filter(Boolean)
      }, null, 2)
    }]
  };
}
// Create the MCP server
const server = new Server(
  {
    name: 'mlgym-deploy',
    version: CURRENT_VERSION,
  },
  {
    capabilities: {
      tools: {},
    },
  }
);
// Handle tool listing
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'mlgym_auth_status',
        description: 'ALWAYS CALL THIS FIRST! Check authentication status before any other operation.',
        inputSchema: {
          type: 'object',
          properties: {}
        }
      },
      {
        name: 'mlgym_authenticate',
        description: 'PHASE 1: Authentication ONLY. Get email, password, and existing account status in ONE interaction. Never ask for project details here!',
        inputSchema: {
          type: 'object',
          properties: {
            email: {
              type: 'string',
              description: 'Email address',
              pattern: '^[^@]+@[^@]+\\.[^@]+$'
            },
            password: {
              type: 'string',
              description: 'Password (min 8 characters)',
              minLength: 8
            },
            create_if_not_exists: {
              type: 'boolean',
              description: 'If true and login fails, attempt to create new account',
              default: false
            },
            full_name: {
              type: 'string',
              description: 'Full name (required only for new account creation)',
              minLength: 2
            },
            accept_terms: {
              type: 'boolean',
              description: 'Accept terms and conditions (required only for new account creation)',
              default: false
            }
          },
          required: ['email', 'password']
        }
      },
      {
        name: 'mlgym_project_analyze',
        description: 'PHASE 2: Analyze project to detect type, framework, and configuration. Call BEFORE creating project.',
        inputSchema: {
          type: 'object',
          properties: {
            local_path: {
              type: 'string',
              description: 'Local directory path (defaults to current directory)',
              default: '.'
            }
          }
        }
      },
      {
        name: 'mlgym_project_status',
        description: 'PHASE 2: Check if MLGym project exists in current directory.',
        inputSchema: {
          type: 'object',
          properties: {
            local_path: {
              type: 'string',
              description: 'Local directory path (defaults to current directory)',
              default: '.'
            }
          }
        }
      },
      {
        name: 'mlgym_project_init',
        description: 'PHASE 2: Create project ONLY after checking project status. Never ask for email/password here - only project details!',
        inputSchema: {
          type: 'object',
          properties: {
            name: {
              type: 'string',
              description: 'Project name (lowercase alphanumeric with hyphens)',
              pattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$',
              minLength: 3
            },
            description: {
              type: 'string',
              description: 'Project description',
              minLength: 10
            },
            enable_deployment: {
              type: 'boolean',
              description: 'Enable automatic deployment via Coolify',
              default: true
            },
            hostname: {
              type: 'string',
              description: 'Hostname for deployment (required if deployment enabled, will be subdomain)',
              pattern: '^[a-z][a-z0-9-]*[a-z0-9]$',
              minLength: 3,
              maxLength: 63
            },
            local_path: {
              type: 'string',
              description: 'Local directory path (defaults to current directory)',
              default: '.'
            }
          },
          required: ['name', 'description']
        }
      },
      {
        name: 'mlgym_project_prepare',
        description: 'PHASE 2: Prepare project for deployment by generating Dockerfile and config files.',
        inputSchema: {
          type: 'object',
          properties: {
            local_path: {
              type: 'string',
              description: 'Local directory path (defaults to current directory)',
              default: '.'
            },
            project_type: {
              type: 'string',
              description: 'Project type from analysis',
              enum: ['nodejs', 'python', 'static', 'go', 'unknown']
            },
            framework: {
              type: 'string',
              description: 'Framework from analysis',
              enum: ['nextjs', 'express', 'react', 'vue', 'flask', 'fastapi', 'html', null]
            },
            package_manager: {
              type: 'string',
              description: 'Package manager for Node.js projects',
              enum: ['npm', 'yarn'],
              default: 'npm'
            }
          }
        }
      },
      {
        name: 'mlgym_smart_deploy',
        description: 'RECOMMENDED: Smart deployment workflow that automatically analyzes, prepares, and guides you through the entire deployment process. Use this for new projects!',
        inputSchema: {
          type: 'object',
          properties: {
            local_path: {
              type: 'string',
              description: 'Local directory path (defaults to current directory)',
              default: '.'
            }
          }
        }
      }
    ]
  };
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  console.error(`Tool called: ${name}`);
  try {
    switch (name) {
      case 'mlgym_auth_status':
        return await checkAuthStatus();
      case 'mlgym_authenticate':
        return await authenticate(args);
      case 'mlgym_project_analyze':
        const analysis = await analyzeProject(args.local_path);
        return {
          content: [{
            type: 'text',
            text: JSON.stringify(analysis, null, 2)
          }]
        };
      case 'mlgym_project_status':
        const projectStatus = await checkExistingProject(args.local_path);
        return {
          content: [{
            type: 'text',
            text: JSON.stringify(projectStatus, null, 2)
          }]
        };
      case 'mlgym_project_init':
        return await initProject(args);
      case 'mlgym_project_prepare':
        const prepResult = await prepareProject(args);
        return {
          content: [{
            type: 'text',
            text: JSON.stringify(prepResult, null, 2)
          }]
        };
      case 'mlgym_smart_deploy':
        return await smartDeploy(args);
      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    console.error(`Tool execution failed:`, error);
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          status: 'error',
          message: `Tool execution failed: ${error.message}`
        }, null, 2)
      }]
    };
  }
});
// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error(`MLGym MCP Server v${CURRENT_VERSION} started`);
}
main().catch((error) => {
  console.error('Server error:', error);
  process.exit(1);
});