More MCP refactoring

This commit is contained in:
2026-03-08 03:58:06 -04:00
parent a72b65641e
commit 730396753c
8 changed files with 83 additions and 79 deletions

View File

@@ -332,6 +332,21 @@ public:
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
USTRUCT()
struct FBlendSpaceSampleEntry
{
GENERATED_BODY()
UPROPERTY()
FString AnimationAsset;
UPROPERTY()
float X = 0.0f;
UPROPERTY()
float Y = 0.0f;
};
UCLASS(meta=(ToolName="set_blend_space_sample_points")) UCLASS(meta=(ToolName="set_blend_space_sample_points"))
class UMCPHandler_SetBlendSpaceSamples : public UObject, public IMCPHandler class UMCPHandler_SetBlendSpaceSamples : public UObject, public IMCPHandler
{ {
@@ -410,22 +425,18 @@ public:
for (const TSharedPtr<FJsonValue>& SampleVal : Samples.Array) for (const TSharedPtr<FJsonValue>& SampleVal : Samples.Array)
{ {
const TSharedPtr<FJsonObject>& SampleObj = SampleVal->AsObject(); FBlendSpaceSampleEntry Entry;
if (!SampleObj.IsValid()) continue; if (!MCPUtils::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal, Result)) return;
FString AnimAssetName = SampleObj->GetStringField(TEXT("animationAsset"));
float X = (float)SampleObj->GetNumberField(TEXT("x"));
float Y = (float)SampleObj->GetNumberField(TEXT("y"));
UAnimSequence* AnimSeq = nullptr; UAnimSequence* AnimSeq = nullptr;
if (!AnimAssetName.IsEmpty()) if (!Entry.AnimationAsset.IsEmpty())
{ {
MCPAssets<UAnimSequence> AnimAssets; MCPAssets<UAnimSequence> AnimAssets;
if (AnimAssets.Exact(AnimAssetName).Load()) if (AnimAssets.Exact(Entry.AnimationAsset).Load())
AnimSeq = AnimAssets.Object(); AnimSeq = AnimAssets.Object();
} }
FVector SampleValue(X, Y, 0.0f); FVector SampleValue(Entry.X, Entry.Y, 0.0f);
if (AnimSeq) if (AnimSeq)
{ {
BS->AddSample(AnimSeq, SampleValue); BS->AddSample(AnimSeq, SampleValue);

View File

@@ -16,6 +16,18 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
USTRUCT()
struct FDispatcherParamEntry
{
GENERATED_BODY()
UPROPERTY()
FString Name;
UPROPERTY()
FString Type;
};
UCLASS(meta=(ToolName="add_event_dispatcher")) UCLASS(meta=(ToolName="add_event_dispatcher"))
class UMCPHandler_AddEventDispatcher : public UObject, public IMCPHandler class UMCPHandler_AddEventDispatcher : public UObject, public IMCPHandler
{ {
@@ -117,23 +129,19 @@ public:
for (const TSharedPtr<FJsonValue>& ParamVal : Parameters.Array) for (const TSharedPtr<FJsonValue>& ParamVal : Parameters.Array)
{ {
if (!ParamVal.IsValid() || ParamVal->Type != EJson::Object) continue; FDispatcherParamEntry Entry;
TSharedPtr<FJsonObject> ParamObj = ParamVal->AsObject(); if (!MCPUtils::PopulateFromJson(FDispatcherParamEntry::StaticStruct(), &Entry, ParamVal, Result)) return;
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
FString ParamName = ParamObj->GetStringField(TEXT("name"));
FString ParamType = ParamObj->GetStringField(TEXT("type"));
if (ParamName.IsEmpty() || ParamType.IsEmpty()) continue;
FEdGraphPinType PinType; FEdGraphPinType PinType;
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result)) if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, Result))
return; return;
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output); EntryNode->CreateUserDefinedPin(FName(*Entry.Name), PinType, EGPD_Output);
TSharedRef<FJsonObject> ParamJson = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> ParamJson = MakeShared<FJsonObject>();
ParamJson->SetStringField(TEXT("name"), ParamName); ParamJson->SetStringField(TEXT("name"), Entry.Name);
ParamJson->SetStringField(TEXT("type"), ParamType); ParamJson->SetStringField(TEXT("type"), Entry.Type);
AddedParamsJson.Add(MakeShared<FJsonValueObject>(ParamJson)); AddedParamsJson.Add(MakeShared<FJsonValueObject>(ParamJson));
} }
} }

View File

@@ -103,12 +103,7 @@ public:
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FMoveNodeEntry Entry; FMoveNodeEntry Entry;
FString PopulateError = MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal); if (!MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal, &*EntryResult)) continue;
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
}
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node); UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node);
if (!Node) if (!Node)
@@ -362,12 +357,7 @@ public:
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FSpawnNodeEntry Entry; FSpawnNodeEntry Entry;
FString PopulateError = MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal); if (!MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal, &*EntryResult)) continue;
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
}
// Find the spawner by exact full name // Find the spawner by exact full name
TArray<UBlueprintNodeSpawner*> Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true, TargetGraph); TArray<UBlueprintNodeSpawner*> Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true, TargetGraph);

View File

@@ -63,12 +63,7 @@ public:
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FSetPinDefaultEntry Entry; FSetPinDefaultEntry Entry;
FString PopulateError = MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal); if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, &*EntryResult)) continue;
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
}
MCPAssets<UBlueprint> Assets; MCPAssets<UBlueprint> Assets;
if (!Assets.Scan<UBlueprint>().Scan<UWorld>().Exact(Entry.Blueprint).Errors(&*EntryResult).ENone().ETwo().Load()) if (!Assets.Scan<UBlueprint>().Scan<UWorld>().Exact(Entry.Blueprint).Errors(&*EntryResult).ENone().ETwo().Load())
@@ -192,12 +187,7 @@ public:
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FConnectPinsEntry Entry; FConnectPinsEntry Entry;
FString PopulateError = MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal); if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, &*EntryResult)) continue;
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
}
UEdGraph* SourceGraph = nullptr; UEdGraph* SourceGraph = nullptr;
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph); UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph);
@@ -319,12 +309,7 @@ public:
Results.Add(MakeShared<FJsonValueObject>(EntryResult)); Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FDisconnectPinEntry Entry; FDisconnectPinEntry Entry;
FString PopulateError = MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal); if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal, &*EntryResult)) continue;
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
}
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node); UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node);
if (!Node) if (!Node)

View File

@@ -21,6 +21,18 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
USTRUCT()
struct FStructPropertyEntry
{
GENERATED_BODY()
UPROPERTY()
FString Name;
UPROPERTY()
FString Type;
};
UCLASS(meta=(ToolName="create_struct_asset")) UCLASS(meta=(ToolName="create_struct_asset"))
class UMCPHandler_CreateStruct : public UObject, public IMCPHandler class UMCPHandler_CreateStruct : public UObject, public IMCPHandler
{ {
@@ -77,18 +89,15 @@ public:
int32 PropsAdded = 0; int32 PropsAdded = 0;
for (const TSharedPtr<FJsonValue>& PropVal : Properties.Array) for (const TSharedPtr<FJsonValue>& PropVal : Properties.Array)
{ {
TSharedPtr<FJsonObject> PropObj = PropVal->AsObject(); FStructPropertyEntry Entry;
if (!PropObj) continue; if (!MCPUtils::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal, Result)) return;
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
FString PropName = PropObj->GetStringField(TEXT("name"));
FString PropType = PropObj->GetStringField(TEXT("type"));
if (PropName.IsEmpty() || PropType.IsEmpty()) continue;
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; FString TypeError;
if (!MCPUtils::ResolveTypeFromString(PropType, PinType, TypeError)) if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, TypeError))
{ {
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *PropType, *PropName, *TypeError); UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *Entry.Type, *Entry.Name, *TypeError);
continue; continue;
} }
@@ -114,7 +123,7 @@ public:
} }
if (NewPropGuid.IsValid()) if (NewPropGuid.IsValid())
{ {
FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, PropName); FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, Entry.Name);
} }
PropsAdded++; PropsAdded++;
} }

View File

@@ -261,15 +261,10 @@ void FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Pa
TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass)); TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass));
IMCPHandler* Handler = Cast<IMCPHandler>(HandlerObj.Get()); IMCPHandler* Handler = Cast<IMCPHandler>(HandlerObj.Get());
FString PopulateError = MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params); if (MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params, Result))
if (PopulateError.IsEmpty())
{ {
Handler->Handle(Params, Result); Handler->Handle(Params, Result);
} }
else
{
MCPUtils::MakeErrorJson(Result, PopulateError);
}
if (bIsMutation && GEditor) if (bIsMutation && GEditor)
{ {

View File

@@ -1427,22 +1427,25 @@ FString MCPUtils::SetPropertyFromJson(
*FieldName, *Prop->GetCPPType()); *FieldName, *Prop->GetCPPType());
} }
FString MCPUtils::PopulateFromJson( bool MCPUtils::PopulateFromJson(
UStruct* StructType, UStruct* StructType,
void* Container, void* Container,
const TSharedPtr<FJsonValue>& JsonValue) const TSharedPtr<FJsonValue>& JsonValue,
MCPErrorCallback Error)
{ {
if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object)) if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object))
{ {
return TEXT("Expected a JSON object"); Error.SetError(TEXT("Expected a JSON object"));
return false;
} }
return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get()); return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get(), Error);
} }
FString MCPUtils::PopulateFromJson( bool MCPUtils::PopulateFromJson(
UStruct* StructType, UStruct* StructType,
void* Container, void* Container,
const FJsonObject* Json) const FJsonObject* Json,
MCPErrorCallback Error)
{ {
// Build a set of known property names (as JSON keys) for the unknown-field check. // Build a set of known property names (as JSON keys) for the unknown-field check.
TSet<FString> KnownKeys; TSet<FString> KnownKeys;
@@ -1460,7 +1463,8 @@ FString MCPUtils::PopulateFromJson(
{ {
if (!KnownKeys.Contains(KV.Key)) if (!KnownKeys.Contains(KV.Key))
{ {
return FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key); Error.SetError(FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key));
return false;
} }
} }
@@ -1474,17 +1478,19 @@ FString MCPUtils::PopulateFromJson(
{ {
if (!bOptional) if (!bOptional)
{ {
return FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey); Error.SetError(FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey));
return false;
} }
continue; continue;
} }
FString Error = SetPropertyFromJson(Container, Prop, JsonKey, Json); FString PropError = SetPropertyFromJson(Container, Prop, JsonKey, Json);
if (!Error.IsEmpty()) if (!PropError.IsEmpty())
{ {
return Error; Error.SetError(PropError);
return false;
} }
} }
return FString(); return true;
} }

View File

@@ -149,8 +149,8 @@ public:
// ----- Property population ----- // ----- Property population -----
static FString PropertyNameToJsonKey(const FString& PropName); static FString PropertyNameToJsonKey(const FString& PropName);
static FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue); static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue, MCPErrorCallback Error);
static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json); static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json, MCPErrorCallback Error);
private: private:
static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json); static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json);