import Anthropic from '@anthropic-ai/sdk'; import { GeneratorSpec, type GeneratorSpec as GeneratorSpecT } from '@bmm/types'; export const SYSTEM_PROMPT = `You generate production-grade MCP server specifications as STRICT JSON. Output ONE JSON object (no markdown, no prose, no code fences) with this exact shape: { "name": "human-readable server name (max 128 chars)", "description": "1-2 sentence purpose", "tools": [ { "name": "snake_case_tool_name", "description": "what the AI client sees — single sentence, clear", "inputSchema": { "param_name": { "type": "string|number|boolean|array|object", "description": "...", "required": true } }, "implementation": "ASYNC TypeScript body. Receives {args} pre-validated. Must return MCP content blocks: { content: [{ type: 'text', text: '...' }] }. Use process.env.SECRET_NAME for secrets. NEVER use eval/Function/child_process. Use globalThis.fetch for HTTP. Wrap external calls in try/catch and return { content: [{ type: 'text', text: 'Error: ...' }], isError: true } on failure." } ], "resources": [], "prompts": [], "requiredSecrets": ["UPPER_SNAKE_CASE"], "scopes": ["mcp:read"], "dependencies": {} } Rules: - Tools are idempotent unless the description explicitly says destructive. - Validate all string inputs before use. - For databases: parameterized queries only (use the 'pg' library with $1 placeholders). - For HTTP APIs: globalThis.fetch with explicit timeout via AbortSignal.timeout(10000). - Never hardcode credentials; declare them under requiredSecrets and read via process.env. - Keep tool implementations under 5000 characters. - Do not include "import" statements in implementations — the runtime injects fetch, pg, etc. Return JSON only. No explanation.`; const BANNED_PATTERNS = [ /\beval\s*\(/, /\bnew\s+Function\s*\(/, /\brequire\s*\(\s*['"]child_process['"]/, /\bchild_process\b/, /ignore\s+previous\s+instructions/i, /disregard\s+(the\s+)?(above|previous)/i, ]; export interface GenerationResult { spec: GeneratorSpecT; source: 'claude' | 'mock'; } export interface GenerateOptions { apiKey?: string; model?: string; maxTokens?: number; } export async function generateSpec(prompt: string, opts: GenerateOptions = {}): Promise { if (!opts.apiKey) { return { spec: mockSpec(prompt), source: 'mock' }; } const client = new Anthropic({ apiKey: opts.apiKey }); const response = await client.messages.create({ model: opts.model ?? 'claude-opus-4-7', max_tokens: opts.maxTokens ?? 8192, system: SYSTEM_PROMPT, messages: [{ role: 'user', content: prompt }], }); const text = response.content .filter((b): b is { type: 'text'; text: string } => b.type === 'text') .map((b) => b.text) .join(''); const json = extractJson(text); const parsed = GeneratorSpec.safeParse(json); if (!parsed.success) { throw new SpecValidationError(parsed.error.message); } scanForInjection(parsed.data); return { spec: parsed.data, source: 'claude' }; } export class SpecValidationError extends Error { override readonly name = 'SpecValidationError'; } export class BannedPatternError extends Error { override readonly name = 'BannedPatternError'; } function extractJson(text: string): unknown { const trimmed = text.trim(); const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/); const body = fenced ? fenced[1] : trimmed; if (!body) throw new SpecValidationError('empty_generation_output'); try { return JSON.parse(body); } catch (e) { throw new SpecValidationError(`generation_not_json: ${(e as Error).message}`); } } function scanForInjection(spec: GeneratorSpecT): void { for (const tool of spec.tools) { for (const pattern of BANNED_PATTERNS) { if (pattern.test(tool.implementation) || pattern.test(tool.description)) { throw new BannedPatternError(`banned_pattern_detected: ${pattern.source}`); } } } } export function mockSpec(prompt: string): GeneratorSpecT { return { name: 'Echo MCP', description: `Mock server (no ANTHROPIC_API_KEY). Prompt was: ${prompt.slice(0, 200)}`, tools: [ { name: 'echo', description: 'Echoes the input string back to the caller.', inputSchema: { message: { type: 'string', description: 'Message to echo back', required: true }, }, implementation: `const msg = String(args.message ?? '');\nreturn { content: [{ type: 'text', text: \`echo: \${msg}\` }] };`, }, { name: 'now', description: 'Returns the current server UTC timestamp.', inputSchema: {}, implementation: `return { content: [{ type: 'text', text: new Date().toISOString() }] };`, }, ], resources: [], prompts: [], requiredSecrets: [], scopes: ['mcp:read'], dependencies: {}, }; }