Skip to content

Fix/blueprint graph handler bugs#298

Open
Kaen-SG wants to merge 2 commits intoChiR24:mainfrom
Kaen-SG:fix/Blueprint-graph-handler-bugs
Open

Fix/blueprint graph handler bugs#298
Kaen-SG wants to merge 2 commits intoChiR24:mainfrom
Kaen-SG:fix/Blueprint-graph-handler-bugs

Conversation

@Kaen-SG
Copy link
Copy Markdown

@Kaen-SG Kaen-SG commented Mar 19, 2026

Summary

Fixes 5 bugs discovered during Blueprint automation wiring of a project.

Bugs Fixed

Bug 1 — ForEachLoop alias pointed to wrong node

ForEachLoop was aliased to K2Node_ForEachElementInEnum (enum-only, no array pins).
Removed the incorrect alias. Array iteration should use nodeType: K2Node_CallArrayFunction.

Bug 2 — add_variable ignores variablePinType for Object type

When variableType is "Object" or "Class", the handler always set
PinSubCategoryObject = UObject::StaticClass() regardless of the variablePinType field.
Now reads PinSubCategoryObject or objectClass from variablePinType JSON to produce
properly typed component references.

Bug 3 — K2Node_DynamicCast fails for Blueprint classes

ResolveUClass only finds native C++ classes, so Blueprint targets like BP_CharacterBase
always failed, producing "Bad cast node". Added three-stage fallback: native lookup →
LoadBlueprintAsset + GeneratedClassTObjectIterator scan. Added K2Node_DynamicCast
as accepted nodeType. ReconstructNode now called after Finalize to expose the typed
As ClassName output pin.

Bug 4 — K2Node_CallArrayFunction always returns zero pins

The dynamic NewObject fallback doesn't resolve array function wildcard pins. Added an
explicit CallArrayFunction handler using FGraphNodeCreator<UK2Node_CallFunction> with
SetFromFunction + ReconstructNode. All KismetArrayLibrary functions now expose their
pins correctly (Array_Length, Array_Get, etc.).

Bug 5 — connect_pins schema rejection for typed object pins

Single TryCreateConnection call with no fallback. Replaced with a 3-attempt strategy:
normal direction → reversed direction → ReconstructNode both nodes and retry.
ReconstructNode is also called on success so wildcard pins propagate their new type.
Error messages now include pin type diagnostic info.

Bonus — get_pin_details missing pinSubType

pinSubType was only reported in get_nodes, not get_pin_details. Fixed to always
report pinSubType for any pin with a valid PinSubCategoryObject.

Files Changed

  • McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp
  • McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp

Open with Devin image

Kaen-SG added 2 commits March 19, 2026 17:25
…rEachLoop alias pointing to K2Node_ForEachElementInEnum(enum-only node). Array foreach must use CallArrayFunction nodeType.Bug 2: add_variable ignores variablePinType for Object/Class types.Now reads PinSubCategoryObject and objectClass from the variablePinTypeJSON field so typed component references (e.g. AoStatGeneratorComponent)are created with proper pin types instead of generic UObject.Bug 3: K2Node_DynamicCast fails for Blueprint classes with 'Bad cast node'.ResolveUClass only finds native C++ classes. Added three-stage fallback:(1) native lookup, (2) LoadBlueprintAsset + GeneratedClass from commonpath prefixes, (3) TObjectIterator scan for _C generated class. Alsoadded K2Node_DynamicCast as accepted nodeType alias. ReconstructNodecalled after Finalize to expose the typed 'As ClassName' output pin.Bug 4: K2Node_CallArrayFunction always returns zero pins. Dynamic NewObjectfallback path does not resolve array function wildcard pins. Added explicitCallArrayFunction handler using FGraphNodeCreator<UK2Node_CallFunction>with SetFromFunction + ReconstructNode so TargetArray and ReturnValuepins are correctly exposed for all KismetArrayLibrary functions.Bug 5: connect_pins schema rejection for typed object pins. SingleTryCreateConnection call with no fallback. Added 3-attempt strategy:(1) normal direction, (2) reversed direction, (3) ReconstructNode onboth nodes then retry. Also calls ReconstructNode after success sowildcard/array pins update to the connected type. Error messages nowinclude pin type diagnostic info.get_pin_details: always report pinSubType for any pin with a validPinSubCategoryObject so cast output pins expose their target class name.
@github-actions
Copy link
Copy Markdown
Contributor

📏 Large PR Detected

This pull request is quite large (1000+ lines changed), which can make reviewing challenging.

Suggestions:

  • Consider breaking this into smaller, focused PRs
  • Separate refactoring from new features
  • Split bug fixes from feature additions

This helps reviewers provide better feedback and speeds up the merge process. Thank you! 🙏

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

The changes refactor and enhance the McpAutomationBridge plugin across multiple systems. Key modifications include removing explicit world/level cleanup steps (EndFrame calls, CleanupWorld, root removals), expanding Blueprint node creation to support dynamic casts and array functions, enhancing pin connection with retry strategies, adding typed variable pin support, simplifying level and sublevel creation workflows, strengthening widget authoring with duplicate name prevention, and removing path sanitization from asset exports.

Changes

Cohort / File(s) Summary
World & Level Cleanup
McpAutomationBridgeHelpers.h, McpSafeOperations.h
Removed FTickTaskManagerInterface::Get().EndFrame() calls and eliminated subsequent world/package root removal and cleanup operations (CleanupWorld, "remove from root" steps), leaving only transient flag assignment and garbage collection.
Blueprint Node Creation & Pin Handling
McpAutomationBridge_BlueprintGraphHandlers.cpp
Added support for dynamic cast nodes (K2Node_DynamicCast) with multi-step class resolution (native, Blueprint asset lookup, class scan) and array function nodes (K2Node_CallArrayFunction); enhanced pin connection logic with three retry strategies (as-is, reversed, reconstruct-and-retry); expanded pin details reporting to include pinSubType when available.
Blueprint Variable Pin Types
McpAutomationBridge_BlueprintHandlers.cpp
Added support for typed variable pins by reading nested variablePinType JSON object to override PinSubCategoryObject, with fallback to ResolveUClass for subclass names and default to UObject::StaticClass() if resolution fails.
Level & Sublevel Structure
McpAutomationBridge_LevelStructureHandlers.cpp
Removed World Partition and external actors (OFPA) auto-enablement logic, simplified sublevel creation (removed idempotent "already exists" check and disk asset creation), removed sublevel cleanup and save-failure reporting, eliminated fallback disk-scanning logic in streaming configuration handlers, and removed external objects compatibility checks from data layer handlers.
Level Blueprint Access
McpAutomationBridge_LevelStructureHandlers.cpp (partial)
Changed GetLevelScriptBlueprint(false)GetLevelScriptBlueprint(true) to enable automatic creation when unavailable; adjusted error messaging from "get or create" to "get".
Widget Authoring & Naming
McpAutomationBridge_WidgetAuthoringHandlers.cpp
Implemented unique widget name generation with _<n> suffix to prevent duplicates; strengthened parent attachment validation to return errors for missing or invalid parents; added optional visibility configuration for vertical boxes; updated multiple widget types to use name field with fallback to legacy slotName.
Asset Export & Includes
McpAutomationBridge_SystemControlHandlers.cpp, McpHandlerUtils.cpp
Removed path sanitization (SanitizeProjectRelativePath) from asset export flow; reordered #include "EngineUtils.h" placement in editor-only section.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

bug, area/plugin, area/tools, size/l

Poem

🐰 A bridge rebuilt, so spry and clean,
Old cleanup calls no longer seen,
Dynamic casts and widgets named with care,
Pins retry thrice—a charm beyond compare!
Sublevels simplified, where blueprints dance,
McpAutomation takes its finest chance.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Fix/blueprint graph handler bugs' is somewhat vague and generic, using 'bugs' without specificity, though it does reference the main area (blueprint graph handler). Consider a more specific title that highlights the primary bug fix or area, such as 'Fix Blueprint graph handler node resolution and pin connection issues' or 'Fix K2Node_DynamicCast and CallArrayFunction failures'.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description is comprehensive and addresses all required template sections including Summary, detailed explanation of bugs fixed, and files changed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a series of critical bug fixes and usability enhancements across the Blueprint graph, level management, and widget authoring automation handlers. The primary goal is to improve the stability and correctness of automated Blueprint operations, such as node creation and pin connections, especially for complex types like objects and arrays. Additionally, it refines the handling of level creation and cleanup processes and standardizes widget generation for a more consistent and reliable automation experience.

Highlights

  • Blueprint Graph Handler Improvements: Corrected the 'ForEachLoop' alias to prevent incorrect node assignment for array iterations and introduced explicit handling for 'K2Node_CallArrayFunction' to ensure proper pin resolution.
  • Dynamic Cast Node Enhancements: Improved 'K2Node_DynamicCast' node creation with a robust three-stage lookup process to successfully resolve both native C++ and Blueprint classes, ensuring typed output pins are correctly exposed.
  • Pin Connection Reliability: Implemented a more resilient 3-attempt connection strategy for 'connect_pins' to overcome schema rejections, including retries with reversed pin order and node reconstruction, along with enhanced error messages.
  • Variable Type Resolution: Enhanced the 'add_variable' handler to accurately interpret 'variablePinType' for Object and Class types, leading to correctly typed component references.
  • Level Management Streamlining: Simplified internal level cleanup logic in 'McpSafeLoadMap' and 'HandleCreateLevel' by removing redundant calls and updated sublevel creation to use 'ULevelStreamingDynamic' directly, removing 'One File Per Actor' (OFPA) checks.
  • Widget Authoring Standardization: Standardized widget creation handlers to prioritize a 'name' field, ensure unique widget names, and added robust parent widget validation and visibility settings for 'VerticalBox'.
  • Pin Details Reporting: Extended 'get_pin_details' to consistently report 'pinSubType' for object, class, and struct pins, providing more comprehensive information.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai bot added bug Something isn't working size/l area/tools labels Mar 19, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request successfully addresses several bugs in the blueprint graph handlers, notably improving support for Blueprint classes in casts, correcting array function node creation, and making pin connections more robust. However, my review has identified some critical issues in files that were not mentioned in the PR description, which may have been included accidentally. These include the removal of security-related path sanitization, and the reversion of several critical fixes related to level creation and data layers that could lead to editor crashes. I've also pointed out areas with significant code duplication that could be refactored for better maintainability.

I am having trouble creating individual review comments. Click here to see my feedback.

plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp (592-777)

critical

The logic for HandleCreateSublevel has been significantly altered. The previous implementation created a new sublevel asset on disk, saved it, and then added a streaming reference. The new implementation only creates a ULevelStreamingDynamic object in memory. This appears to be a regression in functionality, as it no longer creates the sublevel asset itself, which could lead to broken references if the asset doesn't exist. The original code had a comment indicating this was a 'CRITICAL FIX'. This change should be reviewed to ensure it doesn't break the intended functionality of creating new sublevels.

plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp (1663-1681)

critical

This change removes a critical check for IsUsingExternalObjects() before creating a data layer. The original comment explained that this is necessary to prevent an editor assertion/crash. Removing this check without an alternative safety mechanism is likely to reintroduce crashes when working with data layers on levels that don't have 'One File Per Actor' enabled. This safety check should be restored or replaced with an equivalent validation.

plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp (1831-1848)

critical

This change removes a critical check for IsUsingExternalObjects() before assigning an actor to a data layer. The original comment explained that this is necessary because non-OFPA actors cannot be assigned to data layers. Removing this check is likely to reintroduce crashes or errors. This safety check should be restored.

plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SystemControlHandlers.cpp (341-347)

critical

The call to SanitizeProjectRelativePath for AssetPath has been removed. This function was in place to prevent path traversal attacks (e.g., using .. to access files outside the project directory). Removing this sanitization step introduces a critical security vulnerability. The sanitization should be restored to prevent malicious file access.

plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp (745-754)

medium

The hardcoded list of paths to search for Blueprint assets is a bit brittle. If the project's folder structure changes, this logic will fail. Consider using the Asset Registry to search for the Blueprint asset by name if the initial LoadBlueprintAsset fails. This would be more robust than relying on a fixed set of paths.

plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp (2766-2783)

medium

This block of code for handling variablePinType for 'class' types is identical to the block for 'object' types (lines 2744-2764). This duplication can be avoided by extracting the logic into a helper function. A single function could resolve the PinSubCategoryObject from the payload, which would make the code more maintainable and less error-prone.

plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp (537-544)

medium

The logic to generate a unique widget name by appending a suffix if the name already exists is duplicated across many of the add_* widget handlers in this file (e.g., add_canvas_panel, add_horizontal_box, etc.). This should be refactored into a helper function to improve maintainability and reduce code duplication. A single helper function could take the UWidgetTree and the base name as arguments and return a unique name.

plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp (561-578)

medium

The logic for finding and validating a parent widget is duplicated across many of the add_* widget handlers. This logic, which finds the parent by name, checks if it's null, and validates that it's a UPanelWidget, could be extracted into a helper function to reduce code duplication and improve maintainability.

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Devin Review found 5 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines 390 to +393
}

// Load the asset
UObject* Asset = UEditorAssetLibrary::LoadAsset(SafeAssetPath);
UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
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.

🔴 Security: Removed SanitizeProjectRelativePath for asset path in export_asset handler

The SanitizeProjectRelativePath(AssetPath) call was completely removed from the export_asset handler. The raw, user-provided AssetPath is now passed directly to UEditorAssetLibrary::DoesAssetExist() and UEditorAssetLibrary::LoadAsset() without any sanitization. The old code validated the asset path against traversal attacks (..), absolute Windows paths, and invalid root prefixes before use. This violates the AGENTS.md mandate: SanitizeProjectRelativePath() must be used to "Block traversal attacks" (plugins/McpAutomationBridge/AGENTS.md:29).

(Refers to lines 378-393)

Prompt for agents
In plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SystemControlHandlers.cpp, around lines 340-393, restore the SanitizeProjectRelativePath call for AssetPath before it is used. After the AssetPath.IsEmpty() check (around line 339), add:

FString SafeAssetPath = SanitizeProjectRelativePath(AssetPath);
if (SafeAssetPath.IsEmpty()) {
  SendAutomationError(RequestingSocket, RequestId,
                      TEXT("Invalid asset path for export"),
                      TEXT("SECURITY_VIOLATION"));
  return true;
}

Then replace all subsequent uses of AssetPath (in DoesAssetExist, LoadAsset, GetBaseFilename, and response fields) with SafeAssetPath.
Open in Devin Review

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

Comment on lines +533 to +549
// Add streaming level
ULevelStreamingDynamic* StreamingLevel = NewObject<ULevelStreamingDynamic>(World, ULevelStreamingDynamic::StaticClass());
if (!StreamingLevel)
{
Subsystem->SendAutomationResponse(Socket, RequestId, false,
FString::Printf(TEXT("Failed to create world for sublevel: %s"), *SublevelName), nullptr, TEXT("WORLD_CREATION_FAILED"));
TEXT("Failed to create streaming level object"), nullptr);
return true;
}

// Initialize the world if not already initialized
if (!NewSublevelWorld->bIsWorldInitialized)
{
NewSublevelWorld->InitWorld();
}
// Configure the streaming level
StreamingLevel->SetWorldAssetByPackageName(FName(*SublevelPath));
StreamingLevel->LevelTransform = FTransform::Identity;
StreamingLevel->SetShouldBeVisible(true);
StreamingLevel->SetShouldBeLoaded(true);

// Mark package dirty
SublevelPackage->MarkPackageDirty();
// Add to world's streaming levels
World->AddStreamingLevel(StreamingLevel);
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.

🔴 HandleCreateSublevel no longer creates sublevel .umap on disk — streaming reference points to nothing

The rewritten HandleCreateSublevel only creates a ULevelStreamingDynamic reference pointing to a sublevel package path, but never creates the actual sublevel world/package or saves it to disk as a .umap file. The old code (removed lines 630-756) created a UPackage, called UWorld::CreateWorld, initialized it, saved it via McpSafeLevelSave, and then created the streaming reference. Without the on-disk asset, the streaming reference points to a non-existent package, which will fail when the engine attempts to load it. The handler reports success despite the sublevel not actually existing.

Prompt for agents
In plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp, the HandleCreateSublevel function (starting around line 457) needs to actually create the sublevel on disk before creating a streaming reference. Before line 533 (where ULevelStreamingDynamic is created), restore the sublevel creation logic:

1. Create a UPackage via CreatePackage(*SublevelPath)
2. Create a UWorld via UWorld::CreateWorld(EWorldType::Inactive, false, FName(*SublevelName), Package)
3. Initialize the world if needed
4. Save it to disk via McpSafeLevelSave
5. Then create the streaming reference pointing to the saved package
6. Clean up the created world from memory (mark transient, collect garbage) to prevent World Memory Leaks

Also restore the idempotent check: before creating, check if FPackageName::DoesPackageExist(SublevelPath) and return early with success if the sublevel already exists.
Open in Devin Review

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

Comment on lines +438 to 448
// Mark the world and its package as transient so GC will collect them
NewWorld->SetFlags(RF_Transient);
if (Package)
{
// Also remove package from root if needed
if (Package->IsRooted())
{
Package->RemoveFromRoot();
}
Package->SetFlags(RF_Transient);
}

// STEP 6: Force garbage collection to remove the world from memory
// Force garbage collection to remove the world from memory
// This allows the level to be cleanly loaded later via LoadMap
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
FlushRenderingCommands();
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.

🔴 Removed RemoveFromRoot() makes CollectGarbage ineffective — worlds stay rooted in memory

In HandleCreateLevel's post-save cleanup, RemoveFromRoot() was removed for both the world and its package, while CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS) is still called. Rooted objects (RF_RootSet) are never collected by GC regardless of other flags like RF_Transient. The comments at McpAutomationBridge_LevelStructureHandlers.cpp:397-401 explicitly state the world "stays in memory as a root object" and causes a "World Memory Leaks" fatal error. Without RemoveFromRoot(), SetFlags(RF_Transient) + CollectGarbage has no effect on rooted objects, defeating the entire cleanup block. The same issue exists in McpAutomationBridgeHelpers.h:1383-1388 (McpSafeLoadMap Step 11) where RemoveFromRoot() was also removed for the pre-existing target world and package.

Suggested change
// Mark the world and its package as transient so GC will collect them
NewWorld->SetFlags(RF_Transient);
if (Package)
{
// Also remove package from root if needed
if (Package->IsRooted())
{
Package->RemoveFromRoot();
}
Package->SetFlags(RF_Transient);
}
// STEP 6: Force garbage collection to remove the world from memory
// Force garbage collection to remove the world from memory
// This allows the level to be cleanly loaded later via LoadMap
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
FlushRenderingCommands();
// Mark the world and its package as transient so GC will collect them
NewWorld->SetFlags(RF_Transient);
if (NewWorld->IsRooted())
{
NewWorld->RemoveFromRoot();
}
if (Package)
{
if (Package->IsRooted())
{
Package->RemoveFromRoot();
}
Package->SetFlags(RF_Transient);
}
// Force garbage collection to remove the world from memory
// This allows the level to be cleanly loaded later via LoadMap
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
FlushRenderingCommands();
Open in Devin Review

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

Comment on lines 1383 to 1387
// Mark the world and its package for garbage collection
ExistingWorld->SetFlags(RF_Transient);
if (ExistingPackage->IsRooted())
{
ExistingPackage->RemoveFromRoot();
}
ExistingPackage->SetFlags(RF_Transient);

// Force garbage collection to clean up the existing world
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.

🔴 Removed RemoveFromRoot() in McpSafeLoadMap target world cleanup — same GC failure

Same issue as in HandleCreateLevel: the McpSafeLoadMap Step 11 cleanup of a pre-existing target world package removed the RemoveFromRoot() calls for both ExistingWorld and ExistingPackage. The code at McpAutomationBridgeHelpers.h:1383-1388 sets RF_Transient and calls CollectGarbage, but without removing root flags first, the GC will not collect these objects. This means LoadMap will find the stale world in memory and trigger the "World Memory Leaks" fatal error described in the comments.

(Refers to lines 1383-1389)

Open in Devin Review

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

// because it requires a valid package path
// Pass false to allow creation of Level Blueprint if it doesn't exist
ULevelScriptBlueprint* LevelBP = PersistentLevel->GetLevelScriptBlueprint(false);
ULevelScriptBlueprint* LevelBP = PersistentLevel->GetLevelScriptBlueprint(true);
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.

🟡 GetLevelScriptBlueprint(true) prevents creation of missing level blueprints

In HandleOpenLevelBlueprint and HandleAddLevelBlueprintNode, GetLevelScriptBlueprint(false) was changed to GetLevelScriptBlueprint(true). The parameter is bDontCreatefalse meant "create if missing" and true means "don't create". The old code explicitly commented: "Pass false to allow creation of Level Blueprint if it doesn't exist". The new code will return nullptr for any level that doesn't yet have a level blueprint, causing both handlers to fail with an error instead of creating one. The new comment on line 1872 ("may fail to create") is also inaccurate — true means it won't attempt creation at all.

Suggested change
ULevelScriptBlueprint* LevelBP = PersistentLevel->GetLevelScriptBlueprint(true);
ULevelScriptBlueprint* LevelBP = PersistentLevel->GetLevelScriptBlueprint(false);
Open in Devin Review

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SystemControlHandlers.cpp (1)

328-339: ⚠️ Potential issue | 🔴 Critical

Critical: Missing path sanitization for AssetPath enables directory traversal attacks.

The AssetPath parameter is used directly from user input without sanitization, while ExportPath is properly sanitized. This inconsistency creates a security vulnerability where malicious input like "/Game/../../../SensitiveAsset" could bypass intended access controls.

All sibling handlers (LevelHandlers, TextureHandlers, SkeletonHandlers) use SanitizeProjectRelativePath() before loading assets. This handler should follow the same pattern.

🔒 Proposed fix to add path sanitization
     if (AssetPath.IsEmpty()) {
       SendAutomationError(RequestingSocket, RequestId,
                           TEXT("assetPath is required for export"),
                           TEXT("INVALID_ARGUMENT"));
       return true;
     }
+    
+    FString SafeAssetPath = SanitizeProjectRelativePath(AssetPath);
+    if (SafeAssetPath.IsEmpty()) {
+      SendAutomationError(RequestingSocket, RequestId,
+                          FString::Printf(TEXT("Invalid assetPath: contains path traversal (..) or invalid characters: %s"), *AssetPath),
+                          TEXT("SECURITY_VIOLATION"));
+      return true;
+    }
+    AssetPath = SafeAssetPath;
     
     if (ExportPath.IsEmpty()) {

As per coding guidelines: "Use SanitizeProjectRelativePath() to validate and sanitize all file paths to prevent directory traversal attacks".

Also applies to: 378-378, 393-393

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SystemControlHandlers.cpp`
around lines 328 - 339, AssetPath is used directly from user input causing
possible directory traversal; call SanitizeProjectRelativePath(AssetPath) (same
as done for ExportPath and in LevelHandlers/TextureHandlers/SkeletonHandlers)
immediately after reading AssetPath, validate the result and if sanitization
fails call SendAutomationError(RequestingSocket, RequestId, TEXT("invalid
assetPath"), TEXT("INVALID_ARGUMENT")) and return true; apply the same
SanitizeProjectRelativePath check for the other AssetPath usages referenced
(around the other handlers/lines noted) so all asset loads use the sanitized
path.
plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp (2)

5688-5723: ⚠️ Potential issue | 🟠 Major

Don't silently fall back when parentSlot is invalid.

These branches still treat a bad parentSlot as “attach to root instead” and return success. That reintroduces the silent mis-parenting bug the earlier handlers just fixed, and if RootWidget is null or not a panel the request can even succeed without attaching anything. These should mirror the earlier PARENT_NOT_FOUND / INVALID_PARENT behavior.

Also applies to: 5738-5776, 5793-5830

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp`
around lines 5688 - 5723, When a non-empty ParentSlot is supplied, do not
silently fall back to RootWidget; detect the two error cases and return failures
instead: if ParentSlot is provided but FindWidget(FName(*ParentSlot)) returns
null, call SendAutomationError(RequestingSocket, RequestId, TEXT("Parent slot
not found"), TEXT("PARENT_NOT_FOUND")) and return true; if the found widget or
the RootWidget is not a UPanelWidget (i.e., Cast<UPanelWidget> yields null) call
SendAutomationError(RequestingSocket, RequestId, TEXT("Invalid parent: not a
panel widget"), TEXT("INVALID_PARENT")) and return true; only call
Parent->AddChild(SafeZone) when Parent is valid. Apply the same checks and error
returns for the other similar blocks referenced (lines ~5738-5776 and
~5793-5830) that use ParentSlot, WidgetBP->WidgetTree->RootWidget, and
Parent->AddChild.

4921-4954: ⚠️ Potential issue | 🟠 Major

The later composite adders still skip duplicate-name hardening.

add_minimap now accepts name, but it still constructs SlotName, SlotName_Border, SlotName_MapImage, etc. directly. A second request with the same/default name can still collide or be auto-renamed unpredictably, which undoes the uniqueness guarantee added above. The same pattern repeats in add_compass, add_interaction_prompt, add_objective_tracker, add_damage_indicator, and add_quest_tracker.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp`
around lines 4921 - 4954, The code constructs widgets using raw names (SlotName,
SlotName + "_Border", "_MapImage", "_PlayerIndicator") which can collide on
repeated add_* calls; update the handler to derive a unique base name before
constructing child widgets (e.g., call or add an EnsureUniqueSlotName helper
that checks WidgetBP->WidgetTree for existing widgets and appends a numeric
suffix until unique), then use that returned unique base for MinimapContainer,
MinimapBorder, MapImage and PlayerIndicator creation; apply the same fix pattern
to the other composite creators (add_compass, add_interaction_prompt,
add_objective_tracker, add_damage_indicator, add_quest_tracker) so all
constructed names are passed through the same uniqueness function.
🧹 Nitpick comments (4)
plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp (1)

548-549: Consider adding idempotency check before adding streaming level.

Unlike HandleCreateLevel which checks for existing packages, HandleCreateSublevel doesn't check if a streaming level with the same WorldAssetPackageName already exists. Repeated calls could add duplicate streaming level entries.

🔧 Proposed idempotency check
+    // Check if streaming level already exists (idempotency)
+    for (ULevelStreaming* ExistingLevel : World->GetStreamingLevels())
+    {
+        if (ExistingLevel && ExistingLevel->GetWorldAssetPackageFName().ToString() == SublevelPath)
+        {
+            TSharedPtr<FJsonObject> ResponseJson = McpHandlerUtils::CreateResultObject();
+            McpHandlerUtils::AddVerification(ResponseJson, ExistingLevel);
+            ResponseJson->SetStringField(TEXT("sublevelName"), SublevelName);
+            ResponseJson->SetStringField(TEXT("parentLevel"), World->GetMapName());
+            ResponseJson->SetBoolField(TEXT("alreadyExisted"), true);
+            
+            Subsystem->SendAutomationResponse(Socket, RequestId, true,
+                FString::Printf(TEXT("Streaming level already exists: %s"), *SublevelName), ResponseJson);
+            return true;
+        }
+    }
+
     // Add to world's streaming levels
     World->AddStreamingLevel(StreamingLevel);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp`
around lines 548 - 549, HandleCreateSublevel is adding StreamingLevel
unconditionally which can create duplicate entries; before calling
World->AddStreamingLevel(StreamingLevel) check World’s existing streaming levels
(World->GetStreamingLevels() or World->StreamingLevels) for an entry whose
WorldAssetPackageName (or equivalent package name getter on the streaming level)
matches the new StreamingLevel’s WorldAssetPackageName and skip adding if found.
Ensure you compare the same identifier used elsewhere (WorldAssetPackageName)
and only call AddStreamingLevel when no existing streaming level with that
package name exists.
plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp (2)

750-754: Hardcoded project-specific paths reduce portability.

These path prefixes (/Game/Blueprints/Characters/, /Game/Blueprints/Combat/) appear to be specific to the tactical RPG project mentioned in the PR description. Other projects using this plugin will have different directory structures.

Consider making these configurable or removing the project-specific prefixes, keeping only the generic ones:

♻️ Suggested simplification
         TArray<FString> PathsToTry;
         if (TargetClassName.Contains(TEXT("/"))) {
           PathsToTry.Add(TargetClassName);
         } else {
           PathsToTry.Add(FString::Printf(TEXT("/Game/Blueprints/%s"), *TargetClassName));
           PathsToTry.Add(FString::Printf(TEXT("/Game/%s"), *TargetClassName));
-          PathsToTry.Add(FString::Printf(TEXT("/Game/Blueprints/Characters/%s"), *TargetClassName));
-          PathsToTry.Add(FString::Printf(TEXT("/Game/Blueprints/Combat/%s"), *TargetClassName));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp`
around lines 750 - 754, The hardcoded project-specific path prefixes added to
PathsToTry (e.g., "/Game/Blueprints/Characters/%s" and
"/Game/Blueprints/Combat/%s") reduce portability; modify the logic in
McpAutomationBridge_BlueprintGraphHandlers.cpp where PathsToTry.Add(...) is
called so it either only includes generic paths ("/Game/Blueprints/%s" and
"/Game/%s") or reads additional prefixes from a configurable source (editor
setting, config file, or optional parameter) before formatting with
TargetClassName; update the code that builds PathsToTry to use the configurable
list (or remove the specific entries) and ensure TargetClassName is still used
for formatting.

744-762: Consider sanitizing targetClass input before path construction.

The TargetClassName originates from user input (line 734) and is used to construct asset paths for LoadBlueprintAsset without sanitization. While the /Game/ prefix limits risk, this is inconsistent with the security pattern applied to assetPath/blueprintPath inputs elsewhere in this file (lines 151-168, 209-216).

🛡️ Optional: Add sanitization for consistency
+      // Sanitize class name to prevent path manipulation
+      FString SanitizedClassName = TargetClassName;
+      SanitizedClassName.ReplaceInline(TEXT(".."), TEXT(""));
+      SanitizedClassName.ReplaceInline(TEXT("//"), TEXT("/"));
+
       // 2. If that failed, try loading it as a Blueprint asset and using its
       //    GeneratedClass. This is required for Blueprint targets like
       //    "BP_CharacterBase" which resolve to "BP_CharacterBase_C".
       if (!TargetClass) {
         // Try common path prefixes if no slash is present
         TArray<FString> PathsToTry;
-        if (TargetClassName.Contains(TEXT("/"))) {
-          PathsToTry.Add(TargetClassName);
+        if (SanitizedClassName.Contains(TEXT("/"))) {
+          PathsToTry.Add(SanitizedClassName);
         } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp`
around lines 744 - 762, Sanitize TargetClassName before constructing PathsToTry:
validate and normalize the user-provided TargetClassName (trim whitespace, strip
any leading/trailing slashes or dots, remove extensions, and reject or remove
any path traversal tokens like ".." or drive/URI characters) prior to using it
to build the FStrings and calling LoadBlueprintAsset; update the block that
assembles PathsToTry (the code using TargetClassName, TargetClass, and
LoadBlueprintAsset in McpAutomationBridge_BlueprintGraphHandlers.cpp) to operate
on the sanitized string and bail with a clear error if the name contains
disallowed characters.
plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp (1)

2749-2754: Use FJsonObjectConverter + shared parsing helper for variablePinType

The same manual JSON extraction is duplicated in both branches. Deserialize variablePinType once into a small struct and reuse for object/class handling.

As per coding guidelines: "Use FJsonObjectConverter for JSON parsing and struct serialization instead of manual JSON parsing".

Also applies to: 2768-2773

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp`
around lines 2749 - 2754, The manual JSON extraction for variablePinType is
duplicated; replace it by defining a small struct (e.g., FVariablePinType with
fields PinSubCategoryObject and objectClass) and use
FJsonObjectConverter::JsonObjectToUStruct to deserialize (*PinTypeObj) once
(where PinTypeObj is used in the current branch and the other branch around the
similar block at the other occurrence). After deserializing, use the struct's
fields to pick the subclass name (prefer PinSubCategoryObject then objectClass)
and proceed with the existing object/class handling logic (update usages in the
code paths that previously read PinSubCategoryObject/objectClass manually).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp`:
- Around line 846-848: The node position is being assigned after
NodeCreator.Finalize() (CallFuncNode->NodePosX and CallFuncNode->NodePosY),
which violates the established FGraphNodeCreator pattern; move the position
assignments to before calling NodeCreator.Finalize() so the node's
NodePosX/NodePosY are set prior to finalization (follow the same approach as the
FinalizeAndReport lambda that sets position before Finalize()).

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp`:
- Around line 2755-2763: The code silently falls back to UObject::StaticClass()
when ResolveUClass() fails for an explicitly supplied subtype (variablePinType's
PinSubCategoryObject or objectClass); change this so that if
ResolveUClass(SubClassName) or ResolveUClass(objectClass) returns null AND the
caller explicitly provided that subtype, call SendAutomationError(...) and
return failure instead of assigning UObject::StaticClass(); ensure both the
PinSubCategoryObject branch and the objectClass branch use the same fail-fast
behavior and reference variablePinType/PinType when deciding if the subtype was
explicitly provided.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp`:
- Around line 533-549: The new code creates a ULevelStreamingDynamic
(StreamingLevel) and calls SetWorldAssetByPackageName(FName(*SublevelPath)) then
adds it to the world via World->AddStreamingLevel(StreamingLevel) but does not
ensure the sublevel asset actually exists on disk; restore or add the sublevel
creation step (e.g., call the existing McpSafeLevelSave or equivalent to write
the .umap before creating/adding the streaming reference), or rename this
handler to explicitly be an "add_streaming_level_reference" and require callers
to create the asset, and update SetWorldAssetByPackageName usage and any caller
documentation accordingly so the streaming level doesn't point to a non-existent
package.
- Around line 1938-1943: The error text is misleading because
GetLevelScriptBlueprint(true) attempts to create the blueprint; update the
failure message used in the block that checks ULevelScriptBlueprint* LevelBP =
CurrentLevel->GetLevelScriptBlueprint(true) so it reads "Failed to get or create
Level Blueprint" (matching the behavior and the message used by
HandleOpenLevelBlueprint) by replacing the TEXT("Failed to get Level Blueprint")
passed to Subsystem->SendAutomationResponse with TEXT("Failed to get or create
Level Blueprint").

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp`:
- Around line 676-681: The inline parsing for slot name and visibility drops the
shared visibility mapping (losing SelfHitTestInvisible) and duplicates parsing
logic; replace the manual visibility parse with the centralized
GetVisibility(JsonObject/Payload) call and stop hand-parsing visibility in
McpAutomationBridge_WidgetAuthoringHandlers.cpp (the current block using
GetJsonStringField for "name"/"slotName" and the similar logic at lines
~705-717). Also follow the guideline to use FJsonObjectConverter for
JSON->struct parsing instead of piecemeal GetJsonStringField usage—locate usages
of GetJsonStringField, SlotName, and the ad-hoc visibility handling and switch
them to GetVisibility(Payload) and FJsonObjectConverter-based struct
deserialization to keep behavior consistent and avoid losing
SelfHitTestInvisible.
- Around line 537-545: The code builds a UniqueName when a name collision occurs
but continues to report the original SlotName; change the response payload to
use the resolved UniqueName (the final value after the while loop) wherever the
created widget is referenced so downstream automation targets the real widget
(update the place that assembles the response to use UniqueName instead of
SlotName). Specifically, after the collision loop that uses
WidgetBP->WidgetTree->FindWidget and before/after ConstructWidget (e.g.,
UCanvasPanel* CanvasPanel = ... FName(*UniqueName)), ensure the created widget
name returned/serialized is UniqueName; apply the same change to the other add_*
handlers that perform the same suffixing logic.

---

Outside diff comments:
In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SystemControlHandlers.cpp`:
- Around line 328-339: AssetPath is used directly from user input causing
possible directory traversal; call SanitizeProjectRelativePath(AssetPath) (same
as done for ExportPath and in LevelHandlers/TextureHandlers/SkeletonHandlers)
immediately after reading AssetPath, validate the result and if sanitization
fails call SendAutomationError(RequestingSocket, RequestId, TEXT("invalid
assetPath"), TEXT("INVALID_ARGUMENT")) and return true; apply the same
SanitizeProjectRelativePath check for the other AssetPath usages referenced
(around the other handlers/lines noted) so all asset loads use the sanitized
path.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp`:
- Around line 5688-5723: When a non-empty ParentSlot is supplied, do not
silently fall back to RootWidget; detect the two error cases and return failures
instead: if ParentSlot is provided but FindWidget(FName(*ParentSlot)) returns
null, call SendAutomationError(RequestingSocket, RequestId, TEXT("Parent slot
not found"), TEXT("PARENT_NOT_FOUND")) and return true; if the found widget or
the RootWidget is not a UPanelWidget (i.e., Cast<UPanelWidget> yields null) call
SendAutomationError(RequestingSocket, RequestId, TEXT("Invalid parent: not a
panel widget"), TEXT("INVALID_PARENT")) and return true; only call
Parent->AddChild(SafeZone) when Parent is valid. Apply the same checks and error
returns for the other similar blocks referenced (lines ~5738-5776 and
~5793-5830) that use ParentSlot, WidgetBP->WidgetTree->RootWidget, and
Parent->AddChild.
- Around line 4921-4954: The code constructs widgets using raw names (SlotName,
SlotName + "_Border", "_MapImage", "_PlayerIndicator") which can collide on
repeated add_* calls; update the handler to derive a unique base name before
constructing child widgets (e.g., call or add an EnsureUniqueSlotName helper
that checks WidgetBP->WidgetTree for existing widgets and appends a numeric
suffix until unique), then use that returned unique base for MinimapContainer,
MinimapBorder, MapImage and PlayerIndicator creation; apply the same fix pattern
to the other composite creators (add_compass, add_interaction_prompt,
add_objective_tracker, add_damage_indicator, add_quest_tracker) so all
constructed names are passed through the same uniqueness function.

---

Nitpick comments:
In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp`:
- Around line 750-754: The hardcoded project-specific path prefixes added to
PathsToTry (e.g., "/Game/Blueprints/Characters/%s" and
"/Game/Blueprints/Combat/%s") reduce portability; modify the logic in
McpAutomationBridge_BlueprintGraphHandlers.cpp where PathsToTry.Add(...) is
called so it either only includes generic paths ("/Game/Blueprints/%s" and
"/Game/%s") or reads additional prefixes from a configurable source (editor
setting, config file, or optional parameter) before formatting with
TargetClassName; update the code that builds PathsToTry to use the configurable
list (or remove the specific entries) and ensure TargetClassName is still used
for formatting.
- Around line 744-762: Sanitize TargetClassName before constructing PathsToTry:
validate and normalize the user-provided TargetClassName (trim whitespace, strip
any leading/trailing slashes or dots, remove extensions, and reject or remove
any path traversal tokens like ".." or drive/URI characters) prior to using it
to build the FStrings and calling LoadBlueprintAsset; update the block that
assembles PathsToTry (the code using TargetClassName, TargetClass, and
LoadBlueprintAsset in McpAutomationBridge_BlueprintGraphHandlers.cpp) to operate
on the sanitized string and bail with a clear error if the name contains
disallowed characters.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp`:
- Around line 2749-2754: The manual JSON extraction for variablePinType is
duplicated; replace it by defining a small struct (e.g., FVariablePinType with
fields PinSubCategoryObject and objectClass) and use
FJsonObjectConverter::JsonObjectToUStruct to deserialize (*PinTypeObj) once
(where PinTypeObj is used in the current branch and the other branch around the
similar block at the other occurrence). After deserializing, use the struct's
fields to pick the subclass name (prefer PinSubCategoryObject then objectClass)
and proceed with the existing object/class handling logic (update usages in the
code paths that previously read PinSubCategoryObject/objectClass manually).

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp`:
- Around line 548-549: HandleCreateSublevel is adding StreamingLevel
unconditionally which can create duplicate entries; before calling
World->AddStreamingLevel(StreamingLevel) check World’s existing streaming levels
(World->GetStreamingLevels() or World->StreamingLevels) for an entry whose
WorldAssetPackageName (or equivalent package name getter on the streaming level)
matches the new StreamingLevel’s WorldAssetPackageName and skip adding if found.
Ensure you compare the same identifier used elsewhere (WorldAssetPackageName)
and only call AddStreamingLevel when no existing streaming level with that
package name exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: efc73c0f-3d3e-48bc-a890-50616f0b8278

📥 Commits

Reviewing files that changed from the base of the PR and between fd94f7a and b65ef9e.

📒 Files selected for processing (8)
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SystemControlHandlers.cpp
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpHandlerUtils.cpp
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpSafeOperations.h
💤 Files with no reviewable changes (2)
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h
  • plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpSafeOperations.h

Comment on lines +846 to +848
NodeCreator.Finalize();
CallFuncNode->NodePosX = X;
CallFuncNode->NodePosY = Y;
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.

⚠️ Potential issue | 🟡 Minor

Position set after Finalize() is inconsistent with the established pattern.

The FinalizeAndReport lambda (lines 335-342) explicitly sets position before Finalize() with the comment "Set position BEFORE finalization per FGraphNodeCreator pattern". However, this handler sets position after finalization.

While this may work functionally for UK2Node_CallFunction, it deviates from the documented pattern and could cause subtle issues with node positioning in the graph editor.

🔧 Proposed fix to match established pattern
       FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
       UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
       CallFuncNode->SetFromFunction(ArrayFunc);
-      // Finalize allocates default pins (wildcard at this point)
-      NodeCreator.Finalize();
+      // Set position BEFORE finalization per FGraphNodeCreator pattern
       CallFuncNode->NodePosX = X;
       CallFuncNode->NodePosY = Y;
+      // Finalize allocates default pins (wildcard at this point)
+      NodeCreator.Finalize();
       // ReconstructNode forces pin re-evaluation which is required for array
       // function nodes so that TargetArray and ReturnValue pins appear correctly.
       CallFuncNode->ReconstructNode();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
NodeCreator.Finalize();
CallFuncNode->NodePosX = X;
CallFuncNode->NodePosY = Y;
FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
CallFuncNode->SetFromFunction(ArrayFunc);
// Set position BEFORE finalization per FGraphNodeCreator pattern
CallFuncNode->NodePosX = X;
CallFuncNode->NodePosY = Y;
// Finalize allocates default pins (wildcard at this point)
NodeCreator.Finalize();
// ReconstructNode forces pin re-evaluation which is required for array
// function nodes so that TargetArray and ReturnValue pins appear correctly.
CallFuncNode->ReconstructNode();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp`
around lines 846 - 848, The node position is being assigned after
NodeCreator.Finalize() (CallFuncNode->NodePosX and CallFuncNode->NodePosY),
which violates the established FGraphNodeCreator pattern; move the position
assignments to before calling NodeCreator.Finalize() so the node's
NodePosX/NodePosY are set prior to finalization (follow the same approach as the
FinalizeAndReport lambda that sets position before Finalize()).

Comment on lines +2755 to +2763
if (!SubClassName.IsEmpty()) {
if (UClass *SubClass = ResolveUClass(SubClassName)) {
PinType.PinSubCategoryObject = SubClass;
}
}
}
if (!PinType.PinSubCategoryObject.IsValid()) {
PinType.PinSubCategoryObject = UObject::StaticClass();
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify current subtype resolution + fallback behavior in add_variable branch
rg -n -C6 'variablePinType|PinSubCategoryObject|objectClass|ResolveUClass|UObject::StaticClass' \
  plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp

# Verify ResolveUClass capabilities/limits used by this handler
rg -n -C10 'static inline UClass \*ResolveUClass|FindObject<UClass>|LoadObject<UClass>|TObjectIterator<UClass>' \
  plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h

Repository: ChiR24/Unreal_mcp

Length of output: 8217


Fail fast when an explicit variablePinType subtype cannot be resolved

When caller supplies a variablePinType with a PinSubCategoryObject or objectClass field (lines 2755–2758 and 2774–2777), the code attempts to resolve it via ResolveUClass(). On resolution failure, both the "object" and "class" type handlers silently default to UObject::StaticClass() rather than failing. This is inconsistent with the main type resolution path (lines 2790–2793), which sends a SendAutomationError() when the primary type cannot be resolved. An explicit but unresolvable subtype should not silently succeed with an incorrect, overly broad type; instead, return an error to the caller.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp`
around lines 2755 - 2763, The code silently falls back to UObject::StaticClass()
when ResolveUClass() fails for an explicitly supplied subtype (variablePinType's
PinSubCategoryObject or objectClass); change this so that if
ResolveUClass(SubClassName) or ResolveUClass(objectClass) returns null AND the
caller explicitly provided that subtype, call SendAutomationError(...) and
return failure instead of assigning UObject::StaticClass(); ensure both the
PinSubCategoryObject branch and the objectClass branch use the same fail-fast
behavior and reference variablePinType/PinType when deciding if the subtype was
explicitly provided.

Comment on lines +533 to +549
// Add streaming level
ULevelStreamingDynamic* StreamingLevel = NewObject<ULevelStreamingDynamic>(World, ULevelStreamingDynamic::StaticClass());
if (!StreamingLevel)
{
Subsystem->SendAutomationResponse(Socket, RequestId, false,
FString::Printf(TEXT("Failed to create world for sublevel: %s"), *SublevelName), nullptr, TEXT("WORLD_CREATION_FAILED"));
TEXT("Failed to create streaming level object"), nullptr);
return true;
}

// Initialize the world if not already initialized
if (!NewSublevelWorld->bIsWorldInitialized)
{
NewSublevelWorld->InitWorld();
}
// Configure the streaming level
StreamingLevel->SetWorldAssetByPackageName(FName(*SublevelPath));
StreamingLevel->LevelTransform = FTransform::Identity;
StreamingLevel->SetShouldBeVisible(true);
StreamingLevel->SetShouldBeLoaded(true);

// Mark package dirty
SublevelPackage->MarkPackageDirty();
// Add to world's streaming levels
World->AddStreamingLevel(StreamingLevel);
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.

⚠️ Potential issue | 🟠 Major

Streaming level references a non-existent sublevel asset.

The new implementation creates a ULevelStreamingDynamic reference pointing to SublevelPath, but no actual sublevel .umap file is created on disk. When the streaming level attempts to load, it will fail because the referenced package doesn't exist.

Previous behavior (per AI summary) created the sublevel file via McpSafeLevelSave. The current code only adds a streaming reference to a path that may not exist.

Consider either:

  1. Restoring sublevel file creation before adding the streaming reference, or
  2. Renaming this action to add_streaming_level_reference and adding a separate create_sublevel that creates the actual sublevel file, or
  3. Documenting that sublevelPath must point to an existing level asset
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp`
around lines 533 - 549, The new code creates a ULevelStreamingDynamic
(StreamingLevel) and calls SetWorldAssetByPackageName(FName(*SublevelPath)) then
adds it to the world via World->AddStreamingLevel(StreamingLevel) but does not
ensure the sublevel asset actually exists on disk; restore or add the sublevel
creation step (e.g., call the existing McpSafeLevelSave or equivalent to write
the .umap before creating/adding the streaming reference), or rename this
handler to explicitly be an "add_streaming_level_reference" and require callers
to create the asset, and update SetWorldAssetByPackageName usage and any caller
documentation accordingly so the streaming level doesn't point to a non-existent
package.

Comment on lines +1938 to 1943
ULevelScriptBlueprint* LevelBP = CurrentLevel->GetLevelScriptBlueprint(true);
if (!LevelBP)
{
Subsystem->SendAutomationResponse(Socket, RequestId, false,
TEXT("Failed to get or create Level Blueprint"), nullptr);
TEXT("Failed to get Level Blueprint"), nullptr);
return true;
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.

⚠️ Potential issue | 🟡 Minor

Error message inconsistent with behavior.

Line 1938 calls GetLevelScriptBlueprint(true) which attempts to create the blueprint if it doesn't exist. However, line 1942 says "Failed to get Level Blueprint" - this should say "Failed to get or create Level Blueprint" to match the behavior (and to be consistent with HandleOpenLevelBlueprint at line 1885).

📝 Proposed fix
     if (!LevelBP)
     {
         Subsystem->SendAutomationResponse(Socket, RequestId, false,
-            TEXT("Failed to get Level Blueprint"), nullptr);
+            TEXT("Failed to get or create Level Blueprint"), nullptr);
         return true;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp`
around lines 1938 - 1943, The error text is misleading because
GetLevelScriptBlueprint(true) attempts to create the blueprint; update the
failure message used in the block that checks ULevelScriptBlueprint* LevelBP =
CurrentLevel->GetLevelScriptBlueprint(true) so it reads "Failed to get or create
Level Blueprint" (matching the behavior and the message used by
HandleOpenLevelBlueprint) by replacing the TEXT("Failed to get Level Blueprint")
passed to Subsystem->SendAutomationResponse with TEXT("Failed to get or create
Level Blueprint").

Comment on lines +537 to +545
FString UniqueName = SlotName;
{
int32 Suffix = 1;
while (WidgetBP->WidgetTree->FindWidget(FName(*UniqueName)) != nullptr)
{
UniqueName = FString::Printf(TEXT("%s_%d"), *SlotName, Suffix++);
}
}
UCanvasPanel* CanvasPanel = WidgetBP->WidgetTree->ConstructWidget<UCanvasPanel>(UCanvasPanel::StaticClass(), FName(*UniqueName));
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.

⚠️ Potential issue | 🟠 Major

Return the actual widget name after collision handling.

When a duplicate exists, this branch creates UniqueName but still reports the original slotName. Any follow-up automation that uses the response payload will then target a widget that was never created. The same mismatch is repeated in the other add_* handlers that now suffix duplicates.

💡 Suggested fix
-        ResultJson->SetStringField(TEXT("slotName"), SlotName);
+        ResultJson->SetStringField(TEXT("slotName"), UniqueName);

Also applies to: 584-585

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp`
around lines 537 - 545, The code builds a UniqueName when a name collision
occurs but continues to report the original SlotName; change the response
payload to use the resolved UniqueName (the final value after the while loop)
wherever the created widget is referenced so downstream automation targets the
real widget (update the place that assembles the response to use UniqueName
instead of SlotName). Specifically, after the collision loop that uses
WidgetBP->WidgetTree->FindWidget and before/after ConstructWidget (e.g.,
UCanvasPanel* CanvasPanel = ... FName(*UniqueName)), ensure the created widget
name returned/serialized is UniqueName; apply the same change to the other add_*
handlers that perform the same suffixing logic.

Comment on lines +676 to +681
// Accept "name" (tool schema field) or "slotName" (legacy), fall back to type default
FString SlotName = GetJsonStringField(Payload, TEXT("name"));
if (SlotName.IsEmpty())
{
SlotName = GetJsonStringField(Payload, TEXT("slotName"), TEXT("VerticalBox"));
}
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.

⚠️ Potential issue | 🟡 Minor

Use the shared visibility mapping here.

This inline parser drops SelfHitTestInvisible, so valid input now becomes Visible. It also adds another hand-maintained payload branch in a file that already has a lot of parsing drift; GetVisibility() keeps the behavior centralized.

💡 Suggested fix
-        FString VisibilityStr = GetJsonStringField(Payload, TEXT("visibility"));
-        if (!VisibilityStr.IsEmpty())
-        {
-            if (VisibilityStr.Equals(TEXT("Collapsed"), ESearchCase::IgnoreCase))
-                VBox->SetVisibility(ESlateVisibility::Collapsed);
-            else if (VisibilityStr.Equals(TEXT("Hidden"), ESearchCase::IgnoreCase))
-                VBox->SetVisibility(ESlateVisibility::Hidden);
-            else if (VisibilityStr.Equals(TEXT("HitTestInvisible"), ESearchCase::IgnoreCase))
-                VBox->SetVisibility(ESlateVisibility::HitTestInvisible);
-            else
-                VBox->SetVisibility(ESlateVisibility::Visible);
-        }
+        const FString VisibilityStr = GetJsonStringField(Payload, TEXT("visibility"));
+        if (!VisibilityStr.IsEmpty())
+        {
+            VBox->SetVisibility(GetVisibility(VisibilityStr));
+        }
As per coding guidelines `Use FJsonObjectConverter for JSON parsing and struct serialization instead of manual JSON parsing`.

Also applies to: 705-717

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WidgetAuthoringHandlers.cpp`
around lines 676 - 681, The inline parsing for slot name and visibility drops
the shared visibility mapping (losing SelfHitTestInvisible) and duplicates
parsing logic; replace the manual visibility parse with the centralized
GetVisibility(JsonObject/Payload) call and stop hand-parsing visibility in
McpAutomationBridge_WidgetAuthoringHandlers.cpp (the current block using
GetJsonStringField for "name"/"slotName" and the similar logic at lines
~705-717). Also follow the guideline to use FJsonObjectConverter for
JSON->struct parsing instead of piecemeal GetJsonStringField usage—locate usages
of GetJsonStringField, SlotName, and the ad-hoc visibility handling and switch
them to GetVisibility(Payload) and FJsonObjectConverter-based struct
deserialization to keep behavior consistent and avoid losing
SelfHitTestInvisible.

@ChiR24 ChiR24 self-assigned this Mar 19, 2026
@ChiR24
Copy link
Copy Markdown
Owner

ChiR24 commented Mar 20, 2026

🔴 Request Changes

Thanks for tackling these Blueprint bugs! A couple of issues need fixing.


Must Fix

1. OFPA Check Removal — Will Crash on UE 5.4+

AWorldDataLayers::AddDataLayerInstance() in the engine has:

check(GetLevel()->IsUsingExternalObjects());

This assertion exists in UE 5.4 through 5.7. Your removed validation was preventing this crash. Creating data layers on non-OFPA levels will now hit this engine check and crash.

Please restore the OFPA validation.


2. Wrong Node Class for Array Functions

Your PR uses UK2Node_CallFunction, but array functions should use UK2Node_CallArrayFunction — it has special wildcard pin handling that the base class lacks.

This explains the zero-pins bug you're trying to fix.

Use UK2Node_CallArrayFunction instead.


Need Clarification

3. World cleanup code removed — The EndFrame(), CleanupWorld(), RemoveFromRoot() calls were added to prevent crashes. Can you explain why they're safe to remove?

4. Sublevel creation — The new code only adds a streaming reference but doesn't create the .umap file. Is this intentional?


What Looks Good ✅

  • add_variable typed pin support
  • get_pin_details exposing pinSubType
  • Widget unique naming
  • ForEachLoop alias removal

Summary: Items 1-2 will cause crashes/bugs. Please fix before merging. Happy to discuss!

@Kaen-SG
Copy link
Copy Markdown
Author

Kaen-SG commented Mar 20, 2026

🔴 Request Changes

Thanks for tackling these Blueprint bugs! A couple of issues need fixing.

Must Fix

1. OFPA Check Removal — Will Crash on UE 5.4+

AWorldDataLayers::AddDataLayerInstance() in the engine has:

check(GetLevel()->IsUsingExternalObjects());

This assertion exists in UE 5.4 through 5.7. Your removed validation was preventing this crash. Creating data layers on non-OFPA levels will now hit this engine check and crash.

Please restore the OFPA validation.

2. Wrong Node Class for Array Functions

Your PR uses UK2Node_CallFunction, but array functions should use UK2Node_CallArrayFunction — it has special wildcard pin handling that the base class lacks.

This explains the zero-pins bug you're trying to fix.

Use UK2Node_CallArrayFunction instead.

Need Clarification

3. World cleanup code removed — The EndFrame(), CleanupWorld(), RemoveFromRoot() calls were added to prevent crashes. Can you explain why they're safe to remove?

4. Sublevel creation — The new code only adds a streaming reference but doesn't create the .umap file. Is this intentional?

What Looks Good ✅

* `add_variable` typed pin support

* `get_pin_details` exposing `pinSubType`

* Widget unique naming

* ForEachLoop alias removal

Summary: Items 1-2 will cause crashes/bugs. Please fix before merging. Happy to discuss!

image

Claude claims it was a bug in the original repo.
In any case an OFPA Crash guard was added and I'm working on the rest of the changes now.

@ChiR24
Copy link
Copy Markdown
Owner

ChiR24 commented Mar 20, 2026

You're right that McpAutomationBridge_WorldPartitionHandlers.cpp wasn't modified — but the OFPA check was removed from a different file: McpAutomationBridge_LevelStructureHandlers.cpp, specifically in the HandleCreateDataLayer function (lines 1660-1684 in the original file).

The PR diff shows this guard being removed from plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelStructureHandlers.cpp:

// CRITICAL: Check if the level uses External Objects (OFPA)
// Data Layer instances require OFPA to be enabled, otherwise AddDataLayerInstance()
// will hit an assertion: "GetLevel()->IsUsingExternalObjects()"
if (!PersistentLevel || !PersistentLevel->IsUsingExternalObjects())
{
    // ... returns error before calling CreateDataLayerInstance ...
}

A few clarifications:

  1. The guard was in the PR's removal scope — it's being deleted by this PR, not added. The - prefix in the diff indicates lines being removed from LevelStructureHandlers.cpp.

  2. This wasn't a pre-existing bug — the guard was correctly preventing the crash. Removing it creates the bug.

  3. The crash chain — Without this guard, create_data_layer on a non-OFPA level calls CreateDataLayerInstance()AddDataLayerInstance() → hits engine's check(GetLevel()->IsUsingExternalObjects()) → crash.

The guard should stay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants