import { z } from 'zod'; import type { Tool } from '@modelcontextprotocol/sdk/types.js'; import type { WebSocketManager } from '../connection.js'; import { InjectCodeInputSchema } from '../types.js'; import type { InjectCodeInput } from '../types.js'; export const injectCodeSchema = InjectCodeInputSchema; export type { InjectCodeInput }; export const injectCodeTool: Tool = { name: 'inject_code', description: 'Inject R3F/Three.js component code into the running browser scene for immediate live preview. ' + 'The code is evaluated in the browser — no file writing, no dev server restart. ' + 'Runs a quality check before injection and returns warnings for common issues. ' + 'Errors are returned so you can self-correct. ' + 'Available scope: React hooks (useState, useEffect, useRef, useMemo, useCallback), ' + 'R3F hooks (useFrame, useThree), THREE (full namespace). ' + 'Code must default-export a functional component.', inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'Complete TSX component source. Must have a default export.', }, name: { type: 'string', description: 'Identifier for this injection — used by commit_component and remove_injection', }, replace: { type: 'string', description: 'Name of a previous injection to replace (enables iteration)', }, }, required: ['code'], }, }; // ─── Quality validator ──────────────────────────────────────────────────────── interface QualityWarning { rule: string; message: string; suggestion: string; } function validateQuality(code: string): QualityWarning[] { const warnings: QualityWarning[] = []; // 1. meshBasicMaterial for visible geometry if (/meshBasicMaterial/i.test(code) && !/wireframe.*true/i.test(code)) { warnings.push({ rule: 'flat-material', message: 'Uses meshBasicMaterial (unlit — no shadows, no environment reflections)', suggestion: 'Switch to meshStandardMaterial with metalness/roughness for realistic lighting', }); } // 2. No lighting references at all const hasLighting = /directionalLight|pointLight|spotLight|ambientLight|Environment|hemisphereLight/i.test(code); if (!hasLighting) { warnings.push({ rule: 'no-lighting', message: 'No lighting or environment defined in this component', suggestion: 'If the scene already has lighting this is fine; otherwise add ' + ' and a directionalLight, or ', }); } // 3. useFrame without delta — catches the common mistake of omitting delta // Only flag if transforms are being mutated (rotation, position, scale) if (/useFrame/.test(code) && /\.(rotation|position)\.[xyz]\s*[+\-]?=/.test(code)) { const hasDelta = /useFrame\s*\(\s*\([^)]*,\s*delta/.test(code); if (!hasDelta) { warnings.push({ rule: 'no-delta', message: 'useFrame mutates transforms but the callback is missing the delta parameter', suggestion: 'Change signature to useFrame((state, delta) => { ... }) and multiply all ' + 'speed values by delta for frame-rate-independent animation', }); } } // 4. Individual meshes in a map/loop — likely should be instancedMesh const hasLoopMesh = /(?:\.map|for\s*\(|Array\.from)\s*[^}]{0,200} elements inside a loop/map', suggestion: 'For many identical objects use (single draw call) — ' + 'e.g. with setMatrixAt in useFrame', }); } // 5. High-poly geometry for potentially small objects const highPolyMatch = code.match(/ parseFloat(s.trim())); const wSeg = parts[1] ?? 0; const hSeg = parts[2] ?? 0; if (wSeg > 24 || hSeg > 24) { warnings.push({ rule: 'high-poly', message: `sphereGeometry uses high segment counts (${wSeg}×${hSeg})`, suggestion: 'For objects smaller than ~1 unit use args={[r, 12, 12]}; ' + 'high segments are only needed for close-up hero objects', }); } } return warnings; } // ─── Handler ────────────────────────────────────────────────────────────────── type InjectContent = { content: Array<{ type: 'text'; text: string }> }; export async function handleInjectCode( args: InjectCodeInput, connection: WebSocketManager, ): Promise { const name = args.name ?? `injection_${Date.now()}`; // ── Quality check before injection ──────────────────────────────────────── const warnings = validateQuality(args.code); // ── Inject ──────────────────────────────────────────────────────────────── const result = await connection.requestInjectCode(args.code, name, args.replace); // ── Build response ──────────────────────────────────────────────────────── const lines: string[] = []; if (result.success) { lines.push( `Injected "${result.name}" (UUID: ${result.uuid}).`, `The component is now live in the running scene.`, `Call commit_component with name="${result.name}" to save it to a file.`, ); } else { lines.push( `Injection failed — ${result.error ?? 'unknown error'}.`, `Fix the code and call inject_code again with replace="${name}".`, ); } if (warnings.length > 0) { lines.push(''); lines.push(`⚠️ Quality suggestions (${warnings.length}):`); for (const w of warnings) { lines.push(` • ${w.message}`); lines.push(` → ${w.suggestion}`); } if (result.success) { lines.push(''); lines.push( 'Consider addressing these before committing. ' + 'Re-inject with replace="' + name + '" after fixing.', ); } } return { content: [{ type: 'text', text: lines.join('\n') }] }; }