ue-mcp 0.4.7 → 0.4.9

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.
package/README.md CHANGED
@@ -20,13 +20,15 @@ npx ue-mcp init
20
20
 
21
21
  The interactive setup will:
22
22
 
23
- 1. Ask for your `.uproject` path
23
+ 1. Find your `.uproject` (auto-detects in current directory)
24
24
  2. Let you choose which tool categories to enable
25
25
  3. Deploy the C++ bridge plugin to your project
26
26
  4. Enable required UE plugins (Niagara, PCG, GAS, etc.)
27
27
  5. Detect and configure your MCP client (Claude Code, Claude Desktop, Cursor)
28
28
 
29
- Restart the editor once after setup to load the bridge plugin. Then ask your AI:
29
+ Restart the editor once after setup to load the bridge plugin. To update later: `npx ue-mcp update`
30
+
31
+ Then ask your AI:
30
32
 
31
33
  ```
32
34
  project(action="get_status") — verify connection
@@ -34,9 +34,9 @@ export const blueprintTool = categoryTool("blueprint", "Blueprint reading, autho
34
34
  - create_function: Create function. Params: assetPath, functionName
35
35
  - delete_function: Delete function. Params: assetPath, functionName
36
36
  - rename_function: Rename function. Params: assetPath, oldName, newName
37
- - add_node: Add graph node (K2, AnimGraph, etc any UEdGraphNode subclass). Params: assetPath, graphName?, nodeClass, nodeParams?
37
+ - add_node: Add graph node (any UEdGraphNode subclass). Aliases: CallFunction, Event, CustomEvent, GetVar, SetVar, Branch. nodeParams: functionName+targetClass for CallFunction, eventName+eventClass? for Event, variableName for GetVar/SetVar. Params: assetPath, graphName?, nodeClass, nodeParams?
38
38
  - delete_node: Delete node. Params: assetPath, graphName, nodeName
39
- - set_node_property: Set node property. Params: assetPath, graphName, nodeName, propertyName, value
39
+ - set_node_property: Set node pin default or struct property (supports dot paths like "Node.IKBone.BoneName"). Params: assetPath, graphName, nodeName, propertyName, value
40
40
  - connect_pins: Wire nodes. Params: sourceNode, sourcePin, targetNode, targetPin, assetPath, graphName?
41
41
  - add_component: Add BP component. Params: assetPath, componentClass, componentName?
42
42
  - compile: Compile Blueprint. Params: assetPath
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ue-mcp",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Unreal Engine MCP server — 19 tools, 300+ actions for AI-driven editor control",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,6 +41,22 @@
41
41
  #include "Kismet/KismetStringLibrary.h"
42
42
  #include "Kismet/KismetArrayLibrary.h"
43
43
 
44
+ // Helper: find a UClass by short name (e.g. "AnimInstance" finds UAnimInstance)
45
+ static UClass* FindClassByShortName(const FString& ClassName)
46
+ {
47
+ UClass* PrefixedMatch = nullptr;
48
+ for (TObjectIterator<UClass> It; It; ++It)
49
+ {
50
+ const FString& Name = It->GetName();
51
+ if (Name == ClassName) return *It;
52
+ if (!PrefixedMatch && (Name == TEXT("U") + ClassName || Name == TEXT("A") + ClassName))
53
+ {
54
+ PrefixedMatch = *It;
55
+ }
56
+ }
57
+ return PrefixedMatch;
58
+ }
59
+
44
60
  void FBlueprintHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
45
61
  {
46
62
  Registry.RegisterHandler(TEXT("create_blueprint"), &CreateBlueprint);
@@ -996,8 +1012,12 @@ TSharedPtr<FJsonValue> FBlueprintHandlers::AddNode(const TSharedPtr<FJsonObject>
996
1012
 
997
1013
  // Resolve short aliases to full class names
998
1014
  FString ResolvedClass = NodeClass;
999
- if (NodeClass == TEXT("CallFunction")) ResolvedClass = TEXT("K2Node_CallFunction");
1000
- else if (NodeClass == TEXT("Event")) ResolvedClass = TEXT("K2Node_Event");
1015
+ if (NodeClass == TEXT("CallFunction")) ResolvedClass = TEXT("K2Node_CallFunction");
1016
+ else if (NodeClass == TEXT("Event")) ResolvedClass = TEXT("K2Node_Event");
1017
+ else if (NodeClass == TEXT("GetVar")) ResolvedClass = TEXT("K2Node_VariableGet");
1018
+ else if (NodeClass == TEXT("SetVar")) ResolvedClass = TEXT("K2Node_VariableSet");
1019
+ else if (NodeClass == TEXT("Branch")) ResolvedClass = TEXT("K2Node_IfThenElse");
1020
+ else if (NodeClass == TEXT("CustomEvent")) ResolvedClass = TEXT("K2Node_CustomEvent");
1001
1021
 
1002
1022
  // Find the UEdGraphNode subclass by name (works for K2, AnimGraph, and any other graph node types)
1003
1023
  UClass* NodeUClass = nullptr;
@@ -1026,34 +1046,34 @@ TSharedPtr<FJsonValue> FBlueprintHandlers::AddNode(const TSharedPtr<FJsonObject>
1026
1046
  return MakeShared<FJsonValueObject>(Result);
1027
1047
  }
1028
1048
 
1029
- // Special-case initialization for known types
1049
+ // Special-case initialization for known types (must happen BEFORE AllocateDefaultPins)
1030
1050
  if (UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(NewNode))
1031
1051
  {
1032
1052
  if (NodeParams)
1033
1053
  {
1034
1054
  FString FunctionName;
1035
- if ((*NodeParams)->TryGetStringField(TEXT("functionName"), FunctionName))
1055
+ if (!(*NodeParams)->TryGetStringField(TEXT("functionName"), FunctionName))
1056
+ (*NodeParams)->TryGetStringField(TEXT("memberName"), FunctionName);
1057
+
1058
+ if (!FunctionName.IsEmpty())
1036
1059
  {
1037
1060
  FString TargetClassName;
1038
- if ((*NodeParams)->TryGetStringField(TEXT("targetClass"), TargetClassName))
1061
+ if (!(*NodeParams)->TryGetStringField(TEXT("targetClass"), TargetClassName))
1062
+ (*NodeParams)->TryGetStringField(TEXT("memberParent"), TargetClassName);
1063
+
1064
+ UClass* TargetClass = nullptr;
1065
+ if (!TargetClassName.IsEmpty())
1039
1066
  {
1040
- UClass* TargetClass = FindObject<UClass>(nullptr, *TargetClassName);
1041
- if (!TargetClass)
1042
- {
1043
- TargetClass = FindObject<UClass>(nullptr, *(TEXT("U") + TargetClassName));
1044
- }
1045
- if (TargetClass)
1046
- {
1047
- UFunction* Func = TargetClass->FindFunctionByName(FName(*FunctionName));
1048
- if (Func)
1049
- {
1050
- CallNode->SetFromFunction(Func);
1051
- }
1052
- }
1067
+ TargetClass = FindClassByShortName(TargetClassName);
1068
+ }
1069
+ if (!TargetClass)
1070
+ {
1071
+ TargetClass = Blueprint->ParentClass;
1053
1072
  }
1054
- else if (Blueprint->ParentClass)
1073
+
1074
+ if (TargetClass)
1055
1075
  {
1056
- UFunction* Func = Blueprint->ParentClass->FindFunctionByName(FName(*FunctionName));
1076
+ UFunction* Func = TargetClass->FindFunctionByName(FName(*FunctionName));
1057
1077
  if (Func)
1058
1078
  {
1059
1079
  CallNode->SetFromFunction(Func);
@@ -1067,9 +1087,58 @@ TSharedPtr<FJsonValue> FBlueprintHandlers::AddNode(const TSharedPtr<FJsonObject>
1067
1087
  if (NodeParams)
1068
1088
  {
1069
1089
  FString EventName;
1070
- if ((*NodeParams)->TryGetStringField(TEXT("eventName"), EventName))
1090
+ if (!(*NodeParams)->TryGetStringField(TEXT("eventName"), EventName))
1091
+ (*NodeParams)->TryGetStringField(TEXT("memberName"), EventName);
1092
+
1093
+ if (!EventName.IsEmpty())
1094
+ {
1095
+ FString EventClassName;
1096
+ if (!(*NodeParams)->TryGetStringField(TEXT("eventClass"), EventClassName))
1097
+ (*NodeParams)->TryGetStringField(TEXT("memberParent"), EventClassName);
1098
+
1099
+ if (!EventClassName.IsEmpty())
1100
+ {
1101
+ // Engine event override — bind via EventReference
1102
+ UClass* EventClass = FindClassByShortName(EventClassName);
1103
+ if (!EventClass) EventClass = Blueprint->ParentClass;
1104
+
1105
+ if (EventClass)
1106
+ {
1107
+ UFunction* EventFunc = EventClass->FindFunctionByName(FName(*EventName));
1108
+ if (EventFunc)
1109
+ {
1110
+ bool bIsSelf = Blueprint->ParentClass && Blueprint->ParentClass->IsChildOf(EventClass);
1111
+ EventNode->EventReference.SetFromField<UFunction>(EventFunc, bIsSelf);
1112
+ }
1113
+ }
1114
+ }
1115
+ else
1116
+ {
1117
+ // Custom event — just set the name
1118
+ EventNode->CustomFunctionName = FName(*EventName);
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ else if (UK2Node_VariableGet* GetNode = Cast<UK2Node_VariableGet>(NewNode))
1124
+ {
1125
+ if (NodeParams)
1126
+ {
1127
+ FString VarName;
1128
+ if ((*NodeParams)->TryGetStringField(TEXT("variableName"), VarName))
1071
1129
  {
1072
- EventNode->CustomFunctionName = FName(*EventName);
1130
+ GetNode->VariableReference.SetSelfMember(FName(*VarName));
1131
+ }
1132
+ }
1133
+ }
1134
+ else if (UK2Node_VariableSet* SetNode = Cast<UK2Node_VariableSet>(NewNode))
1135
+ {
1136
+ if (NodeParams)
1137
+ {
1138
+ FString VarName;
1139
+ if ((*NodeParams)->TryGetStringField(TEXT("variableName"), VarName))
1140
+ {
1141
+ SetNode->VariableReference.SetSelfMember(FName(*VarName));
1073
1142
  }
1074
1143
  }
1075
1144
  }
@@ -1986,7 +2055,7 @@ TSharedPtr<FJsonValue> FBlueprintHandlers::SetNodeProperty(const TSharedPtr<FJso
1986
2055
  return MakeShared<FJsonValueObject>(Result);
1987
2056
  }
1988
2057
 
1989
- // Find the pin on the node
2058
+ // First try to find a pin with this name
1990
2059
  UEdGraphPin* TargetPin = nullptr;
1991
2060
  for (UEdGraphPin* Pin : TargetNode->Pins)
1992
2061
  {
@@ -1997,29 +2066,81 @@ TSharedPtr<FJsonValue> FBlueprintHandlers::SetNodeProperty(const TSharedPtr<FJso
1997
2066
  }
1998
2067
  }
1999
2068
 
2000
- if (!TargetPin)
2069
+ bool bSetViaPin = false;
2070
+ bool bSetViaProperty = false;
2071
+
2072
+ if (TargetPin)
2001
2073
  {
2002
- // List available pins for better error message
2003
- TArray<FString> PinNames;
2004
- for (UEdGraphPin* Pin : TargetNode->Pins)
2074
+ // Set pin default value using the graph's own schema
2075
+ const UEdGraphSchema* Schema = TargetGraph->GetSchema();
2076
+ if (Schema)
2005
2077
  {
2006
- if (Pin)
2078
+ Schema->TrySetDefaultValue(*TargetPin, DefaultValue);
2079
+ TargetNode->PinDefaultValueChanged(TargetPin);
2080
+ bSetViaPin = true;
2081
+ }
2082
+ }
2083
+
2084
+ if (!bSetViaPin)
2085
+ {
2086
+ // No pin found — try setting as a node property via reflection.
2087
+ // Supports dotted paths like "Node.IKBone.BoneName" for AnimGraph inner structs.
2088
+ TArray<FString> PathParts;
2089
+ PinName.ParseIntoArray(PathParts, TEXT("."));
2090
+
2091
+ UStruct* CurrentStruct = TargetNode->GetClass();
2092
+ void* CurrentContainer = TargetNode;
2093
+ FProperty* FinalProp = nullptr;
2094
+
2095
+ for (int32 i = 0; i < PathParts.Num(); i++)
2096
+ {
2097
+ FProperty* Prop = CurrentStruct->FindPropertyByName(FName(*PathParts[i]));
2098
+ if (!Prop) break;
2099
+
2100
+ if (i < PathParts.Num() - 1)
2007
2101
  {
2008
- PinNames.Add(Pin->PinName.ToString());
2102
+ // Intermediate path segment — drill into struct
2103
+ FStructProperty* StructProp = CastField<FStructProperty>(Prop);
2104
+ if (!StructProp) break;
2105
+ CurrentContainer = StructProp->ContainerPtrToValuePtr<void>(CurrentContainer);
2106
+ CurrentStruct = StructProp->Struct;
2107
+ }
2108
+ else
2109
+ {
2110
+ FinalProp = Prop;
2009
2111
  }
2010
2112
  }
2011
- FString AvailablePins = FString::Join(PinNames, TEXT(", "));
2012
- Result->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin not found: '%s'. Available pins: [%s]"), *PinName, *AvailablePins));
2013
- Result->SetBoolField(TEXT("success"), false);
2014
- return MakeShared<FJsonValueObject>(Result);
2113
+
2114
+ if (FinalProp)
2115
+ {
2116
+ void* ValuePtr = FinalProp->ContainerPtrToValuePtr<void>(CurrentContainer);
2117
+ FinalProp->ImportText_Direct(*DefaultValue, ValuePtr, nullptr, PPF_None);
2118
+ TargetNode->PostEditChange();
2119
+ bSetViaProperty = true;
2120
+ }
2015
2121
  }
2016
2122
 
2017
- // Set the default value using the schema
2018
- const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
2019
- Schema->TrySetDefaultValue(*TargetPin, DefaultValue);
2123
+ if (!bSetViaPin && !bSetViaProperty)
2124
+ {
2125
+ // Neither pin nor property found — build a helpful error
2126
+ TArray<FString> PinNames;
2127
+ for (UEdGraphPin* Pin : TargetNode->Pins)
2128
+ {
2129
+ if (Pin) PinNames.Add(Pin->PinName.ToString());
2130
+ }
2131
+
2132
+ TArray<FString> PropNames;
2133
+ for (TFieldIterator<FProperty> It(TargetNode->GetClass()); It; ++It)
2134
+ {
2135
+ PropNames.Add(It->GetName());
2136
+ }
2020
2137
 
2021
- // Notify the node that a pin default changed
2022
- TargetNode->PinDefaultValueChanged(TargetPin);
2138
+ Result->SetStringField(TEXT("error"), FString::Printf(
2139
+ TEXT("'%s' not found as pin or property. Pins: [%s]. Properties: [%s]"),
2140
+ *PinName, *FString::Join(PinNames, TEXT(", ")), *FString::Join(PropNames, TEXT(", "))));
2141
+ Result->SetBoolField(TEXT("success"), false);
2142
+ return MakeShared<FJsonValueObject>(Result);
2143
+ }
2023
2144
 
2024
2145
  // Compile and save
2025
2146
  FKismetEditorUtilities::CompileBlueprint(Blueprint);
@@ -2036,8 +2157,9 @@ TSharedPtr<FJsonValue> FBlueprintHandlers::SetNodeProperty(const TSharedPtr<FJso
2036
2157
  Result->SetStringField(TEXT("path"), AssetPath);
2037
2158
  Result->SetStringField(TEXT("graphName"), GraphName);
2038
2159
  Result->SetStringField(TEXT("nodeId"), NodeId);
2039
- Result->SetStringField(TEXT("pinName"), PinName);
2040
- Result->SetStringField(TEXT("defaultValue"), DefaultValue);
2160
+ Result->SetStringField(TEXT("propertyName"), PinName);
2161
+ Result->SetStringField(TEXT("value"), DefaultValue);
2162
+ Result->SetStringField(TEXT("setVia"), bSetViaPin ? TEXT("pin") : TEXT("property"));
2041
2163
  Result->SetBoolField(TEXT("success"), true);
2042
2164
 
2043
2165
  return MakeShared<FJsonValueObject>(Result);