Skip to main content
Glama
introspection.ts29.1 kB
import { UnrealBridge } from '../unreal-bridge.js'; import { Logger } from '../utils/logger.js'; import { bestEffortInterpretedText, interpretStandardResult } from '../utils/result-helpers.js'; export interface ObjectInfo { class: string; name: string; path: string; properties: PropertyInfo[]; functions?: FunctionInfo[]; parent?: string; interfaces?: string[]; flags?: string[]; } export interface PropertyInfo { name: string; type: string; value?: any; flags?: string[]; metadata?: Record<string, any>; category?: string; tooltip?: string; } export interface FunctionInfo { name: string; parameters: ParameterInfo[]; returnType?: string; flags?: string[]; category?: string; } export interface ParameterInfo { name: string; type: string; defaultValue?: any; isOptional?: boolean; } export class IntrospectionTools { private log = new Logger('IntrospectionTools'); private objectCache = new Map<string, ObjectInfo>(); private retryAttempts = 3; private retryDelay = 1000; constructor(private bridge: UnrealBridge) {} /** * Execute with retry logic for transient failures */ private async executeWithRetry<T>( operation: () => Promise<T>, operationName: string ): Promise<T> { let lastError: any; for (let attempt = 1; attempt <= this.retryAttempts; attempt++) { try { return await operation(); } catch (error: any) { lastError = error; this.log.warn(`${operationName} attempt ${attempt} failed: ${error.message || error}`); if (attempt < this.retryAttempts) { await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt) ); } } } throw lastError; } /** * Parse Python execution result with better error handling */ private parsePythonResult(resp: any, operationName: string): any { const interpreted = interpretStandardResult(resp, { successMessage: `${operationName} succeeded`, failureMessage: `${operationName} failed` }); if (interpreted.success) { return { ...interpreted.payload, success: true }; } const output = bestEffortInterpretedText(interpreted) ?? ''; if (output) { this.log.error(`Failed to parse ${operationName} result: ${output}`); } if (output.includes('ModuleNotFoundError')) { return { success: false, error: 'Reflection module not available.' }; } if (output.includes('AttributeError')) { return { success: false, error: 'Reflection API method not found. Check Unreal Engine version compatibility.' }; } return { success: false, error: `${interpreted.error ?? `${operationName} did not return a valid result`}: ${output.substring(0, 200)}` }; } /** * Convert Unreal property value to JavaScript-friendly format */ private convertPropertyValue(value: any, typeName: string): any { // Handle vectors, rotators, transforms if (typeName.includes('Vector')) { if (typeof value === 'object' && value !== null) { return { x: value.X || 0, y: value.Y || 0, z: value.Z || 0 }; } } if (typeName.includes('Rotator')) { if (typeof value === 'object' && value !== null) { return { pitch: value.Pitch || 0, yaw: value.Yaw || 0, roll: value.Roll || 0 }; } } if (typeName.includes('Transform')) { if (typeof value === 'object' && value !== null) { return { location: this.convertPropertyValue(value.Translation || value.Location, 'Vector'), rotation: this.convertPropertyValue(value.Rotation, 'Rotator'), scale: this.convertPropertyValue(value.Scale3D || value.Scale, 'Vector') }; } } return value; } async inspectObject(params: { objectPath: string; detailed?: boolean }) { // Check cache first if not requesting detailed info if (!params.detailed && this.objectCache.has(params.objectPath)) { const cached = this.objectCache.get(params.objectPath); if (cached) { return { success: true, info: cached }; } } const py = ` import unreal, json, inspect path = r"${params.objectPath}" detailed = ${params.detailed ? 'True' : 'False'} def get_property_info(prop, obj=None): """Extract detailed property information""" try: info = { 'name': prop.get_name(), 'type': prop.get_property_class_name() if hasattr(prop, 'get_property_class_name') else 'Unknown' } # Try to get property flags flags = [] if hasattr(prop, 'has_any_property_flags'): if prop.has_any_property_flags(unreal.PropertyFlags.CPF_EDIT_CONST): flags.append('ReadOnly') if prop.has_any_property_flags(unreal.PropertyFlags.CPF_BLUEPRINT_READ_ONLY): flags.append('BlueprintReadOnly') if prop.has_any_property_flags(unreal.PropertyFlags.CPF_TRANSIENT): flags.append('Transient') info['flags'] = flags # Try to get metadata if hasattr(prop, 'get_metadata'): try: info['category'] = prop.get_metadata('Category') info['tooltip'] = prop.get_metadata('ToolTip') except Exception: pass # Try to get current value if object provided if obj and detailed: try: value = getattr(obj, prop.get_name()) # Convert complex types to serializable format if hasattr(value, '__dict__'): value = str(value) info['value'] = value except Exception: pass return info except Exception as e: return {'name': str(prop) if prop else 'Unknown', 'type': 'Unknown', 'error': str(e)} try: obj = unreal.load_object(None, path) if not obj: # Try as class if object load fails try: obj = unreal.load_class(None, path) if not obj: print('RESULT:' + json.dumps({'success': False, 'error': 'Object or class not found'})) raise SystemExit(0) except Exception: print('RESULT:' + json.dumps({'success': False, 'error': 'Object not found'})) raise SystemExit(0) info = { 'class': obj.get_class().get_name() if hasattr(obj, 'get_class') else str(type(obj)), 'name': obj.get_name() if hasattr(obj, 'get_name') else '', 'path': path, 'properties': [], 'functions': [], 'flags': [] } # Get parent class try: if hasattr(obj, 'get_class'): cls = obj.get_class() if hasattr(cls, 'get_super_class'): super_cls = cls.get_super_class() if super_cls: info['parent'] = super_cls.get_name() except Exception: pass # Get object flags try: if hasattr(obj, 'has_any_flags'): flags = [] if obj.has_any_flags(unreal.ObjectFlags.RF_PUBLIC): flags.append('Public') if obj.has_any_flags(unreal.ObjectFlags.RF_TRANSIENT): flags.append('Transient') if obj.has_any_flags(unreal.ObjectFlags.RF_DEFAULT_SUB_OBJECT): flags.append('DefaultSubObject') info['flags'] = flags except Exception: pass # Get properties - AVOID deprecated properties completely props = [] # List of deprecated properties to completely skip deprecated_props = [ 'life_span', 'on_actor_touch', 'on_actor_un_touch', 'get_touching_actors', 'get_touching_components', 'controller_class', 'look_up_scale', 'sound_wave_param', 'material_substitute', 'texture_object' ] try: cls = obj.get_class() if hasattr(obj, 'get_class') else obj # Try UE5 reflection API with safe property access if hasattr(cls, 'get_properties'): for prop in cls.get_properties(): prop_name = None try: # Safe property name extraction if hasattr(prop, 'get_name'): prop_name = prop.get_name() elif hasattr(prop, 'name'): prop_name = prop.name # Skip if deprecated if prop_name and prop_name not in deprecated_props: prop_info = get_property_info(prop, obj if detailed else None) if prop_info.get('name') not in deprecated_props: props.append(prop_info) except Exception: pass # If reflection API didn't work, use a safe property list if not props: # Only access known safe properties safe_properties = [ 'actor_guid', 'actor_instance_guid', 'always_relevant', 'auto_destroy_when_finished', 'can_be_damaged', 'content_bundle_guid', 'custom_time_dilation', 'enable_auto_lod_generation', 'find_camera_component_when_view_target', 'generate_overlap_events_during_level_streaming', 'hidden', 'initial_life_span', # Use new name instead of life_span 'instigator', 'is_spatially_loaded', 'min_net_update_frequency', 'net_cull_distance_squared', 'net_dormancy', 'net_priority', 'net_update_frequency', 'net_use_owner_relevancy', 'only_relevant_to_owner', 'pivot_offset', 'replicate_using_registered_sub_object_list', 'replicates', 'root_component', 'runtime_grid', 'spawn_collision_handling_method', 'sprite_scale', 'tags', 'location', 'rotation', 'scale' ] for prop_name in safe_properties: try: # Use get_editor_property for safer access if hasattr(obj, 'get_editor_property'): val = obj.get_editor_property(prop_name) props.append({ 'name': prop_name, 'type': type(val).__name__ if val is not None else 'None', 'value': str(val)[:100] if detailed and val is not None else None }) elif hasattr(obj, prop_name): # Direct access only for safe properties val = getattr(obj, prop_name) if not callable(val): props.append({ 'name': prop_name, 'type': type(val).__name__, 'value': str(val)[:100] if detailed else None }) except Exception: pass except Exception as e: # Minimal fallback with only essential safe properties pass info['properties'] = props # Get functions/methods if detailed if detailed: funcs = [] try: cls = obj.get_class() if hasattr(obj, 'get_class') else obj # Try to get UFunctions if hasattr(cls, 'get_functions'): for func in cls.get_functions(): func_info = { 'name': func.get_name(), 'parameters': [], 'flags': [] } # Get parameters if possible if hasattr(func, 'get_params'): for param in func.get_params(): func_info['parameters'].append({ 'name': param.get_name() if hasattr(param, 'get_name') else str(param), 'type': 'Unknown' }) funcs.append(func_info) else: # Fallback: use known safe function names safe_functions = [ 'get_actor_location', 'set_actor_location', 'get_actor_rotation', 'set_actor_rotation', 'get_actor_scale', 'set_actor_scale', 'destroy_actor', 'destroy_component', 'get_components', 'get_component_by_class', 'add_actor_component', 'add_component', 'get_world', 'get_name', 'get_path_name', 'is_valid', 'is_a', 'has_authority' ] for func_name in safe_functions: if hasattr(obj, func_name): try: attr_value = getattr(obj, func_name) if callable(attr_value): funcs.append({ 'name': func_name, 'parameters': [], 'flags': [] }) except Exception: pass except Exception: pass info['functions'] = funcs print('RESULT:' + json.dumps({'success': True, 'info': info})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': str(e)})) `.trim(); const resp = await this.executeWithRetry( () => this.bridge.executePython(py), 'inspectObject' ); const result = this.parsePythonResult(resp, 'inspectObject'); // Cache the result if successful and not detailed if (result.success && result.info && !params.detailed) { this.objectCache.set(params.objectPath, result.info); } return result; } async setProperty(params: { objectPath: string; propertyName: string; value: any }) { return this.executeWithRetry(async () => { try { // Validate and convert value type if needed let processedValue = params.value; // Handle special Unreal types if (typeof params.value === 'object' && params.value !== null) { // Vector conversion if ('x' in params.value || 'X' in params.value) { processedValue = { X: params.value.x || params.value.X || 0, Y: params.value.y || params.value.Y || 0, Z: params.value.z || params.value.Z || 0 }; } // Rotator conversion else if ('pitch' in params.value || 'Pitch' in params.value) { processedValue = { Pitch: params.value.pitch || params.value.Pitch || 0, Yaw: params.value.yaw || params.value.Yaw || 0, Roll: params.value.roll || params.value.Roll || 0 }; } // Transform conversion else if ('location' in params.value || 'Location' in params.value) { processedValue = { Translation: this.convertPropertyValue( params.value.location || params.value.Location, 'Vector' ), Rotation: this.convertPropertyValue( params.value.rotation || params.value.Rotation, 'Rotator' ), Scale3D: this.convertPropertyValue( params.value.scale || params.value.Scale || {x: 1, y: 1, z: 1}, 'Vector' ) }; } } const res = await this.bridge.httpCall('/remote/object/property', 'PUT', { objectPath: params.objectPath, propertyName: params.propertyName, propertyValue: processedValue }); // Clear cache for this object this.objectCache.delete(params.objectPath); return { success: true, result: res }; } catch (err: any) { const errorMsg = err?.message || String(err); if (errorMsg.includes('404')) { return { success: false, error: `Property '${params.propertyName}' not found on object '${params.objectPath}'` }; } if (errorMsg.includes('400')) { return { success: false, error: `Invalid value type for property '${params.propertyName}'` }; } return { success: false, error: errorMsg }; } }, 'setProperty'); } /** * Get property value of an object */ async getProperty(params: { objectPath: string; propertyName: string }) { const py = ` import unreal, json path = r"${params.objectPath}" prop_name = r"${params.propertyName}" try: obj = unreal.load_object(None, path) if not obj: print('RESULT:' + json.dumps({'success': False, 'error': 'Object not found'})) else: # Try different methods to get property value = None found = False # Method 1: Direct attribute access if hasattr(obj, prop_name): try: value = getattr(obj, prop_name) found = True except Exception: pass # Method 2: get_editor_property (UE4/5) if not found and hasattr(obj, 'get_editor_property'): try: value = obj.get_editor_property(prop_name) found = True except Exception: pass # Method 3: Try with common property name variations if not found: # Try common property name variations variations = [ prop_name, prop_name.lower(), prop_name.upper(), prop_name.capitalize(), # Convert snake_case to CamelCase ''.join(word.capitalize() for word in prop_name.split('_')), # Convert CamelCase to snake_case ''.join(['_' + c.lower() if c.isupper() else c for c in prop_name]).lstrip('_') ] for variant in variations: if hasattr(obj, variant): try: value = getattr(obj, variant) found = True break except Exception: pass if found: # Convert complex types to string if hasattr(value, '__dict__'): value = str(value) elif isinstance(value, (list, tuple, dict)): value = json.dumps(value) print('RESULT:' + json.dumps({'success': True, 'value': value})) else: print('RESULT:' + json.dumps({'success': False, 'error': f'Property {prop_name} not found'})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': str(e)})) `.trim(); const resp = await this.executeWithRetry( () => this.bridge.executePython(py), 'getProperty' ); return this.parsePythonResult(resp, 'getProperty'); } /** * Call a function on an object */ async callFunction(params: { objectPath: string; functionName: string; parameters?: any[]; }) { const py = ` import unreal, json path = r"${params.objectPath}" func_name = r"${params.functionName}" params = ${JSON.stringify(params.parameters || [])} try: obj = unreal.load_object(None, path) if not obj: # Try loading as class if object fails try: obj = unreal.load_class(None, path) except: pass if not obj: print('RESULT:' + json.dumps({'success': False, 'error': 'Object not found'})) else: # For KismetMathLibrary or similar utility classes, use static method call if 'KismetMathLibrary' in path or 'MathLibrary' in path or 'GameplayStatics' in path: try: # Use Unreal's MathLibrary (KismetMathLibrary is exposed as MathLibrary in Python) if func_name.lower() == 'abs': # Use Unreal's MathLibrary.abs function result = unreal.MathLibrary.abs(float(params[0])) if params else 0 print('RESULT:' + json.dumps({'success': True, 'result': result})) elif func_name.lower() == 'sqrt': # Use Unreal's MathLibrary.sqrt function result = unreal.MathLibrary.sqrt(float(params[0])) if params else 0 print('RESULT:' + json.dumps({'success': True, 'result': result})) else: # Try to call as static method if hasattr(obj, func_name): func = getattr(obj, func_name) if callable(func): result = func(*params) if params else func() if hasattr(result, '__dict__'): result = str(result) print('RESULT:' + json.dumps({'success': True, 'result': result})) else: print('RESULT:' + json.dumps({'success': False, 'error': f'{func_name} is not callable'})) else: # Try snake_case version snake_case_name = ''.join(['_' + c.lower() if c.isupper() else c for c in func_name]).lstrip('_') if hasattr(obj, snake_case_name): func = getattr(obj, snake_case_name) result = func(*params) if params else func() print('RESULT:' + json.dumps({'success': True, 'result': result})) else: print('RESULT:' + json.dumps({'success': False, 'error': f'Function {func_name} not found'})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': f'Function call failed: {str(e)}'})) else: # Regular object method call if hasattr(obj, func_name): func = getattr(obj, func_name) if callable(func): try: result = func(*params) if params else func() # Convert result to serializable format if hasattr(result, '__dict__'): result = str(result) print('RESULT:' + json.dumps({'success': True, 'result': result})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': f'Function call failed: {str(e)}'})) else: print('RESULT:' + json.dumps({'success': False, 'error': f'{func_name} is not callable'})) else: print('RESULT:' + json.dumps({'success': False, 'error': f'Function {func_name} not found'})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': str(e)})) `.trim(); const resp = await this.executeWithRetry( () => this.bridge.executePython(py), 'callFunction' ); return this.parsePythonResult(resp, 'callFunction'); } /** * Get Class Default Object (CDO) for a class */ async getCDO(className: string) { const py = ` import unreal, json class_name = r"${className}" try: # Try to find the class cls = None # Method 1: Direct class load try: cls = unreal.load_class(None, class_name) except Exception: pass # Method 2: Find class by name if not cls: try: cls = unreal.find_class(class_name) except Exception: pass # Method 3: Search in loaded classes if not cls: for obj in unreal.ObjectLibrary.get_all_objects(): if hasattr(obj, 'get_class'): obj_cls = obj.get_class() if obj_cls.get_name() == class_name: cls = obj_cls break if not cls: print('RESULT:' + json.dumps({'success': False, 'error': 'Class not found'})) else: # Get CDO cdo = cls.get_default_object() if hasattr(cls, 'get_default_object') else None if cdo: info = { 'className': cls.get_name(), 'cdoPath': cdo.get_path_name() if hasattr(cdo, 'get_path_name') else '', 'properties': [] } # Get default property values using safe property list safe_cdo_properties = [ 'initial_life_span', 'hidden', 'can_be_damaged', 'replicates', 'always_relevant', 'net_dormancy', 'net_priority', 'net_update_frequency', 'replicate_movement', 'actor_guid', 'tags', 'root_component', 'auto_destroy_when_finished', 'enable_auto_lod_generation' ] for prop_name in safe_cdo_properties: try: if hasattr(cdo, 'get_editor_property'): value = cdo.get_editor_property(prop_name) info['properties'].append({ 'name': prop_name, 'defaultValue': str(value)[:100] }) elif hasattr(cdo, prop_name): value = getattr(cdo, prop_name) if not callable(value): info['properties'].append({ 'name': prop_name, 'defaultValue': str(value)[:100] }) except Exception: pass print('RESULT:' + json.dumps({'success': True, 'cdo': info})) else: print('RESULT:' + json.dumps({'success': False, 'error': 'Could not get CDO'})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': str(e)})) `.trim(); const resp = await this.executeWithRetry( () => this.bridge.executePython(py), 'getCDO' ); return this.parsePythonResult(resp, 'getCDO'); } /** * Search for objects by class */ async findObjectsByClass(className: string, limit: number = 100) { const py = ` import unreal, json class_name = r"${className}" limit = ${limit} try: objects = [] count = 0 # Use EditorAssetLibrary to find assets try: all_assets = unreal.EditorAssetLibrary.list_assets("/Game", recursive=True) for asset_path in all_assets: if count >= limit: break try: asset = unreal.EditorAssetLibrary.load_asset(asset_path) if asset: asset_class = asset.get_class() if hasattr(asset, 'get_class') else None if asset_class and class_name in asset_class.get_name(): objects.append({ 'path': asset_path, 'name': asset.get_name() if hasattr(asset, 'get_name') else '', 'class': asset_class.get_name() }) count += 1 except Exception: pass except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': f'Asset search failed: {str(e)}'})) raise SystemExit(0) # Also search in level actors try: actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) if actor_sub: for actor in actor_sub.get_all_level_actors(): if count >= limit: break if actor: actor_class = actor.get_class() if hasattr(actor, 'get_class') else None if actor_class and class_name in actor_class.get_name(): objects.append({ 'path': actor.get_path_name() if hasattr(actor, 'get_path_name') else '', 'name': actor.get_actor_label() if hasattr(actor, 'get_actor_label') else '', 'class': actor_class.get_name() }) count += 1 except Exception: pass print('RESULT:' + json.dumps({'success': True, 'objects': objects, 'count': len(objects)})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': str(e)})) `.trim(); const resp = await this.executeWithRetry( () => this.bridge.executePython(py), 'findObjectsByClass' ); return this.parsePythonResult(resp, 'findObjectsByClass'); } /** * Clear object cache */ clearCache(): void { this.objectCache.clear(); } }

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