ue-mcp 1.0.37 → 1.0.38

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.
@@ -26,6 +26,6 @@
26
26
  "statetree": 35,
27
27
  "plugins": 2
28
28
  },
29
- "generatedAt": "2026-05-22T18:44:19.419Z",
30
- "version": "1.0.37"
29
+ "generatedAt": "2026-05-23T01:50:22.435Z",
30
+ "version": "1.0.38"
31
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ue-mcp",
3
- "version": "1.0.37",
3
+ "version": "1.0.38",
4
4
  "description": "Unreal Engine MCP server - 21 tools, 544+ actions for AI-driven editor control",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,6 +36,190 @@ void FReflectionHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
36
36
  Registry.RegisterHandler(TEXT("set_enum_entries"), &SetEnumEntries);
37
37
  }
38
38
 
39
+ namespace
40
+ {
41
+ /**
42
+ * UHT compiles `///` doc comments into ToolTip metadata at build time.
43
+ * Returns the ToolTip if present, otherwise an empty string. Caller is
44
+ * responsible for omitting empty values from the JSON payload.
45
+ */
46
+ FString ReadTooltip(const UField* Field)
47
+ {
48
+ #if WITH_EDITOR
49
+ if (!Field) return FString();
50
+ // UField::GetMetaData("ToolTip") returns the raw text. Trimming
51
+ // matches what the editor's tooltip panel does.
52
+ const FString Raw = Field->GetMetaData(TEXT("ToolTip"));
53
+ return Raw.TrimStartAndEnd();
54
+ #else
55
+ return FString();
56
+ #endif
57
+ }
58
+
59
+ FString ReadPropertyTooltip(const FProperty* Prop)
60
+ {
61
+ #if WITH_EDITOR
62
+ if (!Prop) return FString();
63
+ return Prop->GetMetaData(TEXT("ToolTip")).TrimStartAndEnd();
64
+ #else
65
+ return FString();
66
+ #endif
67
+ }
68
+
69
+ void AddIfNonEmpty(TSharedPtr<FJsonObject> Obj, const TCHAR* Key, const FString& Value)
70
+ {
71
+ if (!Value.IsEmpty())
72
+ {
73
+ Obj->SetStringField(Key, Value);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Encode the common EPropertyFlags bits the editor surfaces in its
79
+ * property panel + Blueprint nodes. Skips internal bookkeeping flags
80
+ * (CPF_NativeAccessSpecifier*, CPF_PersistentInstance, etc.) that aren't
81
+ * useful to AI callers reasoning about how to use a property.
82
+ */
83
+ TArray<TSharedPtr<FJsonValue>> EncodePropertyFlags(const FProperty* Prop)
84
+ {
85
+ TArray<TSharedPtr<FJsonValue>> Out;
86
+ if (!Prop) return Out;
87
+ const EPropertyFlags F = Prop->PropertyFlags;
88
+ auto Push = [&Out](const TCHAR* Name) { Out.Add(MakeShared<FJsonValueString>(Name)); };
89
+
90
+ if (F & CPF_Edit)
91
+ {
92
+ if (F & CPF_DisableEditOnInstance) Push(TEXT("EditDefaultsOnly"));
93
+ else if (F & CPF_DisableEditOnTemplate) Push(TEXT("EditInstanceOnly"));
94
+ else Push(TEXT("EditAnywhere"));
95
+ }
96
+ if (F & CPF_EditConst) Push(TEXT("VisibleAnywhere"));
97
+ if (F & CPF_BlueprintVisible)
98
+ {
99
+ if (F & CPF_BlueprintReadOnly) Push(TEXT("BlueprintReadOnly"));
100
+ else Push(TEXT("BlueprintReadWrite"));
101
+ }
102
+ if (F & CPF_Net) Push(TEXT("Replicated"));
103
+ if (F & CPF_RepNotify) Push(TEXT("RepNotify"));
104
+ if (F & CPF_Transient) Push(TEXT("Transient"));
105
+ if (F & CPF_Config) Push(TEXT("Config"));
106
+ if (F & CPF_GlobalConfig) Push(TEXT("GlobalConfig"));
107
+ if (F & CPF_SaveGame) Push(TEXT("SaveGame"));
108
+ if (F & CPF_Interp) Push(TEXT("Interp"));
109
+ if (F & CPF_AdvancedDisplay) Push(TEXT("AdvancedDisplay"));
110
+ if (F & CPF_Deprecated) Push(TEXT("Deprecated"));
111
+ if (F & CPF_NoClear) Push(TEXT("NoClear"));
112
+ if (F & CPF_ExposeOnSpawn) Push(TEXT("ExposeOnSpawn"));
113
+ return Out;
114
+ }
115
+
116
+ /**
117
+ * Function flags the editor cares about: how Blueprints can call it, how
118
+ * the network treats it, and whether it's an Exec console command.
119
+ */
120
+ TArray<TSharedPtr<FJsonValue>> EncodeFunctionFlags(const UFunction* Func)
121
+ {
122
+ TArray<TSharedPtr<FJsonValue>> Out;
123
+ if (!Func) return Out;
124
+ const EFunctionFlags F = Func->FunctionFlags;
125
+ auto Push = [&Out](const TCHAR* Name) { Out.Add(MakeShared<FJsonValueString>(Name)); };
126
+
127
+ if (F & FUNC_BlueprintCallable) Push(TEXT("BlueprintCallable"));
128
+ if (F & FUNC_BlueprintEvent) Push(TEXT("BlueprintImplementableEvent"));
129
+ if (F & FUNC_BlueprintPure) Push(TEXT("BlueprintPure"));
130
+ if (F & FUNC_Exec) Push(TEXT("Exec"));
131
+ if (F & FUNC_NetServer) Push(TEXT("Server"));
132
+ if (F & FUNC_NetClient) Push(TEXT("Client"));
133
+ if (F & FUNC_NetMulticast) Push(TEXT("NetMulticast"));
134
+ if (F & FUNC_NetReliable) Push(TEXT("Reliable"));
135
+ if (F & FUNC_Static) Push(TEXT("Static"));
136
+ return Out;
137
+ }
138
+
139
+ /**
140
+ * Build the per-property JSON block surfaced under a class/struct's
141
+ * `properties:` / `fields:` array. Includes name + type (the old
142
+ * contract) plus all metadata the editor exposes: tooltip, category,
143
+ * display name, edit-condition predicate, clamp range, and the flag
144
+ * names a UHEADER author would have typed.
145
+ */
146
+ TSharedPtr<FJsonObject> SerializePropertyMeta(FProperty* Prop)
147
+ {
148
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
149
+ Obj->SetStringField(TEXT("name"), Prop->GetName());
150
+ Obj->SetStringField(TEXT("type"), Prop->GetCPPType());
151
+ #if WITH_EDITOR
152
+ AddIfNonEmpty(Obj, TEXT("tooltip"), ReadPropertyTooltip(Prop));
153
+ AddIfNonEmpty(Obj, TEXT("category"), Prop->GetMetaData(TEXT("Category")));
154
+ AddIfNonEmpty(Obj, TEXT("displayName"), Prop->GetMetaData(TEXT("DisplayName")));
155
+ AddIfNonEmpty(Obj, TEXT("editCondition"), Prop->GetMetaData(TEXT("EditCondition")));
156
+ AddIfNonEmpty(Obj, TEXT("clampMin"), Prop->GetMetaData(TEXT("ClampMin")));
157
+ AddIfNonEmpty(Obj, TEXT("clampMax"), Prop->GetMetaData(TEXT("ClampMax")));
158
+ AddIfNonEmpty(Obj, TEXT("uiMin"), Prop->GetMetaData(TEXT("UIMin")));
159
+ AddIfNonEmpty(Obj, TEXT("uiMax"), Prop->GetMetaData(TEXT("UIMax")));
160
+ AddIfNonEmpty(Obj, TEXT("units"), Prop->GetMetaData(TEXT("Units")));
161
+ #endif
162
+ TArray<TSharedPtr<FJsonValue>> Flags = EncodePropertyFlags(Prop);
163
+ if (Flags.Num() > 0)
164
+ {
165
+ Obj->SetArrayField(TEXT("flags"), Flags);
166
+ }
167
+ return Obj;
168
+ }
169
+
170
+ /**
171
+ * Build the per-function JSON block surfaced under a class's
172
+ * `functions:` array. Returns a structured params array (each with
173
+ * name + type + optional out/ref markers) and a separate returnType
174
+ * string when the function has a CPF_ReturnParm property.
175
+ */
176
+ TSharedPtr<FJsonObject> SerializeFunctionMeta(UFunction* Func)
177
+ {
178
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
179
+ Obj->SetStringField(TEXT("name"), Func->GetName());
180
+ #if WITH_EDITOR
181
+ AddIfNonEmpty(Obj, TEXT("tooltip"), ReadTooltip(Func));
182
+ AddIfNonEmpty(Obj, TEXT("category"), Func->GetMetaData(TEXT("Category")));
183
+ AddIfNonEmpty(Obj, TEXT("displayName"), Func->GetMetaData(TEXT("DisplayName")));
184
+ AddIfNonEmpty(Obj, TEXT("keywords"), Func->GetMetaData(TEXT("Keywords")));
185
+ #endif
186
+
187
+ TArray<TSharedPtr<FJsonValue>> Params;
188
+ FString ReturnType;
189
+ for (TFieldIterator<FProperty> PIt(Func); PIt; ++PIt)
190
+ {
191
+ FProperty* P = *PIt;
192
+ if (P->PropertyFlags & CPF_ReturnParm)
193
+ {
194
+ ReturnType = P->GetCPPType();
195
+ continue;
196
+ }
197
+ TSharedPtr<FJsonObject> ParamObj = MakeShared<FJsonObject>();
198
+ ParamObj->SetStringField(TEXT("name"), P->GetName());
199
+ ParamObj->SetStringField(TEXT("type"), P->GetCPPType());
200
+ if (P->PropertyFlags & CPF_OutParm) ParamObj->SetBoolField(TEXT("out"), true);
201
+ if (P->PropertyFlags & CPF_ReferenceParm) ParamObj->SetBoolField(TEXT("byRef"), true);
202
+ Params.Add(MakeShared<FJsonValueObject>(ParamObj));
203
+ }
204
+ Obj->SetArrayField(TEXT("params"), Params);
205
+ if (!ReturnType.IsEmpty())
206
+ {
207
+ Obj->SetStringField(TEXT("returnType"), ReturnType);
208
+ }
209
+ else
210
+ {
211
+ Obj->SetStringField(TEXT("returnType"), TEXT("void"));
212
+ }
213
+
214
+ TArray<TSharedPtr<FJsonValue>> Flags = EncodeFunctionFlags(Func);
215
+ if (Flags.Num() > 0)
216
+ {
217
+ Obj->SetArrayField(TEXT("flags"), Flags);
218
+ }
219
+ return Obj;
220
+ }
221
+ }
222
+
39
223
  TSharedPtr<FJsonValue> FReflectionHandlers::ReflectClass(const TSharedPtr<FJsonObject>& Params)
40
224
  {
41
225
  FString ClassName;
@@ -51,6 +235,11 @@ TSharedPtr<FJsonValue> FReflectionHandlers::ReflectClass(const TSharedPtr<FJsonO
51
235
 
52
236
  auto Result = MCPSuccess();
53
237
  Result->SetStringField(TEXT("className"), Class->GetName());
238
+ AddIfNonEmpty(Result, TEXT("tooltip"), ReadTooltip(Class));
239
+ #if WITH_EDITOR
240
+ AddIfNonEmpty(Result, TEXT("displayName"), Class->GetMetaData(TEXT("DisplayName")));
241
+ AddIfNonEmpty(Result, TEXT("category"), Class->GetMetaData(TEXT("Category")));
242
+ #endif
54
243
 
55
244
  if (Class->GetSuperClass())
56
245
  {
@@ -69,27 +258,22 @@ TSharedPtr<FJsonValue> FReflectionHandlers::ReflectClass(const TSharedPtr<FJsonO
69
258
 
70
259
  Result->SetBoolField(TEXT("isAbstract"), Class->HasAnyClassFlags(CLASS_Abstract));
71
260
 
72
- // Get properties
261
+ // Properties: name + type plus every metadata field the editor's property
262
+ // panel renders - tooltip text, category, clamps, replication flags, etc.
73
263
  TArray<TSharedPtr<FJsonValue>> PropertiesArray;
74
264
  for (TFieldIterator<FProperty> PropIt(Class, bIncludeInherited ? EFieldIteratorFlags::IncludeSuper : EFieldIteratorFlags::ExcludeSuper); PropIt; ++PropIt)
75
265
  {
76
- FProperty* Prop = *PropIt;
77
- TSharedPtr<FJsonObject> PropObj = MakeShared<FJsonObject>();
78
- PropObj->SetStringField(TEXT("name"), Prop->GetName());
79
- PropObj->SetStringField(TEXT("type"), Prop->GetCPPType());
80
- PropertiesArray.Add(MakeShared<FJsonValueObject>(PropObj));
266
+ PropertiesArray.Add(MakeShared<FJsonValueObject>(SerializePropertyMeta(*PropIt)));
81
267
  }
82
268
  Result->SetArrayField(TEXT("properties"), PropertiesArray);
83
269
  Result->SetNumberField(TEXT("propertyCount"), PropertiesArray.Num());
84
270
 
85
- // Get functions
271
+ // Functions: structured params + return type + tooltip + flags so callers
272
+ // can see how to invoke each function without grepping the header.
86
273
  TArray<TSharedPtr<FJsonValue>> FunctionsArray;
87
274
  for (TFieldIterator<UFunction> FuncIt(Class, bIncludeInherited ? EFieldIteratorFlags::IncludeSuper : EFieldIteratorFlags::ExcludeSuper); FuncIt; ++FuncIt)
88
275
  {
89
- UFunction* Func = *FuncIt;
90
- TSharedPtr<FJsonObject> FuncObj = MakeShared<FJsonObject>();
91
- FuncObj->SetStringField(TEXT("name"), Func->GetName());
92
- FunctionsArray.Add(MakeShared<FJsonValueObject>(FuncObj));
276
+ FunctionsArray.Add(MakeShared<FJsonValueObject>(SerializeFunctionMeta(*FuncIt)));
93
277
  }
94
278
  Result->SetArrayField(TEXT("functions"), FunctionsArray);
95
279
  Result->SetNumberField(TEXT("functionCount"), FunctionsArray.Num());
@@ -110,15 +294,15 @@ TSharedPtr<FJsonValue> FReflectionHandlers::ReflectStruct(const TSharedPtr<FJson
110
294
 
111
295
  auto Result = MCPSuccess();
112
296
  Result->SetStringField(TEXT("structName"), Struct->GetName());
297
+ AddIfNonEmpty(Result, TEXT("tooltip"), ReadTooltip(Struct));
298
+ #if WITH_EDITOR
299
+ AddIfNonEmpty(Result, TEXT("displayName"), Struct->GetMetaData(TEXT("DisplayName")));
300
+ #endif
113
301
 
114
302
  TArray<TSharedPtr<FJsonValue>> FieldsArray;
115
303
  for (TFieldIterator<FProperty> PropIt(Struct); PropIt; ++PropIt)
116
304
  {
117
- FProperty* Prop = *PropIt;
118
- TSharedPtr<FJsonObject> FieldObj = MakeShared<FJsonObject>();
119
- FieldObj->SetStringField(TEXT("name"), Prop->GetName());
120
- FieldObj->SetStringField(TEXT("type"), Prop->GetCPPType());
121
- FieldsArray.Add(MakeShared<FJsonValueObject>(FieldObj));
305
+ FieldsArray.Add(MakeShared<FJsonValueObject>(SerializePropertyMeta(*PropIt)));
122
306
  }
123
307
  Result->SetArrayField(TEXT("fields"), FieldsArray);
124
308
  Result->SetNumberField(TEXT("fieldCount"), FieldsArray.Num());
@@ -139,6 +323,7 @@ TSharedPtr<FJsonValue> FReflectionHandlers::ReflectEnum(const TSharedPtr<FJsonOb
139
323
 
140
324
  auto Result = MCPSuccess();
141
325
  Result->SetStringField(TEXT("enumName"), Enum->GetName());
326
+ AddIfNonEmpty(Result, TEXT("tooltip"), ReadTooltip(Enum));
142
327
 
143
328
  TArray<TSharedPtr<FJsonValue>> ValuesArray;
144
329
  int32 NumEnums = Enum->NumEnums();
@@ -151,6 +336,13 @@ TSharedPtr<FJsonValue> FReflectionHandlers::ReflectEnum(const TSharedPtr<FJsonOb
151
336
  ValueObj->SetStringField(TEXT("name"), EnumNameStr);
152
337
  ValueObj->SetNumberField(TEXT("value"), Enum->GetValueByIndex(i));
153
338
  ValueObj->SetStringField(TEXT("displayName"), Enum->GetDisplayNameTextByIndex(i).ToString());
339
+ #if WITH_EDITOR
340
+ // Per-value tooltip - UEnum stores ToolTip metadata per enum
341
+ // entry, addressable by index. Editor uses the same call to
342
+ // populate the dropdown tooltips in Details panels.
343
+ FString ValueTooltip = Enum->GetMetaData(TEXT("ToolTip"), i).TrimStartAndEnd();
344
+ AddIfNonEmpty(ValueObj, TEXT("tooltip"), ValueTooltip);
345
+ #endif
154
346
  ValuesArray.Add(MakeShared<FJsonValueObject>(ValueObj));
155
347
  }
156
348
  }
@@ -315,34 +507,23 @@ TSharedPtr<FJsonValue> FReflectionHandlers::CreateGameplayTag(const TSharedPtr<F
315
507
 
316
508
  UClass* FReflectionHandlers::FindClass(const FString& ClassName)
317
509
  {
318
- // Try direct lookup
510
+ // Full-path lookup first: /Script/Engine.Actor, /Script/Foo.UBar, etc.
319
511
  UClass* Class = FindObject<UClass>(nullptr, *ClassName);
320
512
  if (Class)
321
513
  {
322
514
  return Class;
323
515
  }
324
516
 
325
- // Try with /Script/ prefix
326
- FString ScriptPath = FString::Printf(TEXT("/Script/%s"), *ClassName);
327
- Class = FindObject<UClass>(nullptr, *ScriptPath);
328
- if (Class)
329
- {
330
- return Class;
331
- }
332
-
333
- // Try with common prefixes
334
- TArray<FString> Prefixes = { TEXT(""), TEXT("A"), TEXT("U"), TEXT("F") };
517
+ // Short-name lookup. In UE 5.6+, FindObject(nullptr, "Actor") no longer
518
+ // scans every package - it only finds top-level objects. FindFirstObject
519
+ // is the replacement that walks all loaded UClass objects by leaf name.
520
+ // Try the caller's spelling, then common UE prefixes (A/U/F) that agents
521
+ // often drop when typing class names.
522
+ const TArray<FString> Prefixes = { TEXT(""), TEXT("A"), TEXT("U"), TEXT("F") };
335
523
  for (const FString& Prefix : Prefixes)
336
524
  {
337
- FString CandidateName = Prefix + ClassName;
338
- Class = FindObject<UClass>(nullptr, *CandidateName);
339
- if (Class)
340
- {
341
- return Class;
342
- }
343
-
344
- FString CandidatePath = FString::Printf(TEXT("/Script/%s"), *CandidateName);
345
- Class = FindObject<UClass>(nullptr, *CandidatePath);
525
+ const FString Candidate = Prefix + ClassName;
526
+ Class = FindFirstObject<UClass>(*Candidate, EFindFirstObjectOptions::NativeFirst);
346
527
  if (Class)
347
528
  {
348
529
  return Class;
@@ -361,43 +542,17 @@ UScriptStruct* FReflectionHandlers::FindStruct(const FString& StructName)
361
542
  return Struct;
362
543
  }
363
544
 
364
- // Try with /Script/ prefix
365
- FString ScriptPath = FString::Printf(TEXT("/Script/%s"), *StructName);
366
- Struct = FindObject<UScriptStruct>(nullptr, *ScriptPath);
367
- if (Struct)
545
+ // Short-name lookup via FindFirstObject (UE 5.6+ replacement for the
546
+ // "any package" FindObject pattern). Tries the caller's spelling first,
547
+ // then F-prefixed - matches the convention agents typically use ("Vector"
548
+ // vs "FVector").
549
+ const TArray<FString> Candidates = { StructName, TEXT("F") + StructName };
550
+ for (const FString& Candidate : Candidates)
368
551
  {
369
- return Struct;
370
- }
371
-
372
- // Try with F prefix
373
- FString FName = TEXT("F") + StructName;
374
- Struct = FindObject<UScriptStruct>(nullptr, *FName);
375
- if (Struct)
376
- {
377
- return Struct;
378
- }
379
-
380
- FString FPath = FString::Printf(TEXT("/Script/%s"), *FName);
381
- Struct = FindObject<UScriptStruct>(nullptr, *FPath);
382
- if (Struct)
383
- {
384
- return Struct;
385
- }
386
-
387
- // Iterate all loaded UScriptStruct objects to find by short name
388
- // This catches project-defined structs in any module (e.g. /Script/MyModule.FMyStruct)
389
- FString NameToFind = StructName;
390
- FString FNameToFind = FName;
391
- for (TObjectIterator<UScriptStruct> It; It; ++It)
392
- {
393
- UScriptStruct* Current = *It;
394
- if (Current)
552
+ Struct = FindFirstObject<UScriptStruct>(*Candidate, EFindFirstObjectOptions::NativeFirst);
553
+ if (Struct)
395
554
  {
396
- FString CurrentName = Current->GetName();
397
- if (CurrentName == NameToFind || CurrentName == FNameToFind)
398
- {
399
- return Current;
400
- }
555
+ return Struct;
401
556
  }
402
557
  }
403
558
 
@@ -406,34 +561,23 @@ UScriptStruct* FReflectionHandlers::FindStruct(const FString& StructName)
406
561
 
407
562
  UEnum* FReflectionHandlers::FindEnum(const FString& EnumName)
408
563
  {
409
- // Try direct lookup
564
+ // Full-path lookup first: /Script/Engine.ECollisionChannel, etc.
410
565
  UEnum* Enum = FindObject<UEnum>(nullptr, *EnumName);
411
566
  if (Enum)
412
567
  {
413
568
  return Enum;
414
569
  }
415
570
 
416
- // Try with /Script/ prefix
417
- FString ScriptPath = FString::Printf(TEXT("/Script/%s"), *EnumName);
418
- Enum = FindObject<UEnum>(nullptr, *ScriptPath);
419
- if (Enum)
420
- {
421
- return Enum;
422
- }
423
-
424
- // Try with E prefix
425
- FString EName = TEXT("E") + EnumName;
426
- Enum = FindObject<UEnum>(nullptr, *EName);
427
- if (Enum)
571
+ // Short-name lookup via FindFirstObject (UE 5.6+ replacement). Tries
572
+ // the caller's spelling, then E-prefixed - the standard UE convention.
573
+ const TArray<FString> Candidates = { EnumName, TEXT("E") + EnumName };
574
+ for (const FString& Candidate : Candidates)
428
575
  {
429
- return Enum;
430
- }
431
-
432
- FString EPath = FString::Printf(TEXT("/Script/%s"), *EName);
433
- Enum = FindObject<UEnum>(nullptr, *EPath);
434
- if (Enum)
435
- {
436
- return Enum;
576
+ Enum = FindFirstObject<UEnum>(*Candidate, EFindFirstObjectOptions::NativeFirst);
577
+ if (Enum)
578
+ {
579
+ return Enum;
580
+ }
437
581
  }
438
582
 
439
583
  return nullptr;