diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h index 4c80ad9c..4a3e915a 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h @@ -19,10 +19,10 @@ class UWing_GraphNode_SearchTypes : public UWingHandler GENERATED_BODY() public: - UPROPERTY(EditAnywhere, meta=(Description="Query string, can contain * wildcards")) - FString Query; + UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain * wildcards")) + FWingJsonArray Queries; - UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)")) + UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)")) int32 MaxResults = 50; UPROPERTY(EditAnywhere, meta=(Description="Target graph")) @@ -40,20 +40,38 @@ public: UEdGraph* TargetGraph = F.Walk(Graph).Cast(); if (!TargetGraph) return; - FWingGraphActions GraphActions(TargetGraph); - TArray Results = GraphActions.Search(Query, MaxResults, false); - for (const FWingGraphAction* Action : Results) + // Validate all entries are strings before running any searches. + TArray QueryStrings; + QueryStrings.Reserve(Queries.Array.Num()); + for (const TSharedPtr& QueryVal : Queries.Array) { - WingOut::Stdout.Printf(TEXT("%s\n"), *Action->Name); + FString QueryStr; + if (!QueryVal->TryGetString(QueryStr)) + { + WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n")); + return; + } + QueryStrings.Add(QueryStr); } - if (Results.Num() == 0) + FWingGraphActions GraphActions(TargetGraph); + for (const FString& Query : QueryStrings) { - WingOut::Stdout.Print(TEXT("No matching node types found.\n")); - } - else if (Results.Num() >= MaxResults) - { - WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults); + WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query); + TArray Results = GraphActions.Search(Query, MaxResults, false); + for (const FWingGraphAction* Action : Results) + { + WingOut::Stdout.Printf(TEXT("%s\n"), *Action->Name); + } + + if (Results.Num() == 0) + { + WingOut::Stdout.Print(TEXT("No matching node types found.\n")); + } + else if (Results.Num() >= MaxResults) + { + WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults); + } } } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_SearchTypes.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_SearchTypes.h index 3e68f29d..082abd2d 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_SearchTypes.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_SearchTypes.h @@ -17,10 +17,10 @@ class UWing_Widget_SearchTypes : public UWingHandler GENERATED_BODY() public: - UPROPERTY(EditAnywhere, meta=(Description="Query string, can contain *")) - FString Query; + UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain *")) + FWingJsonArray Queries; - UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)")) + UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)")) int32 MaxResults = 50; virtual void Register() override @@ -31,20 +31,38 @@ public: } virtual void Handle() override { - WingWidgets Widgets; - TArray Results = Widgets.Search(Query, MaxResults, false); - for (const WingWidgets::Type& Entry : Results) + // Validate all entries are strings before running any searches. + TArray QueryStrings; + QueryStrings.Reserve(Queries.Array.Num()); + for (const TSharedPtr& QueryVal : Queries.Array) { - WingOut::Stdout.Printf(TEXT("%s\n"), *Entry.MenuName); + FString QueryStr; + if (!QueryVal->TryGetString(QueryStr)) + { + WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n")); + return; + } + QueryStrings.Add(QueryStr); } - if (Results.Num() == 0) + WingWidgets Widgets; + for (const FString& Query : QueryStrings) { - WingOut::Stdout.Print(TEXT("No matching widget types found.\n")); - } - else if (Results.Num() >= MaxResults) - { - WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults); + WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query); + TArray Results = Widgets.Search(Query, MaxResults, false); + for (const WingWidgets::Type& Entry : Results) + { + WingOut::Stdout.Printf(TEXT("%s\n"), *Entry.MenuName); + } + + if (Results.Num() == 0) + { + WingOut::Stdout.Print(TEXT("No matching widget types found.\n")); + } + else if (Results.Num() >= MaxResults) + { + WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults); + } } } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp index 02060f1c..17369eed 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp @@ -206,7 +206,19 @@ FString UWingServer::HandleRequest(const FString& Line) { if (Result[i] == TEXT('\0')) Result[i] = TEXT(' '); } - return Result; + + // Wrap the text in an MCP content-block array: [{"type":"text","text":"..."}] + TSharedPtr Block = MakeShared(); + Block->SetStringField(TEXT("type"), TEXT("text")); + Block->SetStringField(TEXT("text"), Result); + + TArray> Blocks; + Blocks.Add(MakeShared(Block)); + + FString OutJson; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutJson); + FJsonSerializer::Serialize(Blocks, Writer); + return OutJson; } void UWingServer::TryCallHandler(const FString &Line) diff --git a/Plugins/UEWingman/ue-wingman-mcp.py b/Plugins/UEWingman/ue-wingman-mcp.py index 79a0dd21..b36edb4a 100644 --- a/Plugins/UEWingman/ue-wingman-mcp.py +++ b/Plugins/UEWingman/ue-wingman-mcp.py @@ -128,11 +128,14 @@ def handle_message(msg): arguments = params.get("arguments", {}) result = forward_to_editor(arguments) if isinstance(result, dict) and "error" in result: - text = result["error"] + content = [{"type": "text", "text": result["error"]}] else: - text = result + try: + content = json.loads(result) + except json.JSONDecodeError: + content = [{"type": "text", "text": "Malformed response from editor."}] return make_jsonrpc(msg_id, { - "content": [{"type": "text", "text": text}], + "content": content, }) return { diff --git a/Plugins/UEWingman/ue-wingman.py b/Plugins/UEWingman/ue-wingman.py index 6a3edd05..b4a5c4d6 100755 --- a/Plugins/UEWingman/ue-wingman.py +++ b/Plugins/UEWingman/ue-wingman.py @@ -57,9 +57,21 @@ def main(): try: parsed = json.loads(result) - print(json.dumps(parsed, indent=2)) except json.JSONDecodeError: - print(result) + print("Error: response is not valid JSON.") + sys.exit(1) + + if not isinstance(parsed, list): + print("Error: response is not a list of content blocks.") + sys.exit(1) + for block in parsed: + if not (isinstance(block, dict) + and block.get("type") == "text" + and isinstance(block.get("text"), str)): + print("Error: response contains a non-text block.") + sys.exit(1) + + print("\n---\n".join(block["text"] for block in parsed)) if __name__ == "__main__": main()