ultimate-unreal-engine-mcp 0.1.0

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 (124) hide show
  1. package/README.md +729 -0
  2. package/dist/build/error-parser.js +51 -0
  3. package/dist/build/fix-suggester.js +84 -0
  4. package/dist/build/ubt-runner.js +146 -0
  5. package/dist/cli.js +13 -0
  6. package/dist/config.js +8 -0
  7. package/dist/docs/data/ue57-api.js +228 -0
  8. package/dist/docs/doc-index.js +110 -0
  9. package/dist/docs/types.js +4 -0
  10. package/dist/generators/class-generator.js +363 -0
  11. package/dist/generators/file-modifier.js +276 -0
  12. package/dist/generators/uht-validator.js +177 -0
  13. package/dist/index.js +89 -0
  14. package/dist/parsers/cpp-class-index.js +230 -0
  15. package/dist/parsers/cpp-parser.js +369 -0
  16. package/dist/parsers/ini-parser.js +216 -0
  17. package/dist/parsers/uproject-parser.js +130 -0
  18. package/dist/plugin-bridge/client.js +217 -0
  19. package/dist/plugin-bridge/protocol.js +6 -0
  20. package/dist/plugin-bridge/retry.js +23 -0
  21. package/dist/setup.js +209 -0
  22. package/dist/tools/ai-systems/index.js +247 -0
  23. package/dist/tools/ai-systems/types.js +4 -0
  24. package/dist/tools/animation/index.js +241 -0
  25. package/dist/tools/animation/types.js +4 -0
  26. package/dist/tools/audio/index.js +204 -0
  27. package/dist/tools/audio/types.js +4 -0
  28. package/dist/tools/blueprint/index.js +495 -0
  29. package/dist/tools/blueprint/types.js +4 -0
  30. package/dist/tools/build/index.js +163 -0
  31. package/dist/tools/chaos/index.js +230 -0
  32. package/dist/tools/chaos/types.js +4 -0
  33. package/dist/tools/collision-physics/index.js +211 -0
  34. package/dist/tools/config/index.js +288 -0
  35. package/dist/tools/cpp/index.js +305 -0
  36. package/dist/tools/docs/index.js +251 -0
  37. package/dist/tools/editor/index.js +242 -0
  38. package/dist/tools/gas/index.js +222 -0
  39. package/dist/tools/gas/types.js +5 -0
  40. package/dist/tools/import-export/index.js +218 -0
  41. package/dist/tools/input/index.js +146 -0
  42. package/dist/tools/known-issues/index.js +88 -0
  43. package/dist/tools/known-issues/middleware.js +55 -0
  44. package/dist/tools/known-issues/store.js +125 -0
  45. package/dist/tools/livelink/index.js +203 -0
  46. package/dist/tools/livelink/types.js +4 -0
  47. package/dist/tools/material/index.js +190 -0
  48. package/dist/tools/motion-design/index.js +251 -0
  49. package/dist/tools/motion-design/types.js +6 -0
  50. package/dist/tools/movie-render/index.js +220 -0
  51. package/dist/tools/networking/index.js +149 -0
  52. package/dist/tools/pcg/index.js +164 -0
  53. package/dist/tools/selection/index.js +180 -0
  54. package/dist/tools/sequencer/index.js +218 -0
  55. package/dist/tools/validation/index.js +183 -0
  56. package/dist/tools/validation/types.js +4 -0
  57. package/dist/tools/viewport/index.js +310 -0
  58. package/dist/tools/worldpartition/index.js +226 -0
  59. package/dist/tools/worldpartition/types.js +4 -0
  60. package/dist/utils/execFileNoThrow.js +40 -0
  61. package/dist/utils/logger.js +27 -0
  62. package/dist/utils/path-guard.js +26 -0
  63. package/package.json +40 -0
  64. package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
  65. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
  66. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
  67. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
  68. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
  69. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
  70. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
  71. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
  72. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
  73. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
  74. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
  75. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
  76. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
  77. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
  78. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
  79. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
  80. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
  81. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
  82. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
  83. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
  84. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
  85. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
  86. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
  87. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
  88. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
  89. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
  90. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
  91. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
  92. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
  93. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
  94. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
  95. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
  96. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
  97. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
  98. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
  99. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
  100. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
  101. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
  102. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
  103. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
  104. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
  105. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
  106. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
  107. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
  108. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
  109. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
  110. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
  111. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
  112. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
  113. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
  114. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
  115. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
  116. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
  117. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
  118. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
  119. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
  120. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
  121. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
  122. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
  123. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
  124. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPTcpServer.h +59 -0
@@ -0,0 +1,715 @@
1
+ // MCPGASCommands.cpp (Plan 25-01)
2
+ // Implements four Gameplay Ability System inspection command handlers for the MCP bridge:
3
+ // gas.abilities -- list all Gameplay Ability classes with tags, costs, and cooldowns (GAS-01)
4
+ // gas.effects -- inspect Gameplay Effect modifiers, duration policy, stacking, period (GAS-02)
5
+ // gas.attributes -- read Attribute Set definitions with base values and clamping info (GAS-03)
6
+ // gas.tags -- query Gameplay Tag hierarchy and find assets using specific tags (GAS-04)
7
+ //
8
+ // All handlers run on the game thread via FMCPCommandRouter::Dispatch.
9
+ // All operations are read-only -- no Modify() calls needed.
10
+ // asset_path is validated to start with "/Game/" or "/Engine/" before any
11
+ // StaticLoadObject call to prevent path traversal (T-25-01).
12
+ // gas.abilities results are capped at 500 to prevent DoS (T-25-02).
13
+ // gas.tags reverse lookup tagged_assets are capped at 200 to prevent DoS (T-25-03).
14
+
15
+ #include "MCPGASCommands.h"
16
+
17
+ // Gameplay Ability System headers
18
+ #include "Abilities/GameplayAbility.h"
19
+ #include "GameplayEffect.h"
20
+ #include "AttributeSet.h"
21
+ #include "GameplayEffectTypes.h"
22
+
23
+ // Gameplay Tags headers
24
+ #include "GameplayTagsManager.h"
25
+ #include "GameplayTagContainer.h"
26
+
27
+ // Asset Registry
28
+ #include "AssetRegistry/AssetRegistryModule.h"
29
+ #include "AssetRegistry/IAssetRegistry.h"
30
+
31
+ // UObject reflection
32
+ #include "UObject/Class.h"
33
+ #include "UObject/UnrealType.h"
34
+ #include "UObject/ObjectMacros.h"
35
+
36
+ // Engine
37
+ #include "Engine/Blueprint.h"
38
+
39
+ // JSON
40
+ #include "Serialization/JsonSerializer.h"
41
+ #include "Serialization/JsonWriter.h"
42
+ #include "Dom/JsonObject.h"
43
+ #include "Dom/JsonValue.h"
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Internal helpers
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /** Returns a JSON success response string (without trailing newline). */
50
+ static FString BuildGASSuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
51
+ {
52
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
53
+ Obj->SetBoolField(TEXT("success"), true);
54
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
55
+ if (Data.IsValid())
56
+ {
57
+ Obj->SetObjectField(TEXT("data"), Data);
58
+ }
59
+
60
+ FString Output;
61
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
62
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
63
+ return Output;
64
+ }
65
+
66
+ /** Returns a JSON error response string (without trailing newline). */
67
+ static FString BuildGASErrorResponse(const FString& CorrId, const FString& Error)
68
+ {
69
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
70
+ Obj->SetBoolField(TEXT("success"), false);
71
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
72
+ Obj->SetStringField(TEXT("error"), Error);
73
+
74
+ FString Output;
75
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
76
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
77
+ return Output;
78
+ }
79
+
80
+ /**
81
+ * Validate that asset_path starts with "/Game/" or "/Engine/" to prevent
82
+ * path traversal attacks (T-25-01).
83
+ */
84
+ static bool IsValidAssetPath(const FString& AssetPath)
85
+ {
86
+ return AssetPath.StartsWith(TEXT("/Game/")) || AssetPath.StartsWith(TEXT("/Engine/"));
87
+ }
88
+
89
+ /**
90
+ * Convert a FGameplayTagContainer to a JSON array of tag name strings.
91
+ */
92
+ static TArray<TSharedPtr<FJsonValue>> GameplayTagContainerToJsonArray(const FGameplayTagContainer& Container)
93
+ {
94
+ TArray<TSharedPtr<FJsonValue>> TagArray;
95
+ for (const FGameplayTag& Tag : Container)
96
+ {
97
+ TagArray.Add(MakeShared<FJsonValueString>(Tag.ToString()));
98
+ }
99
+ return TagArray;
100
+ }
101
+
102
+ /**
103
+ * Extract the display name of an enum value from a UObject property using reflection.
104
+ * Returns "Unknown" if the property cannot be found or cast.
105
+ */
106
+ static FString GetEnumPropertyDisplayName(const UObject* Obj, const FString& PropertyName)
107
+ {
108
+ if (!Obj)
109
+ {
110
+ return TEXT("Unknown");
111
+ }
112
+
113
+ FProperty* Prop = Obj->GetClass()->FindPropertyByName(FName(*PropertyName));
114
+ if (!Prop)
115
+ {
116
+ return TEXT("Unknown");
117
+ }
118
+
119
+ // Try FEnumProperty (UE5 preferred pattern for typed enums)
120
+ if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
121
+ {
122
+ const void* ValuePtr = EnumProp->ContainerPtrToValuePtr<void>(Obj);
123
+ int64 EnumValue = EnumProp->GetUnderlyingProperty()->GetSignedIntPropertyValue(ValuePtr);
124
+ UEnum* Enum = EnumProp->GetEnum();
125
+ if (Enum)
126
+ {
127
+ return Enum->GetNameStringByValue(EnumValue);
128
+ }
129
+ return FString::Printf(TEXT("%lld"), EnumValue);
130
+ }
131
+
132
+ // Try FByteProperty (older TEnumAsByte<> pattern)
133
+ if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
134
+ {
135
+ const void* ValuePtr = ByteProp->ContainerPtrToValuePtr<void>(Obj);
136
+ uint8 EnumValue = ByteProp->GetPropertyValue(ValuePtr);
137
+ UEnum* Enum = ByteProp->Enum;
138
+ if (Enum)
139
+ {
140
+ return Enum->GetNameStringByValue((int64)EnumValue);
141
+ }
142
+ return FString::Printf(TEXT("%d"), (int32)EnumValue);
143
+ }
144
+
145
+ return TEXT("Unknown");
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // gas.abilities handler (GAS-01)
150
+ // ---------------------------------------------------------------------------
151
+
152
+ static void HandleGasAbilities(TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
153
+ {
154
+ FString CorrId;
155
+ Cmd->TryGetStringField(TEXT("correlationId"), CorrId);
156
+
157
+ // Optional class_filter payload field
158
+ FString ClassFilter;
159
+ const TSharedPtr<FJsonObject>* PayloadPtr = nullptr;
160
+ if (Cmd->TryGetObjectField(TEXT("payload"), PayloadPtr) && PayloadPtr && PayloadPtr->IsValid())
161
+ {
162
+ (*PayloadPtr)->TryGetStringField(TEXT("class_filter"), ClassFilter);
163
+ }
164
+
165
+ // Discover all UGameplayAbility subclass assets via the asset registry
166
+ IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
167
+
168
+ // Ensure registry is up to date
169
+ AssetRegistry.SearchAllAssets(/*bSynchronousSearch=*/false);
170
+
171
+ FTopLevelAssetPath AbilityClassPath(UGameplayAbility::StaticClass()->GetPathName());
172
+ TArray<FAssetData> AssetList;
173
+ AssetRegistry.GetAssetsByClass(AbilityClassPath, AssetList, /*bSearchSubClasses=*/true);
174
+
175
+ // Cap results at 500 (T-25-02)
176
+ constexpr int32 MaxAbilities = 500;
177
+ bool bCapped = AssetList.Num() > MaxAbilities;
178
+ if (bCapped)
179
+ {
180
+ AssetList.SetNum(MaxAbilities);
181
+ }
182
+
183
+ TArray<TSharedPtr<FJsonValue>> AbilitiesArray;
184
+ for (const FAssetData& AssetData : AssetList)
185
+ {
186
+ // Apply optional class filter on asset name
187
+ if (!ClassFilter.IsEmpty() && !AssetData.AssetName.ToString().Contains(ClassFilter))
188
+ {
189
+ continue;
190
+ }
191
+
192
+ // Validate path prefix (T-25-01) -- skip assets outside /Game/ or /Engine/
193
+ FString AssetPath = AssetData.GetObjectPathString();
194
+ if (!IsValidAssetPath(AssetPath))
195
+ {
196
+ continue;
197
+ }
198
+
199
+ // Load the ability CDO
200
+ UGameplayAbility* AbilityCDO = Cast<UGameplayAbility>(
201
+ StaticLoadObject(UGameplayAbility::StaticClass(), nullptr, *AssetPath)
202
+ );
203
+
204
+ // For Blueprint assets, try loading via Blueprint->GeneratedClass
205
+ if (!AbilityCDO)
206
+ {
207
+ UBlueprint* BP = Cast<UBlueprint>(
208
+ StaticLoadObject(UBlueprint::StaticClass(), nullptr, *AssetPath)
209
+ );
210
+ if (BP && BP->GeneratedClass)
211
+ {
212
+ AbilityCDO = Cast<UGameplayAbility>(BP->GeneratedClass->GetDefaultObject());
213
+ }
214
+ }
215
+
216
+ if (!AbilityCDO)
217
+ {
218
+ continue;
219
+ }
220
+
221
+ TSharedPtr<FJsonObject> AbilityObj = MakeShared<FJsonObject>();
222
+ AbilityObj->SetStringField(TEXT("class_name"), AbilityCDO->GetClass()->GetName());
223
+ AbilityObj->SetStringField(TEXT("asset_path"), AssetPath);
224
+
225
+ // Ability tags
226
+ AbilityObj->SetArrayField(TEXT("ability_tags"),
227
+ GameplayTagContainerToJsonArray(AbilityCDO->AbilityTags));
228
+
229
+ // Cancel / block tags
230
+ AbilityObj->SetArrayField(TEXT("cancel_abilities_with_tag"),
231
+ GameplayTagContainerToJsonArray(AbilityCDO->CancelAbilitiesWithTag));
232
+ AbilityObj->SetArrayField(TEXT("block_abilities_with_tag"),
233
+ GameplayTagContainerToJsonArray(AbilityCDO->BlockAbilitiesWithTag));
234
+
235
+ // Cost and cooldown GE class references
236
+ FString CostGEName = AbilityCDO->CostGameplayEffectClass
237
+ ? AbilityCDO->CostGameplayEffectClass->GetName()
238
+ : TEXT("None");
239
+ FString CooldownGEName = AbilityCDO->CooldownGameplayEffectClass
240
+ ? AbilityCDO->CooldownGameplayEffectClass->GetName()
241
+ : TEXT("None");
242
+ AbilityObj->SetStringField(TEXT("cost_gameplay_effect_class"), CostGEName);
243
+ AbilityObj->SetStringField(TEXT("cooldown_gameplay_effect_class"), CooldownGEName);
244
+
245
+ // Instancing policy via reflection
246
+ FString InstancingPolicy = GetEnumPropertyDisplayName(AbilityCDO, TEXT("InstancingPolicy"));
247
+ AbilityObj->SetStringField(TEXT("instancing_policy"), InstancingPolicy);
248
+
249
+ AbilitiesArray.Add(MakeShared<FJsonValueObject>(AbilityObj));
250
+ }
251
+
252
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
253
+ Data->SetArrayField(TEXT("abilities"), AbilitiesArray);
254
+ if (bCapped)
255
+ {
256
+ Data->SetBoolField(TEXT("capped"), true);
257
+ Data->SetNumberField(TEXT("cap_limit"), MaxAbilities);
258
+ }
259
+
260
+ SendResponse(BuildGASSuccessResponse(CorrId, Data) + TEXT("\n"));
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // gas.effects handler (GAS-02)
265
+ // ---------------------------------------------------------------------------
266
+
267
+ static void HandleGasEffects(TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
268
+ {
269
+ FString CorrId;
270
+ Cmd->TryGetStringField(TEXT("correlationId"), CorrId);
271
+
272
+ FString AssetPath;
273
+ const TSharedPtr<FJsonObject>* PayloadPtr = nullptr;
274
+ if (Cmd->TryGetObjectField(TEXT("payload"), PayloadPtr) && PayloadPtr && PayloadPtr->IsValid())
275
+ {
276
+ (*PayloadPtr)->TryGetStringField(TEXT("asset_path"), AssetPath);
277
+ }
278
+
279
+ if (AssetPath.IsEmpty())
280
+ {
281
+ SendResponse(BuildGASErrorResponse(CorrId, TEXT("payload.asset_path is required")) + TEXT("\n"));
282
+ return;
283
+ }
284
+
285
+ // Validate path (T-25-01)
286
+ if (!IsValidAssetPath(AssetPath))
287
+ {
288
+ SendResponse(BuildGASErrorResponse(CorrId,
289
+ TEXT("asset_path must start with /Game/ or /Engine/")) + TEXT("\n"));
290
+ return;
291
+ }
292
+
293
+ // Load as UGameplayEffect CDO
294
+ UGameplayEffect* GEObj = Cast<UGameplayEffect>(
295
+ StaticLoadObject(UGameplayEffect::StaticClass(), nullptr, *AssetPath)
296
+ );
297
+
298
+ // For Blueprint-based GEs
299
+ if (!GEObj)
300
+ {
301
+ UBlueprint* BP = Cast<UBlueprint>(
302
+ StaticLoadObject(UBlueprint::StaticClass(), nullptr, *AssetPath)
303
+ );
304
+ if (BP && BP->GeneratedClass)
305
+ {
306
+ GEObj = Cast<UGameplayEffect>(BP->GeneratedClass->GetDefaultObject());
307
+ }
308
+ }
309
+
310
+ if (!GEObj)
311
+ {
312
+ SendResponse(BuildGASErrorResponse(CorrId,
313
+ FString::Printf(TEXT("Failed to load UGameplayEffect at: %s"), *AssetPath)) + TEXT("\n"));
314
+ return;
315
+ }
316
+
317
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
318
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
319
+
320
+ // Duration policy via reflection
321
+ FString DurationPolicy = GetEnumPropertyDisplayName(GEObj, TEXT("DurationPolicy"));
322
+ Data->SetStringField(TEXT("duration_policy"), DurationPolicy);
323
+
324
+ // Modifiers array
325
+ TArray<TSharedPtr<FJsonValue>> ModifiersArray;
326
+ for (const FGameplayModifierInfo& ModInfo : GEObj->Modifiers)
327
+ {
328
+ TSharedPtr<FJsonObject> ModObj = MakeShared<FJsonObject>();
329
+
330
+ // Attribute name and owning set
331
+ ModObj->SetStringField(TEXT("attribute"), ModInfo.Attribute.GetName());
332
+ FString AttrSetName = ModInfo.Attribute.GetAttributeSetClass()
333
+ ? ModInfo.Attribute.GetAttributeSetClass()->GetName()
334
+ : TEXT("None");
335
+ ModObj->SetStringField(TEXT("attribute_set"), AttrSetName);
336
+
337
+ // Modifier operation via reflection on FGameplayModifierInfo
338
+ // ModifierOp is an EGameplayModOp::Type inside FGameplayModifierInfo
339
+ // We reflect on the struct property directly
340
+ FString ModOpStr = TEXT("Unknown");
341
+ {
342
+ UScriptStruct* ModInfoStruct = FGameplayModifierInfo::StaticStruct();
343
+ if (ModInfoStruct)
344
+ {
345
+ FProperty* ModOpProp = ModInfoStruct->FindPropertyByName(TEXT("ModifierOp"));
346
+ if (FByteProperty* ByteProp = CastField<FByteProperty>(ModOpProp))
347
+ {
348
+ const void* ValuePtr = ByteProp->ContainerPtrToValuePtr<void>(&ModInfo);
349
+ uint8 EnumVal = ByteProp->GetPropertyValue(ValuePtr);
350
+ UEnum* Enum = ByteProp->Enum;
351
+ if (Enum)
352
+ {
353
+ ModOpStr = Enum->GetNameStringByValue((int64)EnumVal);
354
+ }
355
+ else
356
+ {
357
+ ModOpStr = FString::Printf(TEXT("%d"), (int32)EnumVal);
358
+ }
359
+ }
360
+ else if (FEnumProperty* EnumProp = CastField<FEnumProperty>(ModOpProp))
361
+ {
362
+ const void* ValuePtr = EnumProp->ContainerPtrToValuePtr<void>(&ModInfo);
363
+ int64 EnumVal = EnumProp->GetUnderlyingProperty()->GetSignedIntPropertyValue(ValuePtr);
364
+ UEnum* Enum = EnumProp->GetEnum();
365
+ if (Enum)
366
+ {
367
+ ModOpStr = Enum->GetNameStringByValue(EnumVal);
368
+ }
369
+ else
370
+ {
371
+ ModOpStr = FString::Printf(TEXT("%lld"), EnumVal);
372
+ }
373
+ }
374
+ }
375
+ }
376
+ ModObj->SetStringField(TEXT("modifier_op"), ModOpStr);
377
+
378
+ // Magnitude value
379
+ FString MagnitudeDesc;
380
+ {
381
+ const FGameplayEffectModifierMagnitude& Mag = ModInfo.ModifierMagnitude;
382
+ EGameplayEffectMagnitudeCalculation MagType = Mag.GetMagnitudeCalculationType();
383
+ switch (MagType)
384
+ {
385
+ case EGameplayEffectMagnitudeCalculation::ScalableFloat:
386
+ {
387
+ float ScalarValue = 0.0f;
388
+ Mag.GetStaticMagnitudeIfPossible(1.0f, ScalarValue);
389
+ MagnitudeDesc = FString::Printf(TEXT("%f"), ScalarValue);
390
+ break;
391
+ }
392
+ case EGameplayEffectMagnitudeCalculation::AttributeBased:
393
+ MagnitudeDesc = TEXT("AttributeBased");
394
+ break;
395
+ case EGameplayEffectMagnitudeCalculation::CustomCalculationClass:
396
+ MagnitudeDesc = TEXT("Custom");
397
+ break;
398
+ case EGameplayEffectMagnitudeCalculation::SetByCaller:
399
+ MagnitudeDesc = TEXT("SetByCaller");
400
+ break;
401
+ default:
402
+ MagnitudeDesc = TEXT("Calculated");
403
+ break;
404
+ }
405
+ }
406
+ ModObj->SetStringField(TEXT("magnitude_value"), MagnitudeDesc);
407
+
408
+ ModifiersArray.Add(MakeShared<FJsonValueObject>(ModObj));
409
+ }
410
+ Data->SetArrayField(TEXT("modifiers"), ModifiersArray);
411
+
412
+ // Stacking type and limit via reflection
413
+ FString StackingType = GetEnumPropertyDisplayName(GEObj, TEXT("StackingType"));
414
+ Data->SetStringField(TEXT("stacking_type"), StackingType);
415
+ Data->SetNumberField(TEXT("stack_limit_count"), (double)GEObj->StackLimitCount);
416
+
417
+ // Period interval (period is a FScalableFloat, extract base value)
418
+ float PeriodValue = 0.0f;
419
+ GEObj->Period.GetStaticValue(PeriodValue);
420
+ Data->SetNumberField(TEXT("period_interval"), (double)PeriodValue);
421
+
422
+ // Gameplay Cue tags
423
+ Data->SetArrayField(TEXT("gameplay_cue_tags"),
424
+ GameplayTagContainerToJsonArray(GEObj->GameplayCueTags));
425
+
426
+ SendResponse(BuildGASSuccessResponse(CorrId, Data) + TEXT("\n"));
427
+ }
428
+
429
+ // ---------------------------------------------------------------------------
430
+ // gas.attributes handler (GAS-03)
431
+ // ---------------------------------------------------------------------------
432
+
433
+ static void HandleGasAttributes(TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
434
+ {
435
+ FString CorrId;
436
+ Cmd->TryGetStringField(TEXT("correlationId"), CorrId);
437
+
438
+ FString AssetPath;
439
+ const TSharedPtr<FJsonObject>* PayloadPtr = nullptr;
440
+ if (Cmd->TryGetObjectField(TEXT("payload"), PayloadPtr) && PayloadPtr && PayloadPtr->IsValid())
441
+ {
442
+ (*PayloadPtr)->TryGetStringField(TEXT("asset_path"), AssetPath);
443
+ }
444
+
445
+ if (AssetPath.IsEmpty())
446
+ {
447
+ SendResponse(BuildGASErrorResponse(CorrId, TEXT("payload.asset_path is required")) + TEXT("\n"));
448
+ return;
449
+ }
450
+
451
+ // Validate path (T-25-01)
452
+ if (!IsValidAssetPath(AssetPath))
453
+ {
454
+ SendResponse(BuildGASErrorResponse(CorrId,
455
+ TEXT("asset_path must start with /Game/ or /Engine/")) + TEXT("\n"));
456
+ return;
457
+ }
458
+
459
+ // Attempt to load as AttributeSet -- try Blueprint first, then direct CDO
460
+ UClass* AttrSetClass = nullptr;
461
+ UAttributeSet* AttrSetCDO = nullptr;
462
+
463
+ // Try Blueprint path
464
+ UBlueprint* BP = Cast<UBlueprint>(
465
+ StaticLoadObject(UBlueprint::StaticClass(), nullptr, *AssetPath)
466
+ );
467
+ if (BP && BP->GeneratedClass && BP->GeneratedClass->IsChildOf(UAttributeSet::StaticClass()))
468
+ {
469
+ AttrSetClass = BP->GeneratedClass;
470
+ AttrSetCDO = Cast<UAttributeSet>(AttrSetClass->GetDefaultObject());
471
+ }
472
+
473
+ // Try loading directly as a native class CDO
474
+ if (!AttrSetCDO)
475
+ {
476
+ AttrSetCDO = Cast<UAttributeSet>(
477
+ StaticLoadObject(UAttributeSet::StaticClass(), nullptr, *AssetPath)
478
+ );
479
+ if (AttrSetCDO)
480
+ {
481
+ AttrSetClass = AttrSetCDO->GetClass();
482
+ }
483
+ }
484
+
485
+ // Try StaticLoadClass for native classes
486
+ if (!AttrSetCDO)
487
+ {
488
+ AttrSetClass = StaticLoadClass(UAttributeSet::StaticClass(), nullptr, *AssetPath);
489
+ if (AttrSetClass)
490
+ {
491
+ AttrSetCDO = Cast<UAttributeSet>(AttrSetClass->GetDefaultObject());
492
+ }
493
+ }
494
+
495
+ if (!AttrSetCDO || !AttrSetClass)
496
+ {
497
+ SendResponse(BuildGASErrorResponse(CorrId,
498
+ FString::Printf(TEXT("Failed to load UAttributeSet at: %s"), *AssetPath)) + TEXT("\n"));
499
+ return;
500
+ }
501
+
502
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
503
+ Data->SetStringField(TEXT("class_name"), AttrSetClass->GetName());
504
+
505
+ // Iterate numeric properties (float attributes on attribute sets)
506
+ TArray<TSharedPtr<FJsonValue>> AttributesArray;
507
+ for (TFieldIterator<FNumericProperty> PropIt(AttrSetClass, EFieldIteratorFlags::IncludeSuper); PropIt; ++PropIt)
508
+ {
509
+ FNumericProperty* NumProp = *PropIt;
510
+ if (!NumProp)
511
+ {
512
+ continue;
513
+ }
514
+
515
+ // Only include replicated or BlueprintReadOnly properties (typical for GAS attributes)
516
+ // We include all numeric properties on AttributeSet subclasses as they are attributes
517
+ TSharedPtr<FJsonObject> AttrObj = MakeShared<FJsonObject>();
518
+ AttrObj->SetStringField(TEXT("attribute_name"), NumProp->GetName());
519
+
520
+ // Read base value from CDO
521
+ double BaseValue = 0.0;
522
+ if (NumProp->IsFloatingPoint())
523
+ {
524
+ const void* ValuePtr = NumProp->ContainerPtrToValuePtr<void>(AttrSetCDO);
525
+ BaseValue = NumProp->GetFloatingPointPropertyValue(ValuePtr);
526
+ }
527
+ else if (NumProp->IsInteger())
528
+ {
529
+ const void* ValuePtr = NumProp->ContainerPtrToValuePtr<void>(AttrSetCDO);
530
+ BaseValue = (double)NumProp->GetSignedIntPropertyValue(ValuePtr);
531
+ }
532
+ AttrObj->SetNumberField(TEXT("base_value"), BaseValue);
533
+
534
+ // Replication check
535
+ bool bReplicated = NumProp->HasAnyPropertyFlags(CPF_Net);
536
+ AttrObj->SetBoolField(TEXT("replicated"), bReplicated);
537
+
538
+ // Clamping note -- exact clamp values require inspecting PreAttributeBaseChange
539
+ // override implementation which is not reliably extractable at CDO level
540
+ bool bHasClamping = AttrSetClass->IsFunctionImplementedInScript(TEXT("PreAttributeBaseChange")) ||
541
+ AttrSetClass->IsFunctionImplementedInScript(TEXT("PostAttributeChange"));
542
+ AttrObj->SetBoolField(TEXT("has_clamping"), bHasClamping);
543
+ if (bHasClamping)
544
+ {
545
+ AttrObj->SetStringField(TEXT("clamp_note"), TEXT("check_implementation"));
546
+ }
547
+
548
+ AttributesArray.Add(MakeShared<FJsonValueObject>(AttrObj));
549
+ }
550
+ Data->SetArrayField(TEXT("attributes"), AttributesArray);
551
+
552
+ SendResponse(BuildGASSuccessResponse(CorrId, Data) + TEXT("\n"));
553
+ }
554
+
555
+ // ---------------------------------------------------------------------------
556
+ // gas.tags handler (GAS-04)
557
+ // ---------------------------------------------------------------------------
558
+
559
+ static void HandleGasTags(TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
560
+ {
561
+ FString CorrId;
562
+ Cmd->TryGetStringField(TEXT("correlationId"), CorrId);
563
+
564
+ FString TagFilter;
565
+ FString FindAssetsWithTag;
566
+ const TSharedPtr<FJsonObject>* PayloadPtr = nullptr;
567
+ if (Cmd->TryGetObjectField(TEXT("payload"), PayloadPtr) && PayloadPtr && PayloadPtr->IsValid())
568
+ {
569
+ (*PayloadPtr)->TryGetStringField(TEXT("tag_filter"), TagFilter);
570
+ (*PayloadPtr)->TryGetStringField(TEXT("find_assets_with_tag"), FindAssetsWithTag);
571
+ }
572
+
573
+ UGameplayTagsManager& TagsManager = UGameplayTagsManager::Get();
574
+
575
+ // Gather all registered gameplay tags
576
+ FGameplayTagContainer AllTagsContainer;
577
+ TagsManager.RequestAllGameplayTags(AllTagsContainer, /*bOnlyIncludeDictionaryTags=*/false);
578
+
579
+ TArray<TSharedPtr<FJsonValue>> TagsArray;
580
+ for (const FGameplayTag& Tag : AllTagsContainer)
581
+ {
582
+ FString TagName = Tag.ToString();
583
+
584
+ // Apply filter if provided
585
+ if (!TagFilter.IsEmpty() && !TagName.StartsWith(TagFilter) && !TagName.Contains(TagFilter))
586
+ {
587
+ continue;
588
+ }
589
+
590
+ TagsArray.Add(MakeShared<FJsonValueString>(TagName));
591
+ }
592
+
593
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
594
+ Data->SetArrayField(TEXT("tags"), TagsArray);
595
+ Data->SetNumberField(TEXT("total_registered"), (double)AllTagsContainer.Num());
596
+ if (!TagFilter.IsEmpty())
597
+ {
598
+ Data->SetStringField(TEXT("tag_filter"), TagFilter);
599
+ }
600
+
601
+ // Optional reverse lookup: find assets that reference a given tag (T-25-03: cap at 200)
602
+ if (!FindAssetsWithTag.IsEmpty())
603
+ {
604
+ // Validate the tag exists
605
+ FGameplayTag LookupTag = TagsManager.RequestGameplayTag(FName(*FindAssetsWithTag), /*ErrorIfNotFound=*/false);
606
+
607
+ TArray<TSharedPtr<FJsonValue>> TaggedAssetsArray;
608
+
609
+ if (LookupTag.IsValid())
610
+ {
611
+ IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
612
+
613
+ // Search /Game/ for assets
614
+ TArray<FAssetData> AllAssets;
615
+ AssetRegistry.GetAllAssets(AllAssets, /*bSkipARFilteredAssets=*/false);
616
+
617
+ constexpr int32 MaxTaggedAssets = 200;
618
+ int32 Found = 0;
619
+ bool bTaggedCapped = false;
620
+
621
+ for (const FAssetData& AssetData : AllAssets)
622
+ {
623
+ if (Found >= MaxTaggedAssets)
624
+ {
625
+ bTaggedCapped = true;
626
+ break;
627
+ }
628
+
629
+ FString AssetPath = AssetData.GetObjectPathString();
630
+ if (!IsValidAssetPath(AssetPath))
631
+ {
632
+ continue;
633
+ }
634
+
635
+ // Check if this asset has the tag in its tags metadata
636
+ // FAssetData::TagsAndValues contains gameplay-relevant tags
637
+ bool bHasTag = false;
638
+ FAssetDataTagMapSharedView::FFindTagResult TagResult =
639
+ AssetData.TagsAndValues.FindTag(TEXT("GameplayTags"));
640
+ if (TagResult.IsSet())
641
+ {
642
+ bHasTag = TagResult.GetValue().Contains(FindAssetsWithTag);
643
+ }
644
+
645
+ if (!bHasTag)
646
+ {
647
+ // Also check AssetBundleData tag
648
+ FAssetDataTagMapSharedView::FFindTagResult BundleResult =
649
+ AssetData.TagsAndValues.FindTag(TEXT("AssetBundleData"));
650
+ if (BundleResult.IsSet())
651
+ {
652
+ bHasTag = BundleResult.GetValue().Contains(FindAssetsWithTag);
653
+ }
654
+ }
655
+
656
+ if (bHasTag)
657
+ {
658
+ TaggedAssetsArray.Add(MakeShared<FJsonValueString>(AssetPath));
659
+ ++Found;
660
+ }
661
+ }
662
+
663
+ if (bTaggedCapped)
664
+ {
665
+ Data->SetBoolField(TEXT("tagged_assets_capped"), true);
666
+ Data->SetNumberField(TEXT("tagged_assets_cap_limit"), MaxTaggedAssets);
667
+ }
668
+ }
669
+ else
670
+ {
671
+ UE_LOG(LogTemp, Warning, TEXT("[MCPGASCommands] gas.tags: tag '%s' not found in registry"),
672
+ *FindAssetsWithTag);
673
+ }
674
+
675
+ Data->SetArrayField(TEXT("tagged_assets"), TaggedAssetsArray);
676
+ Data->SetStringField(TEXT("find_assets_with_tag"), FindAssetsWithTag);
677
+ }
678
+
679
+ SendResponse(BuildGASSuccessResponse(CorrId, Data) + TEXT("\n"));
680
+ }
681
+
682
+ // ---------------------------------------------------------------------------
683
+ // RegisterGASCommands -- wire all four handlers into the router
684
+ // ---------------------------------------------------------------------------
685
+
686
+ void RegisterGASCommands(FMCPCommandRouter& Router)
687
+ {
688
+ // gas.abilities -- list all Gameplay Ability classes with tags, costs, and cooldowns (GAS-01)
689
+ Router.RegisterHandler(TEXT("gas.abilities"),
690
+ [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
691
+ {
692
+ HandleGasAbilities(Cmd, SendResponse);
693
+ });
694
+
695
+ // gas.effects -- inspect Gameplay Effect modifiers, duration policy, stacking, period (GAS-02)
696
+ Router.RegisterHandler(TEXT("gas.effects"),
697
+ [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
698
+ {
699
+ HandleGasEffects(Cmd, SendResponse);
700
+ });
701
+
702
+ // gas.attributes -- read Attribute Set definitions with base values and clamping info (GAS-03)
703
+ Router.RegisterHandler(TEXT("gas.attributes"),
704
+ [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
705
+ {
706
+ HandleGasAttributes(Cmd, SendResponse);
707
+ });
708
+
709
+ // gas.tags -- query Gameplay Tag hierarchy and find assets using specific tags (GAS-04)
710
+ Router.RegisterHandler(TEXT("gas.tags"),
711
+ [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
712
+ {
713
+ HandleGasTags(Cmd, SendResponse);
714
+ });
715
+ }