import Anthropic from '@anthropic-ai/sdk'; import { readFileSync } from 'fs'; import { join } from 'path'; function getClient() { return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }); } function loadSkill(name: string): string { const skillsDir = join(process.cwd(), '../../packages/ai-pipeline/skills/data'); return readFileSync(join(skillsDir, `${name}.md`), 'utf-8'); } export async function POST(request: Request) { try { const body = await request.json(); const { analysis, tools, serviceName } = body; if (!analysis && !tools) { return Response.json({ error: 'Provide analysis or tools config' }, { status: 400 }); } // Load our server-builder + server-development skills const builderSkill = loadSkill('mcp-server-builder'); const devSkill = loadSkill('mcp-server-development'); const systemPrompt = builderSkill + '\n\n---\n\n' + devSkill; const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { try { const send = (event: string, data: unknown) => { controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)); }; send('progress', { step: 'Generating MCP server...', percent: 10 }); const response = await getClient().messages.create({ model: 'claude-sonnet-4-5-20250514', max_tokens: 16384, system: systemPrompt + '\n\nIMPORTANT: Generate a complete, working MCP server. Output each file in this format:\n\n--- FILE: path/to/file ---\n```typescript\n// file content\n```\n\nGenerate at minimum: src/index.ts, package.json, tsconfig.json, README.md, .env.example', messages: [{ role: 'user', content: `Generate a complete MCP server for "${serviceName || 'custom'}" based on this analysis:\n\n${JSON.stringify(analysis || { tools })}\n\nThe server should compile with TypeScript and use the MCP SDK ^1.26.0.` }], }); send('progress', { step: 'Parsing generated files...', percent: 80 }); const text = response.content .filter((b): b is Anthropic.TextBlock => b.type === 'text') .map(b => b.text) .join(''); // Parse files from the output const files: { path: string; content: string }[] = []; const filePattern = /--- FILE: (.+?) ---\s*```(?:typescript|json|markdown|ts|md)?\s*([\s\S]*?)```/g; let match; while ((match = filePattern.exec(text)) !== null) { files.push({ path: match[1].trim(), content: match[2].trim() }); send('file_ready', { path: match[1].trim(), preview: match[2].trim().substring(0, 200) }); } // If no file pattern found, try to extract code blocks if (files.length === 0) { const codeBlocks = text.match(/```(?:typescript|ts|json)\s*([\s\S]*?)```/g) || []; if (codeBlocks.length > 0) { files.push({ path: 'src/index.ts', content: codeBlocks[0].replace(/```(?:typescript|ts)?\s*/, '').replace(/```$/, '').trim() }); send('file_ready', { path: 'src/index.ts', preview: 'Main server file' }); } } send('complete', { files, totalFiles: files.length, rawOutput: files.length === 0 ? text : undefined }); controller.close(); } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Unknown error'; controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ message: msg })}\n\n`)); controller.close(); } } }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, }); } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Unknown error'; return Response.json({ error: msg }, { status: 500 }); } }