Skip to content

feat: add SCS-template lookup to set_property/get_property for Blueprint-added components#358

Merged
ChiR24 merged 52 commits into
ChiR24:devfrom
nekwo:feat/scs-template-property-lookup
Jun 3, 2026
Merged

feat: add SCS-template lookup to set_property/get_property for Blueprint-added components#358
ChiR24 merged 52 commits into
ChiR24:devfrom
nekwo:feat/scs-template-property-lookup

Conversation

@nekwo

@nekwo nekwo commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Summary

When blueprintPath is used with dot-notation property paths (e.g., MyBPComponent.RelativeLocation), HandleSetObjectProperty and HandleGetObjectProperty now resolve the first path segment via FindCdoComponent, which checks both CDO native components and SCS node templates (Blueprint-added components). Previously, these paths returned PROPERTY_NOT_FOUND because BP-added components live on USCS_Node::ComponentTemplate, not on the CDO.

Changes

  • Added forward declaration for FindCdoComponent (defined later in the file for HandleInspectCdoAction)
  • HandleSetObjectProperty: after Blueprint CDO resolution, parse first dot-notation segment and attempt SCS-template lookup via FindCdoComponent; if found, use the component template as RootObject for remaining property resolution
  • HandleGetObjectProperty: same SCS-template lookup pattern before McpHandlerUtils::ResolveProperty
  • Both handlers fall back to existing CDO-based ResolveNestedPropertyPath if the component isn't found via SCS lookup
  • Renamed local Blueprint variable to ResolvedBlueprint to keep it in scope past CDO resolution for the SCS lookup stage

Related Issues

Closes #357

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to change)
  • 📚 Documentation update
  • 🔧 Configuration/build change
  • ♻️ Refactoring (no functional changes)
  • 🧪 Test addition/update

Testing

  • set_property with blueprintPath + propertyName: "MyBPComponent.RelativeLocation" resolves to the SCS template and sets the value with proper Modify()/MarkPackageDirty()/PostEditChange()
  • get_property with blueprintPath + propertyName: "MyBPComponent.RelativeLocation" returns the template default value
  • Native component paths (e.g., DefaultSceneRoot.RelativeLocation) still resolve via CDO as before
  • Simple (non-dotted) property names on CDO are unaffected — SCS lookup is skipped
  • objectPath-based calls are unaffected — ResolvedBlueprint is null, SCS lookup block is never entered
  • Tested with Unreal Engine (version: ___)
  • Tested MCP client integration (client: ___)
  • Added/updated tests

Pre-Merge Checklist

  • Code follows project style guidelines
  • Self-reviewed the code
  • Updated relevant documentation (if needed)
  • Added/updated tests (if applicable)
  • CI passes

Open with Devin

google-labs-jules Bot and others added 30 commits April 9, 2026 18:08
…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>
devin-ai-integration[bot]

This comment was marked as resolved.

… 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>
coderabbitai[bot]

This comment was marked as resolved.

…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>
@github-actions github-actions Bot removed the size/m label Apr 16, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 13 additional findings in Devin Review.

Open in Devin Review

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}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@ChiR24 ChiR24 self-assigned this Jun 2, 2026
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_property to resolve the first segment of dot-notation paths against Blueprint SCS component templates (via FindCdoComponent) when using blueprintPath.
  • Enhanced material connect_nodes to interpret sourcePin as 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.

Comment on lines +255 to +258
FString Directory = InDirectory.TrimStartAndEnd();
if (!Directory.IsEmpty() && !Directory.StartsWith(TEXT("/"))) {
Directory = TEXT("/Game/") + Directory;
}
@ChiR24 ChiR24 merged commit ea957a1 into ChiR24:dev Jun 3, 2026
5 checks passed
@ChiR24 ChiR24 moved this from In Progress to Done in 🎮 Unreal MCP Development Roadmap Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

feat: add SCS-template lookup to set_property/get_property for Blueprint-added components

4 participants