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,624 @@
1
+ // MCPAudioCommands.cpp (Plan 23-01)
2
+ // Implements four audio system inspection command handlers for the MCP bridge:
3
+ // audio.list -- list all sound assets by type (AUD-01)
4
+ // audio.metasound -- inspect MetaSound patch graph nodes, inputs, outputs (AUD-02)
5
+ // audio.soundcue -- read SoundCue node graph and attenuation settings (AUD-03)
6
+ // audio.insights -- query Audio Insights monitoring data (AUD-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-23-01).
12
+ // audio.soundcue recursion is capped at 50 levels to prevent DoS (T-23-02).
13
+ // audio.list type_filter is validated against known types (T-23-03).
14
+
15
+ #include "MCPAudioCommands.h"
16
+
17
+ // Sound headers
18
+ #include "Sound/SoundWave.h"
19
+ #include "Sound/SoundCue.h"
20
+ #include "Sound/SoundNode.h"
21
+ #include "Sound/SoundNodeWavePlayer.h"
22
+ #include "Sound/SoundNodeModulator.h"
23
+ #include "Sound/SoundNodeAttenuation.h"
24
+ #include "Sound/SoundAttenuation.h"
25
+ #include "Engine/Attenuation.h"
26
+
27
+ // MetaSound headers
28
+ #include "MetasoundSource.h"
29
+ #include "MetasoundFrontendDocument.h"
30
+ #include "MetasoundDocumentInterface.h"
31
+
32
+ // Asset Registry
33
+ #include "AssetRegistry/AssetRegistryModule.h"
34
+ #include "AssetRegistry/IAssetRegistry.h"
35
+
36
+ // Editor / Engine
37
+ #include "Editor.h"
38
+ #include "AudioDevice.h"
39
+ #include "AudioDeviceHandle.h"
40
+ #include "Modules/ModuleManager.h"
41
+
42
+ // JSON
43
+ #include "Serialization/JsonSerializer.h"
44
+ #include "Serialization/JsonWriter.h"
45
+ #include "Dom/JsonObject.h"
46
+ #include "Dom/JsonValue.h"
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Internal helpers
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /** Returns a JSON success response string (without trailing newline). */
53
+ static FString BuildAudioSuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
54
+ {
55
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
56
+ Obj->SetBoolField(TEXT("success"), true);
57
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
58
+ if (Data.IsValid())
59
+ {
60
+ Obj->SetObjectField(TEXT("data"), Data);
61
+ }
62
+
63
+ FString Output;
64
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
65
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
66
+ return Output;
67
+ }
68
+
69
+ /** Returns a JSON error response string (without trailing newline). */
70
+ static FString BuildAudioErrorResponse(const FString& CorrId, const FString& Error)
71
+ {
72
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
73
+ Obj->SetBoolField(TEXT("success"), false);
74
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
75
+ Obj->SetStringField(TEXT("error"), Error);
76
+
77
+ FString Output;
78
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
79
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
80
+ return Output;
81
+ }
82
+
83
+ /**
84
+ * Validate that asset_path starts with "/Game/" or "/Engine/" to prevent
85
+ * path traversal attacks (T-23-01).
86
+ */
87
+ static bool IsValidAssetPath(const FString& AssetPath)
88
+ {
89
+ return AssetPath.StartsWith(TEXT("/Game/")) || AssetPath.StartsWith(TEXT("/Engine/"));
90
+ }
91
+
92
+ /**
93
+ * Convert EAttenuationDistanceModel to string for SoundCue attenuation (AUD-03).
94
+ * Enum defined in Engine/Attenuation.h (Linear, Logarithmic, Inverse, LogReverse, NaturalSound, Custom).
95
+ */
96
+ static FString AttenuationFalloffModelToString(EAttenuationDistanceModel Model)
97
+ {
98
+ switch (Model)
99
+ {
100
+ case EAttenuationDistanceModel::Linear: return TEXT("Linear");
101
+ case EAttenuationDistanceModel::Logarithmic: return TEXT("Logarithmic");
102
+ case EAttenuationDistanceModel::Inverse: return TEXT("Inverse");
103
+ case EAttenuationDistanceModel::LogReverse: return TEXT("LogReverse");
104
+ case EAttenuationDistanceModel::NaturalSound: return TEXT("NaturalSound");
105
+ case EAttenuationDistanceModel::Custom: return TEXT("Custom");
106
+ default: return TEXT("Linear");
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Convert ESpatialization to string for SoundCue attenuation (AUD-03).
112
+ */
113
+ static FString SpatializationMethodToString(ESoundSpatializationAlgorithm Method)
114
+ {
115
+ switch (Method)
116
+ {
117
+ case ESoundSpatializationAlgorithm::SPATIALIZATION_Default: return TEXT("Panning");
118
+ case ESoundSpatializationAlgorithm::SPATIALIZATION_HRTF: return TEXT("Binaural");
119
+ default: return TEXT("Panning");
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Build attenuation data JSON from FSoundAttenuationSettings.
125
+ */
126
+ static TSharedPtr<FJsonObject> BuildAttenuationObject(const FSoundAttenuationSettings& Settings)
127
+ {
128
+ TSharedPtr<FJsonObject> AttObj = MakeShared<FJsonObject>();
129
+ AttObj->SetNumberField(TEXT("attenuation_inner_radius"), static_cast<double>(Settings.AttenuationShapeExtents.X));
130
+ AttObj->SetNumberField(TEXT("attenuation_falloff_distance"), static_cast<double>(Settings.FalloffDistance));
131
+ AttObj->SetStringField(TEXT("attenuation_falloff_model"), AttenuationFalloffModelToString(Settings.DistanceAlgorithm));
132
+ AttObj->SetStringField(TEXT("spatialization_method"), SpatializationMethodToString(Settings.SpatializationAlgorithm));
133
+ return AttObj;
134
+ }
135
+
136
+ /**
137
+ * Recursively build a JSON node tree from a USoundNode (AUD-03).
138
+ * Depth is tracked to cap recursion at MaxDepth levels (T-23-02).
139
+ * Returns nullptr if depth exceeded or node is null.
140
+ */
141
+ static TSharedPtr<FJsonObject> BuildSoundNodeTree(USoundNode* Node, int32 Depth, int32 MaxDepth, bool& bDepthExceeded)
142
+ {
143
+ if (!Node)
144
+ {
145
+ return nullptr;
146
+ }
147
+
148
+ if (Depth >= MaxDepth)
149
+ {
150
+ bDepthExceeded = true;
151
+ TSharedPtr<FJsonObject> TruncObj = MakeShared<FJsonObject>();
152
+ TruncObj->SetStringField(TEXT("node_name"), TEXT("TRUNCATED"));
153
+ TruncObj->SetStringField(TEXT("node_class"), TEXT("TRUNCATED"));
154
+ TruncObj->SetBoolField(TEXT("depth_exceeded"), true);
155
+ return TruncObj;
156
+ }
157
+
158
+ TSharedPtr<FJsonObject> NodeObj = MakeShared<FJsonObject>();
159
+ NodeObj->SetStringField(TEXT("node_name"), Node->GetName());
160
+ NodeObj->SetStringField(TEXT("node_class"), Node->GetClass()->GetName());
161
+
162
+ // Type-specific fields for USoundNodeWavePlayer.
163
+ if (USoundNodeWavePlayer* WavePlayer = Cast<USoundNodeWavePlayer>(Node))
164
+ {
165
+ FString WavePath;
166
+ if (WavePlayer->GetSoundWave())
167
+ {
168
+ WavePath = WavePlayer->GetSoundWave()->GetPathName();
169
+ }
170
+ NodeObj->SetStringField(TEXT("wave_asset_path"), WavePath);
171
+ }
172
+
173
+ // Type-specific fields for USoundNodeModulator.
174
+ if (USoundNodeModulator* Modulator = Cast<USoundNodeModulator>(Node))
175
+ {
176
+ NodeObj->SetNumberField(TEXT("pitch_min"), static_cast<double>(Modulator->PitchMin));
177
+ NodeObj->SetNumberField(TEXT("pitch_max"), static_cast<double>(Modulator->PitchMax));
178
+ NodeObj->SetNumberField(TEXT("volume_min"), static_cast<double>(Modulator->VolumeMin));
179
+ NodeObj->SetNumberField(TEXT("volume_max"), static_cast<double>(Modulator->VolumeMax));
180
+ }
181
+
182
+ // Type-specific fields for USoundNodeAttenuation.
183
+ if (USoundNodeAttenuation* AttNode = Cast<USoundNodeAttenuation>(Node))
184
+ {
185
+ const bool bHasCustomAttenuation = (AttNode->AttenuationSettings != nullptr)
186
+ || (AttNode->bOverrideAttenuation);
187
+ NodeObj->SetBoolField(TEXT("has_custom_attenuation"), bHasCustomAttenuation);
188
+
189
+ if (bHasCustomAttenuation)
190
+ {
191
+ const FSoundAttenuationSettings* SettingsPtr = nullptr;
192
+ if (AttNode->AttenuationSettings)
193
+ {
194
+ SettingsPtr = &AttNode->AttenuationSettings->Attenuation;
195
+ }
196
+ else if (AttNode->bOverrideAttenuation)
197
+ {
198
+ SettingsPtr = &AttNode->AttenuationOverrides;
199
+ }
200
+
201
+ if (SettingsPtr)
202
+ {
203
+ TSharedPtr<FJsonObject> AttObj = BuildAttenuationObject(*SettingsPtr);
204
+ NodeObj->SetObjectField(TEXT("attenuation"), AttObj);
205
+ }
206
+ }
207
+ }
208
+
209
+ // Recurse into child nodes.
210
+ TArray<TSharedPtr<FJsonValue>> ChildrenArray;
211
+ for (USoundNode* Child : Node->ChildNodes)
212
+ {
213
+ TSharedPtr<FJsonObject> ChildObj = BuildSoundNodeTree(Child, Depth + 1, MaxDepth, bDepthExceeded);
214
+ if (ChildObj.IsValid())
215
+ {
216
+ ChildrenArray.Add(MakeShared<FJsonValueObject>(ChildObj));
217
+ }
218
+ }
219
+ NodeObj->SetArrayField(TEXT("children"), ChildrenArray);
220
+
221
+ return NodeObj;
222
+ }
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // RegisterAudioCommands
226
+ // ---------------------------------------------------------------------------
227
+
228
+ void RegisterAudioCommands(FMCPCommandRouter& Router)
229
+ {
230
+ // -----------------------------------------------------------------------
231
+ // audio.list (AUD-01)
232
+ // Lists sound assets in the project, optionally filtered by type.
233
+ // Optional payload field: type_filter ("SoundWave", "SoundCue", "MetaSound", or empty for all).
234
+ // Returns JSON array `assets` with: asset_path, asset_name, asset_type.
235
+ // For SoundWave assets: duration, sample_rate, num_channels, compression_name also returned.
236
+ // Threat T-23-03: unrecognized type_filter values query all types (permissive fallback).
237
+ // -----------------------------------------------------------------------
238
+ Router.RegisterHandler(TEXT("audio.list"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
239
+ {
240
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
241
+
242
+ // Extract optional payload.
243
+ TSharedPtr<FJsonObject> Payload;
244
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
245
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
246
+ {
247
+ Payload = (*PayloadVal)->AsObject();
248
+ }
249
+
250
+ FString TypeFilter;
251
+ if (Payload.IsValid())
252
+ {
253
+ Payload->TryGetStringField(TEXT("type_filter"), TypeFilter);
254
+ }
255
+
256
+ // T-23-03: Validate type_filter against known values. Unrecognized values fall back to all.
257
+ const bool bFilterSoundWave = TypeFilter.IsEmpty() || TypeFilter == TEXT("SoundWave");
258
+ const bool bFilterSoundCue = TypeFilter.IsEmpty() || TypeFilter == TEXT("SoundCue");
259
+ const bool bFilterMetaSound = TypeFilter.IsEmpty() || TypeFilter == TEXT("MetaSound");
260
+
261
+ // Access the asset registry.
262
+ FAssetRegistryModule& RegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
263
+ IAssetRegistry& Registry = RegistryModule.Get();
264
+
265
+ TArray<TSharedPtr<FJsonValue>> AssetsArray;
266
+
267
+ // Query SoundWave assets.
268
+ if (bFilterSoundWave)
269
+ {
270
+ TArray<FAssetData> AssetList;
271
+ FTopLevelAssetPath ClassPath(TEXT("/Script/Engine"), TEXT("SoundWave"));
272
+ Registry.GetAssetsByClass(ClassPath, AssetList, /*bSearchSubClasses=*/true);
273
+
274
+ for (const FAssetData& Asset : AssetList)
275
+ {
276
+ TSharedPtr<FJsonObject> AssetObj = MakeShared<FJsonObject>();
277
+ AssetObj->SetStringField(TEXT("asset_path"), Asset.GetObjectPathString());
278
+ AssetObj->SetStringField(TEXT("asset_name"), Asset.AssetName.ToString());
279
+ AssetObj->SetStringField(TEXT("asset_type"), TEXT("SoundWave"));
280
+
281
+ // Attempt to load to get audio metadata.
282
+ USoundWave* SoundWave = Cast<USoundWave>(
283
+ StaticLoadObject(USoundWave::StaticClass(), nullptr, *Asset.GetObjectPathString()));
284
+ if (SoundWave)
285
+ {
286
+ AssetObj->SetNumberField(TEXT("duration"), static_cast<double>(SoundWave->GetDuration()));
287
+ AssetObj->SetNumberField(TEXT("sample_rate"), static_cast<double>(SoundWave->GetSampleRateForCurrentPlatform()));
288
+ AssetObj->SetNumberField(TEXT("num_channels"), static_cast<double>(SoundWave->NumChannels));
289
+ // Compression name from the compression format enum via Audio::ToName().
290
+ const FName CompressionFName = Audio::ToName(SoundWave->GetSoundCompressionType());
291
+ AssetObj->SetStringField(TEXT("compression_name"), CompressionFName.IsNone() ? TEXT("Unknown") : CompressionFName.ToString());
292
+ }
293
+ else
294
+ {
295
+ AssetObj->SetNumberField(TEXT("duration"), 0.0);
296
+ AssetObj->SetNumberField(TEXT("sample_rate"), 0.0);
297
+ AssetObj->SetNumberField(TEXT("num_channels"), 0.0);
298
+ AssetObj->SetStringField(TEXT("compression_name"), TEXT("Unknown"));
299
+ }
300
+
301
+ AssetsArray.Add(MakeShared<FJsonValueObject>(AssetObj));
302
+ }
303
+ }
304
+
305
+ // Query SoundCue assets.
306
+ if (bFilterSoundCue)
307
+ {
308
+ TArray<FAssetData> AssetList;
309
+ FTopLevelAssetPath ClassPath(TEXT("/Script/Engine"), TEXT("SoundCue"));
310
+ Registry.GetAssetsByClass(ClassPath, AssetList, /*bSearchSubClasses=*/true);
311
+
312
+ for (const FAssetData& Asset : AssetList)
313
+ {
314
+ TSharedPtr<FJsonObject> AssetObj = MakeShared<FJsonObject>();
315
+ AssetObj->SetStringField(TEXT("asset_path"), Asset.GetObjectPathString());
316
+ AssetObj->SetStringField(TEXT("asset_name"), Asset.AssetName.ToString());
317
+ AssetObj->SetStringField(TEXT("asset_type"), TEXT("SoundCue"));
318
+ AssetsArray.Add(MakeShared<FJsonValueObject>(AssetObj));
319
+ }
320
+ }
321
+
322
+ // Query MetaSound assets.
323
+ if (bFilterMetaSound)
324
+ {
325
+ TArray<FAssetData> AssetList;
326
+ FTopLevelAssetPath ClassPath(TEXT("/Script/MetasoundEngine"), TEXT("MetaSoundSource"));
327
+ Registry.GetAssetsByClass(ClassPath, AssetList, /*bSearchSubClasses=*/true);
328
+
329
+ for (const FAssetData& Asset : AssetList)
330
+ {
331
+ TSharedPtr<FJsonObject> AssetObj = MakeShared<FJsonObject>();
332
+ AssetObj->SetStringField(TEXT("asset_path"), Asset.GetObjectPathString());
333
+ AssetObj->SetStringField(TEXT("asset_name"), Asset.AssetName.ToString());
334
+ AssetObj->SetStringField(TEXT("asset_type"), TEXT("MetaSound"));
335
+ AssetsArray.Add(MakeShared<FJsonValueObject>(AssetObj));
336
+ }
337
+ }
338
+
339
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
340
+ Data->SetArrayField(TEXT("assets"), AssetsArray);
341
+ Data->SetNumberField(TEXT("count"), static_cast<double>(AssetsArray.Num()));
342
+ if (!TypeFilter.IsEmpty())
343
+ {
344
+ Data->SetStringField(TEXT("type_filter"), TypeFilter);
345
+ }
346
+
347
+ SendResponse(BuildAudioSuccessResponse(CorrId, Data) + TEXT("\n"));
348
+ });
349
+
350
+ // -----------------------------------------------------------------------
351
+ // audio.metasound (AUD-02)
352
+ // Inspects a MetaSound patch graph: nodes, edges, inputs, outputs.
353
+ // Required payload field: asset_path (must start with /Game/ or /Engine/).
354
+ // Returns: asset_path, nodes[], edges[], inputs[], outputs[].
355
+ // Threat T-23-01: asset_path validated before StaticLoadObject.
356
+ // -----------------------------------------------------------------------
357
+ Router.RegisterHandler(TEXT("audio.metasound"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
358
+ {
359
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
360
+
361
+ // Extract payload.
362
+ TSharedPtr<FJsonObject> Payload;
363
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
364
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
365
+ {
366
+ Payload = (*PayloadVal)->AsObject();
367
+ }
368
+
369
+ FString AssetPath;
370
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
371
+ {
372
+ SendResponse(BuildAudioErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
373
+ return;
374
+ }
375
+
376
+ // Validate path prefix (T-23-01).
377
+ if (!IsValidAssetPath(AssetPath))
378
+ {
379
+ SendResponse(BuildAudioErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
380
+ return;
381
+ }
382
+
383
+ // Load the MetaSoundSource asset.
384
+ UMetaSoundSource* MetaSound = Cast<UMetaSoundSource>(
385
+ StaticLoadObject(UMetaSoundSource::StaticClass(), nullptr, *AssetPath));
386
+ if (!MetaSound)
387
+ {
388
+ SendResponse(BuildAudioErrorResponse(CorrId, TEXT("metasound_not_found")) + TEXT("\n"));
389
+ return;
390
+ }
391
+
392
+ // Access the MetaSound document via IMetaSoundDocumentInterface.
393
+ // UMetaSoundSource directly inherits IMetaSoundDocumentInterface, providing GetConstDocument().
394
+ const IMetaSoundDocumentInterface* DocInterface = Cast<IMetaSoundDocumentInterface>(MetaSound);
395
+ if (!DocInterface)
396
+ {
397
+ SendResponse(BuildAudioErrorResponse(CorrId, TEXT("metasound_document_interface_unavailable")) + TEXT("\n"));
398
+ return;
399
+ }
400
+
401
+ const FMetasoundFrontendDocument& Document = DocInterface->GetConstDocument();
402
+ const FMetasoundFrontendGraphClass& RootGraph = Document.RootGraph;
403
+
404
+ // Build nodes and edges arrays by iterating paged graphs (UE 5.7 API).
405
+ // RootGraph.Graph is deprecated since 5.5; use GetConstGraphPages() instead.
406
+ TArray<TSharedPtr<FJsonValue>> NodesArray;
407
+ TArray<TSharedPtr<FJsonValue>> EdgesArray;
408
+
409
+ for (const FMetasoundFrontendGraph& GraphPage : RootGraph.GetConstGraphPages())
410
+ {
411
+ for (const FMetasoundFrontendNode& Node : GraphPage.Nodes)
412
+ {
413
+ TSharedPtr<FJsonObject> NodeObj = MakeShared<FJsonObject>();
414
+ NodeObj->SetStringField(TEXT("node_id"), Node.GetID().ToString());
415
+ NodeObj->SetStringField(TEXT("node_class_name"), Node.ClassID.ToString());
416
+ NodeObj->SetStringField(TEXT("node_name"), Node.Name.ToString());
417
+ NodesArray.Add(MakeShared<FJsonValueObject>(NodeObj));
418
+ }
419
+
420
+ for (const FMetasoundFrontendEdge& Edge : GraphPage.Edges)
421
+ {
422
+ TSharedPtr<FJsonObject> EdgeObj = MakeShared<FJsonObject>();
423
+ EdgeObj->SetStringField(TEXT("from_node"), Edge.FromNodeID.ToString());
424
+ EdgeObj->SetStringField(TEXT("from_output"), Edge.FromVertexID.ToString());
425
+ EdgeObj->SetStringField(TEXT("to_node"), Edge.ToNodeID.ToString());
426
+ EdgeObj->SetStringField(TEXT("to_input"), Edge.ToVertexID.ToString());
427
+ EdgesArray.Add(MakeShared<FJsonValueObject>(EdgeObj));
428
+ }
429
+ }
430
+
431
+ // Build inputs array using GetDefaultInterface() (replaces deprecated Interface property in UE 5.6+).
432
+ TArray<TSharedPtr<FJsonValue>> InputsArray;
433
+ for (const FMetasoundFrontendClassInput& Input : RootGraph.GetDefaultInterface().Inputs)
434
+ {
435
+ TSharedPtr<FJsonObject> InObj = MakeShared<FJsonObject>();
436
+ InObj->SetStringField(TEXT("name"), Input.Name.ToString());
437
+ InObj->SetStringField(TEXT("type_name"), Input.TypeName.ToString());
438
+ InputsArray.Add(MakeShared<FJsonValueObject>(InObj));
439
+ }
440
+
441
+ // Build outputs array.
442
+ TArray<TSharedPtr<FJsonValue>> OutputsArray;
443
+ for (const FMetasoundFrontendClassOutput& Output : RootGraph.GetDefaultInterface().Outputs)
444
+ {
445
+ TSharedPtr<FJsonObject> OutObj = MakeShared<FJsonObject>();
446
+ OutObj->SetStringField(TEXT("name"), Output.Name.ToString());
447
+ OutObj->SetStringField(TEXT("type_name"), Output.TypeName.ToString());
448
+ OutputsArray.Add(MakeShared<FJsonValueObject>(OutObj));
449
+ }
450
+
451
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
452
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
453
+ Data->SetArrayField(TEXT("nodes"), NodesArray);
454
+ Data->SetArrayField(TEXT("edges"), EdgesArray);
455
+ Data->SetArrayField(TEXT("inputs"), InputsArray);
456
+ Data->SetArrayField(TEXT("outputs"), OutputsArray);
457
+
458
+ SendResponse(BuildAudioSuccessResponse(CorrId, Data) + TEXT("\n"));
459
+ });
460
+
461
+ // -----------------------------------------------------------------------
462
+ // audio.soundcue (AUD-03)
463
+ // Reads a SoundCue node graph starting from FirstNode and recursively builds
464
+ // a tree of node objects. Includes type-specific fields for WavePlayer,
465
+ // Modulator, and Attenuation nodes. Also returns cue-level attenuation if set.
466
+ //
467
+ // Required payload field: asset_path (must start with /Game/ or /Engine/).
468
+ // Returns: asset_path, first_node (recursive tree), cue_attenuation (optional).
469
+ //
470
+ // Threat T-23-01: asset_path validated before StaticLoadObject.
471
+ // Threat T-23-02: node tree recursion capped at 50 levels.
472
+ // -----------------------------------------------------------------------
473
+ Router.RegisterHandler(TEXT("audio.soundcue"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
474
+ {
475
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
476
+
477
+ // Extract payload.
478
+ TSharedPtr<FJsonObject> Payload;
479
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
480
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
481
+ {
482
+ Payload = (*PayloadVal)->AsObject();
483
+ }
484
+
485
+ FString AssetPath;
486
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
487
+ {
488
+ SendResponse(BuildAudioErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
489
+ return;
490
+ }
491
+
492
+ // Validate path prefix (T-23-01).
493
+ if (!IsValidAssetPath(AssetPath))
494
+ {
495
+ SendResponse(BuildAudioErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
496
+ return;
497
+ }
498
+
499
+ // Load the SoundCue asset.
500
+ USoundCue* SoundCue = Cast<USoundCue>(
501
+ StaticLoadObject(USoundCue::StaticClass(), nullptr, *AssetPath));
502
+ if (!SoundCue)
503
+ {
504
+ SendResponse(BuildAudioErrorResponse(CorrId, TEXT("sound_cue_not_found")) + TEXT("\n"));
505
+ return;
506
+ }
507
+
508
+ // Build the node tree starting from FirstNode (T-23-02: cap at 50 levels).
509
+ constexpr int32 MaxDepth = 50;
510
+ bool bDepthExceeded = false;
511
+ TSharedPtr<FJsonObject> FirstNodeObj = BuildSoundNodeTree(SoundCue->FirstNode, 0, MaxDepth, bDepthExceeded);
512
+
513
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
514
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
515
+
516
+ if (FirstNodeObj.IsValid())
517
+ {
518
+ Data->SetObjectField(TEXT("first_node"), FirstNodeObj);
519
+ }
520
+
521
+ if (bDepthExceeded)
522
+ {
523
+ Data->SetBoolField(TEXT("depth_truncated"), true);
524
+ Data->SetStringField(TEXT("depth_truncated_message"),
525
+ TEXT("Node tree truncated at 50 levels to prevent runaway on malformed assets (T-23-02)."));
526
+ }
527
+
528
+ // Check for cue-level attenuation settings via GetAttenuationSettingsToApply()
529
+ // (returns nullptr if no attenuation override is set).
530
+ const FSoundAttenuationSettings* CueAttenuation = SoundCue->GetAttenuationSettingsToApply();
531
+ if (CueAttenuation)
532
+ {
533
+ TSharedPtr<FJsonObject> CueAttObj = BuildAttenuationObject(*CueAttenuation);
534
+ Data->SetObjectField(TEXT("cue_attenuation"), CueAttObj);
535
+ }
536
+
537
+ SendResponse(BuildAudioSuccessResponse(CorrId, Data) + TEXT("\n"));
538
+ });
539
+
540
+ // -----------------------------------------------------------------------
541
+ // audio.insights (AUD-04)
542
+ // Queries Audio Insights monitoring data. Gracefully handles the case where
543
+ // the AudioInsights plugin is not enabled.
544
+ //
545
+ // Payload: none required.
546
+ // Returns: available (bool), message (if unavailable), active_sound_count (int),
547
+ // max_channels (int), recent_events[] (empty array -- detailed event history
548
+ // requires direct Audio Insights dashboard access).
549
+ //
550
+ // Threat T-23-04: editor-only monitoring data; accepted risk (not sensitive).
551
+ // -----------------------------------------------------------------------
552
+ Router.RegisterHandler(TEXT("audio.insights"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
553
+ {
554
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
555
+
556
+ // Check if AudioInsights module is loaded at runtime (no hard Build.cs dependency).
557
+ const bool bAudioInsightsLoaded = FModuleManager::Get().IsModuleLoaded(TEXT("AudioInsights"));
558
+
559
+ if (!bAudioInsightsLoaded)
560
+ {
561
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
562
+ Data->SetBoolField(TEXT("available"), false);
563
+ Data->SetStringField(TEXT("message"),
564
+ TEXT("AudioInsights plugin is not enabled. Enable it in Plugins > Audio > Audio Insights to use this command."));
565
+
566
+ SendResponse(BuildAudioSuccessResponse(CorrId, Data) + TEXT("\n"));
567
+ return;
568
+ }
569
+
570
+ // AudioInsights is available. Get the editor world audio device.
571
+ if (!GEditor)
572
+ {
573
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
574
+ Data->SetBoolField(TEXT("available"), true);
575
+ Data->SetStringField(TEXT("message"), TEXT("GEditor not available."));
576
+ Data->SetNumberField(TEXT("active_sound_count"), 0.0);
577
+ Data->SetNumberField(TEXT("max_channels"), 0.0);
578
+ Data->SetArrayField(TEXT("recent_events"), TArray<TSharedPtr<FJsonValue>>());
579
+
580
+ SendResponse(BuildAudioSuccessResponse(CorrId, Data) + TEXT("\n"));
581
+ return;
582
+ }
583
+
584
+ UWorld* World = GEditor->GetEditorWorldContext().World();
585
+ if (!World)
586
+ {
587
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
588
+ Data->SetBoolField(TEXT("available"), true);
589
+ Data->SetStringField(TEXT("message"), TEXT("No world open in editor."));
590
+ Data->SetNumberField(TEXT("active_sound_count"), 0.0);
591
+ Data->SetNumberField(TEXT("max_channels"), 0.0);
592
+ Data->SetArrayField(TEXT("recent_events"), TArray<TSharedPtr<FJsonValue>>());
593
+
594
+ SendResponse(BuildAudioSuccessResponse(CorrId, Data) + TEXT("\n"));
595
+ return;
596
+ }
597
+
598
+ FAudioDeviceHandle AudioDeviceHandle = World->GetAudioDevice();
599
+ FAudioDevice* AudioDevice = AudioDeviceHandle.GetAudioDevice();
600
+
601
+ int32 ActiveSoundCount = 0;
602
+ int32 MaxChannels = 0;
603
+
604
+ if (AudioDevice)
605
+ {
606
+ ActiveSoundCount = AudioDevice->GetNumActiveSources();
607
+ MaxChannels = AudioDevice->GetMaxChannels();
608
+ }
609
+
610
+ // recent_events: detailed event history requires direct Audio Insights dashboard access.
611
+ // Return empty array with informational note.
612
+ TArray<TSharedPtr<FJsonValue>> RecentEvents;
613
+
614
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
615
+ Data->SetBoolField(TEXT("available"), true);
616
+ Data->SetNumberField(TEXT("active_sound_count"), static_cast<double>(ActiveSoundCount));
617
+ Data->SetNumberField(TEXT("max_channels"), static_cast<double>(MaxChannels));
618
+ Data->SetArrayField(TEXT("recent_events"), RecentEvents);
619
+ Data->SetStringField(TEXT("recent_events_note"),
620
+ TEXT("Detailed event history requires direct Audio Insights dashboard access in the editor."));
621
+
622
+ SendResponse(BuildAudioSuccessResponse(CorrId, Data) + TEXT("\n"));
623
+ });
624
+ }
@@ -0,0 +1,22 @@
1
+ // MCPAudioCommands.h (Plan 23-01)
2
+ // Declares the registration function for all audio system inspection MCP command handlers.
3
+ // Handlers: audio.list, audio.metasound, audio.soundcue, audio.insights
4
+ //
5
+ // Call RegisterAudioCommands(*Router) in MCPBridgeSubsystem::Initialize()
6
+ // BEFORE the TCP server starts accepting connections.
7
+
8
+ #pragma once
9
+
10
+ #include "MCPCommandRouter.h"
11
+
12
+ /**
13
+ * Register all four audio command handlers into the given router.
14
+ * Must be called on the game thread before connections arrive.
15
+ *
16
+ * Registered commands:
17
+ * audio.list -- list sound assets filtered by type (AUD-01)
18
+ * audio.metasound -- inspect MetaSound patch graph nodes, inputs, outputs (AUD-02)
19
+ * audio.soundcue -- read SoundCue node graph and attenuation settings (AUD-03)
20
+ * audio.insights -- query Audio Insights monitoring data with graceful fallback (AUD-04)
21
+ */
22
+ void RegisterAudioCommands(FMCPCommandRouter& Router);