/** * MCPEngine Studio — Deploy as Download * * Creates a zip-ready directory structure with all generated files, * package.json, tsconfig, README, Dockerfile, and .env.example. */ import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import type { ServerBundle, DeployResult, GeneratedFile, } from '@mcpengine/ai-pipeline/types'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface DownloadProgress { step: string; message: string; percent: number; level: 'info' | 'success' | 'warning' | 'error'; } // --------------------------------------------------------------------------- // Template files // --------------------------------------------------------------------------- function generateReadme(bundle: ServerBundle, slug: string): string { const toolFiles = bundle.files.filter((f) => f.path.includes('tools/')); const toolList = toolFiles .map((f) => `- \`${f.path.replace(/^.*tools\//, '').replace(/\.ts$/, '')}\``) .join('\n'); return `# ${slug} — MCP Server Generated by [MCPEngine Studio](https://mcpengine.ai) ## Quick Start \`\`\`bash # Install dependencies npm install # Set up environment variables cp .env.example .env # Edit .env with your API keys # Build npm run build # Start the server npm start \`\`\` ## Tools (${bundle.toolCount}) ${toolList || '_(No tools found)_'} ## Development \`\`\`bash # Run in development mode with auto-reload npm run dev # Run tests npm test # Type check npm run typecheck \`\`\` ## Docker \`\`\`bash # Build the Docker image docker build -t ${slug}-mcp . # Run the container docker run -p 3000:3000 --env-file .env ${slug}-mcp \`\`\` ## Add to Claude Desktop Add this to your Claude Desktop config (\`~/Library/Application Support/Claude/claude_desktop_config.json\`): \`\`\`json { "mcpServers": { "${slug}": { "command": "node", "args": ["${slug}/dist/index.js"], "env": {} } } } \`\`\` ## License MIT `; } function generateDockerfile(slug: string): string { return `# ${slug} MCP Server FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/package*.json ./ RUN npm ci --omit=dev COPY --from=builder /app/dist ./dist EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\ CMD wget -q --spider http://localhost:3000/health || exit 1 CMD ["node", "dist/index.js"] `; } function generateDockerignore(): string { return `node_modules dist .env .git .gitignore *.md Dockerfile .dockerignore .mcpengine-output `; } function generateEnvExample(bundle: ServerBundle): string { const lines = [ '# MCPEngine Server Environment Variables', '# Copy this file to .env and fill in your values', '', '# Server Configuration', 'PORT=3000', 'NODE_ENV=production', '', '# API Authentication', 'API_KEY=your_api_key_here', 'API_BASE_URL=https://api.example.com', '', '# Optional: OAuth2 (if required by the target API)', '# OAUTH_CLIENT_ID=', '# OAUTH_CLIENT_SECRET=', '# OAUTH_TOKEN_URL=', '', ]; return lines.join('\n'); } function generateTsConfig(): string { return JSON.stringify( { compilerOptions: { target: 'ES2022', module: 'NodeNext', moduleResolution: 'NodeNext', lib: ['ES2022'], outDir: './dist', rootDir: './src', strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true, declaration: true, declarationMap: true, sourceMap: true, }, include: ['src/**/*'], exclude: ['node_modules', 'dist'], }, null, 2, ); } function generatePackageJson(bundle: ServerBundle, slug: string): string { const pkg = { name: `@mcpengine/${slug}`, version: '1.0.0', description: `MCP Server generated by MCPEngine Studio`, type: 'module', main: 'dist/index.js', types: 'dist/index.d.ts', scripts: { build: 'tsc', dev: 'tsx watch src/index.ts', start: 'node dist/index.js', typecheck: 'tsc --noEmit', test: 'vitest run', }, dependencies: { '@modelcontextprotocol/sdk': '^1.0.0', zod: '^3.23.0', }, devDependencies: { typescript: '^5.5.0', tsx: '^4.16.0', vitest: '^2.0.0', '@types/node': '^20.14.0', }, engines: { node: '>=20', }, license: 'MIT', // Merge with bundle's package.json if present ...(typeof bundle.packageJson === 'object' ? bundle.packageJson : {}), }; // Ensure name/version stay correct pkg.name = `@mcpengine/${slug}`; return JSON.stringify(pkg, null, 2); } function generateGitignore(): string { return `node_modules/ dist/ .env .env.local *.log .DS_Store .mcpengine-output/ `; } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- export async function* deployAsDownload( bundle: ServerBundle, slug?: string, ): AsyncGenerator { const serverSlug = slug ?? 'mcp-server'; const deployId = crypto.randomUUID(); const startedAt = new Date().toISOString(); const logs: string[] = []; const log = (msg: string) => { logs.push(`[${new Date().toISOString()}] ${msg}`); }; // ── Step 1: Create temp directory ────────────────────────────────────── yield { step: 'package', message: 'Creating project structure…', percent: 10, level: 'info', }; const tmpDir = path.join(os.tmpdir(), `mcpengine-download-${deployId}`); const projectDir = path.join(tmpDir, serverSlug); const srcDir = path.join(projectDir, 'src'); await fs.mkdir(srcDir, { recursive: true }); log(`Created project directory: ${projectDir}`); // ── Step 2: Write source files ───────────────────────────────────────── yield { step: 'package', message: `Writing ${bundle.files.length} source files…`, percent: 30, level: 'info', }; for (const file of bundle.files) { const filePath = path.join(srcDir, file.path); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, file.content, 'utf-8'); log(`Wrote ${file.path}`); } yield { step: 'package', message: `${bundle.files.length} source files written`, percent: 50, level: 'success', }; // ── Step 3: Write config / meta files ────────────────────────────────── yield { step: 'package', message: 'Generating project configuration…', percent: 60, level: 'info', }; const configFiles: { name: string; content: string }[] = [ { name: 'package.json', content: generatePackageJson(bundle, serverSlug) }, { name: 'tsconfig.json', content: generateTsConfig() }, { name: 'README.md', content: generateReadme(bundle, serverSlug) }, { name: 'Dockerfile', content: generateDockerfile(serverSlug) }, { name: '.dockerignore', content: generateDockerignore() }, { name: '.env.example', content: generateEnvExample(bundle) }, { name: '.gitignore', content: generateGitignore() }, ]; for (const cf of configFiles) { await fs.writeFile(path.join(projectDir, cf.name), cf.content, 'utf-8'); log(`Wrote ${cf.name}`); } yield { step: 'package', message: 'Project configuration complete', percent: 80, level: 'success', }; // ── Step 4: Calculate stats ──────────────────────────────────────────── yield { step: 'verify', message: 'Verifying download package…', percent: 90, level: 'info', }; const totalFiles = bundle.files.length + configFiles.length; log(`Download package ready: ${totalFiles} files in ${projectDir}`); yield { step: 'verify', message: `Package ready (${totalFiles} files)`, percent: 100, level: 'success', }; // ── Return result ────────────────────────────────────────────────────── return { id: deployId, target: 'download', status: 'live', url: projectDir, endpoint: projectDir, logs, createdAt: startedAt, }; }