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"))
class UMCPHandler_SetBlendSpaceSamples : public UObject, public IMCPHandler
{
@@ -410,22 +425,18 @@ public:
for (const TSharedPtr<FJsonValue>& SampleVal : Samples.Array)
{
const TSharedPtr<FJsonObject>& SampleObj = SampleVal->AsObject();
if (!SampleObj.IsValid()) continue;
FString AnimAssetName = SampleObj->GetStringField(TEXT("animationAsset"));
float X = (float)SampleObj->GetNumberField(TEXT("x"));
float Y = (float)SampleObj->GetNumberField(TEXT("y"));
FBlendSpaceSampleEntry Entry;
if (!MCPUtils::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal, Result)) return;
UAnimSequence* AnimSeq = nullptr;
if (!AnimAssetName.IsEmpty())
if (!Entry.AnimationAsset.IsEmpty())
{
MCPAssets<UAnimSequence> AnimAssets;
if (AnimAssets.Exact(AnimAssetName).Load())
if (AnimAssets.Exact(Entry.AnimationAsset).Load())
AnimSeq = AnimAssets.Object();
}
FVector SampleValue(X, Y, 0.0f);
FVector SampleValue(Entry.X, Entry.Y, 0.0f);
if (AnimSeq)
{
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"))
class UMCPHandler_AddEventDispatcher : public UObject, public IMCPHandler
{
@@ -117,23 +129,19 @@ public:
for (const TSharedPtr<FJsonValue>& ParamVal : Parameters.Array)
{
if (!ParamVal.IsValid() || ParamVal->Type != EJson::Object) continue;
TSharedPtr<FJsonObject> ParamObj = ParamVal->AsObject();
FString ParamName = ParamObj->GetStringField(TEXT("name"));
FString ParamType = ParamObj->GetStringField(TEXT("type"));
if (ParamName.IsEmpty() || ParamType.IsEmpty()) continue;
FDispatcherParamEntry Entry;
if (!MCPUtils::PopulateFromJson(FDispatcherParamEntry::StaticStruct(), &Entry, ParamVal, Result)) return;
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
FEdGraphPinType PinType;
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, Result))
return;
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
EntryNode->CreateUserDefinedPin(FName(*Entry.Name), PinType, EGPD_Output);
TSharedRef<FJsonObject> ParamJson = MakeShared<FJsonObject>();
ParamJson->SetStringField(TEXT("name"), ParamName);
ParamJson->SetStringField(TEXT("type"), ParamType);
ParamJson->SetStringField(TEXT("name"), Entry.Name);
ParamJson->SetStringField(TEXT("type"), Entry.Type);
AddedParamsJson.Add(MakeShared<FJsonValueObject>(ParamJson));
}
}

View File

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

View File

@@ -63,12 +63,7 @@ public:
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FSetPinDefaultEntry Entry;
FString PopulateError = MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal);
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
}
if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, &*EntryResult)) continue;
MCPAssets<UBlueprint> Assets;
if (!Assets.Scan<UBlueprint>().Scan<UWorld>().Exact(Entry.Blueprint).Errors(&*EntryResult).ENone().ETwo().Load())
@@ -192,12 +187,7 @@ public:
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FConnectPinsEntry Entry;
FString PopulateError = MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal);
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
}
if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, &*EntryResult)) continue;
UEdGraph* SourceGraph = nullptr;
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph);
@@ -319,12 +309,7 @@ public:
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FDisconnectPinEntry Entry;
FString PopulateError = MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal);
if (!PopulateError.IsEmpty())
{
EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
}
if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal, &*EntryResult)) continue;
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.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"))
class UMCPHandler_CreateStruct : public UObject, public IMCPHandler
{
@@ -77,18 +89,15 @@ public:
int32 PropsAdded = 0;
for (const TSharedPtr<FJsonValue>& PropVal : Properties.Array)
{
TSharedPtr<FJsonObject> PropObj = PropVal->AsObject();
if (!PropObj) continue;
FString PropName = PropObj->GetStringField(TEXT("name"));
FString PropType = PropObj->GetStringField(TEXT("type"));
if (PropName.IsEmpty() || PropType.IsEmpty()) continue;
FStructPropertyEntry Entry;
if (!MCPUtils::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal, Result)) return;
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
FEdGraphPinType PinType;
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;
}
@@ -114,7 +123,7 @@ public:
}
if (NewPropGuid.IsValid())
{
FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, PropName);
FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, Entry.Name);
}
PropsAdded++;
}

View File

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

View File

@@ -1427,22 +1427,25 @@ FString MCPUtils::SetPropertyFromJson(
*FieldName, *Prop->GetCPPType());
}
FString MCPUtils::PopulateFromJson(
bool MCPUtils::PopulateFromJson(
UStruct* StructType,
void* Container,
const TSharedPtr<FJsonValue>& JsonValue)
const TSharedPtr<FJsonValue>& JsonValue,
MCPErrorCallback Error)
{
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,
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.
TSet<FString> KnownKeys;
@@ -1460,7 +1463,8 @@ FString MCPUtils::PopulateFromJson(
{
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)
{
return FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey);
Error.SetError(FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey));
return false;
}
continue;
}
FString Error = SetPropertyFromJson(Container, Prop, JsonKey, Json);
if (!Error.IsEmpty())
FString PropError = SetPropertyFromJson(Container, Prop, JsonKey, Json);
if (!PropError.IsEmpty())
{
return Error;
Error.SetError(PropError);
return false;
}
}
return FString();
return true;
}

View File

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