diff --git a/.mcp.json b/.mcp.json index 27fa8451..45d7e5fa 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,8 +1,8 @@ { "mcpServers": { "blueprint-mcp": { - "command": "/usr/bin/nc", - "args": ["localhost", "9847"] + "command": "python3", + "args": ["tools/mcp-bridge.py"] } } } diff --git a/Content/Tangibles/TAN_Character.uasset b/Content/Tangibles/TAN_Character.uasset index 9156611c..497c6e0d 100644 --- a/Content/Tangibles/TAN_Character.uasset +++ b/Content/Tangibles/TAN_Character.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fef91fc73422c67d7f09f4ea1d0ec0e21fdd88c829de1c3011857d87b3defb2 -size 375026 +oid sha256:f6b9c67d63af89f0ddb1624557c85f25bf6ce097e286f8f52f1e558469549581 +size 372097 diff --git a/Content/Widgets/WB_Crosshair.uasset b/Content/Widgets/WB_Crosshair.uasset index e965787a..a77a0395 100644 --- a/Content/Widgets/WB_Crosshair.uasset +++ b/Content/Widgets/WB_Crosshair.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f13d4b679f3bea54960e95f91bb86de0c36fc3a28d2c5c19bfcc7dcb958e6f36 -size 64665 +oid sha256:cdd9faef681ec42e4a2f1953c62593d4f0aea969d116af0677075a796c320ebf +size 67366 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp index 1e198119..3d1d6981 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp @@ -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& UMCPAssetFinder::GetBlueprintAssets() return Self ? Self->AllBlueprintAssets : EmptyAssetArray; } +const TArray& UMCPAssetFinder::GetBlueprintAndMapAssets() +{ + UMCPAssetFinder* Self = GetUpdatedAssets(); + return Self ? Self->AllBlueprintAndMapAssets : EmptyAssetArray; +} + const TArray& UMCPAssetFinder::GetMapAssets() { UMCPAssetFinder* Self = GetUpdatedAssets(); @@ -144,11 +154,34 @@ namespace } return Found; } + + TArray SearchInList(const TArray& List, const FString& Filter) + { + TArray 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(&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& List = bIncludeLevelBlueprints ? GetBlueprintAndMapAssets() : GetBlueprintAssets(); + return FindInList(List, NameOrPath, OutError); +} + +TArray UMCPAssetFinder::SearchBlueprintAssets(const FString& Filter, bool bIncludeLevelBlueprints) +{ + const TArray& 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(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(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(Asset->GetAsset()); + if (BP) + { + return BP; + } + + // Map asset — extract the level blueprint + UWorld* World = Cast(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; } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp index 2f712102..e4f87159 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers.cpp @@ -3,3 +3,4 @@ #include "MCPHandlers_Mutation.h" #include "MCPHandlers_PinMutation.h" #include "MCPHandlers_AssetMutation.h" +#include "MCPHandlers_Validation.h" diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.cpp deleted file mode 100644 index e5549572..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.cpp +++ /dev/null @@ -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 CapturedErrors; - TArray 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 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> ErrorsArr; - TArray> WarningsArr; - - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - for (UEdGraph* Graph : AllGraphs) - { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) - { - if (!Node) continue; - if (Node->bHasCompilerMessage) - { - TSharedRef Msg = MakeShared(); - 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(Msg)); - } - else - { - Msg->SetStringField(TEXT("severity"), TEXT("warning")); - WarningsArr.Add(MakeShared(Msg)); - } - } - } - } - - for (const FString& LogErr : LogCapture.CapturedErrors) - { - TSharedRef Msg = MakeShared(); - Msg->SetStringField(TEXT("source"), TEXT("log")); - Msg->SetStringField(TEXT("message"), LogErr); - Msg->SetStringField(TEXT("severity"), TEXT("error")); - ErrorsArr.Add(MakeShared(Msg)); - } - - for (const FString& LogWarn : LogCapture.CapturedWarnings) - { - TSharedRef Msg = MakeShared(); - Msg->SetStringField(TEXT("source"), TEXT("log")); - Msg->SetStringField(TEXT("message"), LogWarn); - Msg->SetStringField(TEXT("severity"), TEXT("warning")); - WarningsArr.Add(MakeShared(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 Result = MakeShared(); - 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 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 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> 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(Asset.GetAsset()); - if (!BP) - { - continue; - } - - TotalChecked++; - - TSharedRef 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(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); - } -} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h new file mode 100644 index 00000000..f922e835 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h @@ -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 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> ErrorsArr; + TArray> WarningsArr; + + TArray AllGraphs; + BP->GetAllGraphs(AllGraphs); + + for (UEdGraph* Graph : AllGraphs) + { + if (!Graph) continue; + for (UEdGraphNode* Node : Graph->Nodes) + { + if (!Node) continue; + if (Node->bHasCompilerMessage) + { + TSharedRef Msg = MakeShared(); + 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(Msg)); + } + else + { + Msg->SetStringField(TEXT("severity"), TEXT("warning")); + WarningsArr.Add(MakeShared(Msg)); + } + } + } + } + + for (const FString& LogErr : LogCapture.CapturedErrors) + { + TSharedRef Msg = MakeShared(); + Msg->SetStringField(TEXT("source"), TEXT("log")); + Msg->SetStringField(TEXT("message"), LogErr); + Msg->SetStringField(TEXT("severity"), TEXT("error")); + ErrorsArr.Add(MakeShared(Msg)); + } + + for (const FString& LogWarn : LogCapture.CapturedWarnings) + { + TSharedRef Msg = MakeShared(); + Msg->SetStringField(TEXT("source"), TEXT("log")); + Msg->SetStringField(TEXT("message"), LogWarn); + Msg->SetStringField(TEXT("severity"), TEXT("warning")); + WarningsArr.Add(MakeShared(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 Result = MakeShared(); + 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 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> FailedArr; + TArray> 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 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(ValidationResult)); + } + } + else + { + TotalFailed++; + ValidationResult->SetStringField(TEXT("path"), PackagePath); + FailedArr.Add(MakeShared(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); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp index 275ccd46..7d300f2c 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp @@ -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); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h index dfcd884d..2c384864 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h @@ -25,6 +25,7 @@ public: virtual void Deinitialize() override; // --- Static API: asset lists --- static const TArray& GetBlueprintAssets(); + static const TArray& GetBlueprintAndMapAssets(); static const TArray& GetMapAssets(); static const TArray& GetMaterialAssets(); static const TArray& 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 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 AllBlueprintAssets; TArray AllMapAssets; + TArray AllBlueprintAndMapAssets; TArray AllMaterialAssets; TArray AllMaterialInstanceAssets; TArray AllMaterialFunctionAssets; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h index 2a9d7939..977e3b44 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h @@ -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); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 15ab1d63..764724a6 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -48,6 +48,58 @@ struct FGraphSnapshot TMap Graphs; // graphName -> data }; +// ----- Log capture ----- + +class FLogCaptureOutputDevice : public FOutputDevice +{ +public: + TArray CapturedErrors; + TArray 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 diff --git a/tools/mcp-bridge.py b/tools/mcp-bridge.py new file mode 100644 index 00000000..b8d28d18 --- /dev/null +++ b/tools/mcp-bridge.py @@ -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()