diff --git a/Content/Testing/BP_Test.uasset b/Content/Testing/BP_Test.uasset new file mode 100644 index 00000000..03cc8235 --- /dev/null +++ b/Content/Testing/BP_Test.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e27dd9414b234eabaf81287996097a56f2bb670a10a84f8b125233caebb5ae8 +size 30335 diff --git a/Content/Testing/BP_VarTest2.uasset b/Content/Testing/BP_VarTest2.uasset deleted file mode 100644 index ad8c8a00..00000000 --- a/Content/Testing/BP_VarTest2.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa63fd28082b228d968772edf2460867625c5dfe36accffb8d6ce9836e462bf5 -size 47196 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetArgs.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetArgs.h new file mode 100644 index 00000000..893a0adb --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetArgs.h @@ -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(); + 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)); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h index 2ba13fc4..f0aef34c 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h @@ -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(); } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/UserManual.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/UserManual.h new file mode 100644 index 00000000..fcd1bbd1 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/UserManual.h @@ -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, class, softclass" + "\n array, set, map" + "\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 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" + )); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/FunctionArgs.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/FunctionArgs.cpp new file mode 100644 index 00000000..aedca1c0 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/FunctionArgs.cpp @@ -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(Node); + if (!Editable) return false; + return Editable->IsEditable(); +} + +FString MCPFunctionArgs::GetArgs(UEdGraphNode* Node) +{ + UK2Node_EditablePinBase* Editable = Cast(Node); + if (!Editable) return FString(); + + TStringBuilder<256> SB; + for (const TSharedPtr& 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()) + return EGPD_Input; + if (UK2Node_Tunnel* Tunnel = Cast(Node)) + return Tunnel->bCanHaveInputs ? EGPD_Input : EGPD_Output; + return EGPD_Output; +} + +bool MCPFunctionArgs::ParseArgs(const FString& Args, TArray& OutArgs) +{ + FString Trimmed = Args.TrimStartAndEnd(); + if (Trimmed.IsEmpty()) return true; + + TArray 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(Node); + if (!Editable || !Editable->IsEditable()) + { + UMCPServer::Printf(TEXT("ERROR: Node does not support editable pins\n")); + return false; + } + + // Parse the args string. + TArray 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 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; +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/GraphExport.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/GraphExport.cpp index 063147c4..94363836 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/GraphExport.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/GraphExport.cpp @@ -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. diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp index c418cbf1..f4e2bc75 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp @@ -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) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp index cd8b6c9b..8e4f69fa 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp @@ -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, class, softclass\n")); - UMCPServer::Printf(TEXT(" array, set, map\n")); - UMCPServer::Printf(TEXT("\n")); -} \ No newline at end of file diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/FunctionArgs.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/FunctionArgs.h new file mode 100644 index 00000000..0070d4ad --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/FunctionArgs.h @@ -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& OutArgs); + + // Determine the pin direction for user-defined pins on this node. + static EEdGraphPinDirection GetPinDirection(UK2Node_EditablePinBase* Node); +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h index 79538eec..33dbcbdf 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h @@ -125,9 +125,6 @@ public: // MCPFetcher(UObject* O) : Obj(O) {} - // Print out the documentation for paths, for the LLM. - // - static void PrintDocs(); private: diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h index 3b583a7d..7e93f6c1 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h @@ -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); diff --git a/tools/mcp-bridge.py b/tools/mcp-bridge.py index 1a519fca..527348ee 100644 --- a/tools/mcp-bridge.py +++ b/tools/mcp-bridge.py @@ -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." )