feat: add SCS-template lookup to set_property/get_property for Blueprint-added components#358
Conversation
…ync in pipeline handlers - **🚨 Severity:** LOW / HARDENING - **💡 Issue:** Pipeline handlers (`src/tools/handlers/pipeline-handlers.ts`) used blocking, synchronous operations like `fs.readdirSync`, `fs.readFileSync`, `fs.accessSync`, and `execSync` to locate the UnrealBuildTool. - **🎯 Impact:** These operations block the Node.js event loop, preventing the MCP server from processing concurrent requests or heartbeats, which could lead to unresponsiveness or Denial of Service (DoS) if directories are large or drives are slow. - **🔧 Fix:** Refactored `tryUbtpath` and `findUbtExecutable` to use their asynchronous equivalents (`fs.promises.access`, `fs.promises.readdir`, `fs.promises.readFile`, and `util.promisify(exec)`). - **✅ Verification:** `npm run lint`, `npm run type-check`, `npm run test:unit`, and `npm run build` pass without issue. - **📝 Pattern Used:** Used `fs.promises` and `util.promisify` consistent with asynchronous event loop best practices in Node.js.
…e sentinel.md date Agent-Logs-Url: https://github.com/ChiR24/Unreal_mcp/sessions/50f83e4e-aea5-489f-aa86-dfbe1b87be29 Co-authored-by: ChiR24 <125826529+ChiR24@users.noreply.github.com>
…ync in pipeline handlers - **🚨 Severity:** LOW / HARDENING - **💡 Issue:** Pipeline handlers (`src/tools/handlers/pipeline-handlers.ts`) used blocking, synchronous operations like `fs.readdirSync`, `fs.readFileSync`, `fs.accessSync`, and `execSync` to locate the UnrealBuildTool. - **🎯 Impact:** These operations block the Node.js event loop, preventing the MCP server from processing concurrent requests or heartbeats, which could lead to unresponsiveness or Denial of Service (DoS) if directories are large or drives are slow. - **🔧 Fix:** Refactored `tryUbtpath` and `findUbtExecutable` to use their asynchronous equivalents (`fs.promises.access`, `fs.promises.readdir`, `fs.promises.readFile`, and `util.promisify(exec)`). Fixed bug where `tryUbtpath` was incorrectly returning an error on Linux machines by rejecting .dlls with `fs.constants.X_OK`. - **✅ Verification:** `npm run lint`, `npm run type-check`, `npm run test:unit`, and `npm run build` pass without issue. - **📝 Pattern Used:** Used `fs.promises` and `util.promisify` consistent with asynchronous event loop best practices in Node.js.
…peline-5414201795584645671 🛡️ Sentinel: [Hardening] Refactor synchronous FS and exec calls to async in pipeline handlers
get_material_info, add_material_node, get_material_node_details, and use_material_function now accept UMaterialFunction paths in addition to UMaterial. Added get_material_function_info sub-action that returns function inputs (name, EFunctionInputType, preview values, sort priority) and outputs. use_material_function supports MF-in-MF calls with self-reference guard. Includes MCP_GET_FUNCTION_EXPRESSIONS version-compat macro for UE 5.0-5.7+ and improved path validation using FPackageName::IsValidLongPackageName. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ies, batch operations New actions: - get_node_properties: full property dump per node type (CE code/inputs, params, MFC pins, generic UPROPERTY fallback) - set_static_switch_parameter_value: set static switch on material instances - delete_node: batch removal with auto-disconnect (single or array) - update_custom_expression: modify code/inputs/outputs of existing CE - get_node_chain: BFS path trace between two nodes or to a material pin - get_connected_subgraph: island detection with orphansOnly flag Enhanced actions: - get_node_connections: direction/depth/upstream/downstream graph traversal with hop distances, fixes missing output connections - get_material_info: filter=expressions includes CE code, filter=connections supports nodeId/nodeIds filtering - find_node: deduplicated results with connectionCount - compile_material: UMaterialFunction fallback - connect_nodes: MaterialFunctionCall input/output pin iteration - set_blend_mode/set_shading_model/set_material_domain: graceful MF rejection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ndling GUID-based node IDs caused silent collisions on copy-pasted expressions. Switch to GetName() (e.g. "MaterialExpressionCustom_0") as canonical ID, add MCP_NODE_ID() macro, template-based FindExpressionInArray with 5-tier resolution (expr_N index, object name, GUID backwards-compat with collision warning, full path, semantic name match). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
auto* range-for fails to deduce type from TObjectPtr elements in UE 5.1+. Switch to indexed for-loops with explicit static_cast<UMaterialExpression*>. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The path normalization pattern `if (!StartsWith("/Game/")) prepend "/Game/"`
clobbered valid plugin mount paths like /EterniaCore/. Since
SanitizeProjectRelativePath already validates against registered engine
mount points via FPackageName::IsValidLongPackageName, only prepend
/Game/ for bare relative paths that lack a leading /.
Also accept 'path' as an alias for 'folder' in all widget creation
handlers (create_widget_blueprint, create_settings_menu, etc.).
Fixed in: WidgetAuthoringHandlers, AudioHandlers, LevelStructureHandlers,
McpAutomationBridgeHelpers.h (ValidateAndBuildAssetPath).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
set_property previously required objectPath which couldn't resolve Blueprint Class Default Objects (CDO path format Default__ClassName_C is not findable via standard object resolution). Now accepts blueprintPath as an alternative — loads the Blueprint, gets GeneratedClass->GetDefaultObject(), and mutates properties on the CDO directly. This enables scripted style/theme authoring for CommonUI button styles and other Blueprint-based data assets. Also fixes ResolveObjectFromPath to support plugin content mount points (e.g. /EterniaCore/) — previously only /Game/, /Engine/, /Script/ were recognized. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ImportText Uses FProperty::ImportText_Direct for fully reflection-based property setting on any widget class. Supports two modes: - Legacy: pass style param to set the Style property (backwards compat) - Generic: pass propertyName + value to set any property via UE reflection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Read mode: omit value to export current property value via ExportText - Write mode: Modify() + ImportText + PostEditChangeProperty instead of full blueprint recompile (MarkBlueprintAsStructurallyModified only for structural changes like clipping) - Returns exportedValue for write verification - Validates propertyName instead of value (value empty = read mode) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…operty Add IsValid() check before accessing Payload fields to prevent null dereference on malformed requests, matching HandleGetObjectProperty. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- set_style: use bHasValueField (field presence) instead of Value.IsEmpty() to distinguish read mode (omitted field) from write with empty string - set_style: coerce JSON number/bool values to string for ImportText - set_style: add UE 5.0 compat (#if for ImportText vs ImportText_Direct) - set_style: use MCP_PROPERTY_EXPORT_TEXT macro for ExportText compat - set_style: restore MarkBlueprintAsStructurallyModified for set_clipping, remove unreachable dead else branch - inspect:set_property: normalize blueprintPath (trim + strip trailing /) and add tool context to validation error message - pipeline-handlers: add 5s timeout to execAsync PATH lookup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nections error, path sanitization - Rename nodeGuid to nodeId in add_material_node response - Add AddVerification to connect_material_pins response - Guard FinalizeHost behind bCleared and report INVALID_PIN for unknown pins - Add SanitizeProjectRelativePath to all 7 remaining widget template actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Accept 'blueprint_path' as alias and route through the standard normalizeArgs pipeline instead of raw extractOptionalString. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Accept 'blueprint_path' as alias and route through the standard normalizeArgs pipeline instead of raw extractOptionalString. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ion, perf - Move inspect_cdo to Added section in CHANGELOG - Add SanitizeProjectRelativePath to all 5 audio handler path locations - Trim objectPath/blueprintPath in PropertyHandlers before emptiness check - Handle JSON object/array/null types in set_style value parsing - Reject invalid template paths instead of silently defaulting - Allow 2-segment plugin paths in ValidateAssetPath - Add outputSchema fields for new material graph query actions - Validate boolean for set_static_switch_parameter_value - Reject delete_node with no nodeId/nodeIds - Skip shader recompile on add_material_node (MarkPackageDirty only) - Fix double recompile in HandleAddMaterialParameter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…zation, docstrings - Parse sourcePin and propagate OutputIndex on all material connection sites - Fail fast on unresolved targetNodeId instead of silently routing to main node - Skip runtime actor setters on CDOs (won't persist to Blueprint defaults) - Replace MarkBlueprintAsStructurallyModified with MarkPackageDirty for set_clipping - Add normalizeAssetPath for material function/info handlers (/Content/→/Game/) - Reject empty update_custom_expression requests - Add docstrings to reach 80% coverage threshold Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sonObjectConverter for structs, normalizeAssetPath fixes - Remove unnecessary AsyncTask wrapper in rebuild_material (already on game thread) - Re-sanitize FullPath after combining directory + asset name in all 5 audio handler locations - Use FJsonObjectConverter::JsonValueToUProperty for struct-backed widget properties instead of string serialization - Fix normalizeAssetPath to handle Content/Foo and Game/Foo correctly - Add functionPath alias to all graph query/mutation cases - Apply normalizeAssetPath to all assetPath extractions in graph cases - Add section comments for docstring coverage on graph query cases - Use ConnectMainInput lambda for OutputIndex propagation on all 11 main material pins Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…riable fix, action list update - Remove unreachable 4-parameter HandleGetAssetGraph overload (dead code) - Rename misleading RemovedGuid to RemovedStableName (holds UObject name, not GUID) - Move GetHostExpressions/ExpressionIndex into correct preprocessor branches to avoid unused-variable warning on UE 5.1+ - Update default error message to list all available material authoring actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… docs - ResolveSoundWaveAsset: try UEditorAssetLibrary first for package-style paths - Fix misleading error message when SoundWave asset not found - Add SanitizeProjectRelativePath inside LoadWidgetBlueprint for all callers - Correct sourcePin description to document index-or-name usage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ssets - LoadWidgetBlueprint: prefix /Game/ before SanitizeProjectRelativePath - All widget template creators: normalize relative folder paths before sanitizing - SoundAttenuation: register with asset system even when save=false Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| if (res.success === false) { | ||
| return ResponseFactory.error(res.error ?? 'Failed to set blend mode', res.errorCode); | ||
| } | ||
| return ResponseFactory.success(res, res.message ?? `Blend mode set to ${blendMode}`); |
There was a problem hiding this comment.
🟡 Inconsistent normalizeAssetPath application causes some material authoring actions to reject paths that others accept
The newly introduced normalizeAssetPath helper (which converts Content/... → /Game/..., adds leading /, etc.) is applied to some material authoring actions but not others. For example, create_material at line 80 normalizes the path arg, and get_material_info at line 942 normalizes assetPath, but set_blend_mode at line 122, add_texture_sample at line 205, add_scalar_parameter at line 273, and many other expression-adding actions pass assetPath straight through without normalization. A user who creates a material with Content/Materials/M_Test (normalized to /Game/Materials/M_Test) would then fail calling set_blend_mode or add_scalar_parameter with the same Content/Materials/M_Test string, since the C++ SanitizeProjectRelativePath rejects /Content/ as an invalid root.
(Refers to lines 119-141)
Prompt for agents
The normalizeAssetPath function is applied inconsistently across material authoring actions. It is applied to create_material, create_material_instance, use_material_function, get_material_info, find_node, get_node_connections, get_node_properties, set_static_switch_parameter_value, delete_node, update_custom_expression, get_node_chain, get_connected_subgraph, get_material_function_info, add_material_node, and set_material_parameter. But it is NOT applied to set_blend_mode, set_shading_model, set_material_domain, add_texture_sample, add_texture_coordinate, add_scalar_parameter, add_vector_parameter, add_static_switch_parameter, add_math_node, add_world_position and other utility nodes, add_if/add_switch, add_custom_expression, connect_nodes, or disconnect_nodes. All actions that accept an assetPath (or materialPath alias) should apply normalizeAssetPath before sending to the C++ bridge.
Was this helpful? React with 👍 or 👎 to provide feedback.
| TSharedPtr<FJsonObject> Result = McpHandlerUtils::CreateResultObject(); | ||
| McpHandlerUtils::AddVerification(Result, Material); | ||
| Result->SetStringField(TEXT("nodeId"), NewExpr->MaterialExpressionGuid.ToString()); | ||
| Result->SetStringField(TEXT("nodeId"), NewExpr->GetName()); |
There was a problem hiding this comment.
🟡 Material node IDs changed from stable GUIDs to potentially unstable UObject names
All material graph handlers now return NewExpr->GetName() (e.g., MaterialExpressionAdd_0) instead of NewExpr->MaterialExpressionGuid.ToString() as the nodeId. While GetName() is unique within a material, these names can change if expressions are reordered, removed and re-added, or if the material is duplicated. GUIDs are immutable. Clients that store node IDs between sessions (e.g., for subsequent connect_nodes or remove_node calls in a multi-step workflow) may find that stored IDs no longer resolve after the material is modified or the editor is restarted. The FindExpressionByIdOrNameOrIndex lookup function (McpAutomationBridge_MaterialGraphHandlers.cpp:121) still supports GUID-based lookups, but newly created nodes will only return name-based IDs, so any client following a create→connect workflow within the same session will work, but cross-session persistence is weakened.
Was this helpful? React with 👍 or 👎 to provide feedback.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
There was a problem hiding this comment.
Pull request overview
This PR extends the Unreal Editor bridge plugin’s property and material tooling so automation calls can correctly target Blueprint-authored component templates and named material outputs, improving parity between native and Blueprint-added assets during editor automation.
Changes:
- Updated
set_object_property/get_object_propertyto resolve the first segment of dot-notation paths against Blueprint SCS component templates (viaFindCdoComponent) when usingblueprintPath. - Enhanced material
connect_nodesto interpretsourcePinas either a numeric index or a named output (Material Function Call outputs and Custom node additional outputs). - Adjusted audio asset-path building to better handle relative directories by prefixing
/Game(plus minor formatting/control-flow tweaks).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp | Adds SCS-template component lookup so dotted property paths can target Blueprint-added component templates when resolving from a Blueprint CDO. |
| plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialAuthoringHandlers.cpp | Improves connect_nodes by resolving sourcePin via numeric index or named outputs for MF call/custom nodes, then consistently applying OutputIndex. |
| plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp | Updates sanitized asset-path construction to prefix relative directories with /Game and applies small routing/formatting adjustments. |
| FString Directory = InDirectory.TrimStartAndEnd(); | ||
| if (!Directory.IsEmpty() && !Directory.StartsWith(TEXT("/"))) { | ||
| Directory = TEXT("/Game/") + Directory; | ||
| } |
Summary
When
blueprintPathis used with dot-notation property paths (e.g.,MyBPComponent.RelativeLocation),HandleSetObjectPropertyandHandleGetObjectPropertynow resolve the first path segment viaFindCdoComponent, which checks both CDO native components and SCS node templates (Blueprint-added components). Previously, these paths returnedPROPERTY_NOT_FOUNDbecause BP-added components live onUSCS_Node::ComponentTemplate, not on the CDO.Changes
FindCdoComponent(defined later in the file forHandleInspectCdoAction)HandleSetObjectProperty: after Blueprint CDO resolution, parse first dot-notation segment and attempt SCS-template lookup viaFindCdoComponent; if found, use the component template asRootObjectfor remaining property resolutionHandleGetObjectProperty: same SCS-template lookup pattern beforeMcpHandlerUtils::ResolvePropertyResolveNestedPropertyPathif the component isn't found via SCS lookupBlueprintvariable toResolvedBlueprintto keep it in scope past CDO resolution for the SCS lookup stageRelated Issues
Closes #357
Type of Change
Testing
set_propertywithblueprintPath+propertyName: "MyBPComponent.RelativeLocation"resolves to the SCS template and sets the value with properModify()/MarkPackageDirty()/PostEditChange()get_propertywithblueprintPath+propertyName: "MyBPComponent.RelativeLocation"returns the template default valueDefaultSceneRoot.RelativeLocation) still resolve via CDO as beforeobjectPath-based calls are unaffected —ResolvedBlueprintis null, SCS lookup block is never enteredPre-Merge Checklist