MCP can now control function args

This commit is contained in:
2026-03-17 13:16:48 -04:00
parent b58532324a
commit a1131130af
13 changed files with 284 additions and 48 deletions

BIN
Content/Testing/BP_Test.uasset LFS Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,44 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPServer.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "FunctionArgs.h"
#include "GraphNode_SetArgs.generated.h"
UCLASS()
class UMCP_GraphNode_SetArgs : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a graph node (function entry, function result, custom event, or tunnel)"))
FString Node;
UPROPERTY(meta=(Description="Comma-separated args, e.g. 'int x, float y'"))
FString Args;
virtual FString GetDescription() const override
{
return TEXT("Set the user-defined pins (arguments or return values) on a function entry, result, custom event, or tunnel node.");
}
virtual void Handle() override
{
MCPFetcher F;
UEdGraphNode* NodeObj = F.Walk(Node).Cast<UEdGraphNode>();
if (!NodeObj) return;
if (!MCPFunctionArgs::HasArgs(NodeObj))
{
UMCPServer::Printf(TEXT("ERROR: Node does not support editable args\n"));
return;
}
if (!MCPFunctionArgs::SetArgs(NodeObj, Args)) return;
UMCPServer::Printf(TEXT("Args set to: %s\n"), *MCPFunctionArgs::GetArgs(NodeObj));
}
};

View File

@@ -81,7 +81,5 @@ public:
UMCPServer::Print(TEXT("\n--- Half-Baked (may have issues) ---\n\n"));
EmitCommandList(true);
UMCPServer::Printf(TEXT("\n"));
MCPFetcher::PrintDocs();
UMCPTypes::PrintDocs();
}
};

View File

@@ -0,0 +1,86 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPServer.h"
#include "UserManual.generated.h"
UCLASS()
class UMCP_UserManual : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
virtual FString GetDescription() const override
{
return TEXT("Print the user manual.");
}
virtual void Handle() override
{
UMCPServer::Print(TEXT(
"\n PATHS:"
"\n"
"\n Most commands require you to specify a path. A path starts"
"\n with an asset name, followed by comma-separated steps that"
"\n navigate into the asset. Example:"
"\n"
"\n /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self03,pin:Result"
"\n"
"\n The navigation steps supported are:"
"\n"
"\n graph — move from a blueprint or material to a graph."
"\n node — move from a graph to a graph node"
"\n pin — move from a graph node to a pin"
"\n component — move from a blueprint to a component"
"\n levelblueprint — move from a world to a blueprint"
"\n"
"\n DUMP COMMANDS:"
"\n"
"\n There are several commands whose names end in 'Dump'. These"
"\n are essential tools for viewing the state of the world."
"\n They are particularly important because they show you what"
"\n unique IDs exist that you can refer to."
"\n"
"\n TYPES:"
"\n"
"\n To change variable types, or function prototypes, you will"
"\n use our syntax for types. Here are some simple examples:"
"\n"
"\n boolean, int64, double, string, etc"
"\n vector, rotator, hitresult, etc"
"\n actor, character, playercontroller, etc"
"\n eblendmode, emovementmode, etc"
"\n"
"\n Notice that it's 'actor', not 'AActor'."
"\n You can use the following notations for complex types:"
"\n"
"\n soft<abp_manny>, class<pawn>, softclass<pawn>"
"\n array<int>, set<string>, map<int, string>"
"\n"
"\n FUNCTION ARGUMENTS AND RETURN VALUES:"
"\n"
"\n Function argument lists are expressed as comma-separated"
"\n lists containing type and variable name:"
"\n"
"\n double D, PlayerController P, array<int> A"
"\n"
"\n To change the arguments or return values of a function, edit the"
"\n entry or exit node of the graph using GraphNode_SetArgs."
"\n You can view the arguments using GraphNode_Dump. If a return "
"\n node doesn't exist, you may have to create it using GraphNode_Create"
"\n before you can set return values. Custom event nodes also have"
"\n editable arguments."
"\n"
"\n MATERIAL EDITING:"
"\n"
"\n We do not expose material expressions directly. Instead, you"
"\n will be editing the material graph. However, if you Graph_Dump"
"\n a material graph, you will see that the nodes contain mxprop"
"\n properties, which actually come from the material expressions."
"\n You can edit these using Property_Set on the node."
"\n"
"\n"
));
}
};

View File

@@ -0,0 +1,107 @@
#include "FunctionArgs.h"
#include "K2Node_EditablePinBase.h"
#include "K2Node_FunctionResult.h"
#include "K2Node_Tunnel.h"
#include "MCPTypes.h"
#include "MCPServer.h"
bool MCPFunctionArgs::HasArgs(UEdGraphNode* Node)
{
UK2Node_EditablePinBase* Editable = Cast<UK2Node_EditablePinBase>(Node);
if (!Editable) return false;
return Editable->IsEditable();
}
FString MCPFunctionArgs::GetArgs(UEdGraphNode* Node)
{
UK2Node_EditablePinBase* Editable = Cast<UK2Node_EditablePinBase>(Node);
if (!Editable) return FString();
TStringBuilder<256> SB;
for (const TSharedPtr<FUserPinInfo>& Pin : Editable->UserDefinedPins)
{
if (SB.Len() > 0) SB << TEXT(", ");
SB << UMCPTypes::TypeToText(Pin->PinType) << TEXT(" ") << Pin->PinName.ToString();
}
return FString(SB);
}
EEdGraphPinDirection MCPFunctionArgs::GetPinDirection(UK2Node_EditablePinBase* Node)
{
// FunctionResult takes inputs; Tunnel depends on its flags; everything else outputs.
if (Node->IsA<UK2Node_FunctionResult>())
return EGPD_Input;
if (UK2Node_Tunnel* Tunnel = Cast<UK2Node_Tunnel>(Node))
return Tunnel->bCanHaveInputs ? EGPD_Input : EGPD_Output;
return EGPD_Output;
}
bool MCPFunctionArgs::ParseArgs(const FString& Args, TArray<FParsedArg>& OutArgs)
{
FString Trimmed = Args.TrimStartAndEnd();
if (Trimmed.IsEmpty()) return true;
TArray<FString> Parts;
Trimmed.ParseIntoArray(Parts, TEXT(","));
for (const FString& Part : Parts)
{
FString Token = Part.TrimStartAndEnd();
if (Token.IsEmpty()) continue;
// Split "type name" on the last space.
int32 LastSpace;
if (!Token.FindLastChar(TEXT(' '), LastSpace))
{
UMCPServer::Printf(TEXT("ERROR: Expected 'type name' but got '%s'\n"), *Token);
return false;
}
FString TypeStr = Token.Left(LastSpace).TrimStartAndEnd();
FString NameStr = Token.Mid(LastSpace + 1).TrimStartAndEnd();
if (TypeStr.IsEmpty() || NameStr.IsEmpty())
{
UMCPServer::Printf(TEXT("ERROR: Expected 'type name' but got '%s'\n"), *Token);
return false;
}
FParsedArg Arg;
if (!UMCPTypes::TextToType(TypeStr, Arg.PinType)) return false;
Arg.PinName = FName(*NameStr);
OutArgs.Add(MoveTemp(Arg));
}
return true;
}
bool MCPFunctionArgs::SetArgs(UEdGraphNode* Node, const FString& Args)
{
UK2Node_EditablePinBase* Editable = Cast<UK2Node_EditablePinBase>(Node);
if (!Editable || !Editable->IsEditable())
{
UMCPServer::Printf(TEXT("ERROR: Node does not support editable pins\n"));
return false;
}
// Parse the args string.
TArray<FParsedArg> NewArgs;
if (!ParseArgs(Args, NewArgs)) return false;
EEdGraphPinDirection Direction = GetPinDirection(Editable);
// Replace the UserDefinedPins array directly.
Editable->UserDefinedPins.Empty();
for (const FParsedArg& Arg : NewArgs)
{
TSharedPtr<FUserPinInfo> PinInfo = MakeShareable(new FUserPinInfo());
PinInfo->PinName = Arg.PinName;
PinInfo->PinType = Arg.PinType;
PinInfo->DesiredPinDirection = Direction;
Editable->UserDefinedPins.Add(PinInfo);
}
// ReconstructNode rebuilds real pins from UserDefinedPins
// and rewires old connections by matching pin names.
Editable->ReconstructNode();
return true;
}

View File

@@ -11,6 +11,7 @@
#include "K2Node_VariableGet.h"
#include "K2Node_CallFunction.h"
#include "K2Node_FunctionEntry.h"
#include "FunctionArgs.h"
#include "MaterialGraph/MaterialGraphNode.h"
MCPGraphExport::MCPGraphExport(UEdGraph* InGraph)
@@ -213,6 +214,10 @@ void MCPGraphExport::EmitNode(UEdGraphNode* Node)
Output.Appendf(TEXT("\nnode %s: %s\n"), *MCPUtils::FormatName(Node), *MCPUtils::FormatNodeTitle(Node));
// Emit function args (if applicable).
if (MCPFunctionArgs::HasArgs(Node))
Output.Appendf(TEXT(" args %s\n"), *MCPFunctionArgs::GetArgs(Node));
// Emit material expression properties (if applicable).
EmitMaterialProperties(Node, Output, true);
@@ -239,7 +244,7 @@ void MCPGraphExport::EmitNode(UEdGraphNode* Node)
}
if (!ReturnPins.IsEmpty())
{
Output.Appendf(TEXT(" return %s\n"), *ReturnPins);
Output.Appendf(TEXT(" output-pins %s\n"), *ReturnPins);
}
// Emit output exec pins as goto statements.

View File

@@ -25,27 +25,6 @@ MCPFetcher::WalkFunc MCPFetcher::GetWalker(const FString& Step)
return nullptr;
}
void MCPFetcher::PrintDocs()
{
UMCPServer::Print(TEXT("Most commands require you to specify a path. A path starts\n"));
UMCPServer::Print(TEXT("with an asset name, followed by comma-separated steps that\n"));
UMCPServer::Print(TEXT("navigate into the asset. Example:\n"));
UMCPServer::Print(TEXT("\n"));
UMCPServer::Print(TEXT(" /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self03,pin:Result\n"));
UMCPServer::Print(TEXT("\n"));
UMCPServer::Print(TEXT("The navigation steps supported are:\n"));
UMCPServer::Print(TEXT("\n"));
UMCPServer::Print(TEXT(" graph — move from a blueprint or material to a graph.\n"));
UMCPServer::Print(TEXT(" node — move from a graph to a graph node\n"));
UMCPServer::Print(TEXT(" pin — move from a graph node to a pin\n"));
UMCPServer::Print(TEXT(" component — move from a blueprint to a component\n"));
UMCPServer::Print(TEXT(" levelblueprint — move from a world to a blueprint\n"));
UMCPServer::Print(TEXT("\n"));
UMCPServer::Print(TEXT("It is often useful to use 'dump' commands to see the contents\n"));
UMCPServer::Print(TEXT("of an asset, so you know what objects exist to navigate into.\n"));
UMCPServer::Print(TEXT("\n"));
}
void MCPFetcher::SetObj(UObject* InObj)
{

View File

@@ -522,19 +522,3 @@ UClass* UMCPTypes::TextToOneInterfaceType(const FString& Text)
return Class;
}
void UMCPTypes::PrintDocs()
{
UMCPServer::Printf(TEXT("To express types, use case-insensitive short names:\n"));
UMCPServer::Printf(TEXT("\n"));
UMCPServer::Printf(TEXT(" boolean, int64, double, string, etc\n"));
UMCPServer::Printf(TEXT(" vector, rotator, hitresult, etc\n"));
UMCPServer::Printf(TEXT(" actor, character, playercontroller, etc\n"));
UMCPServer::Printf(TEXT(" eblendmode, emovementmode, etc\n"));
UMCPServer::Printf(TEXT("\n"));
UMCPServer::Printf(TEXT("Notice that it's 'actor', not 'AActor'.\n"));
UMCPServer::Printf(TEXT("You can use the following notations for complex types:\n"));
UMCPServer::Printf(TEXT("\n"));
UMCPServer::Printf(TEXT(" soft<abp_manny>, class<pawn>, softclass<pawn>\n"));
UMCPServer::Printf(TEXT(" array<int>, set<string>, map<int, string>\n"));
UMCPServer::Printf(TEXT("\n"));
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "CoreMinimal.h"
class UEdGraphNode;
class UK2Node_EditablePinBase;
struct FEdGraphPinType;
struct MCPFunctionArgs
{
// Returns true if the node is an EditablePinBase subclass that
// actually supports user-defined pins (FunctionEntry, FunctionResult,
// CustomEvent, or Tunnel).
static bool HasArgs(UEdGraphNode* Node);
// Returns the user-defined pins as a string like "int x, float y".
static FString GetArgs(UEdGraphNode* Node);
// Sets the user-defined pins from a string like "int x, float y".
// Returns true on success.
static bool SetArgs(UEdGraphNode* Node, const FString& Args);
private:
// A parsed argument: type + name.
struct FParsedArg
{
FEdGraphPinType PinType;
FName PinName;
};
// Parse "int x, float y" into an array of FParsedArg.
// Returns false and prints an error on failure.
static bool ParseArgs(const FString& Args, TArray<FParsedArg>& OutArgs);
// Determine the pin direction for user-defined pins on this node.
static EEdGraphPinDirection GetPinDirection(UK2Node_EditablePinBase* Node);
};

View File

@@ -125,9 +125,6 @@ public:
//
MCPFetcher(UObject* O) : Obj(O) {}
// Print out the documentation for paths, for the LLM.
//
static void PrintDocs();
private:

View File

@@ -42,8 +42,6 @@ public:
// no container, no wrapper). Returns nullptr and prints error on failure.
static UClass* TextToOneInterfaceType(const FString& Text);
// Print the documentation for how types are expressed, for the LLM.
static void PrintDocs();
private:
FString TypeToTextInner(FName Category, FName SubCategory, UObject* SubCategoryObject);

View File

@@ -21,6 +21,7 @@ TOOL_DESCRIPTION = (
"The 'command' field specifies which operation to perform; "
"additional fields are command-specific parameters. "
'Use {"command": "ShowCommands"} to list available commands. '
'Use {"command": "UserManual"} to get an overview. '
"If the editor is not running, the call will return an error; "
"just ask the user to start the editor and try again."
)