ultimate-unreal-engine-mcp 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-unreal-engine-mcp",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "MCP server giving AI assistants full access to Unreal Engine 5.7 projects",
5
5
  "type": "module",
6
6
  "engines": {
@@ -348,7 +348,25 @@ void RegisterAssetCommands(FMCPCommandRouter& Router)
348
348
  return;
349
349
  }
350
350
 
351
- // Create package and the object inside it.
351
+ // Check if the asset already exists (e.g. from a previous session that saved to disk).
352
+ UObject* Existing = StaticFindObject(AssetClass, nullptr, *AssetPath);
353
+ if (!Existing)
354
+ {
355
+ // Also check disk — LoadObject will find .uasset files from a prior save.
356
+ Existing = LoadObject<UObject>(nullptr, *AssetPath);
357
+ }
358
+ if (Existing)
359
+ {
360
+ // Asset already exists — return success with existing path.
361
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
362
+ Data->SetStringField(TEXT("path"), AssetPath);
363
+ Data->SetStringField(TEXT("class"), ClassName);
364
+ Data->SetBoolField(TEXT("created"), false);
365
+ Data->SetBoolField(TEXT("already_exists"), true);
366
+ SendResponse(BuildSuccessResponse(CorrId, Data));
367
+ return;
368
+ }
369
+
352
370
  UPackage* Package = CreatePackage(*AssetPath);
353
371
  if (!Package)
354
372
  {
@@ -419,21 +437,26 @@ void RegisterAssetCommands(FMCPCommandRouter& Router)
419
437
  }
420
438
  }
421
439
 
422
- // Notify asset registry and save.
440
+ // Notify asset registry.
423
441
  FAssetRegistryModule::AssetCreated(NewAsset);
424
442
  NewAsset->MarkPackageDirty();
425
443
  Package->SetDirtyFlag(true);
426
444
 
427
- // Auto-save so the asset persists.
445
+ // Save to disk ensure parent directory exists first.
446
+ bool bSaved = false;
428
447
  FString FilePath = FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension());
448
+ FString FileDir = FPaths::GetPath(FilePath);
449
+ IFileManager::Get().MakeDirectory(*FileDir, true);
450
+
429
451
  FSavePackageArgs SaveArgs;
430
452
  SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
431
- UPackage::SavePackage(Package, NewAsset, *FilePath, SaveArgs);
453
+ bSaved = UPackage::SavePackage(Package, NewAsset, *FilePath, SaveArgs);
432
454
 
433
455
  TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
434
456
  Data->SetStringField(TEXT("path"), AssetPath);
435
457
  Data->SetStringField(TEXT("class"), ClassName);
436
458
  Data->SetBoolField(TEXT("created"), true);
459
+ Data->SetBoolField(TEXT("saved_to_disk"), bSaved);
437
460
 
438
461
  SendResponse(BuildSuccessResponse(CorrId, Data));
439
462
  });
@@ -472,6 +495,22 @@ void RegisterAssetCommands(FMCPCommandRouter& Router)
472
495
  FString PackagePath, AssetName;
473
496
  AssetPath.Split(TEXT("/"), &PackagePath, &AssetName, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
474
497
 
498
+ // Check if the curve already exists.
499
+ UObject* Existing = StaticFindObject(UCurveFloat::StaticClass(), nullptr, *AssetPath);
500
+ if (!Existing)
501
+ {
502
+ Existing = LoadObject<UCurveFloat>(nullptr, *AssetPath);
503
+ }
504
+ if (Existing)
505
+ {
506
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
507
+ Data->SetStringField(TEXT("path"), AssetPath);
508
+ Data->SetBoolField(TEXT("created"), false);
509
+ Data->SetBoolField(TEXT("already_exists"), true);
510
+ SendResponse(BuildSuccessResponse(CorrId, Data));
511
+ return;
512
+ }
513
+
475
514
  UPackage* Package = CreatePackage(*AssetPath);
476
515
  if (!Package)
477
516
  {
@@ -522,10 +561,15 @@ void RegisterAssetCommands(FMCPCommandRouter& Router)
522
561
  Curve->MarkPackageDirty();
523
562
  Package->SetDirtyFlag(true);
524
563
 
564
+ // Save to disk.
565
+ bool bSaved = false;
525
566
  FString FilePath = FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension());
567
+ FString FileDir = FPaths::GetPath(FilePath);
568
+ IFileManager::Get().MakeDirectory(*FileDir, true);
569
+
526
570
  FSavePackageArgs SaveArgs;
527
571
  SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
528
- UPackage::SavePackage(Package, Curve, *FilePath, SaveArgs);
572
+ bSaved = UPackage::SavePackage(Package, Curve, *FilePath, SaveArgs);
529
573
 
530
574
  TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
531
575
  Data->SetStringField(TEXT("path"), AssetPath);
@@ -236,9 +236,15 @@ void RegisterImportExportCommands(FMCPCommandRouter& Router)
236
236
  Payload->TryGetBoolField(TEXT("combine_meshes"), bCombineMeshes);
237
237
  Payload->TryGetNumberField(TEXT("scale_factor"), ScaleFactor);
238
238
 
239
- // Capture everything for async dispatch.
240
- AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, SourceFile, DestPath,
241
- bImportMaterials, bCombineMeshes, ScaleFactor]()
239
+ // Defer the import to the next engine tick via FTSTicker.
240
+ // Reason: ImportAssetTasks triggers the Interchange pipeline which
241
+ // enqueues task-graph work. Running it inside the router's
242
+ // AsyncTask(GameThread) hits the recursion guard (Assertion
243
+ // ++Queue.RecursionGuard == 1). A ticker callback runs on the
244
+ // game thread but OUTSIDE the task-graph scope.
245
+ FTSTicker::GetCoreTicker().AddTicker(
246
+ FTickerDelegate::CreateLambda([CorrId, SendResponse, SourceFile, DestPath,
247
+ bImportMaterials, bCombineMeshes, ScaleFactor](float) -> bool
242
248
  {
243
249
  // Split dest_path into package path + asset name.
244
250
  FString PackagePath = DestPath;
@@ -307,7 +313,8 @@ void RegisterImportExportCommands(FMCPCommandRouter& Router)
307
313
  Data->SetNumberField(TEXT("count"), static_cast<double>(AssetsArray.Num()));
308
314
 
309
315
  SendResponse(BuildImpSuccessResponse(CorrId, Data) + TEXT("\n"));
310
- });
316
+ return false; // One-shot: remove ticker after execution
317
+ }), 0.0f);
311
318
  });
312
319
 
313
320
  // -----------------------------------------------------------------------
@@ -753,7 +760,9 @@ void RegisterImportExportCommands(FMCPCommandRouter& Router)
753
760
  Payload->TryGetBoolField(TEXT("import_materials"), bImportMaterials);
754
761
  Payload->TryGetNumberField(TEXT("scale_factor"), ScaleFactor);
755
762
 
756
- AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, Directory, DestPath, Extensions, bImportMaterials, ScaleFactor]()
763
+ // Defer to next tick to avoid Interchange task-graph recursion (same as import.fbx fix).
764
+ FTSTicker::GetCoreTicker().AddTicker(
765
+ FTickerDelegate::CreateLambda([CorrId, SendResponse, Directory, DestPath, Extensions, bImportMaterials, ScaleFactor](float) -> bool
757
766
  {
758
767
  // Enumerate all matching files in the directory.
759
768
  TArray<FString> FoundFiles;
@@ -843,6 +852,7 @@ void RegisterImportExportCommands(FMCPCommandRouter& Router)
843
852
  Data->SetArrayField(TEXT("errors"), ErrorsArray);
844
853
 
845
854
  SendResponse(BuildImpSuccessResponse(CorrId, Data) + TEXT("\n"));
846
- });
855
+ return false; // One-shot: remove ticker after execution
856
+ }), 0.0f);
847
857
  });
848
858
  }