ultimate-unreal-engine-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +729 -0
- package/dist/build/error-parser.js +51 -0
- package/dist/build/fix-suggester.js +84 -0
- package/dist/build/ubt-runner.js +146 -0
- package/dist/cli.js +13 -0
- package/dist/config.js +8 -0
- package/dist/docs/data/ue57-api.js +228 -0
- package/dist/docs/doc-index.js +110 -0
- package/dist/docs/types.js +4 -0
- package/dist/generators/class-generator.js +363 -0
- package/dist/generators/file-modifier.js +276 -0
- package/dist/generators/uht-validator.js +177 -0
- package/dist/index.js +89 -0
- package/dist/parsers/cpp-class-index.js +230 -0
- package/dist/parsers/cpp-parser.js +369 -0
- package/dist/parsers/ini-parser.js +216 -0
- package/dist/parsers/uproject-parser.js +130 -0
- package/dist/plugin-bridge/client.js +217 -0
- package/dist/plugin-bridge/protocol.js +6 -0
- package/dist/plugin-bridge/retry.js +23 -0
- package/dist/setup.js +209 -0
- package/dist/tools/ai-systems/index.js +247 -0
- package/dist/tools/ai-systems/types.js +4 -0
- package/dist/tools/animation/index.js +241 -0
- package/dist/tools/animation/types.js +4 -0
- package/dist/tools/audio/index.js +204 -0
- package/dist/tools/audio/types.js +4 -0
- package/dist/tools/blueprint/index.js +495 -0
- package/dist/tools/blueprint/types.js +4 -0
- package/dist/tools/build/index.js +163 -0
- package/dist/tools/chaos/index.js +230 -0
- package/dist/tools/chaos/types.js +4 -0
- package/dist/tools/collision-physics/index.js +211 -0
- package/dist/tools/config/index.js +288 -0
- package/dist/tools/cpp/index.js +305 -0
- package/dist/tools/docs/index.js +251 -0
- package/dist/tools/editor/index.js +242 -0
- package/dist/tools/gas/index.js +222 -0
- package/dist/tools/gas/types.js +5 -0
- package/dist/tools/import-export/index.js +218 -0
- package/dist/tools/input/index.js +146 -0
- package/dist/tools/known-issues/index.js +88 -0
- package/dist/tools/known-issues/middleware.js +55 -0
- package/dist/tools/known-issues/store.js +125 -0
- package/dist/tools/livelink/index.js +203 -0
- package/dist/tools/livelink/types.js +4 -0
- package/dist/tools/material/index.js +190 -0
- package/dist/tools/motion-design/index.js +251 -0
- package/dist/tools/motion-design/types.js +6 -0
- package/dist/tools/movie-render/index.js +220 -0
- package/dist/tools/networking/index.js +149 -0
- package/dist/tools/pcg/index.js +164 -0
- package/dist/tools/selection/index.js +180 -0
- package/dist/tools/sequencer/index.js +218 -0
- package/dist/tools/validation/index.js +183 -0
- package/dist/tools/validation/types.js +4 -0
- package/dist/tools/viewport/index.js +310 -0
- package/dist/tools/worldpartition/index.js +226 -0
- package/dist/tools/worldpartition/types.js +4 -0
- package/dist/utils/execFileNoThrow.js +40 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/path-guard.js +26 -0
- package/package.json +40 -0
- package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPTcpServer.h +59 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
// MCPBlueprintWriteHandlers.cpp
|
|
2
|
+
// Implements blueprint.create, blueprint.addNode, blueprint.connectPins,
|
|
3
|
+
// blueprint.addVariable, and blueprint.setDefault command handlers.
|
|
4
|
+
// All handlers are registered on the FMCPCommandRouter and run on the game thread
|
|
5
|
+
// (enforced by FMCPCommandRouter::Dispatch via AsyncTask).
|
|
6
|
+
//
|
|
7
|
+
// INVARIANT: Every write handler calls BP->Modify() before any mutation and
|
|
8
|
+
// FBlueprintEditorUtils::MarkBlueprintAsModified(BP) after -- never omit these.
|
|
9
|
+
//
|
|
10
|
+
// Security mitigations per threat model (T-10-01 through T-10-06):
|
|
11
|
+
// T-10-01: asset_path validated via FPackageName::IsValidLongPackageName()
|
|
12
|
+
// T-10-02: node_type class checked for UEdGraphNode ancestry + non-abstract
|
|
13
|
+
// T-10-04: CanCreateConnection() checked before TryCreateConnection()
|
|
14
|
+
// T-10-05: Named error codes returned, never raw UE internal messages
|
|
15
|
+
// T-10-06: ParentClass verified via IsChildOf(UObject::StaticClass())
|
|
16
|
+
|
|
17
|
+
#include "MCPBlueprintWriteHandlers.h"
|
|
18
|
+
|
|
19
|
+
#include "MCPCommandRouter.h"
|
|
20
|
+
|
|
21
|
+
// Blueprint APIs
|
|
22
|
+
#include "Engine/Blueprint.h"
|
|
23
|
+
#include "Engine/BlueprintGeneratedClass.h"
|
|
24
|
+
#include "Kismet2/KismetEditorUtilities.h"
|
|
25
|
+
#include "Kismet2/BlueprintEditorUtils.h"
|
|
26
|
+
#include "EdGraph/EdGraph.h"
|
|
27
|
+
#include "EdGraph/EdGraphNode.h"
|
|
28
|
+
#include "EdGraph/EdGraphPin.h"
|
|
29
|
+
#include "EdGraphSchema_K2.h"
|
|
30
|
+
|
|
31
|
+
// Asset APIs
|
|
32
|
+
#include "AssetRegistry/AssetRegistryModule.h"
|
|
33
|
+
#include "UObject/Package.h"
|
|
34
|
+
#include "Misc/PackageName.h"
|
|
35
|
+
|
|
36
|
+
// JSON APIs
|
|
37
|
+
#include "Dom/JsonObject.h"
|
|
38
|
+
#include "Serialization/JsonSerializer.h"
|
|
39
|
+
#include "Serialization/JsonWriter.h"
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Internal helper: find a named graph in a Blueprint (same pattern as
|
|
43
|
+
// MCPBlueprintHandlers.cpp blueprint.graph handler).
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
static UEdGraph* FindBlueprintGraph(UBlueprint* BP, const FString& GraphName)
|
|
46
|
+
{
|
|
47
|
+
for (UEdGraph* Graph : BP->UbergraphPages)
|
|
48
|
+
{
|
|
49
|
+
if (Graph && Graph->GetName() == GraphName)
|
|
50
|
+
{
|
|
51
|
+
return Graph;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (UEdGraph* Graph : BP->FunctionGraphs)
|
|
55
|
+
{
|
|
56
|
+
if (Graph && Graph->GetName() == GraphName)
|
|
57
|
+
{
|
|
58
|
+
return Graph;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Default to first ubergraph when "EventGraph" is requested and not found by name
|
|
62
|
+
if (GraphName == TEXT("EventGraph") && BP->UbergraphPages.Num() > 0)
|
|
63
|
+
{
|
|
64
|
+
return BP->UbergraphPages[0];
|
|
65
|
+
}
|
|
66
|
+
return nullptr;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void RegisterBlueprintWriteHandlers(FMCPCommandRouter& Router)
|
|
70
|
+
{
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Shared response helpers -- verbatim pattern from MCPBlueprintHandlers.cpp.
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
auto SendSuccess = [](FMCPResponseSender SendResponse,
|
|
76
|
+
const FString& CorrelationId,
|
|
77
|
+
TSharedPtr<FJsonObject> Data)
|
|
78
|
+
{
|
|
79
|
+
TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
|
|
80
|
+
Response->SetBoolField(TEXT("success"), true);
|
|
81
|
+
if (!CorrelationId.IsEmpty())
|
|
82
|
+
{
|
|
83
|
+
Response->SetStringField(TEXT("correlationId"), CorrelationId);
|
|
84
|
+
}
|
|
85
|
+
Response->SetObjectField(TEXT("data"), Data);
|
|
86
|
+
|
|
87
|
+
FString Output;
|
|
88
|
+
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
|
|
89
|
+
FJsonSerializer::Serialize(Response.ToSharedRef(), Writer);
|
|
90
|
+
Output += TEXT("\n");
|
|
91
|
+
SendResponse(Output);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
auto SendError = [](FMCPResponseSender SendResponse,
|
|
95
|
+
const FString& CorrelationId,
|
|
96
|
+
const FString& ErrorCode)
|
|
97
|
+
{
|
|
98
|
+
TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
|
|
99
|
+
Response->SetBoolField(TEXT("success"), false);
|
|
100
|
+
if (!CorrelationId.IsEmpty())
|
|
101
|
+
{
|
|
102
|
+
Response->SetStringField(TEXT("correlationId"), CorrelationId);
|
|
103
|
+
}
|
|
104
|
+
Response->SetStringField(TEXT("error"), ErrorCode);
|
|
105
|
+
|
|
106
|
+
FString Output;
|
|
107
|
+
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
|
|
108
|
+
FJsonSerializer::Serialize(Response.ToSharedRef(), Writer);
|
|
109
|
+
Output += TEXT("\n");
|
|
110
|
+
SendResponse(Output);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// blueprint.create (BPW-01)
|
|
115
|
+
// Input payload: { "parent_class": "AActor", "asset_path": "/Game/Blueprints/BP_MyActor" }
|
|
116
|
+
// Returns: { "assetPath": string, "parentClass": string }
|
|
117
|
+
//
|
|
118
|
+
// Security: T-10-01 (path validation), T-10-06 (parent class verification)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
Router.RegisterHandler(TEXT("blueprint.create"),
|
|
121
|
+
[SendSuccess, SendError](TSharedPtr<FJsonObject> Command, FMCPResponseSender SendResponse)
|
|
122
|
+
{
|
|
123
|
+
FString CorrelationId;
|
|
124
|
+
Command->TryGetStringField(TEXT("correlationId"), CorrelationId);
|
|
125
|
+
|
|
126
|
+
const TSharedPtr<FJsonObject>* PayloadObj = nullptr;
|
|
127
|
+
if (!Command->TryGetObjectField(TEXT("payload"), PayloadObj))
|
|
128
|
+
{
|
|
129
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_payload"));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
FString ParentClassName;
|
|
134
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("parent_class"), ParentClassName) || ParentClassName.IsEmpty())
|
|
135
|
+
{
|
|
136
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_parent_class"));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
FString AssetPath;
|
|
141
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
|
|
142
|
+
{
|
|
143
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_asset_path"));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// T-10-01: Validate the asset path is a well-formed long package name.
|
|
148
|
+
// Rejects path traversal ("../") and bare filenames.
|
|
149
|
+
FString ValidationError;
|
|
150
|
+
if (!FPackageName::IsValidLongPackageName(AssetPath, /*bIncludeReadOnlyRoots=*/false, &ValidationError))
|
|
151
|
+
{
|
|
152
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.create: Invalid asset path '%s': %s"),
|
|
153
|
+
*AssetPath, *ValidationError);
|
|
154
|
+
SendError(SendResponse, CorrelationId, TEXT("invalid_asset_path"));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Resolve the parent class by name (try short name first, then full path).
|
|
159
|
+
UClass* ParentClass = FindObject<UClass>(ANY_PACKAGE, *ParentClassName);
|
|
160
|
+
if (!ParentClass)
|
|
161
|
+
{
|
|
162
|
+
ParentClass = LoadObject<UClass>(nullptr, *ParentClassName);
|
|
163
|
+
}
|
|
164
|
+
if (!ParentClass)
|
|
165
|
+
{
|
|
166
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.create: Parent class '%s' not found"),
|
|
167
|
+
*ParentClassName);
|
|
168
|
+
SendError(SendResponse, CorrelationId, TEXT("parent_class_not_found"));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// T-10-06: Verify the resolved class is a proper UE class derived from UObject,
|
|
173
|
+
// not an arbitrary object that happens to match the name.
|
|
174
|
+
if (!ParentClass->IsChildOf(UObject::StaticClass()))
|
|
175
|
+
{
|
|
176
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.create: Class '%s' is not a UObject subclass"),
|
|
177
|
+
*ParentClassName);
|
|
178
|
+
SendError(SendResponse, CorrelationId, TEXT("invalid_parent_class"));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Split asset path into package path + asset name.
|
|
183
|
+
// AssetPath = "/Game/Blueprints/BP_MyActor" → AssetName = "BP_MyActor"
|
|
184
|
+
FString AssetName = FPackageName::GetLongPackageAssetName(AssetPath);
|
|
185
|
+
|
|
186
|
+
// Create (or retrieve) the package for this asset.
|
|
187
|
+
UPackage* Pkg = CreatePackage(*AssetPath);
|
|
188
|
+
if (!Pkg)
|
|
189
|
+
{
|
|
190
|
+
UE_LOG(LogTemp, Error, TEXT("[MCPBridge] blueprint.create: Failed to create package for '%s'"),
|
|
191
|
+
*AssetPath);
|
|
192
|
+
SendError(SendResponse, CorrelationId, TEXT("package_create_failed"));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
Pkg->FullyLoad();
|
|
196
|
+
|
|
197
|
+
// Create the Blueprint asset.
|
|
198
|
+
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(
|
|
199
|
+
ParentClass,
|
|
200
|
+
Pkg,
|
|
201
|
+
FName(*AssetName),
|
|
202
|
+
BPTYPE_Normal,
|
|
203
|
+
UBlueprint::StaticClass(),
|
|
204
|
+
UBlueprintGeneratedClass::StaticClass()
|
|
205
|
+
);
|
|
206
|
+
if (!NewBP)
|
|
207
|
+
{
|
|
208
|
+
UE_LOG(LogTemp, Error, TEXT("[MCPBridge] blueprint.create: FKismetEditorUtilities::CreateBlueprint failed for '%s'"),
|
|
209
|
+
*AssetPath);
|
|
210
|
+
SendError(SendResponse, CorrelationId, TEXT("create_failed"));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Save the package to disk so the asset persists.
|
|
215
|
+
FString FilePath = FPackageName::LongPackageNameToFilename(
|
|
216
|
+
AssetPath, FPackageName::GetAssetPackageExtension());
|
|
217
|
+
UPackage::SavePackage(Pkg, NewBP, RF_Standalone, *FilePath,
|
|
218
|
+
GError, nullptr, false, true, SAVE_NoError);
|
|
219
|
+
|
|
220
|
+
// Notify the Asset Registry so the asset appears in the Content Browser.
|
|
221
|
+
FAssetRegistryModule::AssetCreated(NewBP);
|
|
222
|
+
|
|
223
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
224
|
+
Data->SetStringField(TEXT("assetPath"), AssetPath);
|
|
225
|
+
Data->SetStringField(TEXT("parentClass"), ParentClassName);
|
|
226
|
+
SendSuccess(SendResponse, CorrelationId, Data);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// blueprint.addNode (BPW-02)
|
|
231
|
+
// Input payload:
|
|
232
|
+
// { "asset_path": "/Game/BP_X", "graph_name": "EventGraph",
|
|
233
|
+
// "node_type": "K2Node_CallFunction", "pos_x": 0, "pos_y": 0 }
|
|
234
|
+
// Returns: { "nodeGuid": string, "type": string, "posX": int, "posY": int }
|
|
235
|
+
//
|
|
236
|
+
// Security: T-10-02 (node class ancestry check + abstract guard)
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
Router.RegisterHandler(TEXT("blueprint.addNode"),
|
|
239
|
+
[SendSuccess, SendError](TSharedPtr<FJsonObject> Command, FMCPResponseSender SendResponse)
|
|
240
|
+
{
|
|
241
|
+
FString CorrelationId;
|
|
242
|
+
Command->TryGetStringField(TEXT("correlationId"), CorrelationId);
|
|
243
|
+
|
|
244
|
+
const TSharedPtr<FJsonObject>* PayloadObj = nullptr;
|
|
245
|
+
if (!Command->TryGetObjectField(TEXT("payload"), PayloadObj))
|
|
246
|
+
{
|
|
247
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_payload"));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
FString AssetPath;
|
|
252
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
|
|
253
|
+
{
|
|
254
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_asset_path"));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
FString NodeType;
|
|
259
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("node_type"), NodeType) || NodeType.IsEmpty())
|
|
260
|
+
{
|
|
261
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_node_type"));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
FString GraphName = TEXT("EventGraph");
|
|
266
|
+
(*PayloadObj)->TryGetStringField(TEXT("graph_name"), GraphName);
|
|
267
|
+
|
|
268
|
+
int32 PosX = 0;
|
|
269
|
+
int32 PosY = 0;
|
|
270
|
+
(*PayloadObj)->TryGetNumberField(TEXT("pos_x"), PosX);
|
|
271
|
+
(*PayloadObj)->TryGetNumberField(TEXT("pos_y"), PosY);
|
|
272
|
+
|
|
273
|
+
UBlueprint* BP = LoadObject<UBlueprint>(nullptr, *AssetPath);
|
|
274
|
+
if (!BP)
|
|
275
|
+
{
|
|
276
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.addNode: Blueprint not found at '%s'"), *AssetPath);
|
|
277
|
+
SendError(SendResponse, CorrelationId, TEXT("blueprint_not_found"));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
UEdGraph* TargetGraph = FindBlueprintGraph(BP, GraphName);
|
|
282
|
+
if (!TargetGraph)
|
|
283
|
+
{
|
|
284
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.addNode: Graph '%s' not found in '%s'"),
|
|
285
|
+
*GraphName, *AssetPath);
|
|
286
|
+
SendError(SendResponse, CorrelationId, TEXT("graph_not_found"));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// T-10-02: Resolve node class and validate it is a non-abstract UEdGraphNode subclass.
|
|
291
|
+
UClass* NodeClass = FindObject<UClass>(ANY_PACKAGE, *NodeType);
|
|
292
|
+
if (!NodeClass)
|
|
293
|
+
{
|
|
294
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.addNode: Node type '%s' not found"), *NodeType);
|
|
295
|
+
SendError(SendResponse, CorrelationId, TEXT("node_type_not_found"));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!NodeClass->IsChildOf(UEdGraphNode::StaticClass()))
|
|
299
|
+
{
|
|
300
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.addNode: Class '%s' is not a UEdGraphNode subclass"), *NodeType);
|
|
301
|
+
SendError(SendResponse, CorrelationId, TEXT("node_type_not_graph_node"));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (NodeClass->HasAnyClassFlags(CLASS_Abstract))
|
|
305
|
+
{
|
|
306
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.addNode: Node type '%s' is abstract and cannot be instantiated"), *NodeType);
|
|
307
|
+
SendError(SendResponse, CorrelationId, TEXT("node_type_is_abstract"));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// MANDATORY: Modify() before any mutation (Pitfall 5).
|
|
312
|
+
BP->Modify();
|
|
313
|
+
|
|
314
|
+
UEdGraphNode* NewNode = NewObject<UEdGraphNode>(TargetGraph, NodeClass);
|
|
315
|
+
NewNode->NodePosX = PosX;
|
|
316
|
+
NewNode->NodePosY = PosY;
|
|
317
|
+
TargetGraph->AddNode(NewNode, /*bFromUI=*/false, /*bSelectNewNode=*/false);
|
|
318
|
+
NewNode->PostPlacedNewNode();
|
|
319
|
+
NewNode->AllocateDefaultPins();
|
|
320
|
+
|
|
321
|
+
// MANDATORY: MarkBlueprintAsModified() after mutation (Pitfall 5).
|
|
322
|
+
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
323
|
+
|
|
324
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
325
|
+
Data->SetStringField(TEXT("nodeGuid"), NewNode->NodeGuid.ToString());
|
|
326
|
+
Data->SetStringField(TEXT("type"), NodeType);
|
|
327
|
+
Data->SetNumberField(TEXT("posX"), PosX);
|
|
328
|
+
Data->SetNumberField(TEXT("posY"), PosY);
|
|
329
|
+
SendSuccess(SendResponse, CorrelationId, Data);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// blueprint.connectPins (BPW-03)
|
|
334
|
+
// Input payload:
|
|
335
|
+
// { "asset_path": "/Game/BP_X", "graph_name": "EventGraph",
|
|
336
|
+
// "from_node_guid": "GUID", "from_pin_name": "then",
|
|
337
|
+
// "to_node_guid": "GUID", "to_pin_name": "execute" }
|
|
338
|
+
// Returns: { "connected": true, "fromNodeGuid": string, "fromPinName": string,
|
|
339
|
+
// "toNodeGuid": string, "toPinName": string }
|
|
340
|
+
//
|
|
341
|
+
// Security: T-10-04 (CanCreateConnection() checked before TryCreateConnection())
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
Router.RegisterHandler(TEXT("blueprint.connectPins"),
|
|
344
|
+
[SendSuccess, SendError](TSharedPtr<FJsonObject> Command, FMCPResponseSender SendResponse)
|
|
345
|
+
{
|
|
346
|
+
FString CorrelationId;
|
|
347
|
+
Command->TryGetStringField(TEXT("correlationId"), CorrelationId);
|
|
348
|
+
|
|
349
|
+
const TSharedPtr<FJsonObject>* PayloadObj = nullptr;
|
|
350
|
+
if (!Command->TryGetObjectField(TEXT("payload"), PayloadObj))
|
|
351
|
+
{
|
|
352
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_payload"));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
FString AssetPath;
|
|
357
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
|
|
358
|
+
{
|
|
359
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_asset_path"));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
FString FromNodeGuidStr;
|
|
364
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("from_node_guid"), FromNodeGuidStr) || FromNodeGuidStr.IsEmpty())
|
|
365
|
+
{
|
|
366
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_from_node_guid"));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
FString FromPinName;
|
|
371
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("from_pin_name"), FromPinName) || FromPinName.IsEmpty())
|
|
372
|
+
{
|
|
373
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_from_pin_name"));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
FString ToNodeGuidStr;
|
|
378
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("to_node_guid"), ToNodeGuidStr) || ToNodeGuidStr.IsEmpty())
|
|
379
|
+
{
|
|
380
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_to_node_guid"));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
FString ToPinName;
|
|
385
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("to_pin_name"), ToPinName) || ToPinName.IsEmpty())
|
|
386
|
+
{
|
|
387
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_to_pin_name"));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
FString GraphName = TEXT("EventGraph");
|
|
392
|
+
(*PayloadObj)->TryGetStringField(TEXT("graph_name"), GraphName);
|
|
393
|
+
|
|
394
|
+
UBlueprint* BP = LoadObject<UBlueprint>(nullptr, *AssetPath);
|
|
395
|
+
if (!BP)
|
|
396
|
+
{
|
|
397
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.connectPins: Blueprint not found at '%s'"), *AssetPath);
|
|
398
|
+
SendError(SendResponse, CorrelationId, TEXT("blueprint_not_found"));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
UEdGraph* TargetGraph = FindBlueprintGraph(BP, GraphName);
|
|
403
|
+
if (!TargetGraph)
|
|
404
|
+
{
|
|
405
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.connectPins: Graph '%s' not found in '%s'"),
|
|
406
|
+
*GraphName, *AssetPath);
|
|
407
|
+
SendError(SendResponse, CorrelationId, TEXT("graph_not_found"));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Find the from-node by GUID.
|
|
412
|
+
UEdGraphNode* FromNode = nullptr;
|
|
413
|
+
for (UEdGraphNode* Node : TargetGraph->Nodes)
|
|
414
|
+
{
|
|
415
|
+
if (Node && Node->NodeGuid.ToString() == FromNodeGuidStr)
|
|
416
|
+
{
|
|
417
|
+
FromNode = Node;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!FromNode)
|
|
422
|
+
{
|
|
423
|
+
SendError(SendResponse, CorrelationId, TEXT("from_node_not_found"));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Find the from-pin (must be an output pin).
|
|
428
|
+
UEdGraphPin* FromPin = nullptr;
|
|
429
|
+
for (UEdGraphPin* Pin : FromNode->Pins)
|
|
430
|
+
{
|
|
431
|
+
if (Pin && Pin->Direction == EGPD_Output && Pin->PinName.ToString() == FromPinName)
|
|
432
|
+
{
|
|
433
|
+
FromPin = Pin;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (!FromPin)
|
|
438
|
+
{
|
|
439
|
+
SendError(SendResponse, CorrelationId, TEXT("from_pin_not_found"));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Find the to-node by GUID.
|
|
444
|
+
UEdGraphNode* ToNode = nullptr;
|
|
445
|
+
for (UEdGraphNode* Node : TargetGraph->Nodes)
|
|
446
|
+
{
|
|
447
|
+
if (Node && Node->NodeGuid.ToString() == ToNodeGuidStr)
|
|
448
|
+
{
|
|
449
|
+
ToNode = Node;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!ToNode)
|
|
454
|
+
{
|
|
455
|
+
SendError(SendResponse, CorrelationId, TEXT("to_node_not_found"));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Find the to-pin (must be an input pin).
|
|
460
|
+
UEdGraphPin* ToPin = nullptr;
|
|
461
|
+
for (UEdGraphPin* Pin : ToNode->Pins)
|
|
462
|
+
{
|
|
463
|
+
if (Pin && Pin->Direction == EGPD_Input && Pin->PinName.ToString() == ToPinName)
|
|
464
|
+
{
|
|
465
|
+
ToPin = Pin;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!ToPin)
|
|
470
|
+
{
|
|
471
|
+
SendError(SendResponse, CorrelationId, TEXT("to_pin_not_found"));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// T-10-04: Validate the connection is type-safe before making it.
|
|
476
|
+
const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
|
|
477
|
+
FPinConnectionResponse CanConnect = Schema->CanCreateConnection(FromPin, ToPin);
|
|
478
|
+
if (CanConnect.Response != CONNECT_RESPONSE_MAKE)
|
|
479
|
+
{
|
|
480
|
+
// T-10-05: Return a named error code, not the raw UE message.
|
|
481
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.connectPins: Connection not allowed: %s"),
|
|
482
|
+
*CanConnect.Message.ToString());
|
|
483
|
+
SendError(SendResponse, CorrelationId, TEXT("connection_not_allowed"));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// MANDATORY: Modify() before any mutation (Pitfall 5).
|
|
488
|
+
BP->Modify();
|
|
489
|
+
|
|
490
|
+
Schema->TryCreateConnection(FromPin, ToPin);
|
|
491
|
+
|
|
492
|
+
// MANDATORY: MarkBlueprintAsModified() after mutation (Pitfall 5).
|
|
493
|
+
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
494
|
+
|
|
495
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
496
|
+
Data->SetBoolField(TEXT("connected"), true);
|
|
497
|
+
Data->SetStringField(TEXT("fromNodeGuid"), FromNodeGuidStr);
|
|
498
|
+
Data->SetStringField(TEXT("fromPinName"), FromPinName);
|
|
499
|
+
Data->SetStringField(TEXT("toNodeGuid"), ToNodeGuidStr);
|
|
500
|
+
Data->SetStringField(TEXT("toPinName"), ToPinName);
|
|
501
|
+
SendSuccess(SendResponse, CorrelationId, Data);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// blueprint.addVariable (BPW-04)
|
|
506
|
+
// Input payload:
|
|
507
|
+
// { "asset_path": "/Game/BP_X", "variable_name": "Health", "variable_type": "float" }
|
|
508
|
+
// Supported type categories: bool, int, int64, float, double, string, name, text,
|
|
509
|
+
// object, class, struct, vector, rotator, transform
|
|
510
|
+
// Returns: { "variableName": string, "variableType": string }
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
Router.RegisterHandler(TEXT("blueprint.addVariable"),
|
|
513
|
+
[SendSuccess, SendError](TSharedPtr<FJsonObject> Command, FMCPResponseSender SendResponse)
|
|
514
|
+
{
|
|
515
|
+
FString CorrelationId;
|
|
516
|
+
Command->TryGetStringField(TEXT("correlationId"), CorrelationId);
|
|
517
|
+
|
|
518
|
+
const TSharedPtr<FJsonObject>* PayloadObj = nullptr;
|
|
519
|
+
if (!Command->TryGetObjectField(TEXT("payload"), PayloadObj))
|
|
520
|
+
{
|
|
521
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_payload"));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
FString AssetPath;
|
|
526
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
|
|
527
|
+
{
|
|
528
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_asset_path"));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
FString VariableName;
|
|
533
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("variable_name"), VariableName) || VariableName.IsEmpty())
|
|
534
|
+
{
|
|
535
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_variable_name"));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
FString VariableType;
|
|
540
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("variable_type"), VariableType) || VariableType.IsEmpty())
|
|
541
|
+
{
|
|
542
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_variable_type"));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
UBlueprint* BP = LoadObject<UBlueprint>(nullptr, *AssetPath);
|
|
547
|
+
if (!BP)
|
|
548
|
+
{
|
|
549
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.addVariable: Blueprint not found at '%s'"), *AssetPath);
|
|
550
|
+
SendError(SendResponse, CorrelationId, TEXT("blueprint_not_found"));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Check for duplicate variable name.
|
|
555
|
+
for (const FBPVariableDescription& ExistingVar : BP->NewVariables)
|
|
556
|
+
{
|
|
557
|
+
if (ExistingVar.VarName.ToString() == VariableName)
|
|
558
|
+
{
|
|
559
|
+
SendError(SendResponse, CorrelationId, TEXT("variable_already_exists"));
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// MANDATORY: Modify() before any mutation (Pitfall 5).
|
|
565
|
+
BP->Modify();
|
|
566
|
+
|
|
567
|
+
// Build the pin type: map the caller's type string to a PinCategory.
|
|
568
|
+
FEdGraphPinType VarType;
|
|
569
|
+
VarType.PinCategory = FName(*VariableType);
|
|
570
|
+
|
|
571
|
+
FBlueprintEditorUtils::AddMemberVariable(BP, FName(*VariableName), VarType);
|
|
572
|
+
|
|
573
|
+
// MANDATORY: MarkBlueprintAsModified() after mutation (Pitfall 5).
|
|
574
|
+
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
575
|
+
|
|
576
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
577
|
+
Data->SetStringField(TEXT("variableName"), VariableName);
|
|
578
|
+
Data->SetStringField(TEXT("variableType"), VariableType);
|
|
579
|
+
SendSuccess(SendResponse, CorrelationId, Data);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
// blueprint.setDefault (BPW-05)
|
|
584
|
+
// Input payload (variable target):
|
|
585
|
+
// { "asset_path": "/Game/BP_X", "target_type": "variable",
|
|
586
|
+
// "target_name": "Health", "default_value": "100.0" }
|
|
587
|
+
// Input payload (pin target):
|
|
588
|
+
// { "asset_path": "/Game/BP_X", "target_type": "pin",
|
|
589
|
+
// "graph_name": "EventGraph", "node_guid": "GUID",
|
|
590
|
+
// "pin_name": "Value", "default_value": "42" }
|
|
591
|
+
// Returns: { "set": true, "targetType": string, "targetName": string, "defaultValue": string }
|
|
592
|
+
//
|
|
593
|
+
// Security: T-10-05 (named error codes, not raw UE messages)
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
595
|
+
Router.RegisterHandler(TEXT("blueprint.setDefault"),
|
|
596
|
+
[SendSuccess, SendError](TSharedPtr<FJsonObject> Command, FMCPResponseSender SendResponse)
|
|
597
|
+
{
|
|
598
|
+
FString CorrelationId;
|
|
599
|
+
Command->TryGetStringField(TEXT("correlationId"), CorrelationId);
|
|
600
|
+
|
|
601
|
+
const TSharedPtr<FJsonObject>* PayloadObj = nullptr;
|
|
602
|
+
if (!Command->TryGetObjectField(TEXT("payload"), PayloadObj))
|
|
603
|
+
{
|
|
604
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_payload"));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
FString AssetPath;
|
|
609
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
|
|
610
|
+
{
|
|
611
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_asset_path"));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
FString TargetType;
|
|
616
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("target_type"), TargetType) || TargetType.IsEmpty())
|
|
617
|
+
{
|
|
618
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_target_type"));
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
FString DefaultValue;
|
|
623
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("default_value"), DefaultValue))
|
|
624
|
+
{
|
|
625
|
+
// default_value may legitimately be an empty string (e.g. clear a text field),
|
|
626
|
+
// but the field must be present.
|
|
627
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_default_value"));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
UBlueprint* BP = LoadObject<UBlueprint>(nullptr, *AssetPath);
|
|
632
|
+
if (!BP)
|
|
633
|
+
{
|
|
634
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.setDefault: Blueprint not found at '%s'"), *AssetPath);
|
|
635
|
+
SendError(SendResponse, CorrelationId, TEXT("blueprint_not_found"));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// MANDATORY: Modify() before any mutation (Pitfall 5).
|
|
640
|
+
BP->Modify();
|
|
641
|
+
|
|
642
|
+
FString TargetName;
|
|
643
|
+
|
|
644
|
+
if (TargetType == TEXT("variable"))
|
|
645
|
+
{
|
|
646
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("target_name"), TargetName) || TargetName.IsEmpty())
|
|
647
|
+
{
|
|
648
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_target_name"));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// T-10-05: SetBlueprintVariableDefaultValue returns bool; use named error code on failure.
|
|
653
|
+
bool bSet = FBlueprintEditorUtils::SetBlueprintVariableDefaultValue(
|
|
654
|
+
BP, FName(*TargetName), DefaultValue);
|
|
655
|
+
if (!bSet)
|
|
656
|
+
{
|
|
657
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.setDefault: Variable '%s' not found in '%s'"),
|
|
658
|
+
*TargetName, *AssetPath);
|
|
659
|
+
SendError(SendResponse, CorrelationId, TEXT("variable_not_found"));
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else if (TargetType == TEXT("pin"))
|
|
664
|
+
{
|
|
665
|
+
FString PinName;
|
|
666
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("pin_name"), PinName) || PinName.IsEmpty())
|
|
667
|
+
{
|
|
668
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_pin_name"));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
TargetName = PinName;
|
|
672
|
+
|
|
673
|
+
FString NodeGuidStr;
|
|
674
|
+
if (!(*PayloadObj)->TryGetStringField(TEXT("node_guid"), NodeGuidStr) || NodeGuidStr.IsEmpty())
|
|
675
|
+
{
|
|
676
|
+
SendError(SendResponse, CorrelationId, TEXT("missing_node_guid"));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
FString GraphName = TEXT("EventGraph");
|
|
681
|
+
(*PayloadObj)->TryGetStringField(TEXT("graph_name"), GraphName);
|
|
682
|
+
|
|
683
|
+
UEdGraph* TargetGraph = FindBlueprintGraph(BP, GraphName);
|
|
684
|
+
if (!TargetGraph)
|
|
685
|
+
{
|
|
686
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] blueprint.setDefault: Graph '%s' not found in '%s'"),
|
|
687
|
+
*GraphName, *AssetPath);
|
|
688
|
+
SendError(SendResponse, CorrelationId, TEXT("graph_not_found"));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Find the node by GUID.
|
|
693
|
+
UEdGraphNode* TargetNode = nullptr;
|
|
694
|
+
for (UEdGraphNode* Node : TargetGraph->Nodes)
|
|
695
|
+
{
|
|
696
|
+
if (Node && Node->NodeGuid.ToString() == NodeGuidStr)
|
|
697
|
+
{
|
|
698
|
+
TargetNode = Node;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (!TargetNode)
|
|
703
|
+
{
|
|
704
|
+
SendError(SendResponse, CorrelationId, TEXT("node_not_found"));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Find the pin by name (any direction is allowed for defaults).
|
|
709
|
+
UEdGraphPin* TargetPin = nullptr;
|
|
710
|
+
for (UEdGraphPin* Pin : TargetNode->Pins)
|
|
711
|
+
{
|
|
712
|
+
if (Pin && Pin->PinName.ToString() == PinName)
|
|
713
|
+
{
|
|
714
|
+
TargetPin = Pin;
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (!TargetPin)
|
|
719
|
+
{
|
|
720
|
+
SendError(SendResponse, CorrelationId, TEXT("pin_not_found"));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
|
|
725
|
+
Schema->TrySetDefaultValue(*TargetPin, DefaultValue);
|
|
726
|
+
}
|
|
727
|
+
else
|
|
728
|
+
{
|
|
729
|
+
// T-10-05: Named error code, not raw input echo.
|
|
730
|
+
SendError(SendResponse, CorrelationId, TEXT("invalid_target_type"));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// MANDATORY: MarkBlueprintAsModified() after mutation (Pitfall 5).
|
|
735
|
+
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
736
|
+
|
|
737
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
738
|
+
Data->SetBoolField(TEXT("set"), true);
|
|
739
|
+
Data->SetStringField(TEXT("targetType"), TargetType);
|
|
740
|
+
Data->SetStringField(TEXT("targetName"), TargetName);
|
|
741
|
+
Data->SetStringField(TEXT("defaultValue"), DefaultValue);
|
|
742
|
+
SendSuccess(SendResponse, CorrelationId, Data);
|
|
743
|
+
});
|
|
744
|
+
}
|