More work on MCP handlers. Changed the Validation handler.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"blueprint-mcp": {
|
||||
"command": "/usr/bin/nc",
|
||||
"args": ["localhost", "9847"]
|
||||
"command": "python3",
|
||||
"args": ["tools/mcp-bridge.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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())
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Blueprint '%s' not found. Use list_blueprints to see available assets."), *NameOrPath);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
if (!OutError.IsEmpty()) return nullptr;
|
||||
|
||||
// Strategy 2: Try as a level blueprint (from a .umap)
|
||||
FAssetData* MapAsset = FindMapAsset(NameOrPath, &OutError);
|
||||
if (MapAsset)
|
||||
// Regular blueprint asset
|
||||
UBlueprint* BP = Cast<UBlueprint>(Asset->GetAsset());
|
||||
if (BP)
|
||||
{
|
||||
UWorld* World = Cast<UWorld>(MapAsset->GetAsset());
|
||||
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)
|
||||
{
|
||||
UE_LOG(LogTemp, Display, TEXT("MCPAssetFinder: Loaded level blueprint from map '%s'"),
|
||||
*NameOrPath);
|
||||
return LevelBP;
|
||||
}
|
||||
}
|
||||
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);
|
||||
OutError = FString::Printf(TEXT("Asset '%s' loaded but its level blueprint could not be retrieved."), *NameOrPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
#include "MCPHandlers_Mutation.h"
|
||||
#include "MCPHandlers_PinMutation.h"
|
||||
#include "MCPHandlers_AssetMutation.h"
|
||||
#include "MCPHandlers_Validation.h"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
165
tools/mcp-bridge.py
Normal file
165
tools/mcp-bridge.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP stdio-to-TCP bridge for BlueprintMCP.
|
||||
|
||||
Reads JSON-RPC messages from stdin, forwards them to the BlueprintMCP TCP server,
|
||||
and writes responses to stdout. If the editor isn't running, returns valid MCP
|
||||
error responses instead of crashing.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
|
||||
HOST = "localhost"
|
||||
PORT = 9847
|
||||
CONNECT_TIMEOUT = 2
|
||||
READ_TIMEOUT = 120
|
||||
|
||||
sock = None
|
||||
|
||||
|
||||
def connect():
|
||||
"""Try to connect to the editor. Returns True on success."""
|
||||
global sock
|
||||
if sock is not None:
|
||||
return True
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(CONNECT_TIMEOUT)
|
||||
s.connect((HOST, PORT))
|
||||
s.settimeout(READ_TIMEOUT)
|
||||
sock = s
|
||||
return True
|
||||
except (ConnectionRefusedError, socket.timeout, OSError):
|
||||
return False
|
||||
|
||||
|
||||
def disconnect():
|
||||
global sock
|
||||
if sock is not None:
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
sock = None
|
||||
|
||||
|
||||
def send_and_receive(message):
|
||||
"""Send a JSON-RPC message to the editor and return the response."""
|
||||
data = json.dumps(message) + "\n"
|
||||
sock.sendall(data.encode())
|
||||
|
||||
result = b""
|
||||
while True:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
raise ConnectionError("Connection closed")
|
||||
result += chunk
|
||||
try:
|
||||
return json.loads(result)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
|
||||
def editor_down_response(msg):
|
||||
"""Return a valid MCP error response indicating the editor is down."""
|
||||
msg_id = msg.get("id", 0)
|
||||
method = msg.get("method", "")
|
||||
|
||||
if method == "initialize":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {"tools": {}},
|
||||
"serverInfo": {"name": "blueprint-mcp", "version": "1.0.0"},
|
||||
"_notice": "Unreal Editor is not running. No tools are available. Start the editor and restart Claude Code.",
|
||||
},
|
||||
}
|
||||
|
||||
if method == "notifications/initialized":
|
||||
return None # No response needed for notifications
|
||||
|
||||
if method == "tools/list":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": {
|
||||
"tools": [{
|
||||
"name": "editor_not_running",
|
||||
"description": "Unreal Editor is not running. Start the editor and restart Claude Code to get BlueprintMCP tools.",
|
||||
"inputSchema": {"type": "object", "properties": {}},
|
||||
}],
|
||||
},
|
||||
}
|
||||
|
||||
if method == "tools/call":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": {
|
||||
"content": [
|
||||
{"type": "text", "text": json.dumps({"error": "Unreal Editor is down."})}
|
||||
],
|
||||
"isError": True,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"error": {"code": -32000, "message": "Unreal Editor is down."},
|
||||
}
|
||||
|
||||
|
||||
def handle_message(msg):
|
||||
"""Handle one JSON-RPC message. Try the editor first, fall back to offline responses."""
|
||||
method = msg.get("method", "")
|
||||
|
||||
# Notifications don't get responses
|
||||
if "id" not in msg:
|
||||
if connect():
|
||||
try:
|
||||
sock.sendall((json.dumps(msg) + "\n").encode())
|
||||
except Exception:
|
||||
disconnect()
|
||||
return None
|
||||
|
||||
# Try connecting (or reconnecting) to the editor
|
||||
if not connect():
|
||||
return editor_down_response(msg)
|
||||
|
||||
try:
|
||||
return send_and_receive(msg)
|
||||
except Exception:
|
||||
disconnect()
|
||||
# Retry once in case the connection was stale
|
||||
if connect():
|
||||
try:
|
||||
return send_and_receive(msg)
|
||||
except Exception:
|
||||
disconnect()
|
||||
return editor_down_response(msg)
|
||||
|
||||
|
||||
def main():
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
msg = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
response = handle_message(msg)
|
||||
if response is not None:
|
||||
sys.stdout.write(json.dumps(response) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user