// Analyzer Service — streams API spec analysis via Claude import Anthropic from '@anthropic-ai/sdk'; import { getSkill } from '../skills/loader'; import { parseStreamEvents } from '../streaming/parser'; import type { PipelineEvent, AnalysisResult, ToolDefinition, } from '../types'; const MODEL = 'claude-sonnet-4-5-20250514'; const MAX_TOKENS = 8192; /** * Analyze an API spec (OpenAPI/Swagger JSON or YAML string). * Streams PipelineEvent objects as analysis progresses. */ export async function* analyzeSpec(spec: string): AsyncGenerator { const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const systemPrompt = getSkill('analyzer'); yield { type: 'analysis:progress', step: 'Starting API analysis', percent: 0 }; try { const stream = client.messages.stream({ model: MODEL, max_tokens: MAX_TOKENS, system: systemPrompt, messages: [ { role: 'user', content: `Analyze this API specification and produce a complete MCP tool mapping.\n\nReturn your analysis as a JSON object with this structure:\n{\n "service": "service name",\n "baseUrl": "base URL",\n "endpoints": [...],\n "authFlow": { "type": "api_key|oauth2|bearer|basic|custom", ... },\n "toolGroups": [{ "name": "...", "description": "...", "tools": [...] }],\n "appCandidates": [{ "name": "...", "pattern": "...", "description": "...", "dataSource": [...], "suggestedWidgets": [...] }],\n "rateLimits": { ... }\n}\n\nWrap the final JSON in a \`\`\`json code block.\n\nAPI Specification:\n\`\`\`\n${spec}\n\`\`\``, }, ], }); let fullText = ''; let lastPercent = 0; stream.on('text', (text) => { fullText += text; }); // Process the stream for await (const event of stream) { if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { const newPercent = Math.min(90, Math.floor((fullText.length / 4000) * 90)); if (newPercent > lastPercent + 5) { lastPercent = newPercent; yield { type: 'analysis:progress', step: 'Analyzing endpoints and generating tools', percent: newPercent }; } // Check for tool definitions appearing in stream const toolEvents = parseStreamEvents(fullText, 'analysis'); for (const te of toolEvents) { if (te.type === 'analysis:tool_found') { yield te; } } } } // Get final message for token usage const finalMessage = await stream.finalMessage(); const usage = { inputTokens: finalMessage.usage.input_tokens, outputTokens: finalMessage.usage.output_tokens, }; yield { type: 'analysis:progress', step: 'Parsing analysis results', percent: 95 }; // Extract JSON from the full response const result = extractAnalysisResult(fullText); if (result) { // Yield individual tools as they're found for (const group of result.toolGroups) { for (const tool of group.tools) { yield { type: 'analysis:tool_found', tool: tool as ToolDefinition }; } } yield { type: 'analysis:complete', result }; } else { yield { type: 'error', message: 'Failed to parse analysis result from Claude response', recoverable: true, }; } } catch (error) { const msg = error instanceof Error ? error.message : String(error); yield { type: 'error', message: `Analysis failed: ${msg}`, recoverable: error instanceof Anthropic.RateLimitError, }; } } function extractAnalysisResult(text: string): AnalysisResult | null { // Try to find JSON in code blocks const jsonMatch = text.match(/```json\s*\n([\s\S]*?)\n```/); if (jsonMatch) { try { const parsed = JSON.parse(jsonMatch[1]); return { id: crypto.randomUUID(), service: parsed.service || 'unknown', baseUrl: parsed.baseUrl || '', endpoints: parsed.endpoints || [], authFlow: parsed.authFlow || { type: 'api_key' }, toolGroups: parsed.toolGroups || [], appCandidates: parsed.appCandidates || [], rateLimits: parsed.rateLimits || {}, }; } catch { // fall through } } // Try raw JSON parse const braceStart = text.indexOf('{'); const braceEnd = text.lastIndexOf('}'); if (braceStart !== -1 && braceEnd > braceStart) { try { const parsed = JSON.parse(text.slice(braceStart, braceEnd + 1)); return { id: crypto.randomUUID(), service: parsed.service || 'unknown', baseUrl: parsed.baseUrl || '', endpoints: parsed.endpoints || [], authFlow: parsed.authFlow || { type: 'api_key' }, toolGroups: parsed.toolGroups || [], appCandidates: parsed.appCandidates || [], rateLimits: parsed.rateLimits || {}, }; } catch { return null; } } return null; }