More work on MCP handlers. Changed the Validation handler.
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"blueprint-mcp": {
|
"blueprint-mcp": {
|
||||||
"command": "/usr/bin/nc",
|
"command": "python3",
|
||||||
"args": ["localhost", "9847"]
|
"args": ["tools/mcp-bridge.py"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -59,6 +59,7 @@ UMCPAssetFinder* UMCPAssetFinder::GetUpdatedAssets()
|
|||||||
|
|
||||||
Self->AllBlueprintAssets.Empty();
|
Self->AllBlueprintAssets.Empty();
|
||||||
Self->AllMapAssets.Empty();
|
Self->AllMapAssets.Empty();
|
||||||
|
Self->AllBlueprintAndMapAssets.Empty();
|
||||||
Self->AllMaterialAssets.Empty();
|
Self->AllMaterialAssets.Empty();
|
||||||
Self->AllMaterialInstanceAssets.Empty();
|
Self->AllMaterialInstanceAssets.Empty();
|
||||||
Self->AllMaterialFunctionAssets.Empty();
|
Self->AllMaterialFunctionAssets.Empty();
|
||||||
@@ -69,6 +70,9 @@ UMCPAssetFinder* UMCPAssetFinder::GetUpdatedAssets()
|
|||||||
AR.GetAssetsByClass(UMaterialInstanceConstant::StaticClass()->GetClassPathName(), Self->AllMaterialInstanceAssets, false);
|
AR.GetAssetsByClass(UMaterialInstanceConstant::StaticClass()->GetClassPathName(), Self->AllMaterialInstanceAssets, false);
|
||||||
AR.GetAssetsByClass(UMaterialFunction::StaticClass()->GetClassPathName(), Self->AllMaterialFunctionAssets, false);
|
AR.GetAssetsByClass(UMaterialFunction::StaticClass()->GetClassPathName(), Self->AllMaterialFunctionAssets, false);
|
||||||
|
|
||||||
|
Self->AllBlueprintAndMapAssets = Self->AllBlueprintAssets;
|
||||||
|
Self->AllBlueprintAndMapAssets.Append(Self->AllMapAssets);
|
||||||
|
|
||||||
Self->bDirty = false;
|
Self->bDirty = false;
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("MCPAssetFinder: Refreshed — BP %d, Map %d, Mat %d, MI %d, MF %d"),
|
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;
|
return Self ? Self->AllBlueprintAssets : EmptyAssetArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TArray<FAssetData>& UMCPAssetFinder::GetBlueprintAndMapAssets()
|
||||||
|
{
|
||||||
|
UMCPAssetFinder* Self = GetUpdatedAssets();
|
||||||
|
return Self ? Self->AllBlueprintAndMapAssets : EmptyAssetArray;
|
||||||
|
}
|
||||||
|
|
||||||
const TArray<FAssetData>& UMCPAssetFinder::GetMapAssets()
|
const TArray<FAssetData>& UMCPAssetFinder::GetMapAssets()
|
||||||
{
|
{
|
||||||
UMCPAssetFinder* Self = GetUpdatedAssets();
|
UMCPAssetFinder* Self = GetUpdatedAssets();
|
||||||
@@ -144,11 +154,34 @@ namespace
|
|||||||
}
|
}
|
||||||
return Found;
|
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)
|
FAssetData* UMCPAssetFinder::FindMapAsset(const FString& NameOrPath, FString* OutError)
|
||||||
@@ -184,38 +217,37 @@ FAssetData* UMCPAssetFinder::FindAnyAsset(const FString& NameOrPath, FString* Ou
|
|||||||
// Load helpers
|
// 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, bIncludeLevelBlueprints);
|
||||||
FAssetData* Asset = FindBlueprintAsset(NameOrPath, &OutError);
|
if (!Asset)
|
||||||
if (Asset)
|
|
||||||
{
|
{
|
||||||
UBlueprint* BP = Cast<UBlueprint>(Asset->GetAsset());
|
if (OutError.IsEmpty())
|
||||||
if (BP) return BP;
|
{
|
||||||
|
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)
|
// Regular blueprint asset
|
||||||
FAssetData* MapAsset = FindMapAsset(NameOrPath, &OutError);
|
UBlueprint* BP = Cast<UBlueprint>(Asset->GetAsset());
|
||||||
if (MapAsset)
|
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)
|
if (World && World->PersistentLevel)
|
||||||
{
|
{
|
||||||
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(true);
|
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(true);
|
||||||
if (LevelBP)
|
if (LevelBP)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Display, TEXT("MCPAssetFinder: Loaded level blueprint from map '%s'"),
|
|
||||||
*NameOrPath);
|
|
||||||
return LevelBP;
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,4 @@
|
|||||||
#include "MCPHandlers_Mutation.h"
|
#include "MCPHandlers_Mutation.h"
|
||||||
#include "MCPHandlers_PinMutation.h"
|
#include "MCPHandlers_PinMutation.h"
|
||||||
#include "MCPHandlers_AssetMutation.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("search_unreal_classes"), &FMCPServer::HandleListClasses);
|
||||||
H(TEXT("list_class_functions"), &FMCPServer::HandleListFunctions);
|
H(TEXT("list_class_functions"), &FMCPServer::HandleListFunctions);
|
||||||
H(TEXT("list_class_properties"), &FMCPServer::HandleListProperties);
|
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("reparent_blueprint"), &FMCPServer::HandleReparentBlueprint);
|
||||||
H(TEXT("create_blueprint_asset"), &FMCPServer::HandleCreateBlueprint);
|
H(TEXT("create_blueprint_asset"), &FMCPServer::HandleCreateBlueprint);
|
||||||
H(TEXT("create_blueprint_graph"), &FMCPServer::HandleCreateGraph);
|
H(TEXT("create_blueprint_graph"), &FMCPServer::HandleCreateGraph);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public:
|
|||||||
virtual void Deinitialize() override;
|
virtual void Deinitialize() override;
|
||||||
// --- Static API: asset lists ---
|
// --- Static API: asset lists ---
|
||||||
static const TArray<FAssetData>& GetBlueprintAssets();
|
static const TArray<FAssetData>& GetBlueprintAssets();
|
||||||
|
static const TArray<FAssetData>& GetBlueprintAndMapAssets();
|
||||||
static const TArray<FAssetData>& GetMapAssets();
|
static const TArray<FAssetData>& GetMapAssets();
|
||||||
static const TArray<FAssetData>& GetMaterialAssets();
|
static const TArray<FAssetData>& GetMaterialAssets();
|
||||||
static const TArray<FAssetData>& GetMaterialInstanceAssets();
|
static const TArray<FAssetData>& GetMaterialInstanceAssets();
|
||||||
@@ -34,9 +35,10 @@ public:
|
|||||||
// Find functions return nullptr if not found or if the short name is ambiguous.
|
// Find functions return nullptr if not found or if the short name is ambiguous.
|
||||||
// Pass OutError to get a descriptive message on failure.
|
// Pass OutError to get a descriptive message on failure.
|
||||||
static FAssetData* FindAnyAsset(const FString& NameOrPath, FString* OutError = nullptr);
|
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 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 FAssetData* FindMaterialAsset(const FString& NameOrPath, FString* OutError = nullptr);
|
||||||
static UMaterial* LoadMaterialByName(const FString& NameOrPath, FString& OutError);
|
static UMaterial* LoadMaterialByName(const FString& NameOrPath, FString& OutError);
|
||||||
@@ -52,6 +54,7 @@ private:
|
|||||||
// Cached asset lists
|
// Cached asset lists
|
||||||
TArray<FAssetData> AllBlueprintAssets;
|
TArray<FAssetData> AllBlueprintAssets;
|
||||||
TArray<FAssetData> AllMapAssets;
|
TArray<FAssetData> AllMapAssets;
|
||||||
|
TArray<FAssetData> AllBlueprintAndMapAssets;
|
||||||
TArray<FAssetData> AllMaterialAssets;
|
TArray<FAssetData> AllMaterialAssets;
|
||||||
TArray<FAssetData> AllMaterialInstanceAssets;
|
TArray<FAssetData> AllMaterialInstanceAssets;
|
||||||
TArray<FAssetData> AllMaterialFunctionAssets;
|
TArray<FAssetData> AllMaterialFunctionAssets;
|
||||||
|
|||||||
@@ -119,10 +119,6 @@ private:
|
|||||||
void HandleChangeFunctionParamType(const FJsonObject* Json, FJsonObject* Result);
|
void HandleChangeFunctionParamType(const FJsonObject* Json, FJsonObject* Result);
|
||||||
void HandleRemoveFunctionParameter(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) -----
|
// ----- Pin introspection (read-only) -----
|
||||||
void HandleGetPinInfo(const FJsonObject* Json, FJsonObject* Result);
|
void HandleGetPinInfo(const FJsonObject* Json, FJsonObject* Result);
|
||||||
void HandleCheckPinCompatibility(const FJsonObject* Json, FJsonObject* Result);
|
void HandleCheckPinCompatibility(const FJsonObject* Json, FJsonObject* Result);
|
||||||
|
|||||||
@@ -48,6 +48,58 @@ struct FGraphSnapshot
|
|||||||
TMap<FString, FGraphSnapshotData> Graphs; // graphName -> data
|
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.
|
// Stateless utility functions used by MCP handlers and the MCP server.
|
||||||
// This is effectively a namespace — all methods are static.
|
// This is effectively a namespace — all methods are static.
|
||||||
class MCPUtils
|
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