diff --git a/Content/Testing/M_Test.uasset b/Content/Testing/M_Test.uasset index 3a7352d4..60bd062b 100644 --- a/Content/Testing/M_Test.uasset +++ b/Content/Testing/M_Test.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c69da868cefe9949b5bbb2d3a91491c463616835dd39dbe2e2bdcabdda0088fc -size 13809 +oid sha256:e77ffbce63d1d578a6ee0ae763e3d1d59309511455a8592f472c1a5310649a7e +size 13010 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Blueprint_Compile.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Blueprint_Compile.h index 32230cf2..2acae6ec 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Blueprint_Compile.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Blueprint_Compile.h @@ -45,9 +45,6 @@ public: // Returns true if the blueprint compiled cleanly (no errors). static bool CompileAndReport(UBlueprint* BP, FStringBuilderBase& Out) { - FLogCaptureOutputDevice LogCapture; - GLog->AddOutputDevice(&LogCapture); - EBlueprintCompileOptions CompileOpts = EBlueprintCompileOptions::SkipSave | EBlueprintCompileOptions::SkipGarbageCollection | @@ -55,8 +52,6 @@ public: FKismetEditorUtilities::CompileBlueprint(BP, CompileOpts, nullptr); - GLog->RemoveOutputDevice(&LogCapture); - int32 ErrorCount = 0; int32 WarningCount = 0; @@ -74,18 +69,6 @@ public: *Node->ErrorMsg); } - // Collect log-captured errors/warnings - for (const FString& Msg : LogCapture.CapturedErrors) - { - ErrorCount++; - Out.Appendf(TEXT(" ERROR: (log) %s\n"), *Msg); - } - for (const FString& Msg : LogCapture.CapturedWarnings) - { - WarningCount++; - Out.Appendf(TEXT(" WARNING: (log) %s\n"), *Msg); - } - FString StatusStr = MCPUtils::EnumToString((EBlueprintStatus)BP->Status, TEXT("BS_")); bool bIsValid = (BP->Status == BS_UpToDate) && (ErrorCount == 0); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Compile.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Compile.h index 95b8b874..8659ae56 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Compile.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Compile.h @@ -2,10 +2,9 @@ #include "CoreMinimal.h" #include "MCPHandler.h" -#include "MCPAssetFinder.h" +#include "MCPFetcher.h" #include "MCPUtils.h" #include "Materials/Material.h" -#include "MaterialDomain.h" #include "Material_Compile.generated.h" @@ -30,27 +29,25 @@ public: virtual void Handle(FStringBuilderBase& Result) override { // Load material - MCPAssets Assets; - if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - UMaterial* MaterialObj = Assets.Object(); + MCPFetcher F(Result); + UMaterial* MaterialObj = F.Asset(Material).Cast(); + if (!MaterialObj) return; - // Force recompile by triggering PreEdit/PostEdit - TArray Chain = { MaterialObj }; - MCPUtils::PreEdit(Chain); - MCPUtils::PostEdit(Chain); + // Force recompile + MaterialObj->ForceRecompileForRendering(); - // Check for compilation errors via FMaterialResource on current platform - TArray Errors; + // Wait for compilation to finish, then check for errors FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel); + TArray Errors; if (Resource) { + Resource->FinishCompilation(); Errors = Resource->GetCompileErrors(); } if (Errors.IsEmpty()) { Result.Appendf(TEXT("%s compiled successfully.\n"), *MCPUtils::FormatName(MaterialObj)); - Result.Append(TEXT("WARNING: Error detection is not working. GetCompileErrors() returns empty even when shader compilation fails. Do not trust this result.\n")); } else { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/LogCapture.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/LogCapture.h new file mode 100644 index 00000000..e21c97f2 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/LogCapture.h @@ -0,0 +1,34 @@ +#pragma once + +#include "CoreMinimal.h" + +class FLogCaptureOutputDevice : public FOutputDevice +{ +public: + TArray CapturedErrors; + bool bEnabled = true; + + void Install() { GLog->AddOutputDevice(this); } + void Uninstall() { GLog->RemoveOutputDevice(this); } + + // If the device is marked 'CanBeUsedOnMultipleThreads,' + // then UE_LOG will call Serialize from the current + // thread, otherwise, it will call Serialize from the + // logging thread. Without this, we wouldn't be able to + // tell whether an error is coming from the game thread. + virtual bool CanBeUsedOnMultipleThreads() const override { return true; } + + virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override + { + // Only capture messages from the game thread. + // Other threads generate noise we don't care about. + if (!IsInGameThread() || !bEnabled) return; + + if (Verbosity == ELogVerbosity::Warning || + Verbosity == ELogVerbosity::Error || + Verbosity == ELogVerbosity::Fatal) + { + CapturedErrors.Add(FString(V)); + } + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp new file mode 100644 index 00000000..94e77799 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp @@ -0,0 +1,24 @@ +#include "MCPProperty.h" +#include "MCPUtils.h" + +MCPProperty::MCPProperty(FProperty* InProp, void* Container) + : Prop(InProp), ValuePtr(InProp ? InProp->ContainerPtrToValuePtr(Container) : nullptr) {} + +FString MCPProperty::GetText() const +{ + FString Result; + Prop->ExportTextItem_Direct(Result, ValuePtr, nullptr, nullptr, PPF_None); + return Result; +} + +bool MCPProperty::SetText(const FString& Value, MCPErrorCallback Error) +{ + const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, nullptr, PPF_None); + if (!ImportResult) + { + Error.SetError(FString::Printf(TEXT("Failed to parse '%s' for property '%s' (type: %s)"), + *Value, *MCPUtils::FormatName(Prop), *Prop->GetCPPType())); + return false; + } + return true; +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp index f2132c72..165bf5e9 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp @@ -1,5 +1,6 @@ #include "MCPServer.h" #include "MCPHandler.h" +#include "LogCapture.h" #include "MCPUtils.h" #include "MCPAssetFinder.h" #include "UObject/StrongObjectPtr.h" @@ -151,6 +152,8 @@ void UMCPServer::Initialize(FSubsystemCollectionBase& Collection) } BuildMCPHandlerRegistry(); + LogCapture.bEnabled = false; + LogCapture.Install(); bRunning = true; UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MCP server listening on tcp://localhost:%d"), Port); } @@ -204,6 +207,7 @@ void UMCPServer::Deinitialize() ListenSocket = nullptr; } + LogCapture.Uninstall(); bRunning = false; bShuttingDown = false; UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Server stopped.")); @@ -294,9 +298,19 @@ FString UMCPServer::HandleRequest(const FString& Line) return PopulateError.ToString(); } - // Invoke the handler. + // Invoke the handler with log capture. + LogCapture.CapturedErrors.Empty(); + LogCapture.bEnabled = true; TStringBuilder<32768> TextResult; Handler->Handle(TextResult); + LogCapture.bEnabled = false; + for (const FString& Msg : LogCapture.CapturedErrors) + { + TextResult.Append(TEXT("LOG: ")); + TextResult.Append(Msg); + TextResult.Append(TEXT("\n")); + } + LogCapture.CapturedErrors.Empty(); FString Result = TextResult.ToString(); for (int32 i = 0; i < Result.Len(); ++i) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h new file mode 100644 index 00000000..15c1199b --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h @@ -0,0 +1,21 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPUtils.h" + +// A resolved property: the FProperty descriptor plus a pointer to +// the value's storage. operator-> forwards to the FProperty. +struct MCPProperty +{ + FProperty* Prop = nullptr; + void* ValuePtr = nullptr; + + MCPProperty() = default; + MCPProperty(FProperty* InProp, void* Container); + + FString GetText() const; + bool SetText(const FString& Value, MCPErrorCallback Error = nullptr); + + explicit operator bool() const { return Prop != nullptr; } + FProperty* operator->() const { return Prop; } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h index 5c0ae868..cc65548d 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h @@ -7,6 +7,7 @@ #include "Async/Future.h" #include "Dom/JsonObject.h" #include "MCPUtils.h" +#include "LogCapture.h" #include "MCPServer.generated.h" class FSocket; @@ -52,6 +53,7 @@ public: private: // ----- Tool dispatch ----- + FLogCaptureOutputDevice LogCapture; // installed once at startup, enabled per-request TMap MCPHandlerRegistry; // tool name -> UMCPHandler subclass void BuildMCPHandlerRegistry(); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 06107ead..4931e4f3 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -30,58 +30,6 @@ class UEnum; struct FMemberReference; struct FBPVariableDescription; -// ----- 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; - } - } - } -}; - // ----- Error callback ----- struct MCPErrorCallback