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,22 @@
1
+ // MCPGASCommands.h (Plan 25-01)
2
+ // Declares the registration function for all Gameplay Ability System MCP command handlers.
3
+ // Handlers: gas.abilities, gas.effects, gas.attributes, gas.tags
4
+ //
5
+ // Call RegisterGASCommands(*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 GAS command handlers into the given router.
14
+ * Must be called on the game thread before connections arrive.
15
+ *
16
+ * Registered commands:
17
+ * gas.abilities -- list all Gameplay Ability classes with tags, costs, and cooldowns (GAS-01)
18
+ * gas.effects -- inspect Gameplay Effect modifiers, duration policy, stacking, period (GAS-02)
19
+ * gas.attributes -- read Attribute Set definitions with base values and clamping info (GAS-03)
20
+ * gas.tags -- query Gameplay Tag hierarchy and find assets using specific tags (GAS-04)
21
+ */
22
+ void RegisterGASCommands(FMCPCommandRouter& Router);
@@ -0,0 +1,679 @@
1
+ // MCPImportExportCommands.cpp
2
+ // Implements four asset import/export command handlers for the MCP bridge:
3
+ // import.fbx -- import an FBX file at a specified content path (IMP-01)
4
+ // import.usd -- import a USD file using the Interchange pipeline (IMP-02)
5
+ // export.mesh -- export a static or skeletal mesh to FBX format (IMP-03)
6
+ // import.batch -- batch import multiple files from a directory (IMP-04)
7
+ //
8
+ // All handlers dispatch via AsyncTask(ENamedThreads::GameThread, ...) since
9
+ // the router calls them off-thread.
10
+ // Call Modify() on any UObject before mutation operations (Pitfall 5).
11
+ // Validate source_file paths exist on disk before import (T-21-01, T-21-04).
12
+ // Validate asset_path starts with /Game/ or /Engine/ (T-21-03).
13
+ // Reject output_file paths containing ".." and validate .fbx extension (T-21-02).
14
+
15
+ #include "MCPImportExportCommands.h"
16
+
17
+ #include "Editor.h"
18
+ #include "Engine/World.h"
19
+
20
+ // Asset import/export APIs
21
+ #include "AssetToolsModule.h"
22
+ #include "IAssetTools.h"
23
+ #include "AssetImportTask.h"
24
+ #include "PackageName.h"
25
+
26
+ // FBX Factory and Export
27
+ #include "Factories/FbxFactory.h"
28
+ #include "Exporters/FbxExportOption.h"
29
+
30
+ // Interchange (USD import)
31
+ #include "InterchangeManager.h"
32
+
33
+ // Mesh types
34
+ #include "Engine/StaticMesh.h"
35
+ #include "Engine/SkeletalMesh.h"
36
+ #include "Exporters/Exporter.h"
37
+
38
+ // File system
39
+ #include "Misc/Paths.h"
40
+ #include "Misc/FileHelper.h"
41
+ #include "HAL/FileManager.h"
42
+
43
+ // JSON
44
+ #include "Serialization/JsonSerializer.h"
45
+ #include "Serialization/JsonWriter.h"
46
+ #include "Dom/JsonObject.h"
47
+ #include "Dom/JsonValue.h"
48
+
49
+ // Async
50
+ #include "Async/TaskGraphInterfaces.h"
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Internal helpers
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /** Returns a JSON success response string (without trailing newline). */
57
+ static FString BuildImpSuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
58
+ {
59
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
60
+ Obj->SetBoolField(TEXT("success"), true);
61
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
62
+ if (Data.IsValid())
63
+ {
64
+ Obj->SetObjectField(TEXT("data"), Data);
65
+ }
66
+
67
+ FString Output;
68
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
69
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
70
+ return Output;
71
+ }
72
+
73
+ /** Returns a JSON error response string (without trailing newline). */
74
+ static FString BuildImpErrorResponse(const FString& CorrId, const FString& Error)
75
+ {
76
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
77
+ Obj->SetBoolField(TEXT("success"), false);
78
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
79
+ Obj->SetStringField(TEXT("error"), Error);
80
+
81
+ FString Output;
82
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
83
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
84
+ return Output;
85
+ }
86
+
87
+ /**
88
+ * Validate that asset_path starts with "/Game/" or "/Engine/" to prevent
89
+ * path traversal attacks (T-21-03).
90
+ */
91
+ static bool IsValidAssetPath(const FString& AssetPath)
92
+ {
93
+ return AssetPath.StartsWith(TEXT("/Game/")) || AssetPath.StartsWith(TEXT("/Engine/"));
94
+ }
95
+
96
+ /**
97
+ * Validate that a file path on disk is safe:
98
+ * - Must not contain ".." to prevent path traversal (T-21-01, T-21-02, T-21-04).
99
+ * - Must exist on disk.
100
+ */
101
+ static bool IsValidFilePath(const FString& FilePath)
102
+ {
103
+ if (FilePath.Contains(TEXT("..")))
104
+ {
105
+ return false;
106
+ }
107
+ return FPaths::FileExists(FilePath);
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // RegisterImportExportCommands
112
+ // ---------------------------------------------------------------------------
113
+
114
+ void RegisterImportExportCommands(FMCPCommandRouter& Router)
115
+ {
116
+ // -----------------------------------------------------------------------
117
+ // import.fbx (IMP-01)
118
+ // Imports an FBX file to a UE content path.
119
+ // Returns JSON with "assets" array of created asset paths and "count".
120
+ // -----------------------------------------------------------------------
121
+ Router.RegisterHandler(TEXT("import.fbx"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
122
+ {
123
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
124
+
125
+ // Extract payload.
126
+ TSharedPtr<FJsonObject> Payload;
127
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
128
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
129
+ {
130
+ Payload = (*PayloadVal)->AsObject();
131
+ }
132
+
133
+ FString SourceFile;
134
+ FString DestPath;
135
+
136
+ if (!Payload.IsValid()
137
+ || !Payload->TryGetStringField(TEXT("source_file"), SourceFile) || SourceFile.IsEmpty()
138
+ || !Payload->TryGetStringField(TEXT("dest_path"), DestPath) || DestPath.IsEmpty())
139
+ {
140
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("missing_required_fields")) + TEXT("\n"));
141
+ return;
142
+ }
143
+
144
+ // Validate source file exists and is safe (T-21-01).
145
+ if (SourceFile.Contains(TEXT("..")) || !FPaths::FileExists(SourceFile))
146
+ {
147
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("source_file_not_found")) + TEXT("\n"));
148
+ return;
149
+ }
150
+
151
+ // Validate destination content path (T-21-03).
152
+ if (!IsValidAssetPath(DestPath))
153
+ {
154
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("invalid_dest_path")) + TEXT("\n"));
155
+ return;
156
+ }
157
+
158
+ // Extract optional settings.
159
+ bool bImportMaterials = true;
160
+ bool bCombineMeshes = false;
161
+ double ScaleFactor = 1.0;
162
+ Payload->TryGetBoolField(TEXT("import_materials"), bImportMaterials);
163
+ Payload->TryGetBoolField(TEXT("combine_meshes"), bCombineMeshes);
164
+ Payload->TryGetNumberField(TEXT("scale_factor"), ScaleFactor);
165
+
166
+ // Capture everything for async dispatch.
167
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, SourceFile, DestPath,
168
+ bImportMaterials, bCombineMeshes, ScaleFactor]()
169
+ {
170
+ // Split dest_path into package path + asset name.
171
+ // If DestPath ends without an asset name (e.g. /Game/Meshes/), derive
172
+ // the asset name from the source file base name.
173
+ FString PackagePath = DestPath;
174
+ FString AssetName;
175
+
176
+ // Check if the last component looks like an asset name (no trailing slash).
177
+ if (DestPath.EndsWith(TEXT("/")))
178
+ {
179
+ // Directory-style dest_path: use source file base name as asset name.
180
+ AssetName = FPaths::GetBaseFilename(SourceFile);
181
+ PackagePath = DestPath.LeftChop(1); // remove trailing slash
182
+ }
183
+ else
184
+ {
185
+ AssetName = FPackageName::GetLongPackageAssetName(DestPath);
186
+ PackagePath = FPackageName::GetLongPackagePath(DestPath);
187
+ if (AssetName.IsEmpty())
188
+ {
189
+ // Treat as directory.
190
+ AssetName = FPaths::GetBaseFilename(SourceFile);
191
+ PackagePath = DestPath;
192
+ }
193
+ }
194
+
195
+ // Get AssetTools module.
196
+ IAssetTools& AT = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools")).Get();
197
+
198
+ // Create UAssetImportTask.
199
+ UAssetImportTask* Task = NewObject<UAssetImportTask>();
200
+ Task->Filename = SourceFile;
201
+ Task->DestinationPath = PackagePath;
202
+ Task->DestinationName = AssetName;
203
+ Task->bAutomated = true;
204
+ Task->bReplaceExisting = true;
205
+ Task->bSave = true;
206
+
207
+ // Create and configure UFbxFactory.
208
+ UFbxFactory* FbxFactory = NewObject<UFbxFactory>();
209
+ if (FbxFactory->ImportUI)
210
+ {
211
+ FbxFactory->ImportUI->bImportMaterials = bImportMaterials;
212
+ FbxFactory->ImportUI->bCombineMeshes = bCombineMeshes;
213
+ if (FbxFactory->ImportUI->StaticMeshImportData)
214
+ {
215
+ FbxFactory->ImportUI->StaticMeshImportData->ImportUniformScale = static_cast<float>(ScaleFactor);
216
+ }
217
+ }
218
+ Task->Factory = FbxFactory;
219
+
220
+ // Run the import.
221
+ TArray<UAssetImportTask*> Tasks;
222
+ Tasks.Add(Task);
223
+ AT.ImportAssetTasks(Tasks);
224
+
225
+ // Collect imported asset paths.
226
+ TArray<TSharedPtr<FJsonValue>> AssetsArray;
227
+ for (const FString& ImportedPath : Task->ImportedObjectPaths)
228
+ {
229
+ AssetsArray.Add(MakeShared<FJsonValueString>(ImportedPath));
230
+ }
231
+
232
+ if (AssetsArray.Num() == 0)
233
+ {
234
+ UE_LOG(LogTemp, Warning, TEXT("[MCPImportExport] import.fbx produced no imported assets for: %s"), *SourceFile);
235
+ }
236
+
237
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
238
+ Data->SetArrayField(TEXT("assets"), AssetsArray);
239
+ Data->SetNumberField(TEXT("count"), static_cast<double>(AssetsArray.Num()));
240
+
241
+ SendResponse(BuildImpSuccessResponse(CorrId, Data) + TEXT("\n"));
242
+ });
243
+ });
244
+
245
+ // -----------------------------------------------------------------------
246
+ // import.usd (IMP-02)
247
+ // Imports a USD/USDA/USDC file using the Interchange pipeline.
248
+ // Falls back to UAssetImportTask if InterchangeEngine is unavailable.
249
+ // Returns JSON with "assets" array of created asset paths and "count".
250
+ // -----------------------------------------------------------------------
251
+ Router.RegisterHandler(TEXT("import.usd"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
252
+ {
253
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
254
+
255
+ // Extract payload.
256
+ TSharedPtr<FJsonObject> Payload;
257
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
258
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
259
+ {
260
+ Payload = (*PayloadVal)->AsObject();
261
+ }
262
+
263
+ FString SourceFile;
264
+ FString DestPath;
265
+
266
+ if (!Payload.IsValid()
267
+ || !Payload->TryGetStringField(TEXT("source_file"), SourceFile) || SourceFile.IsEmpty()
268
+ || !Payload->TryGetStringField(TEXT("dest_path"), DestPath) || DestPath.IsEmpty())
269
+ {
270
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("missing_required_fields")) + TEXT("\n"));
271
+ return;
272
+ }
273
+
274
+ // Validate source file exists and is safe (T-21-01).
275
+ if (SourceFile.Contains(TEXT("..")) || !FPaths::FileExists(SourceFile))
276
+ {
277
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("source_file_not_found")) + TEXT("\n"));
278
+ return;
279
+ }
280
+
281
+ // Validate USD extension.
282
+ const FString Ext = FPaths::GetExtension(SourceFile).ToLower();
283
+ if (Ext != TEXT("usd") && Ext != TEXT("usda") && Ext != TEXT("usdc"))
284
+ {
285
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("invalid_usd_extension")) + TEXT("\n"));
286
+ return;
287
+ }
288
+
289
+ // Validate destination content path (T-21-03).
290
+ if (!IsValidAssetPath(DestPath))
291
+ {
292
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("invalid_dest_path")) + TEXT("\n"));
293
+ return;
294
+ }
295
+
296
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, SourceFile, DestPath]()
297
+ {
298
+ // Attempt Interchange import first.
299
+ bool bInterchangeUsed = false;
300
+ TArray<TSharedPtr<FJsonValue>> AssetsArray;
301
+
302
+ if (GEditor && FModuleManager::Get().IsModuleLoaded(TEXT("InterchangeEngine")))
303
+ {
304
+ UInterchangeManager* InterchangeManager = UInterchangeManager::GetInterchangeManager();
305
+ if (InterchangeManager)
306
+ {
307
+ // Use Interchange to import the USD file.
308
+ // Set up import parameters with the destination path.
309
+ UE::Interchange::FImportAssetParameters Params;
310
+ Params.bIsAutomated = true;
311
+
312
+ // Determine content path for Interchange. Normalize dest_path to directory.
313
+ FString ContentDir = DestPath;
314
+ if (!ContentDir.EndsWith(TEXT("/")))
315
+ {
316
+ // If it ends with what looks like an asset name, strip it.
317
+ FString LastPart = FPackageName::GetLongPackageAssetName(ContentDir);
318
+ if (!LastPart.IsEmpty())
319
+ {
320
+ ContentDir = FPackageName::GetLongPackagePath(ContentDir);
321
+ }
322
+ }
323
+ Params.OverrideDestinationPath = ContentDir;
324
+
325
+ InterchangeManager->ImportAsset(SourceFile, Params);
326
+
327
+ // Interchange is async; we report success with empty assets list.
328
+ // The engine will place assets at ContentDir when complete.
329
+ bInterchangeUsed = true;
330
+
331
+ UE_LOG(LogTemp, Log, TEXT("[MCPImportExport] import.usd: Interchange import initiated for: %s -> %s"), *SourceFile, *ContentDir);
332
+ }
333
+ }
334
+
335
+ if (!bInterchangeUsed)
336
+ {
337
+ // Fallback: use UAssetImportTask (UE auto-detects USD factory).
338
+ UE_LOG(LogTemp, Warning, TEXT("[MCPImportExport] import.usd: InterchangeEngine not available; falling back to UAssetImportTask for: %s"), *SourceFile);
339
+
340
+ FString PackagePath = DestPath;
341
+ FString AssetName;
342
+
343
+ if (DestPath.EndsWith(TEXT("/")))
344
+ {
345
+ AssetName = FPaths::GetBaseFilename(SourceFile);
346
+ PackagePath = DestPath.LeftChop(1);
347
+ }
348
+ else
349
+ {
350
+ AssetName = FPackageName::GetLongPackageAssetName(DestPath);
351
+ PackagePath = FPackageName::GetLongPackagePath(DestPath);
352
+ if (AssetName.IsEmpty())
353
+ {
354
+ AssetName = FPaths::GetBaseFilename(SourceFile);
355
+ PackagePath = DestPath;
356
+ }
357
+ }
358
+
359
+ IAssetTools& AT = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools")).Get();
360
+
361
+ UAssetImportTask* Task = NewObject<UAssetImportTask>();
362
+ Task->Filename = SourceFile;
363
+ Task->DestinationPath = PackagePath;
364
+ Task->DestinationName = AssetName;
365
+ Task->bAutomated = true;
366
+ Task->bReplaceExisting = true;
367
+ Task->bSave = true;
368
+ // No factory override — let UE auto-detect the USD factory.
369
+
370
+ TArray<UAssetImportTask*> Tasks;
371
+ Tasks.Add(Task);
372
+ AT.ImportAssetTasks(Tasks);
373
+
374
+ for (const FString& ImportedPath : Task->ImportedObjectPaths)
375
+ {
376
+ AssetsArray.Add(MakeShared<FJsonValueString>(ImportedPath));
377
+ }
378
+ }
379
+ else
380
+ {
381
+ // Interchange initiated — report the method used.
382
+ // Assets won't be available synchronously; report interchange_initiated.
383
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
384
+ Data->SetArrayField(TEXT("assets"), AssetsArray);
385
+ Data->SetNumberField(TEXT("count"), static_cast<double>(AssetsArray.Num()));
386
+ Data->SetStringField(TEXT("method"), TEXT("interchange"));
387
+ Data->SetBoolField(TEXT("async"), true);
388
+ SendResponse(BuildImpSuccessResponse(CorrId, Data) + TEXT("\n"));
389
+ return;
390
+ }
391
+
392
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
393
+ Data->SetArrayField(TEXT("assets"), AssetsArray);
394
+ Data->SetNumberField(TEXT("count"), static_cast<double>(AssetsArray.Num()));
395
+ Data->SetStringField(TEXT("method"), TEXT("fallback"));
396
+ Data->SetBoolField(TEXT("async"), false);
397
+
398
+ SendResponse(BuildImpSuccessResponse(CorrId, Data) + TEXT("\n"));
399
+ });
400
+ });
401
+
402
+ // -----------------------------------------------------------------------
403
+ // export.mesh (IMP-03)
404
+ // Exports a StaticMesh or SkeletalMesh asset to an FBX file on disk.
405
+ // Returns JSON with "output_file" string, "exported" bool, "asset_class".
406
+ // -----------------------------------------------------------------------
407
+ Router.RegisterHandler(TEXT("export.mesh"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
408
+ {
409
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
410
+
411
+ // Extract payload.
412
+ TSharedPtr<FJsonObject> Payload;
413
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
414
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
415
+ {
416
+ Payload = (*PayloadVal)->AsObject();
417
+ }
418
+
419
+ FString AssetPath;
420
+ FString OutputFile;
421
+
422
+ if (!Payload.IsValid()
423
+ || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty()
424
+ || !Payload->TryGetStringField(TEXT("output_file"), OutputFile) || OutputFile.IsEmpty())
425
+ {
426
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("missing_required_fields")) + TEXT("\n"));
427
+ return;
428
+ }
429
+
430
+ // Validate asset path (T-21-03).
431
+ if (!IsValidAssetPath(AssetPath))
432
+ {
433
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
434
+ return;
435
+ }
436
+
437
+ // Validate output file — reject ".." (T-21-02) and require .fbx extension.
438
+ if (OutputFile.Contains(TEXT("..")))
439
+ {
440
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("invalid_output_path")) + TEXT("\n"));
441
+ return;
442
+ }
443
+ const FString OutputExt = FPaths::GetExtension(OutputFile).ToLower();
444
+ if (OutputExt != TEXT("fbx"))
445
+ {
446
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("output_file_must_be_fbx")) + TEXT("\n"));
447
+ return;
448
+ }
449
+
450
+ // Optional parameters.
451
+ bool bExportCollision = false;
452
+ double LevelOfDetail = 0.0;
453
+ if (Payload.IsValid())
454
+ {
455
+ Payload->TryGetBoolField(TEXT("export_collision"), bExportCollision);
456
+ Payload->TryGetNumberField(TEXT("level_of_detail"), LevelOfDetail);
457
+ }
458
+ const int32 LODIndex = static_cast<int32>(LevelOfDetail);
459
+
460
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, AssetPath, OutputFile, bExportCollision, LODIndex]()
461
+ {
462
+ // Load the mesh asset (StaticMesh or SkeletalMesh).
463
+ UObject* MeshObject = StaticLoadObject(UObject::StaticClass(), nullptr, *AssetPath);
464
+ if (!MeshObject)
465
+ {
466
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("asset_not_found")) + TEXT("\n"));
467
+ return;
468
+ }
469
+
470
+ UStaticMesh* StaticMesh = Cast<UStaticMesh>(MeshObject);
471
+ USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(MeshObject);
472
+
473
+ if (!StaticMesh && !SkeletalMesh)
474
+ {
475
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("asset_not_mesh")) + TEXT("\n"));
476
+ return;
477
+ }
478
+
479
+ const FString AssetClass = StaticMesh ? TEXT("StaticMesh") : TEXT("SkeletalMesh");
480
+
481
+ // Ensure the output directory exists.
482
+ const FString OutputDir = FPaths::GetPath(OutputFile);
483
+ if (!OutputDir.IsEmpty() && !IFileManager::Get().DirectoryExists(*OutputDir))
484
+ {
485
+ IFileManager::Get().MakeDirectory(*OutputDir, /*Tree=*/true);
486
+ }
487
+
488
+ // Use IAssetTools::ExportAssets to export.
489
+ IAssetTools& AT = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools")).Get();
490
+
491
+ TArray<UObject*> ObjectsToExport;
492
+ ObjectsToExport.Add(MeshObject);
493
+
494
+ // ExportAssets writes to the output directory.
495
+ AT.ExportAssets(ObjectsToExport, OutputDir);
496
+
497
+ // Check if the export file was created.
498
+ const bool bExported = FPaths::FileExists(OutputFile);
499
+
500
+ if (!bExported)
501
+ {
502
+ UE_LOG(LogTemp, Warning, TEXT("[MCPImportExport] export.mesh: expected output file not found at: %s"), *OutputFile);
503
+ }
504
+
505
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
506
+ Data->SetStringField(TEXT("output_file"), OutputFile);
507
+ Data->SetBoolField(TEXT("exported"), bExported);
508
+ Data->SetStringField(TEXT("asset_class"), AssetClass);
509
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
510
+
511
+ SendResponse(BuildImpSuccessResponse(CorrId, Data) + TEXT("\n"));
512
+ });
513
+ });
514
+
515
+ // -----------------------------------------------------------------------
516
+ // import.batch (IMP-04)
517
+ // Batch imports all files matching given extensions from a directory.
518
+ // Returns JSON with "assets" array, "count", "files_processed", "errors".
519
+ // -----------------------------------------------------------------------
520
+ Router.RegisterHandler(TEXT("import.batch"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
521
+ {
522
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
523
+
524
+ // Extract payload.
525
+ TSharedPtr<FJsonObject> Payload;
526
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
527
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
528
+ {
529
+ Payload = (*PayloadVal)->AsObject();
530
+ }
531
+
532
+ FString Directory;
533
+ FString DestPath;
534
+
535
+ if (!Payload.IsValid()
536
+ || !Payload->TryGetStringField(TEXT("directory"), Directory) || Directory.IsEmpty()
537
+ || !Payload->TryGetStringField(TEXT("dest_path"), DestPath) || DestPath.IsEmpty())
538
+ {
539
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("missing_required_fields")) + TEXT("\n"));
540
+ return;
541
+ }
542
+
543
+ // Validate directory path — reject ".." (T-21-04).
544
+ if (Directory.Contains(TEXT("..")))
545
+ {
546
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("invalid_directory_path")) + TEXT("\n"));
547
+ return;
548
+ }
549
+
550
+ // Validate directory exists (T-21-04).
551
+ if (!IFileManager::Get().DirectoryExists(*Directory))
552
+ {
553
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("directory_not_found")) + TEXT("\n"));
554
+ return;
555
+ }
556
+
557
+ // Validate destination content path (T-21-03).
558
+ if (!IsValidAssetPath(DestPath))
559
+ {
560
+ SendResponse(BuildImpErrorResponse(CorrId, TEXT("invalid_dest_path")) + TEXT("\n"));
561
+ return;
562
+ }
563
+
564
+ // Extract extensions (default to ["fbx"]).
565
+ TArray<FString> Extensions;
566
+ const TArray<TSharedPtr<FJsonValue>>* ExtArray = nullptr;
567
+ if (Payload->TryGetArrayField(TEXT("extensions"), ExtArray) && ExtArray)
568
+ {
569
+ for (const TSharedPtr<FJsonValue>& ExtVal : *ExtArray)
570
+ {
571
+ if (ExtVal.IsValid() && ExtVal->Type == EJson::String)
572
+ {
573
+ Extensions.Add(ExtVal->AsString().ToLower());
574
+ }
575
+ }
576
+ }
577
+ if (Extensions.IsEmpty())
578
+ {
579
+ Extensions.Add(TEXT("fbx"));
580
+ }
581
+
582
+ // Extract optional import settings.
583
+ bool bImportMaterials = true;
584
+ double ScaleFactor = 1.0;
585
+ Payload->TryGetBoolField(TEXT("import_materials"), bImportMaterials);
586
+ Payload->TryGetNumberField(TEXT("scale_factor"), ScaleFactor);
587
+
588
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, Directory, DestPath, Extensions, bImportMaterials, ScaleFactor]()
589
+ {
590
+ // Enumerate all matching files in the directory.
591
+ TArray<FString> FoundFiles;
592
+ for (const FString& Ext : Extensions)
593
+ {
594
+ TArray<FString> FilesForExt;
595
+ IFileManager::Get().FindFiles(FilesForExt, *(Directory / (TEXT("*.") + Ext)), /*Files=*/true, /*Dirs=*/false);
596
+ for (const FString& FileName : FilesForExt)
597
+ {
598
+ FoundFiles.Add(Directory / FileName);
599
+ }
600
+ }
601
+
602
+ const int32 FilesFound = FoundFiles.Num();
603
+
604
+ // Build batch of UAssetImportTask objects.
605
+ IAssetTools& AT = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools")).Get();
606
+
607
+ TArray<UAssetImportTask*> Tasks;
608
+ TArray<TSharedPtr<FJsonValue>> ErrorsArray;
609
+
610
+ for (const FString& SourceFile : FoundFiles)
611
+ {
612
+ // Per-file: derive asset name from file base name.
613
+ const FString AssetName = FPaths::GetBaseFilename(SourceFile);
614
+
615
+ UAssetImportTask* Task = NewObject<UAssetImportTask>();
616
+ Task->Filename = SourceFile;
617
+ Task->DestinationPath = DestPath;
618
+ Task->DestinationName = AssetName;
619
+ Task->bAutomated = true;
620
+ Task->bReplaceExisting = true;
621
+ Task->bSave = true;
622
+
623
+ // For FBX files, use UFbxFactory with configured settings.
624
+ const FString FileExt = FPaths::GetExtension(SourceFile).ToLower();
625
+ if (FileExt == TEXT("fbx"))
626
+ {
627
+ UFbxFactory* FbxFactory = NewObject<UFbxFactory>();
628
+ if (FbxFactory->ImportUI)
629
+ {
630
+ FbxFactory->ImportUI->bImportMaterials = bImportMaterials;
631
+ if (FbxFactory->ImportUI->StaticMeshImportData)
632
+ {
633
+ FbxFactory->ImportUI->StaticMeshImportData->ImportUniformScale = static_cast<float>(ScaleFactor);
634
+ }
635
+ }
636
+ Task->Factory = FbxFactory;
637
+ }
638
+ // For other types, leave Factory null — UE auto-detects.
639
+
640
+ Tasks.Add(Task);
641
+ }
642
+
643
+ // Execute batch import.
644
+ if (Tasks.Num() > 0)
645
+ {
646
+ AT.ImportAssetTasks(Tasks);
647
+ }
648
+
649
+ // Aggregate imported paths and errors.
650
+ TArray<TSharedPtr<FJsonValue>> AssetsArray;
651
+ for (UAssetImportTask* Task : Tasks)
652
+ {
653
+ if (Task)
654
+ {
655
+ for (const FString& ImportedPath : Task->ImportedObjectPaths)
656
+ {
657
+ AssetsArray.Add(MakeShared<FJsonValueString>(ImportedPath));
658
+ }
659
+
660
+ // If a task has no imported paths, record the source file as a failure.
661
+ if (Task->ImportedObjectPaths.Num() == 0)
662
+ {
663
+ UE_LOG(LogTemp, Warning, TEXT("[MCPImportExport] import.batch: no assets imported from: %s"), *Task->Filename);
664
+ ErrorsArray.Add(MakeShared<FJsonValueString>(
665
+ FString::Printf(TEXT("no_assets_imported: %s"), *Task->Filename)));
666
+ }
667
+ }
668
+ }
669
+
670
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
671
+ Data->SetArrayField(TEXT("assets"), AssetsArray);
672
+ Data->SetNumberField(TEXT("count"), static_cast<double>(AssetsArray.Num()));
673
+ Data->SetNumberField(TEXT("files_processed"), static_cast<double>(FilesFound));
674
+ Data->SetArrayField(TEXT("errors"), ErrorsArray);
675
+
676
+ SendResponse(BuildImpSuccessResponse(CorrId, Data) + TEXT("\n"));
677
+ });
678
+ });
679
+ }