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 { specUrl, specContent } = body; let spec = specContent || ''; // Fetch spec from URL if provided if (specUrl && !spec) { try { const res = await fetch(specUrl, { headers: { Accept: 'application/json, text/yaml, text/plain, */*' } }); spec = await res.text(); } catch (e) { return Response.json({ error: 'Failed to fetch spec from URL' }, { status: 400 }); } } if (!spec || spec.length < 50) { return Response.json({ error: 'Please provide a valid OpenAPI spec (URL or content)' }, { status: 400 }); } // Load our mcp-api-analyzer skill const analyzerSkill = loadSkill('mcp-api-analyzer'); 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: 'Starting analysis...', percent: 10 }); const response = await getClient().messages.create({ model: 'claude-sonnet-4-5-20250514', max_tokens: 8192, system: analyzerSkill + '\n\nIMPORTANT: Return your analysis as a JSON object wrapped in ```json code fences. The JSON must have this structure: { "service": string, "baseUrl": string, "toolGroups": [{ "name": string, "tools": [{ "name": string, "description": string, "method": "GET"|"POST"|"PUT"|"PATCH"|"DELETE", "endpoint": string, "inputSchema": { "type": "object", "properties": {}, "required": [] } }] }], "authFlow": { "type": "api_key"|"oauth2"|"bearer" }, "appCandidates": [{ "name": string, "pattern": string, "description": string }] }', messages: [{ role: 'user', content: `Analyze this API specification and produce the structured analysis:\n\n${spec.substring(0, 50000)}` }], }); send('progress', { step: 'Analysis complete', percent: 90 }); // Extract the text content const text = response.content .filter((b): b is Anthropic.TextBlock => b.type === 'text') .map(b => b.text) .join(''); // Try to extract JSON from the response const jsonMatch = text.match(/```json\s*([\s\S]*?)```/) || text.match(/\{[\s\S]*\}/); let analysis = null; if (jsonMatch) { try { analysis = JSON.parse(jsonMatch[1] || jsonMatch[0]); } catch { analysis = { rawText: text }; } } else { analysis = { rawText: text }; } // Emit tools one by one if (analysis.toolGroups) { for (const group of analysis.toolGroups) { for (const tool of group.tools || []) { send('tool_found', { ...tool, groupName: group.name }); await new Promise(r => setTimeout(r, 200)); // stagger for UX } } } send('complete', { analysis }); 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 }); } }