// Generator Service — streams MCP server code generation via Claude import Anthropic from '@anthropic-ai/sdk'; import { getSkill } from '../skills/loader'; import type { PipelineEvent, AnalysisResult, ToolDefinition, ServerBundle, GeneratedFile, } from '../types'; const MODEL = 'claude-sonnet-4-5-20250514'; const MAX_TOKENS = 16384; /** * Generate a complete MCP server from analysis results and tool configuration. * Streams file-by-file as they are generated. */ export async function* generateServer( analysis: AnalysisResult, toolConfig: ToolDefinition[] ): AsyncGenerator { const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const systemPrompt = getSkill('builder'); yield { type: 'generate:progress', file: 'Initializing generation', percent: 0 }; const toolSummary = toolConfig.map((t) => ({ name: t.name, description: t.description, endpoint: t.endpoint, method: t.method, inputSchema: t.inputSchema, annotations: t.annotations, })); try { const stream = client.messages.stream({ model: MODEL, max_tokens: MAX_TOKENS, system: systemPrompt, messages: [ { role: 'user', content: `Generate a complete MCP server for the "${analysis.service}" API. ## API Details - Base URL: ${analysis.baseUrl} - Auth: ${JSON.stringify(analysis.authFlow)} - Rate Limits: ${JSON.stringify(analysis.rateLimits)} ## Tools to Implement (${toolConfig.length} total) \`\`\`json ${JSON.stringify(toolSummary, null, 2)} \`\`\` ## Requirements 1. Generate each file separately, wrapped in a code block with the file path as the language tag 2. Use this format for each file: \`\`\`typescript // path: src/index.ts ...code... \`\`\` 3. Include: package.json, tsconfig.json, src/index.ts (entry), src/tools/*.ts, src/auth.ts, src/client.ts 4. Follow MCP SDK best practices from your training 5. Add proper error handling, input validation, and rate limit respect 6. TypeScript strict mode throughout Generate all files now.`, }, ], }); let fullText = ''; const generatedFiles: GeneratedFile[] = []; let lastFileCount = 0; for await (const event of stream) { if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { fullText += event.delta.text; // Check for newly completed file blocks const files = extractFiles(fullText); if (files.length > lastFileCount) { for (let i = lastFileCount; i < files.length; i++) { const file = files[i]; generatedFiles.push(file); yield { type: 'generate:file_ready', path: file.path, content: file.content, }; yield { type: 'generate:progress', file: file.path, percent: Math.min(90, Math.floor((files.length / estimateFileCount(toolConfig.length)) * 90)), }; } lastFileCount = files.length; } } } // Final extraction pass (catch any files missed during streaming) const finalFiles = extractFiles(fullText); for (let i = lastFileCount; i < finalFiles.length; i++) { const file = finalFiles[i]; generatedFiles.push(file); yield { type: 'generate:file_ready', path: file.path, content: file.content, }; } // Get token usage const finalMessage = await stream.finalMessage(); yield { type: 'generate:progress', file: 'Finalizing bundle', percent: 95 }; // Build the server bundle const packageJson = generatedFiles.find((f) => f.path.endsWith('package.json')); const tsConfig = generatedFiles.find((f) => f.path.endsWith('tsconfig.json')); const bundle: ServerBundle = { files: generatedFiles, packageJson: packageJson ? JSON.parse(packageJson.content) : buildDefaultPackageJson(analysis.service), tsConfig: tsConfig ? JSON.parse(tsConfig.content) : buildDefaultTsConfig(), entryPoint: 'src/index.ts', toolCount: toolConfig.length, }; yield { type: 'generate:complete', bundle }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); yield { type: 'error', message: `Generation failed: ${msg}`, recoverable: error instanceof Anthropic.RateLimitError, }; } } /** Extract file blocks from streamed text. */ function extractFiles(text: string): GeneratedFile[] { const files: GeneratedFile[] = []; // Match: ```typescript // path: some/path.ts OR ```json // path: package.json const regex = /```(\w+)\s*\/\/\s*path:\s*(\S+)\s*\n([\s\S]*?)```/g; let match; while ((match = regex.exec(text)) !== null) { const lang = match[1] as GeneratedFile['language']; const path = match[2].trim(); const content = match[3].trim(); files.push({ path, content, language: lang === 'json' ? 'json' : lang === 'markdown' ? 'markdown' : 'typescript', }); } return files; } function estimateFileCount(toolCount: number): number { // base files (index, auth, client, package.json, tsconfig) + one file per tool return 5 + toolCount; } function buildDefaultPackageJson(service: string): object { const slug = service.toLowerCase().replace(/[^a-z0-9]+/g, '-'); return { name: `@mcpengine/${slug}-mcp`, version: '1.0.0', type: 'module', main: 'dist/index.js', scripts: { build: 'tsc', start: 'node dist/index.js', dev: 'tsx src/index.ts', }, dependencies: { '@modelcontextprotocol/sdk': '^1.12.1', }, devDependencies: { typescript: '^5.8.0', tsx: '^4.19.0', }, }; } function buildDefaultTsConfig(): object { return { compilerOptions: { target: 'ES2022', module: 'Node16', moduleResolution: 'Node16', outDir: './dist', rootDir: './src', strict: true, esModuleInterop: true, declaration: true, }, include: ['src/**/*'], }; }