Skip to main content
Glama
consolidated-tool-handlers.ts51.9 kB
// Consolidated tool handlers - maps 13 tools to all 36 operations import { cleanObject } from '../utils/safe-json.js'; import { Logger } from '../utils/logger.js'; const log = new Logger('ConsolidatedToolHandler'); const ACTION_REQUIRED_ERROR = 'Missing required parameter: action'; function ensureArgsPresent(args: any) { if (args === null || args === undefined) { throw new Error('Invalid arguments: null or undefined'); } } function requireAction(args: any): string { ensureArgsPresent(args); const action = args.action; if (typeof action !== 'string' || action.trim() === '') { throw new Error(ACTION_REQUIRED_ERROR); } return action; } function requireNonEmptyString(value: any, field: string, message?: string): string { if (typeof value !== 'string' || value.trim() === '') { throw new Error(message ?? `Invalid ${field}: must be a non-empty string`); } return value; } function requirePositiveNumber(value: any, field: string, message?: string): number { if (typeof value !== 'number' || !isFinite(value) || value <= 0) { throw new Error(message ?? `Invalid ${field}: must be a positive number`); } return value; } function requireVector3Components( vector: any, message: string ): [number, number, number] { if ( !vector || typeof vector.x !== 'number' || typeof vector.y !== 'number' || typeof vector.z !== 'number' ) { throw new Error(message); } return [vector.x, vector.y, vector.z]; } function getElicitationTimeoutMs(tools: any): number | undefined { if (!tools) return undefined; const direct = tools.elicitationTimeoutMs; if (typeof direct === 'number' && Number.isFinite(direct)) { return direct; } if (typeof tools.getElicitationTimeoutMs === 'function') { const value = tools.getElicitationTimeoutMs(); if (typeof value === 'number' && Number.isFinite(value)) { return value; } } return undefined; } async function elicitMissingPrimitiveArgs( tools: any, args: any, prompt: string, fieldSchemas: Record<string, { type: 'string' | 'number' | 'integer' | 'boolean'; title?: string; description?: string; enum?: string[]; enumNames?: string[]; minimum?: number; maximum?: number; minLength?: number; maxLength?: number; pattern?: string; format?: string; default?: unknown }> ) { if ( !tools || typeof tools.supportsElicitation !== 'function' || !tools.supportsElicitation() || typeof tools.elicit !== 'function' ) { return; } const properties: Record<string, any> = {}; const required: string[] = []; for (const [key, schema] of Object.entries(fieldSchemas)) { const value = args?.[key]; const missing = value === undefined || value === null || (typeof value === 'string' && value.trim() === ''); if (missing) { properties[key] = schema; required.push(key); } } if (required.length === 0) return; const timeoutMs = getElicitationTimeoutMs(tools); const options: any = { fallback: async () => ({ ok: false, error: 'missing-params' }) }; if (typeof timeoutMs === 'number') { options.timeoutMs = timeoutMs; } try { const elicited = await tools.elicit( prompt, { type: 'object', properties, required }, options ); if (elicited?.ok && elicited.value) { for (const key of required) { const value = elicited.value[key]; if (value === undefined || value === null) continue; args[key] = typeof value === 'string' ? value.trim() : value; } } } catch (err) { log.debug('Special elicitation fallback skipped', { prompt, err: (err as any)?.message || String(err) }); } } export async function handleConsolidatedToolCall( name: string, args: any, tools: any ) { const startTime = Date.now(); // Use scoped logger (stderr) to avoid polluting stdout JSON log.debug(`Starting execution of ${name} at ${new Date().toISOString()}`); try { ensureArgsPresent(args); switch (name) { // 1. ASSET MANAGER case 'manage_asset': switch (requireAction(args)) { case 'list': { if (args.directory !== undefined && args.directory !== null && typeof args.directory !== 'string') { throw new Error('Invalid directory: must be a string'); } const res = await tools.assetResources.list(args.directory || '/Game', false); return cleanObject({ success: true, ...res }); } case 'import': { let sourcePath = typeof args.sourcePath === 'string' ? args.sourcePath.trim() : ''; let destinationPath = typeof args.destinationPath === 'string' ? args.destinationPath.trim() : ''; if ((!sourcePath || !destinationPath) && typeof tools.supportsElicitation === 'function' && tools.supportsElicitation() && typeof tools.elicit === 'function') { const schemaProps: Record<string, any> = {}; const required: string[] = []; if (!sourcePath) { schemaProps.sourcePath = { type: 'string', title: 'Source File Path', description: 'Full path to the asset file on disk to import' }; required.push('sourcePath'); } if (!destinationPath) { schemaProps.destinationPath = { type: 'string', title: 'Destination Path', description: 'Unreal content path where the asset should be imported (e.g., /Game/MCP/Assets)' }; required.push('destinationPath'); } if (required.length > 0) { const timeoutMs = getElicitationTimeoutMs(tools); const options: any = { fallback: async () => ({ ok: false, error: 'missing-import-params' }) }; if (typeof timeoutMs === 'number') { options.timeoutMs = timeoutMs; } const elicited = await tools.elicit( 'Provide the missing import parameters for manage_asset.import', { type: 'object', properties: schemaProps, required }, options ); if (elicited?.ok && elicited.value) { if (typeof elicited.value.sourcePath === 'string') { sourcePath = elicited.value.sourcePath.trim(); } if (typeof elicited.value.destinationPath === 'string') { destinationPath = elicited.value.destinationPath.trim(); } } } } const sourcePathValidated = requireNonEmptyString(sourcePath || args.sourcePath, 'sourcePath', 'Invalid sourcePath'); const destinationPathValidated = requireNonEmptyString(destinationPath || args.destinationPath, 'destinationPath', 'Invalid destinationPath'); const res = await tools.assetTools.importAsset(sourcePathValidated, destinationPathValidated); return cleanObject(res); } case 'create_material': { await elicitMissingPrimitiveArgs( tools, args, 'Provide the material details for manage_asset.create_material', { name: { type: 'string', title: 'Material Name', description: 'Name for the new material asset' }, path: { type: 'string', title: 'Save Path', description: 'Optional Unreal content path where the material should be saved' } } ); const sanitizedName = typeof args.name === 'string' ? args.name.trim() : args.name; const sanitizedPath = typeof args.path === 'string' ? args.path.trim() : args.path; const name = requireNonEmptyString(sanitizedName, 'name', 'Invalid name: must be a non-empty string'); const res = await tools.materialTools.createMaterial(name, sanitizedPath || '/Game/Materials'); return cleanObject(res); } default: throw new Error(`Unknown asset action: ${args.action}`); } // 2. ACTOR CONTROL case 'control_actor': switch (requireAction(args)) { case 'spawn': { await elicitMissingPrimitiveArgs( tools, args, 'Provide the spawn parameters for control_actor.spawn', { classPath: { type: 'string', title: 'Actor Class or Asset Path', description: 'Class name (e.g., StaticMeshActor) or asset path (e.g., /Engine/BasicShapes/Cube) to spawn' } } ); const classPathInput = typeof args.classPath === 'string' ? args.classPath.trim() : args.classPath; const classPath = requireNonEmptyString(classPathInput, 'classPath', 'Invalid classPath: must be a non-empty string'); const actorNameInput = typeof args.actorName === 'string' && args.actorName.trim() !== '' ? args.actorName : (typeof args.name === 'string' ? args.name : undefined); const res = await tools.actorTools.spawn({ classPath, location: args.location, rotation: args.rotation, actorName: actorNameInput }); return cleanObject(res); } case 'delete': { await elicitMissingPrimitiveArgs( tools, args, 'Which actor should control_actor.delete remove?', { actorName: { type: 'string', title: 'Actor Name', description: 'Exact label of the actor to delete' } } ); const actorNameArg = typeof args.actorName === 'string' && args.actorName.trim() !== '' ? args.actorName : (typeof args.name === 'string' ? args.name : undefined); const actorName = requireNonEmptyString(actorNameArg, 'actorName', 'Invalid actorName'); const res = await tools.bridge.executeEditorFunction('DELETE_ACTOR', { actor_name: actorName }); return cleanObject(res); } case 'apply_force': { await elicitMissingPrimitiveArgs( tools, args, 'Provide the target actor for control_actor.apply_force', { actorName: { type: 'string', title: 'Actor Name', description: 'Physics-enabled actor that should receive the force' } } ); const actorName = requireNonEmptyString(args.actorName, 'actorName', 'Invalid actorName'); const vector = requireVector3Components(args.force, 'Invalid force: must have numeric x,y,z'); const res = await tools.physicsTools.applyForce({ actorName, forceType: 'Force', vector }); return cleanObject(res); } default: throw new Error(`Unknown actor action: ${args.action}`); } // 3. EDITOR CONTROL case 'control_editor': switch (requireAction(args)) { case 'play': { const res = await tools.editorTools.playInEditor(); return cleanObject(res); } case 'stop': { const res = await tools.editorTools.stopPlayInEditor(); return cleanObject(res); } case 'pause': { const res = await tools.editorTools.pausePlayInEditor(); return cleanObject(res); } case 'set_game_speed': { const speed = requirePositiveNumber(args.speed, 'speed', 'Invalid speed: must be a positive number'); // Use console command via bridge const res = await tools.bridge.executeConsoleCommand(`slomo ${speed}`); return cleanObject(res); } case 'eject': { const res = await tools.bridge.executeConsoleCommand('eject'); return cleanObject(res); } case 'possess': { const res = await tools.bridge.executeConsoleCommand('viewself'); return cleanObject(res); } case 'set_camera': { const res = await tools.editorTools.setViewportCamera(args.location, args.rotation); return cleanObject(res); } case 'set_view_mode': { await elicitMissingPrimitiveArgs( tools, args, 'Provide the view mode for control_editor.set_view_mode', { viewMode: { type: 'string', title: 'View Mode', description: 'Viewport view mode (e.g., Lit, Unlit, Wireframe)' } } ); const viewMode = requireNonEmptyString(args.viewMode, 'viewMode', 'Missing required parameter: viewMode'); const res = await tools.bridge.setSafeViewMode(viewMode); return cleanObject(res); } default: throw new Error(`Unknown editor action: ${args.action}`); } // 4. LEVEL MANAGER case 'manage_level': switch (requireAction(args)) { case 'load': { await elicitMissingPrimitiveArgs( tools, args, 'Select the level to load for manage_level.load', { levelPath: { type: 'string', title: 'Level Path', description: 'Content path of the level asset to load (e.g., /Game/Maps/MyLevel)' } } ); const levelPath = requireNonEmptyString(args.levelPath, 'levelPath', 'Missing required parameter: levelPath'); const res = await tools.levelTools.loadLevel({ levelPath, streaming: !!args.streaming }); return cleanObject(res); } case 'save': { const res = await tools.levelTools.saveLevel({ levelName: args.levelName, savePath: args.savePath }); return cleanObject(res); } case 'stream': { await elicitMissingPrimitiveArgs( tools, args, 'Provide the streaming level name for manage_level.stream', { levelName: { type: 'string', title: 'Level Name', description: 'Streaming level name to toggle' } } ); const levelName = requireNonEmptyString(args.levelName, 'levelName', 'Missing required parameter: levelName'); const res = await tools.levelTools.streamLevel({ levelName, shouldBeLoaded: !!args.shouldBeLoaded, shouldBeVisible: !!args.shouldBeVisible }); return cleanObject(res); } case 'create_light': { await elicitMissingPrimitiveArgs( tools, args, 'Provide the light details for manage_level.create_light', { lightType: { type: 'string', title: 'Light Type', description: 'Directional, Point, Spot, Rect, or Sky' }, name: { type: 'string', title: 'Light Name', description: 'Name for the new light actor' } } ); const lightType = requireNonEmptyString(args.lightType, 'lightType', 'Missing required parameter: lightType'); const name = requireNonEmptyString(args.name, 'name', 'Invalid name'); const typeKey = lightType.toLowerCase(); const toVector = (value: any, fallback: [number, number, number]): [number, number, number] => { if (Array.isArray(value) && value.length === 3) { return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0]; } if (value && typeof value === 'object') { return [Number(value.x) || 0, Number(value.y) || 0, Number(value.z) || 0]; } return fallback; }; const toRotator = (value: any, fallback: [number, number, number]): [number, number, number] => { if (Array.isArray(value) && value.length === 3) { return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0]; } if (value && typeof value === 'object') { return [Number(value.pitch) || 0, Number(value.yaw) || 0, Number(value.roll) || 0]; } return fallback; }; const toColor = (value: any): [number, number, number] | undefined => { if (Array.isArray(value) && value.length === 3) { return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0]; } if (value && typeof value === 'object') { return [Number(value.r) || 0, Number(value.g) || 0, Number(value.b) || 0]; } return undefined; }; const location = toVector(args.location, [0, 0, typeKey === 'directional' ? 500 : 0]); const rotation = toRotator(args.rotation, [0, 0, 0]); const color = toColor(args.color); const castShadows = typeof args.castShadows === 'boolean' ? args.castShadows : undefined; if (typeKey === 'directional') { return cleanObject(await tools.lightingTools.createDirectionalLight({ name, intensity: args.intensity, color, rotation, castShadows, temperature: args.temperature })); } if (typeKey === 'point') { return cleanObject(await tools.lightingTools.createPointLight({ name, location, intensity: args.intensity, radius: args.radius, color, falloffExponent: args.falloffExponent, castShadows })); } if (typeKey === 'spot') { const innerCone = typeof args.innerCone === 'number' ? args.innerCone : undefined; const outerCone = typeof args.outerCone === 'number' ? args.outerCone : undefined; if (innerCone !== undefined && outerCone !== undefined && innerCone >= outerCone) { throw new Error('innerCone must be less than outerCone'); } return cleanObject(await tools.lightingTools.createSpotLight({ name, location, rotation, intensity: args.intensity, innerCone: args.innerCone, outerCone: args.outerCone, radius: args.radius, color, castShadows })); } if (typeKey === 'rect') { return cleanObject(await tools.lightingTools.createRectLight({ name, location, rotation, intensity: args.intensity, width: args.width, height: args.height, color })); } if (typeKey === 'sky' || typeKey === 'skylight') { return cleanObject(await tools.lightingTools.createSkyLight({ name, sourceType: args.sourceType, cubemapPath: args.cubemapPath, intensity: args.intensity, recapture: args.recapture })); } throw new Error(`Unknown light type: ${lightType}`); } case 'build_lighting': { const res = await tools.lightingTools.buildLighting({ quality: args.quality || 'High', buildReflectionCaptures: true }); return cleanObject(res); } default: throw new Error(`Unknown level action: ${args.action}`); } // 5. ANIMATION & PHYSICS case 'animation_physics': switch (requireAction(args)) { case 'create_animation_bp': { await elicitMissingPrimitiveArgs( tools, args, 'Provide details for animation_physics.create_animation_bp', { name: { type: 'string', title: 'Blueprint Name', description: 'Name of the Animation Blueprint to create' }, skeletonPath: { type: 'string', title: 'Skeleton Path', description: 'Content path of the skeleton asset to bind' } } ); const name = requireNonEmptyString(args.name, 'name', 'Invalid name'); const skeletonPath = requireNonEmptyString(args.skeletonPath, 'skeletonPath', 'Invalid skeletonPath'); const res = await tools.animationTools.createAnimationBlueprint({ name, skeletonPath, savePath: args.savePath }); return cleanObject(res); } case 'play_montage': { await elicitMissingPrimitiveArgs( tools, args, 'Provide playback details for animation_physics.play_montage', { actorName: { type: 'string', title: 'Actor Name', description: 'Actor that should play the montage' }, montagePath: { type: 'string', title: 'Montage Path', description: 'Montage or animation asset path to play' } } ); const actorName = requireNonEmptyString(args.actorName, 'actorName', 'Invalid actorName'); const montagePath = args.montagePath || args.animationPath; const validatedMontage = requireNonEmptyString(montagePath, 'montagePath', 'Invalid montagePath'); const res = await tools.animationTools.playAnimation({ actorName, animationType: 'Montage', animationPath: validatedMontage, playRate: args.playRate }); return cleanObject(res); } case 'setup_ragdoll': { await elicitMissingPrimitiveArgs( tools, args, 'Provide setup details for animation_physics.setup_ragdoll', { skeletonPath: { type: 'string', title: 'Skeleton Path', description: 'Content path for the skeleton asset' }, physicsAssetName: { type: 'string', title: 'Physics Asset Name', description: 'Name of the physics asset to apply' } } ); const skeletonPath = requireNonEmptyString(args.skeletonPath, 'skeletonPath', 'Invalid skeletonPath'); const physicsAssetName = requireNonEmptyString(args.physicsAssetName, 'physicsAssetName', 'Invalid physicsAssetName'); const res = await tools.physicsTools.setupRagdoll({ skeletonPath, physicsAssetName, blendWeight: args.blendWeight, savePath: args.savePath }); return cleanObject(res); } default: throw new Error(`Unknown animation/physics action: ${args.action}`); } // 6. EFFECTS SYSTEM case 'create_effect': switch (requireAction(args)) { case 'particle': { await elicitMissingPrimitiveArgs( tools, args, 'Provide the particle effect details for create_effect.particle', { effectType: { type: 'string', title: 'Effect Type', description: 'Preset effect type to spawn (e.g., Fire, Smoke)' } } ); const res = await tools.niagaraTools.createEffect({ effectType: args.effectType, name: args.name, location: args.location, scale: args.scale, customParameters: args.customParameters }); return cleanObject(res); } case 'niagara': { await elicitMissingPrimitiveArgs( tools, args, 'Provide the Niagara system path for create_effect.niagara', { systemPath: { type: 'string', title: 'Niagara System Path', description: 'Asset path of the Niagara system to spawn' } } ); const systemPath = requireNonEmptyString(args.systemPath, 'systemPath', 'Invalid systemPath'); const verifyResult = await tools.bridge.executePythonWithResult(` import unreal, json path = r"${systemPath}" exists = unreal.EditorAssetLibrary.does_asset_exist(path) print('RESULT:' + json.dumps({'success': exists, 'exists': exists, 'path': path})) `.trim()); if (!verifyResult?.exists) { return cleanObject({ success: false, error: `Niagara system not found at ${systemPath}` }); } const loc = Array.isArray(args.location) ? { x: args.location[0], y: args.location[1], z: args.location[2] } : args.location || { x: 0, y: 0, z: 0 }; const res = await tools.niagaraTools.spawnEffect({ systemPath, location: [loc.x ?? 0, loc.y ?? 0, loc.z ?? 0], rotation: Array.isArray(args.rotation) ? args.rotation : undefined, scale: args.scale }); return cleanObject(res); } case 'debug_shape': { const shapeInput = args.shape ?? 'Sphere'; const shape = String(shapeInput).trim().toLowerCase(); const originalShapeLabel = String(shapeInput).trim() || 'shape'; const loc = args.location || { x: 0, y: 0, z: 0 }; const size = args.size || 100; const color = args.color || [255, 0, 0, 255]; const duration = args.duration || 5; if (shape === 'line') { const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z }; return cleanObject(await tools.debugTools.drawDebugLine({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration })); } else if (shape === 'box') { const extent = [size, size, size]; return cleanObject(await tools.debugTools.drawDebugBox({ center: [loc.x, loc.y, loc.z], extent, color, duration })); } else if (shape === 'sphere') { return cleanObject(await tools.debugTools.drawDebugSphere({ center: [loc.x, loc.y, loc.z], radius: size, color, duration })); } else if (shape === 'capsule') { return cleanObject(await tools.debugTools.drawDebugCapsule({ center: [loc.x, loc.y, loc.z], halfHeight: size, radius: Math.max(10, size/3), color, duration })); } else if (shape === 'cone') { return cleanObject(await tools.debugTools.drawDebugCone({ origin: [loc.x, loc.y, loc.z], direction: [0,0,1], length: size, angleWidth: 0.5, angleHeight: 0.5, color, duration })); } else if (shape === 'arrow') { const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z }; return cleanObject(await tools.debugTools.drawDebugArrow({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration })); } else if (shape === 'point') { return cleanObject(await tools.debugTools.drawDebugPoint({ location: [loc.x, loc.y, loc.z], size, color, duration })); } else if (shape === 'text' || shape === 'string') { const text = args.text || 'Debug'; return cleanObject(await tools.debugTools.drawDebugString({ location: [loc.x, loc.y, loc.z], text, color, duration })); } // Default fallback return cleanObject({ success: false, error: `Unsupported debug shape: ${originalShapeLabel}` }); } default: throw new Error(`Unknown effect action: ${args.action}`); } // 7. BLUEPRINT MANAGER case 'manage_blueprint': switch (requireAction(args)) { case 'create': { await elicitMissingPrimitiveArgs( tools, args, 'Provide details for manage_blueprint.create', { name: { type: 'string', title: 'Blueprint Name', description: 'Name for the new Blueprint asset' }, blueprintType: { type: 'string', title: 'Blueprint Type', description: 'Base type such as Actor, Pawn, Character, etc.' } } ); const res = await tools.blueprintTools.createBlueprint({ name: args.name, blueprintType: args.blueprintType || 'Actor', savePath: args.savePath, parentClass: args.parentClass }); return cleanObject(res); } case 'add_component': { await elicitMissingPrimitiveArgs( tools, args, 'Provide details for manage_blueprint.add_component', { name: { type: 'string', title: 'Blueprint Name', description: 'Blueprint asset to modify' }, componentType: { type: 'string', title: 'Component Type', description: 'Component class to add (e.g., StaticMeshComponent)' }, componentName: { type: 'string', title: 'Component Name', description: 'Name for the new component' } } ); const res = await tools.blueprintTools.addComponent({ blueprintName: args.name, componentType: args.componentType, componentName: args.componentName }); return cleanObject(res); } default: throw new Error(`Unknown blueprint action: ${args.action}`); } // 8. ENVIRONMENT BUILDER case 'build_environment': switch (requireAction(args)) { case 'create_landscape': { const res = await tools.landscapeTools.createLandscape({ name: args.name, sizeX: args.sizeX, sizeY: args.sizeY, materialPath: args.materialPath }); return cleanObject(res); } case 'sculpt': { const res = await tools.landscapeTools.sculptLandscape({ landscapeName: args.name, tool: args.tool, brushSize: args.brushSize, strength: args.strength }); return cleanObject(res); } case 'add_foliage': { const res = await tools.foliageTools.addFoliageType({ name: args.name, meshPath: args.meshPath, density: args.density }); return cleanObject(res); } case 'paint_foliage': { const pos = args.position ? [args.position.x || 0, args.position.y || 0, args.position.z || 0] : [0,0,0]; const res = await tools.foliageTools.paintFoliage({ foliageType: args.foliageType, position: pos, brushSize: args.brushSize, paintDensity: args.paintDensity, eraseMode: args.eraseMode }); return cleanObject(res); } case 'create_procedural_terrain': { const loc = args.location ? [args.location.x||0, args.location.y||0, args.location.z||0] : [0,0,0]; const res = await tools.buildEnvAdvanced.createProceduralTerrain({ name: args.name || 'ProceduralTerrain', location: loc as [number,number,number], sizeX: args.sizeX, sizeY: args.sizeY, subdivisions: args.subdivisions, heightFunction: args.heightFunction, material: args.materialPath }); return cleanObject(res); } case 'create_procedural_foliage': { if (!args.bounds || !args.bounds.location || !args.bounds.size) throw new Error('bounds.location and bounds.size are required'); const bounds = { location: [args.bounds.location.x||0, args.bounds.location.y||0, args.bounds.location.z||0] as [number,number,number], size: [args.bounds.size.x||1000, args.bounds.size.y||1000, args.bounds.size.z||100] as [number,number,number] }; const res = await tools.buildEnvAdvanced.createProceduralFoliage({ name: args.name || 'ProceduralFoliage', bounds, foliageTypes: args.foliageTypes || [], seed: args.seed }); return cleanObject(res); } case 'add_foliage_instances': { if (!args.foliageType) throw new Error('foliageType is required'); if (!Array.isArray(args.transforms)) throw new Error('transforms array is required'); const transforms = (args.transforms as any[]).map(t => ({ location: [t.location?.x||0, t.location?.y||0, t.location?.z||0] as [number,number,number], rotation: t.rotation ? [t.rotation.pitch||0, t.rotation.yaw||0, t.rotation.roll||0] as [number,number,number] : undefined, scale: t.scale ? [t.scale.x||1, t.scale.y||1, t.scale.z||1] as [number,number,number] : undefined })); const res = await tools.buildEnvAdvanced.addFoliageInstances({ foliageType: args.foliageType, transforms }); return cleanObject(res); } case 'create_landscape_grass_type': { const res = await tools.buildEnvAdvanced.createLandscapeGrassType({ name: args.name || 'GrassType', meshPath: args.meshPath, density: args.density, minScale: args.minScale, maxScale: args.maxScale }); return cleanObject(res); } default: throw new Error(`Unknown environment action: ${args.action}`); } // 9. SYSTEM CONTROL case 'system_control': switch (requireAction(args)) { case 'read_log': { const filterCategoryRaw = args.filter_category; const filterCategory = Array.isArray(filterCategoryRaw) ? filterCategoryRaw : typeof filterCategoryRaw === 'string' && filterCategoryRaw.trim() !== '' ? filterCategoryRaw.split(',').map((s: string) => s.trim()).filter(Boolean) : undefined; const res = await tools.logTools.readOutputLog({ filterCategory, filterLevel: args.filter_level, lines: typeof args.lines === 'number' ? args.lines : undefined, logPath: typeof args.log_path === 'string' ? args.log_path : undefined, includePrefixes: Array.isArray(args.include_prefixes) ? args.include_prefixes : undefined, excludeCategories: Array.isArray(args.exclude_categories) ? args.exclude_categories : undefined }); return cleanObject(res); } case 'profile': { const res = await tools.performanceTools.startProfiling({ type: args.profileType, duration: args.duration }); return cleanObject(res); } case 'show_fps': { const res = await tools.performanceTools.showFPS({ enabled: !!args.enabled, verbose: !!args.verbose }); return cleanObject(res); } case 'set_quality': { const res = await tools.performanceTools.setScalability({ category: args.category, level: args.level }); return cleanObject(res); } case 'play_sound': { await elicitMissingPrimitiveArgs( tools, args, 'Provide the audio asset for system_control.play_sound', { soundPath: { type: 'string', title: 'Sound Asset Path', description: 'Asset path of the sound to play' } } ); const soundPath = requireNonEmptyString(args.soundPath, 'soundPath', 'Missing required parameter: soundPath'); if (args.location && typeof args.location === 'object') { const loc = [args.location.x || 0, args.location.y || 0, args.location.z || 0]; const res = await tools.audioTools.playSoundAtLocation({ soundPath, location: loc as [number, number, number], volume: args.volume, pitch: args.pitch, startTime: args.startTime }); return cleanObject(res); } const res = await tools.audioTools.playSound2D({ soundPath, volume: args.volume, pitch: args.pitch, startTime: args.startTime }); return cleanObject(res); } case 'create_widget': { await elicitMissingPrimitiveArgs( tools, args, 'Provide details for system_control.create_widget', { widgetName: { type: 'string', title: 'Widget Name', description: 'Name for the new UI widget asset' }, widgetType: { type: 'string', title: 'Widget Type', description: 'Widget type such as HUD, Menu, Overlay, etc.' } } ); const widgetName = requireNonEmptyString(args.widgetName ?? args.name, 'widgetName', 'Missing required parameter: widgetName'); const widgetType = requireNonEmptyString(args.widgetType, 'widgetType', 'Missing required parameter: widgetType'); const res = await tools.uiTools.createWidget({ name: widgetName, type: widgetType as any, savePath: args.savePath }); return cleanObject(res); } case 'show_widget': { const res = await tools.uiTools.setWidgetVisibility({ widgetName: args.widgetName, visible: args.visible !== false }); return cleanObject(res); } case 'screenshot': { const res = await tools.visualTools.takeScreenshot({ resolution: args.resolution }); return cleanObject(res); } case 'engine_start': { const res = await tools.engineTools.launchEditor({ editorExe: args.editorExe, projectPath: args.projectPath }); return cleanObject(res); } case 'engine_quit': { const res = await tools.engineTools.quitEditor(); return cleanObject(res); } default: throw new Error(`Unknown system action: ${args.action}`); } // 10. CONSOLE COMMAND - handle validation here case 'console_command': if (!args.command || typeof args.command !== 'string' || args.command.trim() === '') { return { success: true, message: 'Empty command' } as any; } // Basic safety filter const cmd = String(args.command).trim(); const blocked = [/\bquit\b/i, /\bexit\b/i, /debugcrash/i]; if (blocked.some(r => r.test(cmd))) { return { success: false, error: 'Command blocked for safety' } as any; } try { const raw = await tools.bridge.executeConsoleCommand(cmd); const summary = tools.bridge.summarizeConsoleCommand(cmd, raw); const output = summary.output || ''; const looksInvalid = /unknown|invalid/i.test(output); return cleanObject({ success: summary.returnValue !== false && !looksInvalid, command: summary.command, output: output || undefined, logLines: summary.logLines?.length ? summary.logLines : undefined, returnValue: summary.returnValue, message: !looksInvalid ? (output || 'Command executed') : undefined, error: looksInvalid ? output : undefined, raw: summary.raw }); } catch (e: any) { return cleanObject({ success: false, command: cmd, error: e?.message || String(e) }); } // 11. REMOTE CONTROL PRESETS - Direct implementation case 'manage_rc': // Handle RC operations directly through RcTools let rcResult: any; const rcAction = requireAction(args); switch (rcAction) { // Support both 'create_preset' and 'create' for compatibility case 'create_preset': case 'create': // Support both 'name' and 'presetName' parameter names const presetName = args.name || args.presetName; if (!presetName) throw new Error('Missing required parameter: name or presetName'); rcResult = await tools.rcTools.createPreset({ name: presetName, path: args.path }); // Return consistent output with presetId for tests if (rcResult.success) { rcResult.message = `Remote Control preset created: ${presetName}`; // Ensure presetId is set (for test compatibility) if (rcResult.presetPath && !rcResult.presetId) { rcResult.presetId = rcResult.presetPath; } } break; case 'list': // List all presets - implement via RcTools rcResult = await tools.rcTools.listPresets(); break; case 'delete': case 'delete_preset': const presetIdentifier = args.presetId || args.presetPath; if (!presetIdentifier) throw new Error('Missing required parameter: presetId'); rcResult = await tools.rcTools.deletePreset(presetIdentifier); if (rcResult.success) { rcResult.message = 'Preset deleted successfully'; } break; case 'expose_actor': if (!args.presetPath) throw new Error('Missing required parameter: presetPath'); if (!args.actorName) throw new Error('Missing required parameter: actorName'); rcResult = await tools.rcTools.exposeActor({ presetPath: args.presetPath, actorName: args.actorName }); if (rcResult.success) { rcResult.message = `Actor '${args.actorName}' exposed to preset`; } break; case 'expose_property': case 'expose': // Support simplified name from tests // Support both presetPath and presetId const presetPathExp = args.presetPath || args.presetId; if (!presetPathExp) throw new Error('Missing required parameter: presetPath or presetId'); if (!args.objectPath) throw new Error('Missing required parameter: objectPath'); if (!args.propertyName) throw new Error('Missing required parameter: propertyName'); rcResult = await tools.rcTools.exposeProperty({ presetPath: presetPathExp, objectPath: args.objectPath, propertyName: args.propertyName }); if (rcResult.success) { rcResult.message = `Property '${args.propertyName}' exposed to preset`; } break; case 'list_fields': case 'get_exposed': // Support test naming const presetPathList = args.presetPath || args.presetId; if (!presetPathList) throw new Error('Missing required parameter: presetPath or presetId'); rcResult = await tools.rcTools.listFields({ presetPath: presetPathList }); // Map 'fields' to 'exposedProperties' for test compatibility if (rcResult.success && rcResult.fields) { rcResult.exposedProperties = rcResult.fields; } break; case 'set_property': case 'set_value': // Support test naming // Support both patterns const objPathSet = args.objectPath || args.presetId; const propNameSet = args.propertyName || args.propertyLabel; if (!objPathSet) throw new Error('Missing required parameter: objectPath or presetId'); if (!propNameSet) throw new Error('Missing required parameter: propertyName or propertyLabel'); if (args.value === undefined) throw new Error('Missing required parameter: value'); rcResult = await tools.rcTools.setProperty({ objectPath: objPathSet, propertyName: propNameSet, value: args.value }); if (rcResult.success) { rcResult.message = `Property '${propNameSet}' value updated`; } break; case 'get_property': case 'get_value': // Support test naming const objPathGet = args.objectPath || args.presetId; const propNameGet = args.propertyName || args.propertyLabel; if (!objPathGet) throw new Error('Missing required parameter: objectPath or presetId'); if (!propNameGet) throw new Error('Missing required parameter: propertyName or propertyLabel'); rcResult = await tools.rcTools.getProperty({ objectPath: objPathGet, propertyName: propNameGet }); break; case 'call_function': if (!args.presetId) throw new Error('Missing required parameter: presetId'); if (!args.functionLabel) throw new Error('Missing required parameter: functionLabel'); // For now, return not implemented rcResult = { success: false, error: 'Function calls not yet implemented' }; break; default: throw new Error(`Unknown RC action: ${rcAction}. Valid actions are: create_preset, expose_actor, expose_property, list_fields, set_property, get_property, or their simplified versions: create, list, delete, expose, get_exposed, set_value, get_value, call_function`); } // Return result directly - MCP formatting will be handled by response validator // Clean to prevent circular references return cleanObject(rcResult); // 12. SEQUENCER / CINEMATICS case 'manage_sequence': // Direct handling for sequence operations const seqResult = await (async () => { const sequenceTools = tools.sequenceTools; if (!sequenceTools) throw new Error('Sequence tools not available'); const action = requireAction(args); switch (action) { case 'create': return await sequenceTools.create({ name: args.name, path: args.path }); case 'open': return await sequenceTools.open({ path: args.path }); case 'add_camera': return await sequenceTools.addCamera({ spawnable: args.spawnable !== false }); case 'add_actor': return await sequenceTools.addActor({ actorName: args.actorName }); case 'add_actors': if (!args.actorNames) throw new Error('Missing required parameter: actorNames'); return await sequenceTools.addActors({ actorNames: args.actorNames }); case 'remove_actors': if (!args.actorNames) throw new Error('Missing required parameter: actorNames'); return await sequenceTools.removeActors({ actorNames: args.actorNames }); case 'get_bindings': return await sequenceTools.getBindings({ path: args.path }); case 'add_spawnable_from_class': if (!args.className) throw new Error('Missing required parameter: className'); return await sequenceTools.addSpawnableFromClass({ className: args.className, path: args.path }); case 'play': return await sequenceTools.play({ loopMode: args.loopMode }); case 'pause': return await sequenceTools.pause(); case 'stop': return await sequenceTools.stop(); case 'set_properties': return await sequenceTools.setSequenceProperties({ path: args.path, frameRate: args.frameRate, lengthInFrames: args.lengthInFrames, playbackStart: args.playbackStart, playbackEnd: args.playbackEnd }); case 'get_properties': return await sequenceTools.getSequenceProperties({ path: args.path }); case 'set_playback_speed': if (args.speed === undefined) throw new Error('Missing required parameter: speed'); return await sequenceTools.setPlaybackSpeed({ speed: args.speed }); default: throw new Error(`Unknown sequence action: ${action}`); } })(); // Return result directly - MCP formatting will be handled by response validator // Clean to prevent circular references return cleanObject(seqResult); // 13. INTROSPECTION case 'inspect': const inspectAction = requireAction(args); switch (inspectAction) { case 'inspect_object': { const res = await tools.introspectionTools.inspectObject({ objectPath: args.objectPath, detailed: args.detailed }); return cleanObject(res); } case 'set_property': { const res = await tools.introspectionTools.setProperty({ objectPath: args.objectPath, propertyName: args.propertyName, value: args.value }); return cleanObject(res); } default: throw new Error(`Unknown inspect action: ${inspectAction}`); } default: throw new Error(`Unknown consolidated tool: ${name}`); } // All cases return (or throw) above; this is a type guard for exhaustiveness. } catch (err: any) { const duration = Date.now() - startTime; console.log(`[ConsolidatedToolHandler] Failed execution of ${name} after ${duration}ms: ${err?.message || String(err)}`); // Return consistent error structure matching regular tool handlers const errorMessage = err?.message || String(err); const isTimeout = errorMessage.includes('timeout'); return { content: [{ type: 'text', text: isTimeout ? `Tool ${name} timed out. Please check Unreal Engine connection.` : `Failed to execute ${name}: ${errorMessage}` }], isError: true }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiR24/Unreal_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server