More work on MCP handlers. Changed the Validation handler.

This commit is contained in:
2026-03-06 21:46:03 -05:00
parent b2e8b231fb
commit 7c66aee47a
12 changed files with 528 additions and 359 deletions

View File

@@ -59,6 +59,7 @@ UMCPAssetFinder* UMCPAssetFinder::GetUpdatedAssets()
Self->AllBlueprintAssets.Empty();
Self->AllMapAssets.Empty();
Self->AllBlueprintAndMapAssets.Empty();
Self->AllMaterialAssets.Empty();
Self->AllMaterialInstanceAssets.Empty();
Self->AllMaterialFunctionAssets.Empty();
@@ -69,6 +70,9 @@ UMCPAssetFinder* UMCPAssetFinder::GetUpdatedAssets()
AR.GetAssetsByClass(UMaterialInstanceConstant::StaticClass()->GetClassPathName(), Self->AllMaterialInstanceAssets, false);
AR.GetAssetsByClass(UMaterialFunction::StaticClass()->GetClassPathName(), Self->AllMaterialFunctionAssets, false);
Self->AllBlueprintAndMapAssets = Self->AllBlueprintAssets;
Self->AllBlueprintAndMapAssets.Append(Self->AllMapAssets);
Self->bDirty = false;
UE_LOG(LogTemp, Display, TEXT("MCPAssetFinder: Refreshed — BP %d, Map %d, Mat %d, MI %d, MF %d"),
@@ -88,6 +92,12 @@ const TArray<FAssetData>& UMCPAssetFinder::GetBlueprintAssets()
return Self ? Self->AllBlueprintAssets : EmptyAssetArray;
}
const TArray<FAssetData>& UMCPAssetFinder::GetBlueprintAndMapAssets()
{
UMCPAssetFinder* Self = GetUpdatedAssets();
return Self ? Self->AllBlueprintAndMapAssets : EmptyAssetArray;
}
const TArray<FAssetData>& UMCPAssetFinder::GetMapAssets()
{
UMCPAssetFinder* Self = GetUpdatedAssets();
@@ -144,11 +154,34 @@ namespace
}
return Found;
}
TArray<FAssetData*> SearchInList(const TArray<FAssetData>& List, const FString& Filter)
{
TArray<FAssetData*> Results;
for (const FAssetData& Asset : List)
{
FString AssetName = Asset.AssetName.ToString();
FString PackagePath = Asset.PackageName.ToString();
if (AssetName.Contains(Filter, ESearchCase::IgnoreCase) ||
PackagePath.Contains(Filter, ESearchCase::IgnoreCase))
{
Results.Add(const_cast<FAssetData*>(&Asset));
}
}
return Results;
}
}
FAssetData* UMCPAssetFinder::FindBlueprintAsset(const FString& NameOrPath, FString* OutError)
FAssetData* UMCPAssetFinder::FindBlueprintAsset(const FString& NameOrPath, FString* OutError, bool bIncludeLevelBlueprints)
{
return FindInList(GetBlueprintAssets(), NameOrPath, OutError);
const TArray<FAssetData>& List = bIncludeLevelBlueprints ? GetBlueprintAndMapAssets() : GetBlueprintAssets();
return FindInList(List, NameOrPath, OutError);
}
TArray<FAssetData*> UMCPAssetFinder::SearchBlueprintAssets(const FString& Filter, bool bIncludeLevelBlueprints)
{
const TArray<FAssetData>& List = bIncludeLevelBlueprints ? GetBlueprintAndMapAssets() : GetBlueprintAssets();
return SearchInList(List, Filter);
}
FAssetData* UMCPAssetFinder::FindMapAsset(const FString& NameOrPath, FString* OutError)
@@ -184,38 +217,37 @@ FAssetData* UMCPAssetFinder::FindAnyAsset(const FString& NameOrPath, FString* Ou
// Load helpers
// ============================================================
UBlueprint* UMCPAssetFinder::LoadBlueprintByName(const FString& NameOrPath, FString& OutError)
UBlueprint* UMCPAssetFinder::LoadBlueprintByName(const FString& NameOrPath, FString& OutError, bool bIncludeLevelBlueprints)
{
// Strategy 1: Try as a regular Blueprint asset
FAssetData* Asset = FindBlueprintAsset(NameOrPath, &OutError);
if (Asset)
FAssetData* Asset = FindBlueprintAsset(NameOrPath, &OutError, bIncludeLevelBlueprints);
if (!Asset)
{
UBlueprint* BP = Cast<UBlueprint>(Asset->GetAsset());
if (BP) return BP;
}
if (!OutError.IsEmpty()) return nullptr;
// Strategy 2: Try as a level blueprint (from a .umap)
FAssetData* MapAsset = FindMapAsset(NameOrPath, &OutError);
if (MapAsset)
{
UWorld* World = Cast<UWorld>(MapAsset->GetAsset());
if (World && World->PersistentLevel)
if (OutError.IsEmpty())
{
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(true);
if (LevelBP)
{
UE_LOG(LogTemp, Display, TEXT("MCPAssetFinder: Loaded level blueprint from map '%s'"),
*NameOrPath);
return LevelBP;
}
OutError = FString::Printf(TEXT("Blueprint '%s' not found. Use list_blueprints to see available assets."), *NameOrPath);
}
OutError = FString::Printf(TEXT("Map '%s' loaded but its level blueprint could not be retrieved. The map may not contain a level blueprint."), *NameOrPath);
return nullptr;
}
if (!OutError.IsEmpty()) return nullptr;
OutError = FString::Printf(TEXT("Blueprint or map '%s' not found. Use list_blueprints to see available assets. Level blueprints are referenced by their map name (e.g. 'MAP_Ward')."), *NameOrPath);
// Regular blueprint asset
UBlueprint* BP = Cast<UBlueprint>(Asset->GetAsset());
if (BP)
{
return BP;
}
// Map asset — extract the level blueprint
UWorld* World = Cast<UWorld>(Asset->GetAsset());
if (World && World->PersistentLevel)
{
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(true);
if (LevelBP)
{
return LevelBP;
}
}
OutError = FString::Printf(TEXT("Asset '%s' loaded but its level blueprint could not be retrieved."), *NameOrPath);
return nullptr;
}

View File

@@ -3,3 +3,4 @@
#include "MCPHandlers_Mutation.h"
#include "MCPHandlers_PinMutation.h"
#include "MCPHandlers_AssetMutation.h"
#include "MCPHandlers_Validation.h"

View File

@@ -1,318 +0,0 @@
#include "MCPAssetFinder.h"
#include "MCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
// SEH wrapper defined in BlueprintMCPServer.cpp — non-static for cross-TU access
#if PLATFORM_WINDOWS
extern int32 TryCompileBlueprintSEH(UBlueprint* BP, EBlueprintCompileOptions Opts);
#endif
// ============================================================
// Log capture device for intercepting UE_LOG output during compilation
// ============================================================
class FCompileLogCapture : public FOutputDevice
{
public:
TArray<FString> CapturedErrors;
TArray<FString> CapturedWarnings;
virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override
{
FString Msg(V);
if (Verbosity == ELogVerbosity::Error || Verbosity == ELogVerbosity::Fatal)
{
CapturedErrors.Add(Msg);
return;
}
if (Verbosity == ELogVerbosity::Warning)
{
if (!Msg.Contains(TEXT("BlueprintMCP:")))
{
CapturedWarnings.Add(Msg);
}
return;
}
static const TCHAR* ErrorPatterns[] = {
TEXT("Can't connect pins"),
TEXT("Fixed up function"),
TEXT("is not compatible with"),
TEXT("could not find a pin"),
TEXT("has an invalid"),
TEXT("orphaned pin"),
TEXT("is deprecated"),
TEXT("does not implement"),
TEXT("Missing function"),
TEXT("Unable to find"),
TEXT("Failed to resolve"),
};
for (const TCHAR* Pattern : ErrorPatterns)
{
if (Msg.Contains(Pattern))
{
CapturedWarnings.Add(Msg);
return;
}
}
}
};
// Helper: validate a single Blueprint and return structured JSON result
static TSharedRef<FJsonObject> ValidateSingleBlueprint(UBlueprint* BP, const FString& BlueprintName)
{
FCompileLogCapture LogCapture;
GLog->AddOutputDevice(&LogCapture);
EBlueprintCompileOptions CompileOpts =
EBlueprintCompileOptions::SkipSave |
EBlueprintCompileOptions::SkipGarbageCollection |
EBlueprintCompileOptions::SkipFiBSearchMetaUpdate;
bool bCompileCrashed = false;
#if PLATFORM_WINDOWS
int32 CompileResult = TryCompileBlueprintSEH(BP, CompileOpts);
if (CompileResult != 0)
{
bCompileCrashed = true;
}
#else
FKismetEditorUtilities::CompileBlueprint(BP, CompileOpts, nullptr);
#endif
GLog->RemoveOutputDevice(&LogCapture);
TArray<TSharedPtr<FJsonValue>> ErrorsArr;
TArray<TSharedPtr<FJsonValue>> WarningsArr;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (!Node) continue;
if (Node->bHasCompilerMessage)
{
TSharedRef<FJsonObject> Msg = MakeShared<FJsonObject>();
Msg->SetStringField(TEXT("graph"), Graph->GetName());
Msg->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
Msg->SetStringField(TEXT("nodeTitle"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
Msg->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
Msg->SetStringField(TEXT("message"), Node->ErrorMsg);
if (Node->ErrorType == EMessageSeverity::Error)
{
Msg->SetStringField(TEXT("severity"), TEXT("error"));
ErrorsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
else
{
Msg->SetStringField(TEXT("severity"), TEXT("warning"));
WarningsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
}
}
}
for (const FString& LogErr : LogCapture.CapturedErrors)
{
TSharedRef<FJsonObject> Msg = MakeShared<FJsonObject>();
Msg->SetStringField(TEXT("source"), TEXT("log"));
Msg->SetStringField(TEXT("message"), LogErr);
Msg->SetStringField(TEXT("severity"), TEXT("error"));
ErrorsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
for (const FString& LogWarn : LogCapture.CapturedWarnings)
{
TSharedRef<FJsonObject> Msg = MakeShared<FJsonObject>();
Msg->SetStringField(TEXT("source"), TEXT("log"));
Msg->SetStringField(TEXT("message"), LogWarn);
Msg->SetStringField(TEXT("severity"), TEXT("warning"));
WarningsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
FString StatusStr;
switch (BP->Status)
{
case BS_UpToDate: StatusStr = TEXT("UpToDate"); break;
case BS_Dirty: StatusStr = TEXT("Dirty"); break;
case BS_Error: StatusStr = TEXT("Error"); break;
case BS_Unknown: StatusStr = TEXT("Unknown"); break;
default: StatusStr = FString::Printf(TEXT("Status_%d"), (int32)BP->Status); break;
}
bool bIsValid = (BP->Status == BS_UpToDate) && ErrorsArr.Num() == 0;
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetStringField(TEXT("blueprint"), BlueprintName);
Result->SetStringField(TEXT("status"), StatusStr);
Result->SetBoolField(TEXT("isValid"), bIsValid);
Result->SetNumberField(TEXT("errorCount"), ErrorsArr.Num());
Result->SetArrayField(TEXT("errors"), ErrorsArr);
Result->SetNumberField(TEXT("warningCount"), WarningsArr.Num());
Result->SetArrayField(TEXT("warnings"), WarningsArr);
if (bCompileCrashed)
{
Result->SetStringField(TEXT("compileWarning"), TEXT("Compilation crashed (SEH caught), results may be incomplete"));
}
return Result;
}
// HandleValidateBlueprint — compile without saving, report errors + captured log messages
// ============================================================
void FBlueprintMCPServer::HandleValidateBlueprint(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
}
// Load Blueprint
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating blueprint '%s'"), *BlueprintName);
TSharedRef<FJsonObject> ValidationResult = ValidateSingleBlueprint(BP, BlueprintName);
MCPUtils::CopyJsonFields(&*ValidationResult, Result);
}
// ============================================================
// HandleValidateAllBlueprints — bulk validation
// ============================================================
void FBlueprintMCPServer::HandleValidateAllBlueprints(const FJsonObject* Json, FJsonObject* Result)
{
FString Filter = Json->GetStringField(TEXT("filter"));
bool bCountOnly = Json->GetBoolField(TEXT("countOnly"));
int32 Offset = (int32)Json->GetNumberField(TEXT("offset"));
int32 Limit = (int32)Json->GetNumberField(TEXT("limit"));
// First pass: collect matching asset indices (string comparisons only, no GetAsset())
TArray<int32> MatchingIndices;
for (int32 i = 0; i < UMCPAssetFinder::GetBlueprintAssets().Num(); i++)
{
const FAssetData& Asset = UMCPAssetFinder::GetBlueprintAssets()[i];
if (!Filter.IsEmpty())
{
FString AssetName = Asset.AssetName.ToString();
FString PackagePath = Asset.PackageName.ToString();
if (!PackagePath.Contains(Filter) && !AssetName.Contains(Filter))
{
continue;
}
}
MatchingIndices.Add(i);
}
int32 TotalMatching = MatchingIndices.Num();
// countOnly: return count without compiling anything
if (bCountOnly)
{
Result->SetNumberField(TEXT("totalMatching"), TotalMatching);
if (!Filter.IsEmpty())
{
Result->SetStringField(TEXT("filter"), Filter);
}
return;
}
// Compute range
int32 StartIdx = FMath::Clamp(Offset, 0, TotalMatching);
int32 EndIdx = (Limit > 0) ? FMath::Min(StartIdx + Limit, TotalMatching) : TotalMatching;
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Bulk validating blueprints (filter: '%s', range: %d-%d of %d matching)"),
Filter.IsEmpty() ? TEXT("*") : *Filter, StartIdx, EndIdx, TotalMatching);
TArray<TSharedPtr<FJsonValue>> FailedArr;
int32 TotalChecked = 0;
int32 TotalPassed = 0;
int32 TotalFailed = 0;
int32 TotalCrashed = 0;
for (int32 Idx = StartIdx; Idx < EndIdx; Idx++)
{
const FAssetData& Asset = UMCPAssetFinder::GetBlueprintAssets()[MatchingIndices[Idx]];
FString AssetName = Asset.AssetName.ToString();
FString PackagePath = Asset.PackageName.ToString();
// Load the Blueprint
UBlueprint* BP = Cast<UBlueprint>(Asset.GetAsset());
if (!BP)
{
continue;
}
TotalChecked++;
TSharedRef<FJsonObject> ValidationResult = ValidateSingleBlueprint(BP, AssetName);
bool bValid = ValidationResult->GetBoolField(TEXT("isValid"));
int32 Errors = (int32)ValidationResult->GetNumberField(TEXT("errorCount"));
int32 Warnings = (int32)ValidationResult->GetNumberField(TEXT("warningCount"));
if (ValidationResult->HasField(TEXT("compileWarning")))
{
TotalCrashed++;
}
if (bValid && Errors == 0)
{
TotalPassed++;
}
else
{
TotalFailed++;
// Include path for context in bulk results
ValidationResult->SetStringField(TEXT("path"), PackagePath);
FailedArr.Add(MakeShared<FJsonValueObject>(ValidationResult));
}
// Log progress every 50 blueprints
if (TotalChecked % 50 == 0)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validated %d blueprints so far (%d failed)..."),
TotalChecked, TotalFailed);
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Bulk validation complete — %d checked, %d passed, %d failed, %d crashed"),
TotalChecked, TotalPassed, TotalFailed, TotalCrashed);
Result->SetNumberField(TEXT("totalMatching"), TotalMatching);
Result->SetNumberField(TEXT("totalChecked"), TotalChecked);
Result->SetNumberField(TEXT("totalPassed"), TotalPassed);
Result->SetNumberField(TEXT("totalFailed"), TotalFailed);
if (TotalCrashed > 0)
{
Result->SetNumberField(TEXT("totalCrashed"), TotalCrashed);
}
Result->SetArrayField(TEXT("failed"), FailedArr);
if (!Filter.IsEmpty())
{
Result->SetStringField(TEXT("filter"), Filter);
}
}

View File

@@ -0,0 +1,240 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "MCPHandlers_Validation.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="compile_blueprint"))
class UMCPHandler_CompileBlueprint : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Blueprint name or package path. If specified, compile this single blueprint."))
FString Blueprint;
UPROPERTY(meta=(Optional, Description="Substring search query. If specified instead of blueprint, compile all matching blueprints."))
FString Query;
UPROPERTY(meta=(Optional, Description="If true, return the count of matching blueprints without compiling."))
bool CountOnly = false;
UPROPERTY(meta=(Optional, Description="Starting index for pagination (default 0)."))
int32 Offset = 0;
UPROPERTY(meta=(Optional, Description="Maximum number of blueprints to compile (default 0 = no limit)."))
int32 Limit = 0;
virtual FString GetDescription() const override
{
return TEXT("Compile one or more blueprints without saving. "
"Reports errors and warnings. "
"Use 'blueprint' for a single blueprint, or 'query' to bulk-compile matching blueprints.");
}
// Helper: validate a single Blueprint and return structured JSON result
static TSharedRef<FJsonObject> ValidateSingleBlueprint(UBlueprint* BP, const FString& BlueprintName)
{
FLogCaptureOutputDevice LogCapture;
GLog->AddOutputDevice(&LogCapture);
EBlueprintCompileOptions CompileOpts =
EBlueprintCompileOptions::SkipSave |
EBlueprintCompileOptions::SkipGarbageCollection |
EBlueprintCompileOptions::SkipFiBSearchMetaUpdate;
FKismetEditorUtilities::CompileBlueprint(BP, CompileOpts, nullptr);
GLog->RemoveOutputDevice(&LogCapture);
TArray<TSharedPtr<FJsonValue>> ErrorsArr;
TArray<TSharedPtr<FJsonValue>> WarningsArr;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (!Node) continue;
if (Node->bHasCompilerMessage)
{
TSharedRef<FJsonObject> Msg = MakeShared<FJsonObject>();
Msg->SetStringField(TEXT("graph"), Graph->GetName());
Msg->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
Msg->SetStringField(TEXT("nodeTitle"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
Msg->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
Msg->SetStringField(TEXT("message"), Node->ErrorMsg);
if (Node->ErrorType == EMessageSeverity::Error)
{
Msg->SetStringField(TEXT("severity"), TEXT("error"));
ErrorsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
else
{
Msg->SetStringField(TEXT("severity"), TEXT("warning"));
WarningsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
}
}
}
for (const FString& LogErr : LogCapture.CapturedErrors)
{
TSharedRef<FJsonObject> Msg = MakeShared<FJsonObject>();
Msg->SetStringField(TEXT("source"), TEXT("log"));
Msg->SetStringField(TEXT("message"), LogErr);
Msg->SetStringField(TEXT("severity"), TEXT("error"));
ErrorsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
for (const FString& LogWarn : LogCapture.CapturedWarnings)
{
TSharedRef<FJsonObject> Msg = MakeShared<FJsonObject>();
Msg->SetStringField(TEXT("source"), TEXT("log"));
Msg->SetStringField(TEXT("message"), LogWarn);
Msg->SetStringField(TEXT("severity"), TEXT("warning"));
WarningsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
FString StatusStr;
switch (BP->Status)
{
case BS_UpToDate: StatusStr = TEXT("UpToDate"); break;
case BS_Dirty: StatusStr = TEXT("Dirty"); break;
case BS_Error: StatusStr = TEXT("Error"); break;
case BS_Unknown: StatusStr = TEXT("Unknown"); break;
default: StatusStr = FString::Printf(TEXT("Status_%d"), (int32)BP->Status); break;
}
bool bIsValid = (BP->Status == BS_UpToDate) && ErrorsArr.Num() == 0;
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetStringField(TEXT("blueprint"), BlueprintName);
Result->SetStringField(TEXT("status"), StatusStr);
Result->SetBoolField(TEXT("isValid"), bIsValid);
Result->SetNumberField(TEXT("errorCount"), ErrorsArr.Num());
Result->SetArrayField(TEXT("errors"), ErrorsArr);
Result->SetNumberField(TEXT("warningCount"), WarningsArr.Num());
Result->SetArrayField(TEXT("warnings"), WarningsArr);
return Result;
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
TArray<FAssetData*> MatchingAssets;
if (!Blueprint.IsEmpty())
{
FAssetData* Asset = UMCPAssetFinder::FindBlueprintAsset(Blueprint, nullptr, /*bIncludeLevelBlueprints=*/true);
if (!Asset)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blueprint '%s' not found."), *Blueprint));
}
MatchingAssets.Add(Asset);
}
else
{
MatchingAssets = UMCPAssetFinder::SearchBlueprintAssets(Query, /*bIncludeLevelBlueprints=*/true);
}
int32 TotalMatching = MatchingAssets.Num();
// countOnly: return count without compiling anything
if (CountOnly)
{
Result->SetNumberField(TEXT("totalMatching"), TotalMatching);
if (!Query.IsEmpty())
{
Result->SetStringField(TEXT("query"), Query);
}
return;
}
// Compute range
int32 StartIdx = FMath::Clamp(Offset, 0, TotalMatching);
int32 EndIdx = (Limit > 0) ? FMath::Min(StartIdx + Limit, TotalMatching) : TotalMatching;
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Bulk validating blueprints (query: '%s', range: %d-%d of %d matching)"),
Query.IsEmpty() ? TEXT("*") : *Query, StartIdx, EndIdx, TotalMatching);
TArray<TSharedPtr<FJsonValue>> FailedArr;
TArray<TSharedPtr<FJsonValue>> WarningsArr;
int32 TotalChecked = 0;
int32 TotalPassed = 0;
int32 TotalFailed = 0;
for (int32 Idx = StartIdx; Idx < EndIdx; Idx++)
{
FAssetData* Asset = MatchingAssets[Idx];
FString AssetName = Asset->AssetName.ToString();
FString PackagePath = Asset->PackageName.ToString();
// Load the Blueprint (handles both regular and level blueprints)
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(PackagePath, LoadError, /*bIncludeLevelBlueprints=*/true);
if (!BP)
{
continue;
}
TotalChecked++;
TSharedRef<FJsonObject> ValidationResult = ValidateSingleBlueprint(BP, AssetName);
bool bValid = ValidationResult->GetBoolField(TEXT("isValid"));
int32 Errors = (int32)ValidationResult->GetNumberField(TEXT("errorCount"));
int32 Warnings = (int32)ValidationResult->GetNumberField(TEXT("warningCount"));
if (bValid && Errors == 0)
{
TotalPassed++;
if (Warnings > 0)
{
ValidationResult->SetStringField(TEXT("path"), PackagePath);
WarningsArr.Add(MakeShared<FJsonValueObject>(ValidationResult));
}
}
else
{
TotalFailed++;
ValidationResult->SetStringField(TEXT("path"), PackagePath);
FailedArr.Add(MakeShared<FJsonValueObject>(ValidationResult));
}
// Log progress every 50 blueprints
if (TotalChecked % 50 == 0)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validated %d blueprints so far (%d failed)..."),
TotalChecked, TotalFailed);
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Bulk validation complete — %d checked, %d passed, %d failed"),
TotalChecked, TotalPassed, TotalFailed);
Result->SetNumberField(TEXT("totalMatching"), TotalMatching);
Result->SetNumberField(TEXT("totalChecked"), TotalChecked);
Result->SetNumberField(TEXT("totalPassed"), TotalPassed);
Result->SetNumberField(TEXT("totalFailed"), TotalFailed);
Result->SetArrayField(TEXT("failed"), FailedArr);
Result->SetArrayField(TEXT("warnings"), WarningsArr);
if (!Query.IsEmpty())
{
Result->SetStringField(TEXT("query"), Query);
}
}
};

View File

@@ -910,8 +910,6 @@ void FMCPServer::RegisterHandlers()
H(TEXT("search_unreal_classes"), &FMCPServer::HandleListClasses);
H(TEXT("list_class_functions"), &FMCPServer::HandleListFunctions);
H(TEXT("list_class_properties"), &FMCPServer::HandleListProperties);
H(TEXT("compile_blueprint"), &FMCPServer::HandleValidateBlueprint);
H(TEXT("compile_all_blueprints"), &FMCPServer::HandleValidateAllBlueprints);
H(TEXT("reparent_blueprint"), &FMCPServer::HandleReparentBlueprint);
H(TEXT("create_blueprint_asset"), &FMCPServer::HandleCreateBlueprint);
H(TEXT("create_blueprint_graph"), &FMCPServer::HandleCreateGraph);

View File

@@ -25,6 +25,7 @@ public:
virtual void Deinitialize() override;
// --- Static API: asset lists ---
static const TArray<FAssetData>& GetBlueprintAssets();
static const TArray<FAssetData>& GetBlueprintAndMapAssets();
static const TArray<FAssetData>& GetMapAssets();
static const TArray<FAssetData>& GetMaterialAssets();
static const TArray<FAssetData>& GetMaterialInstanceAssets();
@@ -34,9 +35,10 @@ public:
// Find functions return nullptr if not found or if the short name is ambiguous.
// Pass OutError to get a descriptive message on failure.
static FAssetData* FindAnyAsset(const FString& NameOrPath, FString* OutError = nullptr);
static FAssetData* FindBlueprintAsset(const FString& NameOrPath, FString* OutError = nullptr);
static FAssetData* FindBlueprintAsset(const FString& NameOrPath, FString* OutError = nullptr, bool bIncludeLevelBlueprints = false);
static TArray<FAssetData*> SearchBlueprintAssets(const FString& Filter, bool bIncludeLevelBlueprints = false);
static FAssetData* FindMapAsset(const FString& NameOrPath, FString* OutError = nullptr);
static UBlueprint* LoadBlueprintByName(const FString& NameOrPath, FString& OutError);
static UBlueprint* LoadBlueprintByName(const FString& NameOrPath, FString& OutError, bool bIncludeLevelBlueprints = true);
static FAssetData* FindMaterialAsset(const FString& NameOrPath, FString* OutError = nullptr);
static UMaterial* LoadMaterialByName(const FString& NameOrPath, FString& OutError);
@@ -52,6 +54,7 @@ private:
// Cached asset lists
TArray<FAssetData> AllBlueprintAssets;
TArray<FAssetData> AllMapAssets;
TArray<FAssetData> AllBlueprintAndMapAssets;
TArray<FAssetData> AllMaterialAssets;
TArray<FAssetData> AllMaterialInstanceAssets;
TArray<FAssetData> AllMaterialFunctionAssets;

View File

@@ -119,10 +119,6 @@ private:
void HandleChangeFunctionParamType(const FJsonObject* Json, FJsonObject* Result);
void HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result);
// ----- Validation (read-only, no save) -----
void HandleValidateBlueprint(const FJsonObject* Json, FJsonObject* Result);
void HandleValidateAllBlueprints(const FJsonObject* Json, FJsonObject* Result);
// ----- Pin introspection (read-only) -----
void HandleGetPinInfo(const FJsonObject* Json, FJsonObject* Result);
void HandleCheckPinCompatibility(const FJsonObject* Json, FJsonObject* Result);

View File

@@ -48,6 +48,58 @@ struct FGraphSnapshot
TMap<FString, FGraphSnapshotData> Graphs; // graphName -> data
};
// ----- Log capture -----
class FLogCaptureOutputDevice : public FOutputDevice
{
public:
TArray<FString> CapturedErrors;
TArray<FString> CapturedWarnings;
virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override
{
FString Msg(V);
if (Verbosity == ELogVerbosity::Error || Verbosity == ELogVerbosity::Fatal)
{
CapturedErrors.Add(Msg);
return;
}
if (Verbosity == ELogVerbosity::Warning)
{
if (!Msg.Contains(TEXT("BlueprintMCP:")))
{
CapturedWarnings.Add(Msg);
}
return;
}
static const TCHAR* ErrorPatterns[] = {
TEXT("Can't connect pins"),
TEXT("Fixed up function"),
TEXT("is not compatible with"),
TEXT("could not find a pin"),
TEXT("has an invalid"),
TEXT("orphaned pin"),
TEXT("is deprecated"),
TEXT("does not implement"),
TEXT("Missing function"),
TEXT("Unable to find"),
TEXT("Failed to resolve"),
};
for (const TCHAR* Pattern : ErrorPatterns)
{
if (Msg.Contains(Pattern))
{
CapturedWarnings.Add(Msg);
return;
}
}
}
};
// Stateless utility functions used by MCP handlers and the MCP server.
// This is effectively a namespace — all methods are static.
class MCPUtils