unreal-engine-mcp-server 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.github/workflows/publish-mcp.yml +1 -4
  2. package/.github/workflows/release-drafter.yml +2 -1
  3. package/CHANGELOG.md +38 -0
  4. package/dist/automation/bridge.d.ts +1 -2
  5. package/dist/automation/bridge.js +24 -23
  6. package/dist/automation/connection-manager.d.ts +1 -0
  7. package/dist/automation/connection-manager.js +10 -0
  8. package/dist/automation/message-handler.js +5 -4
  9. package/dist/automation/request-tracker.d.ts +4 -0
  10. package/dist/automation/request-tracker.js +11 -3
  11. package/dist/tools/actors.d.ts +19 -1
  12. package/dist/tools/actors.js +15 -5
  13. package/dist/tools/assets.js +1 -1
  14. package/dist/tools/blueprint.d.ts +12 -0
  15. package/dist/tools/blueprint.js +43 -14
  16. package/dist/tools/consolidated-tool-definitions.js +2 -1
  17. package/dist/tools/editor.js +3 -2
  18. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  19. package/dist/tools/handlers/actor-handlers.js +14 -8
  20. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  21. package/dist/tools/handlers/sequence-handlers.js +24 -13
  22. package/dist/tools/introspection.d.ts +1 -1
  23. package/dist/tools/introspection.js +1 -1
  24. package/dist/tools/level.js +3 -3
  25. package/dist/tools/lighting.d.ts +54 -7
  26. package/dist/tools/lighting.js +4 -4
  27. package/dist/tools/materials.d.ts +1 -1
  28. package/dist/types/tool-types.d.ts +2 -0
  29. package/dist/unreal-bridge.js +4 -4
  30. package/dist/utils/command-validator.js +6 -5
  31. package/dist/utils/error-handler.d.ts +24 -2
  32. package/dist/utils/error-handler.js +58 -23
  33. package/dist/utils/normalize.d.ts +7 -4
  34. package/dist/utils/normalize.js +12 -10
  35. package/dist/utils/response-validator.js +88 -73
  36. package/dist/utils/unreal-command-queue.d.ts +2 -0
  37. package/dist/utils/unreal-command-queue.js +8 -1
  38. package/docs/handler-mapping.md +4 -2
  39. package/package.json +1 -1
  40. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  41. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  42. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  43. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  44. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  45. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  46. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  47. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  48. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  49. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  50. package/server.json +3 -3
  51. package/src/automation/bridge.ts +27 -25
  52. package/src/automation/connection-manager.ts +18 -0
  53. package/src/automation/message-handler.ts +33 -8
  54. package/src/automation/request-tracker.ts +39 -7
  55. package/src/server/tool-registry.ts +3 -3
  56. package/src/tools/actors.ts +44 -19
  57. package/src/tools/assets.ts +3 -3
  58. package/src/tools/blueprint.ts +115 -49
  59. package/src/tools/consolidated-tool-definitions.ts +2 -1
  60. package/src/tools/editor.ts +4 -3
  61. package/src/tools/handlers/actor-handlers.ts +14 -9
  62. package/src/tools/handlers/sequence-handlers.ts +86 -63
  63. package/src/tools/introspection.ts +7 -7
  64. package/src/tools/level.ts +6 -6
  65. package/src/tools/lighting.ts +19 -19
  66. package/src/tools/materials.ts +1 -1
  67. package/src/tools/sequence.ts +1 -1
  68. package/src/tools/ui.ts +1 -1
  69. package/src/types/tool-types.ts +4 -0
  70. package/src/unreal-bridge.ts +71 -26
  71. package/src/utils/command-validator.ts +46 -5
  72. package/src/utils/error-handler.ts +128 -45
  73. package/src/utils/normalize.ts +38 -16
  74. package/src/utils/response-validator.ts +103 -87
  75. package/src/utils/unreal-command-queue.ts +13 -1
@@ -40,19 +40,27 @@
40
40
  #endif
41
41
 
42
42
  /**
43
- * Process a "manage_blueprint_graph" automation request to inspect or modify a Blueprint graph.
43
+ * Process a "manage_blueprint_graph" automation request to inspect or modify a
44
+ * Blueprint graph.
44
45
  *
45
- * The Payload JSON controls the specific operation via the "subAction" field (examples: create_node,
46
- * connect_pins, get_nodes, break_pin_links, delete_node, create_reroute_node, set_node_property,
47
- * get_node_details, get_graph_details, get_pin_details). In editor builds this function performs
48
- * graph/blueprint lookups and edits; in non-editor builds it reports an editor-only error.
46
+ * The Payload JSON controls the specific operation via the "subAction" field
47
+ * (examples: create_node, connect_pins, get_nodes, break_pin_links,
48
+ * delete_node, create_reroute_node, set_node_property, get_node_details,
49
+ * get_graph_details, get_pin_details). In editor builds this function performs
50
+ * graph/blueprint lookups and edits; in non-editor builds it reports an
51
+ * editor-only error.
49
52
  *
50
- * @param RequestId Unique identifier for the automation request (used in responses).
51
- * @param Action The requested action name; this handler only processes "manage_blueprint_graph".
52
- * @param Payload JSON object containing action options such as "assetPath"/"blueprintPath", "graphName",
53
- * "subAction" and subaction-specific fields (nodeType, nodeId, pin names, positions, etc.).
54
- * @param RequestingSocket WebSocket used to send responses and errors back to the requester.
55
- * @return `true` if the request was handled by this function (Action == "manage_blueprint_graph"), `false` otherwise.
53
+ * @param RequestId Unique identifier for the automation request (used in
54
+ * responses).
55
+ * @param Action The requested action name; this handler only processes
56
+ * "manage_blueprint_graph".
57
+ * @param Payload JSON object containing action options such as
58
+ * "assetPath"/"blueprintPath", "graphName", "subAction" and subaction-specific
59
+ * fields (nodeType, nodeId, pin names, positions, etc.).
60
+ * @param RequestingSocket WebSocket used to send responses and errors back to
61
+ * the requester.
62
+ * @return `true` if the request was handled by this function (Action ==
63
+ * "manage_blueprint_graph"), `false` otherwise.
56
64
  */
57
65
  bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
58
66
  const FString &RequestId, const FString &Action,
@@ -339,76 +347,39 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
339
347
  return true;
340
348
  }
341
349
 
342
- // Basic node creation logic - this can be expanded significantly
343
- if (NodeType == TEXT("InputAxisEvent")) {
344
- FString InputAxisName;
345
- Payload->TryGetStringField(TEXT("inputAxisName"), InputAxisName);
350
+ // ========================================================================
351
+ // DYNAMIC NODE CREATION - Find node classes at runtime
352
+ // ========================================================================
346
353
 
347
- if (InputAxisName.IsEmpty()) {
348
- SendAutomationError(RequestingSocket, RequestId,
349
- TEXT("inputAxisName required"),
350
- TEXT("INVALID_ARGUMENT"));
351
- return true;
352
- }
353
-
354
- FGraphNodeCreator<UK2Node_InputAxisEvent> NodeCreator(*TargetGraph);
355
- UK2Node_InputAxisEvent *InputNode = NodeCreator.CreateNode(false);
356
- InputNode->InputAxisName = FName(*InputAxisName);
354
+ // Helper: Try to find a UK2Node subclass by name
355
+ auto FindNodeClassByName = [](const FString &TypeName) -> UClass * {
356
+ TArray<FString> NamesToTry;
357
+ NamesToTry.Add(TypeName);
358
+ NamesToTry.Add(FString::Printf(TEXT("K2Node_%s"), *TypeName));
359
+ NamesToTry.Add(FString::Printf(TEXT("UK2Node_%s"), *TypeName));
357
360
 
358
- FinalizeAndReport(NodeCreator, InputNode);
359
- } else if (NodeType == TEXT("CallFunction") ||
360
- NodeType == TEXT("K2Node_CallFunction") ||
361
- NodeType == TEXT("FunctionCall")) {
362
- FString MemberName;
363
- Payload->TryGetStringField(TEXT("memberName"), MemberName);
364
- FString MemberClass;
365
- Payload->TryGetStringField(TEXT("memberClass"),
366
- MemberClass); // Optional, for static functions
361
+ for (TObjectIterator<UClass> It; It; ++It) {
362
+ if (!It->IsChildOf(UEdGraphNode::StaticClass()))
363
+ continue;
364
+ if (It->HasAnyClassFlags(CLASS_Abstract))
365
+ continue;
367
366
 
368
- UFunction *Func = nullptr;
369
- if (!MemberClass.IsEmpty()) {
370
- UClass *Class = ResolveUClass(MemberClass);
371
- if (Class) {
372
- Func = Class->FindFunctionByName(*MemberName);
373
- }
374
- } else {
375
- // Try to find in blueprint context
376
- Func = Blueprint->GeneratedClass->FindFunctionByName(*MemberName);
377
- if (!Func) {
378
- // Try global search if simple name, or check common libraries
379
- Func = FindObject<UFunction>(nullptr, *MemberName);
380
- if (!Func) {
381
- // Fallback: Check common libraries
382
- if (UClass *KSL = UKismetSystemLibrary::StaticClass())
383
- Func = KSL->FindFunctionByName(*MemberName);
384
- if (!Func)
385
- if (UClass *GPS = UGameplayStatics::StaticClass())
386
- Func = GPS->FindFunctionByName(*MemberName);
387
- if (!Func)
388
- if (UClass *KML = UKismetMathLibrary::StaticClass())
389
- Func = KML->FindFunctionByName(*MemberName);
367
+ FString ClassName = It->GetName();
368
+ for (const FString &NameToMatch : NamesToTry) {
369
+ if (ClassName.Equals(NameToMatch, ESearchCase::IgnoreCase)) {
370
+ return *It;
390
371
  }
391
372
  }
392
373
  }
374
+ return nullptr;
375
+ };
393
376
 
394
- if (Func) {
395
- FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
396
- UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
397
- CallFuncNode->SetFromFunction(Func);
398
- FinalizeAndReport(NodeCreator, CallFuncNode);
399
- } else {
400
- SendAutomationError(
401
- RequestingSocket, RequestId,
402
- FString::Printf(TEXT("Could not find function '%s'"), *MemberName),
403
- TEXT("FUNCTION_NOT_FOUND"));
404
- return true;
405
- }
406
- } else if (NodeType == TEXT("VariableGet")) {
377
+ // Special nodes requiring extra parameters
378
+ if (NodeType == TEXT("VariableGet") ||
379
+ NodeType == TEXT("K2Node_VariableGet")) {
407
380
  FString VarName;
408
381
  Payload->TryGetStringField(TEXT("variableName"), VarName);
409
382
  FName VarFName(*VarName);
410
-
411
- // Validation BEFORE creation
412
383
  bool bFound = false;
413
384
  for (const FBPVariableDescription &VarDesc : Blueprint->NewVariables) {
414
385
  if (VarDesc.VarName == VarFName) {
@@ -420,25 +391,25 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
420
391
  Blueprint->GeneratedClass->FindPropertyByName(VarFName)) {
421
392
  bFound = true;
422
393
  }
423
-
424
394
  if (!bFound) {
425
395
  SendAutomationError(
426
396
  RequestingSocket, RequestId,
427
- FString::Printf(TEXT("Could not find variable '%s'"), *VarName),
397
+ FString::Printf(TEXT("Variable '%s' not found"), *VarName),
428
398
  TEXT("VARIABLE_NOT_FOUND"));
429
399
  return true;
430
400
  }
431
-
432
401
  FGraphNodeCreator<UK2Node_VariableGet> NodeCreator(*TargetGraph);
433
402
  UK2Node_VariableGet *VarGet = NodeCreator.CreateNode(false);
434
403
  VarGet->VariableReference.SetSelfMember(VarFName);
435
404
  FinalizeAndReport(NodeCreator, VarGet);
436
- } else if (NodeType == TEXT("VariableSet")) {
405
+ return true;
406
+ }
407
+
408
+ if (NodeType == TEXT("VariableSet") ||
409
+ NodeType == TEXT("K2Node_VariableSet")) {
437
410
  FString VarName;
438
411
  Payload->TryGetStringField(TEXT("variableName"), VarName);
439
412
  FName VarFName(*VarName);
440
-
441
- // Validation BEFORE creation
442
413
  bool bFound = false;
443
414
  for (const FBPVariableDescription &VarDesc : Blueprint->NewVariables) {
444
415
  if (VarDesc.VarName == VarFName) {
@@ -450,97 +421,89 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
450
421
  Blueprint->GeneratedClass->FindPropertyByName(VarFName)) {
451
422
  bFound = true;
452
423
  }
453
-
454
424
  if (!bFound) {
455
425
  SendAutomationError(
456
426
  RequestingSocket, RequestId,
457
- FString::Printf(TEXT("Could not find variable '%s'"), *VarName),
427
+ FString::Printf(TEXT("Variable '%s' not found"), *VarName),
458
428
  TEXT("VARIABLE_NOT_FOUND"));
459
429
  return true;
460
430
  }
461
-
462
431
  FGraphNodeCreator<UK2Node_VariableSet> NodeCreator(*TargetGraph);
463
432
  UK2Node_VariableSet *VarSet = NodeCreator.CreateNode(false);
464
433
  VarSet->VariableReference.SetSelfMember(VarFName);
465
434
  FinalizeAndReport(NodeCreator, VarSet);
466
- } else if (NodeType == TEXT("CustomEvent")) {
467
- FString EventName;
468
- Payload->TryGetStringField(TEXT("eventName"), EventName);
469
-
470
- FGraphNodeCreator<UK2Node_CustomEvent> NodeCreator(*TargetGraph);
471
- UK2Node_CustomEvent *EventNode = NodeCreator.CreateNode(false);
435
+ return true;
436
+ }
472
437
 
473
- EventNode->CustomFunctionName = FName(*EventName);
474
- FinalizeAndReport(NodeCreator, EventNode);
475
- } else if (NodeType == TEXT("Event") || NodeType == TEXT("K2Node_Event")) {
476
- FString EventName;
477
- Payload->TryGetStringField(
478
- TEXT("eventName"),
479
- EventName); // e.g., "ReceiveBeginPlay", "ReceiveTick"
480
- FString MemberClass;
481
- Payload->TryGetStringField(TEXT("memberClass"),
482
- MemberClass); // Optional class override
438
+ if (NodeType == TEXT("CallFunction") ||
439
+ NodeType == TEXT("K2Node_CallFunction") ||
440
+ NodeType == TEXT("FunctionCall")) {
441
+ FString MemberName, MemberClass;
442
+ Payload->TryGetStringField(TEXT("memberName"), MemberName);
443
+ Payload->TryGetStringField(TEXT("memberClass"), MemberClass);
444
+ UFunction *Func = nullptr;
445
+ if (!MemberClass.IsEmpty()) {
446
+ if (UClass *Class = ResolveUClass(MemberClass))
447
+ Func = Class->FindFunctionByName(*MemberName);
448
+ } else {
449
+ Func = Blueprint->GeneratedClass->FindFunctionByName(*MemberName);
450
+ if (!Func) {
451
+ if (UClass *KSL = UKismetSystemLibrary::StaticClass())
452
+ Func = KSL->FindFunctionByName(*MemberName);
453
+ if (!Func)
454
+ if (UClass *GPS = UGameplayStatics::StaticClass())
455
+ Func = GPS->FindFunctionByName(*MemberName);
456
+ if (!Func)
457
+ if (UClass *KML = UKismetMathLibrary::StaticClass())
458
+ Func = KML->FindFunctionByName(*MemberName);
459
+ }
460
+ }
461
+ if (Func) {
462
+ FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
463
+ UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
464
+ CallFuncNode->SetFromFunction(Func);
465
+ FinalizeAndReport(NodeCreator, CallFuncNode);
466
+ } else {
467
+ SendAutomationError(
468
+ RequestingSocket, RequestId,
469
+ FString::Printf(TEXT("Function '%s' not found"), *MemberName),
470
+ TEXT("FUNCTION_NOT_FOUND"));
471
+ }
472
+ return true;
473
+ }
483
474
 
475
+ if (NodeType == TEXT("Event") || NodeType == TEXT("K2Node_Event")) {
476
+ FString EventName, MemberClass;
477
+ Payload->TryGetStringField(TEXT("eventName"), EventName);
478
+ Payload->TryGetStringField(TEXT("memberClass"), MemberClass);
484
479
  if (EventName.IsEmpty()) {
485
480
  SendAutomationError(RequestingSocket, RequestId,
486
- TEXT("eventName required for Event node"),
481
+ TEXT("eventName required"),
487
482
  TEXT("INVALID_ARGUMENT"));
488
483
  return true;
489
484
  }
490
-
491
- // Map common event name aliases to their actual function names
492
- static TMap<FString, FString> EventNameAliases = {
485
+ static TMap<FString, FString> Aliases = {
493
486
  {TEXT("BeginPlay"), TEXT("ReceiveBeginPlay")},
494
487
  {TEXT("Tick"), TEXT("ReceiveTick")},
495
- {TEXT("EndPlay"), TEXT("ReceiveEndPlay")},
496
- {TEXT("ActorBeginOverlap"), TEXT("ReceiveActorBeginOverlap")},
497
- {TEXT("ActorEndOverlap"), TEXT("ReceiveActorEndOverlap")},
498
- {TEXT("Hit"), TEXT("ReceiveHit")},
499
- {TEXT("BeginCursorOver"), TEXT("ReceiveBeginCursorOver")},
500
- {TEXT("EndCursorOver"), TEXT("ReceiveEndCursorOver")},
501
- {TEXT("Clicked"), TEXT("ReceiveClicked")},
502
- {TEXT("Released"), TEXT("ReceiveReleased")},
503
- {TEXT("Destroyed"), TEXT("ReceiveDestroyed")},
504
- };
505
-
506
- if (const FString *Alias = EventNameAliases.Find(EventName)) {
507
- EventName = *Alias;
508
- }
488
+ {TEXT("EndPlay"), TEXT("ReceiveEndPlay")}};
489
+ if (const FString *A = Aliases.Find(EventName))
490
+ EventName = *A;
509
491
 
510
- // Determine target class: use explicit MemberClass or search hierarchy
511
492
  UClass *TargetClass = nullptr;
512
493
  UFunction *EventFunc = nullptr;
513
-
514
494
  if (!MemberClass.IsEmpty()) {
515
- // Explicit class specified
516
495
  TargetClass = ResolveUClass(MemberClass);
517
- if (TargetClass) {
496
+ if (TargetClass)
518
497
  EventFunc = TargetClass->FindFunctionByName(*EventName);
519
- }
520
498
  } else {
521
- // Search up the class hierarchy starting from the Blueprint's parent
522
- // class. Events like ReceiveBeginPlay are defined in AActor, not in
523
- // the generated Blueprint class.
524
- UClass *SearchClass = Blueprint->ParentClass;
525
- while (SearchClass && !EventFunc) {
526
- EventFunc = SearchClass->FindFunctionByName(
527
- *EventName, EIncludeSuperFlag::ExcludeSuper);
528
- if (EventFunc) {
529
- TargetClass = SearchClass;
530
- break;
531
- }
532
- SearchClass = SearchClass->GetSuperClass();
533
- }
534
-
535
- // If not found in hierarchy, try the generated class
536
- if (!EventFunc && Blueprint->GeneratedClass) {
537
- EventFunc = Blueprint->GeneratedClass->FindFunctionByName(*EventName);
538
- if (EventFunc) {
539
- TargetClass = Blueprint->GeneratedClass;
540
- }
499
+ for (UClass *C = Blueprint->ParentClass; C && !EventFunc;
500
+ C = C->GetSuperClass()) {
501
+ EventFunc = C->FindFunctionByName(*EventName,
502
+ EIncludeSuperFlag::ExcludeSuper);
503
+ if (EventFunc)
504
+ TargetClass = C;
541
505
  }
542
506
  }
543
-
544
507
  if (EventFunc && TargetClass) {
545
508
  FGraphNodeCreator<UK2Node_Event> NodeCreator(*TargetGraph);
546
509
  UK2Node_Event *EventNode = NodeCreator.CreateNode(false);
@@ -548,214 +511,92 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
548
511
  EventNode->bOverrideFunction = true;
549
512
  FinalizeAndReport(NodeCreator, EventNode);
550
513
  } else {
551
- // Provide helpful error message
552
- FString SearchedClasses;
553
- UClass *C = Blueprint->ParentClass;
554
- int ClassCount = 0;
555
- while (C && ClassCount < 5) {
556
- if (!SearchedClasses.IsEmpty())
557
- SearchedClasses += TEXT(", ");
558
- SearchedClasses += C->GetName();
559
- C = C->GetSuperClass();
560
- ClassCount++;
561
- }
562
514
  SendAutomationError(
563
515
  RequestingSocket, RequestId,
564
- FString::Printf(TEXT("Could not find event '%s'. Searched classes: "
565
- "%s. Try using the full name like "
566
- "'ReceiveBeginPlay' instead of 'BeginPlay'."),
567
- *EventName, *SearchedClasses),
516
+ FString::Printf(TEXT("Event '%s' not found"), *EventName),
568
517
  TEXT("EVENT_NOT_FOUND"));
569
518
  }
519
+ return true;
520
+ }
570
521
 
571
- } else if (NodeType == TEXT("Cast") ||
572
- NodeType.StartsWith(TEXT("CastTo"))) {
522
+ if (NodeType == TEXT("CustomEvent") ||
523
+ NodeType == TEXT("K2Node_CustomEvent")) {
524
+ FString EventName;
525
+ Payload->TryGetStringField(TEXT("eventName"), EventName);
526
+ FGraphNodeCreator<UK2Node_CustomEvent> NodeCreator(*TargetGraph);
527
+ UK2Node_CustomEvent *EventNode = NodeCreator.CreateNode(false);
528
+ EventNode->CustomFunctionName = FName(*EventName);
529
+ FinalizeAndReport(NodeCreator, EventNode);
530
+ return true;
531
+ }
532
+
533
+ if (NodeType == TEXT("Cast") || NodeType.StartsWith(TEXT("CastTo"))) {
573
534
  FString TargetClassName;
574
535
  Payload->TryGetStringField(TEXT("targetClass"), TargetClassName);
575
-
576
- // If targetClass not specified, try to infer from nodeType
577
- // "CastTo<ClassName>"
578
- if (TargetClassName.IsEmpty() && NodeType.StartsWith(TEXT("CastTo"))) {
579
- TargetClassName = NodeType.Mid(6); // Remove "CastTo" prefix
580
- }
581
-
536
+ if (TargetClassName.IsEmpty() && NodeType.StartsWith(TEXT("CastTo")))
537
+ TargetClassName = NodeType.Mid(6);
582
538
  UClass *TargetClass = ResolveUClass(TargetClassName);
583
539
  if (!TargetClass) {
584
540
  SendAutomationError(
585
541
  RequestingSocket, RequestId,
586
- FString::Printf(
587
- TEXT("Could not resolve target class '%s' for Cast node"),
588
- *TargetClassName),
542
+ FString::Printf(TEXT("Class '%s' not found"), *TargetClassName),
589
543
  TEXT("CLASS_NOT_FOUND"));
590
544
  return true;
591
545
  }
592
-
593
546
  FGraphNodeCreator<UK2Node_DynamicCast> NodeCreator(*TargetGraph);
594
547
  UK2Node_DynamicCast *CastNode = NodeCreator.CreateNode(false);
595
548
  CastNode->TargetType = TargetClass;
596
549
  FinalizeAndReport(NodeCreator, CastNode);
597
- } else if (NodeType == TEXT("Sequence")) {
598
- FGraphNodeCreator<UK2Node_ExecutionSequence> NodeCreator(*TargetGraph);
599
- UK2Node_ExecutionSequence *NewNode = NodeCreator.CreateNode(false);
600
- FinalizeAndReport(NodeCreator, NewNode);
601
- } else if (NodeType == TEXT("Branch") || NodeType == TEXT("IfThenElse") ||
602
- NodeType == TEXT("K2Node_IfThenElse")) {
603
- FGraphNodeCreator<UK2Node_IfThenElse> NodeCreator(*TargetGraph);
604
- UK2Node_IfThenElse *BranchNode = NodeCreator.CreateNode(false);
605
- FinalizeAndReport(NodeCreator, BranchNode);
606
- } else if (NodeType == TEXT("Literal")) {
607
- // Create a literal node that can hold an object reference. This is a
608
- // fully functional K2 literal node that returns the referenced asset
609
- // or object when executed in the graph.
610
- FString LiteralType;
611
- Payload->TryGetStringField(TEXT("literalType"), LiteralType);
612
- LiteralType.TrimStartAndEndInline();
613
- const FString LiteralTypeLower =
614
- LiteralType.IsEmpty() ? TEXT("object") : LiteralType.ToLower();
615
-
616
- if (LiteralTypeLower == TEXT("object") ||
617
- LiteralTypeLower == TEXT("asset")) {
618
- FString ObjectPath;
619
- Payload->TryGetStringField(TEXT("objectPath"), ObjectPath);
620
- if (ObjectPath.IsEmpty()) {
621
- // As a convenience, allow callers to use assetPath as the
622
- // literal source when objectPath is omitted.
623
- Payload->TryGetStringField(TEXT("assetPath"), ObjectPath);
624
- }
625
-
626
- if (ObjectPath.IsEmpty()) {
627
- SendAutomationError(RequestingSocket, RequestId,
628
- TEXT("Literal object creation requires "
629
- "'objectPath' or 'assetPath'."),
630
- TEXT("INVALID_LITERAL"));
631
- return true;
632
- }
633
-
634
- UObject *LoadedObject = LoadObject<UObject>(nullptr, *ObjectPath);
635
- if (!LoadedObject) {
636
- SendAutomationError(
637
- RequestingSocket, RequestId,
638
- FString::Printf(TEXT("Literal object not found at path '%s'"),
639
- *ObjectPath),
640
- TEXT("OBJECT_NOT_FOUND"));
641
- return true;
642
- }
643
-
644
- // Create the node only after successful validation
645
- FGraphNodeCreator<UK2Node_Literal> NodeCreator(*TargetGraph);
646
- UK2Node_Literal *LiteralNode = NodeCreator.CreateNode(false);
647
- if (!LiteralNode) {
648
- SendAutomationError(RequestingSocket, RequestId,
649
- TEXT("Failed to allocate Literal node."),
650
- TEXT("CREATE_FAILED"));
651
- return true;
652
- }
653
-
654
- // UK2Node_Literal stores the referenced UObject in a private
655
- // member; use its public setter rather than touching the
656
- // field directly so we respect engine encapsulation.
657
- LiteralNode->SetObjectRef(LoadedObject);
658
- FinalizeAndReport(NodeCreator, LiteralNode);
659
- } else {
660
- // Primitive literal support (float/int/bool/strings) can be
661
- // added later by wiring value pins. For now, fail fast rather
662
- // than pretending success.
663
- SendAutomationError(
664
- RequestingSocket, RequestId,
665
- FString::Printf(TEXT("Unsupported literalType '%s' (only "
666
- "'object'/'asset' supported)."),
667
- *LiteralType),
668
- TEXT("UNSUPPORTED_LITERAL_TYPE"));
669
- return true;
670
- }
671
- } else if (NodeType == TEXT("Comment")) {
672
- FGraphNodeCreator<UEdGraphNode_Comment> NodeCreator(*TargetGraph);
673
- UEdGraphNode_Comment *CommentNode = NodeCreator.CreateNode(false);
674
-
675
- FString CommentText;
676
- if (Payload->TryGetStringField(TEXT("comment"), CommentText) &&
677
- !CommentText.IsEmpty()) {
678
- CommentNode->NodeComment = CommentText;
679
- } else {
680
- CommentNode->NodeComment = TEXT("Comment");
681
- }
682
-
683
- CommentNode->NodeWidth = 400;
684
- CommentNode->NodeHeight = 100;
685
-
686
- FinalizeAndReport(NodeCreator, CommentNode);
687
- } else if (NodeType == TEXT("MakeArray")) {
688
- FGraphNodeCreator<UK2Node_MakeArray> NodeCreator(*TargetGraph);
689
- UK2Node_MakeArray *MakeArrayNode = NodeCreator.CreateNode(false);
690
- FinalizeAndReport(NodeCreator, MakeArrayNode);
691
- } else if (NodeType == TEXT("Return")) {
692
- FGraphNodeCreator<UK2Node_FunctionResult> NodeCreator(*TargetGraph);
693
- UK2Node_FunctionResult *ReturnNode = NodeCreator.CreateNode(false);
694
- FinalizeAndReport(NodeCreator, ReturnNode);
695
- } else if (NodeType == TEXT("Self")) {
696
- FGraphNodeCreator<UK2Node_Self> NodeCreator(*TargetGraph);
697
- UK2Node_Self *SelfNode = NodeCreator.CreateNode(false);
698
- FinalizeAndReport(NodeCreator, SelfNode);
699
- } else if (NodeType == TEXT("Select")) {
700
- FGraphNodeCreator<UK2Node_Select> NodeCreator(*TargetGraph);
701
- UK2Node_Select *SelectNode = NodeCreator.CreateNode(false);
702
- FinalizeAndReport(NodeCreator, SelectNode);
703
- } else if (NodeType == TEXT("Timeline")) {
704
- FGraphNodeCreator<UK2Node_Timeline> NodeCreator(*TargetGraph);
705
- UK2Node_Timeline *TimelineNode = NodeCreator.CreateNode(false);
706
-
707
- FString TimelineName;
708
- if (Payload->TryGetStringField(TEXT("timelineName"), TimelineName) &&
709
- !TimelineName.IsEmpty()) {
710
- TimelineNode->TimelineName = FName(*TimelineName);
711
- }
550
+ return true;
551
+ }
712
552
 
713
- FinalizeAndReport(NodeCreator, TimelineNode);
714
- } else if (NodeType == TEXT("MakeStruct")) {
715
- FString StructName;
716
- Payload->TryGetStringField(TEXT("structName"), StructName);
717
- if (StructName.IsEmpty()) {
553
+ if (NodeType == TEXT("InputAxisEvent") ||
554
+ NodeType == TEXT("K2Node_InputAxisEvent")) {
555
+ FString InputAxisName;
556
+ Payload->TryGetStringField(TEXT("inputAxisName"), InputAxisName);
557
+ if (InputAxisName.IsEmpty()) {
718
558
  SendAutomationError(RequestingSocket, RequestId,
719
- TEXT("structName required for MakeStruct"),
559
+ TEXT("inputAxisName required"),
720
560
  TEXT("INVALID_ARGUMENT"));
721
561
  return true;
722
562
  }
723
- UScriptStruct *Struct = FindObject<UScriptStruct>(nullptr, *StructName);
724
- if (!Struct) {
725
- SendAutomationError(RequestingSocket, RequestId,
726
- TEXT("Struct not found"), TEXT("STRUCT_NOT_FOUND"));
727
- return true;
728
- }
563
+ FGraphNodeCreator<UK2Node_InputAxisEvent> NodeCreator(*TargetGraph);
564
+ UK2Node_InputAxisEvent *InputNode = NodeCreator.CreateNode(false);
565
+ InputNode->InputAxisName = FName(*InputAxisName);
566
+ FinalizeAndReport(NodeCreator, InputNode);
567
+ return true;
568
+ }
729
569
 
730
- FGraphNodeCreator<UK2Node_MakeStruct> NodeCreator(*TargetGraph);
731
- UK2Node_MakeStruct *MakeStructNode = NodeCreator.CreateNode(false);
732
- MakeStructNode->StructType = Struct;
733
- FinalizeAndReport(NodeCreator, MakeStructNode);
734
- } else if (NodeType == TEXT("BreakStruct")) {
735
- FString StructName;
736
- Payload->TryGetStringField(TEXT("structName"), StructName);
737
- if (StructName.IsEmpty()) {
738
- SendAutomationError(RequestingSocket, RequestId,
739
- TEXT("structName required for BreakStruct"),
740
- TEXT("INVALID_ARGUMENT"));
741
- return true;
742
- }
743
- UScriptStruct *Struct = FindObject<UScriptStruct>(nullptr, *StructName);
744
- if (!Struct) {
570
+ // ========== DYNAMIC FALLBACK: Create ANY node class by name ==========
571
+ UClass *NodeClass = FindNodeClassByName(NodeType);
572
+ if (NodeClass) {
573
+ UEdGraphNode *NewNode = NewObject<UEdGraphNode>(TargetGraph, NodeClass);
574
+ if (NewNode) {
575
+ TargetGraph->AddNode(NewNode, false, false);
576
+ NewNode->CreateNewGuid();
577
+ NewNode->PostPlacedNewNode();
578
+ NewNode->AllocateDefaultPins();
579
+ NewNode->NodePosX = X;
580
+ NewNode->NodePosY = Y;
581
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
582
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
583
+ Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
584
+ Result->SetStringField(TEXT("nodeName"), NewNode->GetName());
585
+ Result->SetStringField(TEXT("nodeClass"), NodeClass->GetName());
586
+ SendAutomationResponse(RequestingSocket, RequestId, true,
587
+ TEXT("Node created."), Result);
588
+ } else {
745
589
  SendAutomationError(RequestingSocket, RequestId,
746
- TEXT("Struct not found"), TEXT("STRUCT_NOT_FOUND"));
747
- return true;
590
+ TEXT("Failed to instantiate node."),
591
+ TEXT("CREATE_FAILED"));
748
592
  }
749
-
750
- FGraphNodeCreator<UK2Node_BreakStruct> NodeCreator(*TargetGraph);
751
- UK2Node_BreakStruct *BreakStructNode = NodeCreator.CreateNode(false);
752
- BreakStructNode->StructType = Struct;
753
- FinalizeAndReport(NodeCreator, BreakStructNode);
754
593
  } else {
755
594
  SendAutomationError(
756
595
  RequestingSocket, RequestId,
757
- TEXT("Failed to create node (unsupported type or internal error)."),
758
- TEXT("CREATE_FAILED"));
596
+ FString::Printf(TEXT("Node type '%s' not found. Use list_node_types "
597
+ "to see available types."),
598
+ *NodeType),
599
+ TEXT("NODE_TYPE_NOT_FOUND"));
759
600
  }
760
601
  return true;
761
602
  } else if (SubAction == TEXT("connect_pins")) {
@@ -1168,6 +1009,75 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
1168
1009
  SendAutomationResponse(RequestingSocket, RequestId, true,
1169
1010
  TEXT("Pin details retrieved."), Result);
1170
1011
  return true;
1012
+ } else if (SubAction == TEXT("list_node_types")) {
1013
+ // List all available UK2Node types for AI discoverability
1014
+ TArray<TSharedPtr<FJsonValue>> NodeTypes;
1015
+ for (TObjectIterator<UClass> It; It; ++It) {
1016
+ if (!It->IsChildOf(UK2Node::StaticClass()))
1017
+ continue;
1018
+ if (It->HasAnyClassFlags(CLASS_Abstract))
1019
+ continue;
1020
+
1021
+ TSharedPtr<FJsonObject> TypeObj = MakeShared<FJsonObject>();
1022
+ TypeObj->SetStringField(TEXT("className"), It->GetName());
1023
+ TypeObj->SetStringField(TEXT("displayName"),
1024
+ It->GetDisplayNameText().ToString());
1025
+ NodeTypes.Add(MakeShared<FJsonValueObject>(TypeObj));
1026
+ }
1027
+
1028
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1029
+ Result->SetArrayField(TEXT("nodeTypes"), NodeTypes);
1030
+ Result->SetNumberField(TEXT("count"), NodeTypes.Num());
1031
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1032
+ TEXT("Node types listed."), Result);
1033
+ return true;
1034
+ } else if (SubAction == TEXT("set_pin_default_value")) {
1035
+ // Set a default value on a node's input pin
1036
+ FString NodeId, PinName, Value;
1037
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
1038
+ Payload->TryGetStringField(TEXT("pinName"), PinName);
1039
+ Payload->TryGetStringField(TEXT("value"), Value);
1040
+
1041
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
1042
+ if (!TargetNode) {
1043
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
1044
+ TEXT("NODE_NOT_FOUND"));
1045
+ return true;
1046
+ }
1047
+
1048
+ UEdGraphPin *Pin = TargetNode->FindPin(*PinName);
1049
+ if (!Pin) {
1050
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Pin not found."),
1051
+ TEXT("PIN_NOT_FOUND"));
1052
+ return true;
1053
+ }
1054
+
1055
+ if (Pin->Direction != EGPD_Input) {
1056
+ SendAutomationError(RequestingSocket, RequestId,
1057
+ TEXT("Can only set default values on input pins."),
1058
+ TEXT("INVALID_PIN_DIRECTION"));
1059
+ return true;
1060
+ }
1061
+
1062
+ const FScopedTransaction Transaction(
1063
+ FText::FromString(TEXT("Set Pin Default Value")));
1064
+ Blueprint->Modify();
1065
+ TargetGraph->Modify();
1066
+ TargetNode->Modify();
1067
+
1068
+ // Use the schema to properly set the default value
1069
+ const UEdGraphSchema *Schema = TargetGraph->GetSchema();
1070
+ Schema->TrySetDefaultValue(*Pin, Value);
1071
+
1072
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
1073
+
1074
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1075
+ Result->SetStringField(TEXT("nodeId"), NodeId);
1076
+ Result->SetStringField(TEXT("pinName"), PinName);
1077
+ Result->SetStringField(TEXT("value"), Value);
1078
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1079
+ TEXT("Pin default value set."), Result);
1080
+ return true;
1171
1081
  }
1172
1082
 
1173
1083
  SendAutomationError(