111 lines
4.1 KiB
TypeScript
111 lines
4.1 KiB
TypeScript
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 });
|
|
}
|
|
}
|