unreal-engine-mcp-server 0.5.2 → 0.5.4
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/CHANGELOG.md +195 -0
- package/README.md +9 -6
- package/dist/automation/bridge.d.ts +1 -0
- package/dist/automation/bridge.js +62 -4
- package/dist/automation/types.d.ts +1 -0
- package/dist/config/class-aliases.d.ts +5 -0
- package/dist/config/class-aliases.js +30 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/graphql/server.d.ts +0 -1
- package/dist/graphql/server.js +15 -16
- package/dist/index.js +1 -1
- package/dist/services/metrics-server.d.ts +2 -1
- package/dist/services/metrics-server.js +29 -4
- package/dist/tools/consolidated-tool-definitions.js +3 -3
- package/dist/tools/debug.d.ts +5 -0
- package/dist/tools/debug.js +7 -0
- package/dist/tools/handlers/actor-handlers.js +4 -27
- package/dist/tools/handlers/asset-handlers.js +13 -1
- package/dist/tools/handlers/blueprint-handlers.d.ts +4 -1
- package/dist/tools/handlers/common-handlers.d.ts +11 -11
- package/dist/tools/handlers/common-handlers.js +6 -4
- package/dist/tools/handlers/editor-handlers.d.ts +2 -1
- package/dist/tools/handlers/editor-handlers.js +6 -6
- package/dist/tools/handlers/effect-handlers.js +3 -0
- package/dist/tools/handlers/graph-handlers.d.ts +2 -1
- package/dist/tools/handlers/graph-handlers.js +1 -1
- package/dist/tools/handlers/input-handlers.d.ts +5 -1
- package/dist/tools/handlers/level-handlers.d.ts +2 -1
- package/dist/tools/handlers/level-handlers.js +3 -3
- package/dist/tools/handlers/lighting-handlers.d.ts +2 -1
- package/dist/tools/handlers/lighting-handlers.js +3 -0
- package/dist/tools/handlers/pipeline-handlers.d.ts +2 -1
- package/dist/tools/handlers/pipeline-handlers.js +64 -10
- package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
- package/dist/tools/handlers/system-handlers.d.ts +1 -1
- package/dist/tools/input.d.ts +5 -1
- package/dist/tools/input.js +37 -1
- package/dist/tools/lighting.d.ts +1 -0
- package/dist/tools/lighting.js +7 -0
- package/dist/tools/physics.d.ts +1 -1
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +7 -0
- package/dist/types/handler-types.d.ts +343 -0
- package/dist/types/handler-types.js +2 -0
- package/dist/unreal-bridge.d.ts +1 -1
- package/dist/unreal-bridge.js +8 -6
- package/dist/utils/command-validator.d.ts +1 -0
- package/dist/utils/command-validator.js +11 -1
- package/dist/utils/error-handler.js +3 -1
- package/dist/utils/response-validator.js +2 -2
- package/dist/utils/safe-json.d.ts +1 -1
- package/dist/utils/safe-json.js +3 -6
- package/dist/utils/unreal-command-queue.js +1 -1
- package/dist/utils/validation.js +6 -2
- package/docs/handler-mapping.md +6 -1
- package/package.json +2 -2
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +25 -1
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +40 -58
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +27 -46
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +16 -1
- package/server.json +2 -2
- package/src/automation/bridge.ts +80 -10
- package/src/automation/types.ts +1 -0
- package/src/config/class-aliases.ts +65 -0
- package/src/constants.ts +10 -0
- package/src/graphql/server.ts +23 -23
- package/src/index.ts +1 -1
- package/src/services/metrics-server.ts +40 -6
- package/src/tools/consolidated-tool-definitions.ts +3 -3
- package/src/tools/debug.ts +8 -0
- package/src/tools/handlers/actor-handlers.ts +5 -31
- package/src/tools/handlers/asset-handlers.ts +19 -1
- package/src/tools/handlers/blueprint-handlers.ts +1 -1
- package/src/tools/handlers/common-handlers.ts +32 -11
- package/src/tools/handlers/editor-handlers.ts +8 -7
- package/src/tools/handlers/effect-handlers.ts +4 -0
- package/src/tools/handlers/graph-handlers.ts +7 -6
- package/src/tools/handlers/level-handlers.ts +5 -4
- package/src/tools/handlers/lighting-handlers.ts +5 -1
- package/src/tools/handlers/pipeline-handlers.ts +83 -16
- package/src/tools/input.ts +60 -1
- package/src/tools/lighting.ts +11 -0
- package/src/tools/physics.ts +1 -1
- package/src/tools/sequence.ts +11 -0
- package/src/types/handler-types.ts +442 -0
- package/src/unreal-bridge.ts +8 -6
- package/src/utils/command-validator.ts +23 -1
- package/src/utils/error-handler.ts +4 -1
- package/src/utils/response-validator.ts +7 -9
- package/src/utils/safe-json.ts +20 -15
- package/src/utils/unreal-command-queue.ts +3 -1
- package/src/utils/validation.test.ts +3 -3
- package/src/utils/validation.ts +36 -26
- package/tests/test-console-command.mjs +1 -1
- package/tests/test-runner.mjs +63 -3
- package/tests/run-unreal-tool-tests.mjs +0 -948
- package/tests/test-asset-errors.mjs +0 -35
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "McpAutomationBridgeGlobals.h"
|
|
2
2
|
#include "McpAutomationBridgeHelpers.h"
|
|
3
3
|
#include "McpAutomationBridgeSubsystem.h"
|
|
4
|
+
#include "UObject/UObjectIterator.h"
|
|
4
5
|
|
|
5
6
|
#include "Components/ExponentialHeightFogComponent.h"
|
|
6
7
|
#include "Engine/ExponentialHeightFog.h"
|
|
@@ -107,71 +108,52 @@ bool UMcpAutomationBridgeSubsystem::HandleLightingAction(
|
|
|
107
108
|
return true;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
LightClass = ADirectionalLight::StaticClass();
|
|
113
|
-
else if (LightClassStr == TEXT("PointLight"))
|
|
114
|
-
LightClass = APointLight::StaticClass();
|
|
115
|
-
else if (LightClassStr == TEXT("SpotLight"))
|
|
116
|
-
LightClass = ASpotLight::StaticClass();
|
|
117
|
-
else if (LightClassStr == TEXT("RectLight"))
|
|
118
|
-
LightClass = ARectLight::StaticClass();
|
|
119
|
-
else {
|
|
120
|
-
// Dynamic fallback: Try to resolve any light class by name
|
|
121
|
-
LightClass = ResolveUClass(LightClassStr);
|
|
122
|
-
|
|
123
|
-
// Try with "A" prefix for actor classes (e.g., "SkyLight" -> "ASkyLight")
|
|
124
|
-
if (!LightClass && !LightClassStr.StartsWith(TEXT("A"))) {
|
|
125
|
-
LightClass =
|
|
126
|
-
ResolveUClass(FString::Printf(TEXT("A%s"), *LightClassStr));
|
|
127
|
-
}
|
|
111
|
+
// Dynamic resolution with heuristics
|
|
112
|
+
UClass *LightClass = ResolveUClass(LightClassStr);
|
|
128
113
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
114
|
+
// Try finding with 'A' prefix (standard Actor prefix)
|
|
115
|
+
if (!LightClass) {
|
|
116
|
+
LightClass = ResolveUClass(TEXT("A") + LightClassStr);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!LightClass || !LightClass->IsChildOf(ALight::StaticClass())) {
|
|
120
|
+
SendAutomationError(
|
|
121
|
+
RequestingSocket, RequestId,
|
|
122
|
+
FString::Printf(TEXT("Invalid light class: %s"), *LightClassStr),
|
|
123
|
+
TEXT("INVALID_ARGUMENT"));
|
|
124
|
+
return true;
|
|
139
125
|
}
|
|
140
126
|
|
|
141
127
|
FVector Location = FVector::ZeroVector;
|
|
142
|
-
const TSharedPtr<FJsonObject> *
|
|
143
|
-
if (Payload->TryGetObjectField(TEXT("location"),
|
|
144
|
-
Location.X = (*
|
|
145
|
-
Location.Y = (*
|
|
146
|
-
Location.Z = (*
|
|
128
|
+
const TSharedPtr<FJsonObject> *LocPtr;
|
|
129
|
+
if (Payload->TryGetObjectField(TEXT("location"), LocPtr)) {
|
|
130
|
+
Location.X = (*LocPtr)->GetNumberField(TEXT("x"));
|
|
131
|
+
Location.Y = (*LocPtr)->GetNumberField(TEXT("y"));
|
|
132
|
+
Location.Z = (*LocPtr)->GetNumberField(TEXT("z"));
|
|
147
133
|
}
|
|
148
134
|
|
|
149
135
|
FRotator Rotation = FRotator::ZeroRotator;
|
|
150
|
-
const TSharedPtr<FJsonObject> *
|
|
151
|
-
if (Payload->TryGetObjectField(TEXT("rotation"),
|
|
152
|
-
Rotation.Pitch = (*
|
|
153
|
-
Rotation.Yaw = (*
|
|
154
|
-
Rotation.Roll = (*
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
NewLight
|
|
170
|
-
|
|
171
|
-
if (NewLight) {
|
|
172
|
-
NewLight->SetActorLocationAndRotation(
|
|
173
|
-
Location, Rotation, false, nullptr, ETeleportType::TeleportPhysics);
|
|
174
|
-
}
|
|
136
|
+
const TSharedPtr<FJsonObject> *RotPtr;
|
|
137
|
+
if (Payload->TryGetObjectField(TEXT("rotation"), RotPtr)) {
|
|
138
|
+
Rotation.Pitch = (*RotPtr)->GetNumberField(TEXT("pitch"));
|
|
139
|
+
Rotation.Yaw = (*RotPtr)->GetNumberField(TEXT("yaw"));
|
|
140
|
+
Rotation.Roll = (*RotPtr)->GetNumberField(TEXT("roll"));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
FActorSpawnParameters SpawnParams;
|
|
144
|
+
SpawnParams.SpawnCollisionHandlingOverride =
|
|
145
|
+
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
|
146
|
+
|
|
147
|
+
// Fix: Declare NewLight before use
|
|
148
|
+
AActor *NewLight = ActorSS->GetWorld()->SpawnActor(LightClass, &Location,
|
|
149
|
+
&Rotation, SpawnParams);
|
|
150
|
+
|
|
151
|
+
// Explicitly set location/rotation
|
|
152
|
+
if (NewLight) {
|
|
153
|
+
// Set label immediately
|
|
154
|
+
NewLight->SetActorLabel(LightClassStr);
|
|
155
|
+
NewLight->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
|
|
156
|
+
ETeleportType::TeleportPhysics);
|
|
175
157
|
}
|
|
176
158
|
|
|
177
159
|
if (!NewLight) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
#include "MovieSceneSection.h"
|
|
8
8
|
#include "MovieSceneSequence.h"
|
|
9
9
|
#include "MovieSceneTrack.h"
|
|
10
|
+
#include "UObject/UObjectIterator.h"
|
|
10
11
|
|
|
11
12
|
#if WITH_EDITOR
|
|
12
13
|
#include "Editor.h"
|
|
@@ -2484,59 +2485,39 @@ bool UMcpAutomationBridgeSubsystem::HandleSequenceAction(
|
|
|
2484
2485
|
|
|
2485
2486
|
// Add the track
|
|
2486
2487
|
UMovieSceneTrack *NewTrack = nullptr;
|
|
2487
|
-
FString TrackTypeLower = TrackType.ToLower();
|
|
2488
2488
|
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
TrackTypeLower == TEXT("3dtransform")) {
|
|
2492
|
-
if (BindingGuid.IsValid()) {
|
|
2493
|
-
NewTrack = MovieScene->AddTrack(
|
|
2494
|
-
UMovieScene3DTransformTrack::StaticClass(), BindingGuid);
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
#endif
|
|
2489
|
+
// Dynamic resolution with heuristics
|
|
2490
|
+
UClass *TrackClass = ResolveUClass(TrackType);
|
|
2498
2491
|
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2492
|
+
// Try with common prefixes
|
|
2493
|
+
if (!TrackClass) {
|
|
2494
|
+
TrackClass = ResolveUClass(
|
|
2495
|
+
FString::Printf(TEXT("UMovieScene%sTrack"), *TrackType));
|
|
2496
|
+
}
|
|
2497
|
+
if (!TrackClass) {
|
|
2498
|
+
TrackClass =
|
|
2499
|
+
ResolveUClass(FString::Printf(TEXT("MovieScene%sTrack"), *TrackType));
|
|
2500
|
+
}
|
|
2501
|
+
// Try simple "U" prefix
|
|
2502
|
+
if (!TrackClass) {
|
|
2503
|
+
TrackClass = ResolveUClass(FString::Printf(TEXT("U%s"), *TrackType));
|
|
2506
2504
|
}
|
|
2507
2505
|
|
|
2508
|
-
|
|
2506
|
+
// Validate it's actually a track class
|
|
2507
|
+
if (TrackClass && TrackClass->IsChildOf(UMovieSceneTrack::StaticClass())) {
|
|
2509
2508
|
if (BindingGuid.IsValid()) {
|
|
2510
|
-
NewTrack = MovieScene->AddTrack(
|
|
2511
|
-
BindingGuid);
|
|
2509
|
+
NewTrack = MovieScene->AddTrack(TrackClass, BindingGuid);
|
|
2512
2510
|
} else {
|
|
2513
|
-
NewTrack = MovieScene->AddTrack(
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
// Dynamic fallback: Try to resolve any track class by name
|
|
2518
|
-
if (!NewTrack) {
|
|
2519
|
-
UClass *TrackClass = ResolveUClass(TrackType);
|
|
2520
|
-
|
|
2521
|
-
// Try with common prefixes
|
|
2522
|
-
if (!TrackClass) {
|
|
2523
|
-
TrackClass = ResolveUClass(
|
|
2524
|
-
FString::Printf(TEXT("UMovieScene%sTrack"), *TrackType));
|
|
2525
|
-
}
|
|
2526
|
-
if (!TrackClass) {
|
|
2527
|
-
TrackClass = ResolveUClass(
|
|
2528
|
-
FString::Printf(TEXT("MovieScene%sTrack"), *TrackType));
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
// Validate it's actually a track class
|
|
2532
|
-
if (TrackClass &&
|
|
2533
|
-
TrackClass->IsChildOf(UMovieSceneTrack::StaticClass())) {
|
|
2534
|
-
if (BindingGuid.IsValid()) {
|
|
2535
|
-
NewTrack = MovieScene->AddTrack(TrackClass, BindingGuid);
|
|
2536
|
-
} else {
|
|
2537
|
-
NewTrack = MovieScene->AddTrack(TrackClass);
|
|
2538
|
-
}
|
|
2511
|
+
NewTrack = MovieScene->AddTrack(TrackClass);
|
|
2539
2512
|
}
|
|
2513
|
+
} else if (TrackClass) {
|
|
2514
|
+
// Found a class but it's not a track
|
|
2515
|
+
SendAutomationError(
|
|
2516
|
+
RequestingSocket, RequestId,
|
|
2517
|
+
FString::Printf(TEXT("Class '%s' is not a UMovieSceneTrack"),
|
|
2518
|
+
*TrackClass->GetName()),
|
|
2519
|
+
TEXT("INVALID_CLASS_TYPE"));
|
|
2520
|
+
return true;
|
|
2540
2521
|
}
|
|
2541
2522
|
|
|
2542
2523
|
if (NewTrack) {
|
package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp
CHANGED
|
@@ -446,6 +446,8 @@ uint32 FMcpBridgeWebSocket::RunServer() {
|
|
|
446
446
|
TSharedRef<FInternetAddr> ListenAddr = SocketSubsystem->CreateInternetAddr();
|
|
447
447
|
|
|
448
448
|
bool bResolvedHost = false;
|
|
449
|
+
bool bExplicitBindAll = false;
|
|
450
|
+
|
|
449
451
|
if (!ListenHost.IsEmpty()) {
|
|
450
452
|
FString HostToBind = ListenHost;
|
|
451
453
|
if (HostToBind.Equals(TEXT("localhost"), ESearchCase::IgnoreCase)) {
|
|
@@ -457,10 +459,23 @@ uint32 FMcpBridgeWebSocket::RunServer() {
|
|
|
457
459
|
if (bIsValidIp) {
|
|
458
460
|
bResolvedHost = true;
|
|
459
461
|
}
|
|
462
|
+
|
|
463
|
+
bExplicitBindAll = HostToBind.Equals(TEXT("0.0.0.0"), ESearchCase::IgnoreCase) ||
|
|
464
|
+
HostToBind.Equals(TEXT("::"), ESearchCase::IgnoreCase);
|
|
460
465
|
}
|
|
461
466
|
|
|
462
467
|
if (!bResolvedHost) {
|
|
463
|
-
|
|
468
|
+
if (!bExplicitBindAll) {
|
|
469
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
470
|
+
TEXT("Invalid ListenHost '%s'. Falling back to 127.0.0.1 for safety. To bind all interfaces, explicitly set ListenHost=0.0.0.0."),
|
|
471
|
+
*ListenHost);
|
|
472
|
+
|
|
473
|
+
bool bFallbackIsValidIp = false;
|
|
474
|
+
ListenAddr->SetIp(TEXT("127.0.0.1"), bFallbackIsValidIp);
|
|
475
|
+
bResolvedHost = bFallbackIsValidIp;
|
|
476
|
+
} else {
|
|
477
|
+
ListenAddr->SetAnyAddress();
|
|
478
|
+
}
|
|
464
479
|
}
|
|
465
480
|
|
|
466
481
|
ListenAddr->SetPort(Port);
|
package/server.json
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.ChiR24/unreal-engine-mcp",
|
|
4
4
|
"description": "MCP server for Unreal Engine 5 with 17 tools for game development automation.",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.4",
|
|
6
6
|
"packages": [
|
|
7
7
|
{
|
|
8
8
|
"registryType": "npm",
|
|
9
9
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
10
10
|
"identifier": "unreal-engine-mcp-server",
|
|
11
|
-
"version": "0.5.
|
|
11
|
+
"version": "0.5.4",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio"
|
|
14
14
|
},
|
package/src/automation/bridge.ts
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
DEFAULT_AUTOMATION_PORT,
|
|
7
7
|
DEFAULT_NEGOTIATED_PROTOCOLS,
|
|
8
8
|
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
9
|
-
DEFAULT_MAX_PENDING_REQUESTS
|
|
9
|
+
DEFAULT_MAX_PENDING_REQUESTS,
|
|
10
|
+
DEFAULT_MAX_QUEUED_REQUESTS,
|
|
11
|
+
MAX_WS_MESSAGE_SIZE_BYTES
|
|
10
12
|
} from '../constants.js';
|
|
11
13
|
import { createRequire } from 'node:module';
|
|
12
14
|
import {
|
|
@@ -45,6 +47,7 @@ export class AutomationBridge extends EventEmitter {
|
|
|
45
47
|
private readonly clientPort: number;
|
|
46
48
|
private readonly serverLegacyEnabled: boolean;
|
|
47
49
|
private readonly maxConcurrentConnections: number;
|
|
50
|
+
private readonly maxQueuedRequests: number;
|
|
48
51
|
|
|
49
52
|
private connectionManager: ConnectionManager;
|
|
50
53
|
private requestTracker: RequestTracker;
|
|
@@ -131,6 +134,7 @@ export class AutomationBridge extends EventEmitter {
|
|
|
131
134
|
|
|
132
135
|
const maxPendingRequests = Math.max(1, options.maxPendingRequests ?? DEFAULT_MAX_PENDING_REQUESTS);
|
|
133
136
|
const maxConcurrentConnections = Math.max(1, options.maxConcurrentConnections ?? 10);
|
|
137
|
+
this.maxQueuedRequests = Math.max(0, options.maxQueuedRequests ?? DEFAULT_MAX_QUEUED_REQUESTS);
|
|
134
138
|
|
|
135
139
|
this.clientHost = options.clientHost ?? process.env.MCP_AUTOMATION_CLIENT_HOST ?? DEFAULT_AUTOMATION_HOST;
|
|
136
140
|
this.clientPort = options.clientPort ?? sanitizePort(process.env.MCP_AUTOMATION_CLIENT_PORT) ?? DEFAULT_AUTOMATION_PORT;
|
|
@@ -185,13 +189,21 @@ export class AutomationBridge extends EventEmitter {
|
|
|
185
189
|
|
|
186
190
|
this.log.debug(`Negotiated protocols: ${JSON.stringify(this.negotiatedProtocols)}`);
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
const protocols = this.negotiatedProtocols.length === 1
|
|
193
|
+
? this.negotiatedProtocols[0]
|
|
194
|
+
: this.negotiatedProtocols;
|
|
190
195
|
|
|
191
196
|
this.log.debug(`Using WebSocket protocols arg: ${JSON.stringify(protocols)}`);
|
|
192
197
|
|
|
198
|
+
const headers: Record<string, string> | undefined = this.capabilityToken
|
|
199
|
+
? {
|
|
200
|
+
'X-MCP-Capability': this.capabilityToken,
|
|
201
|
+
'X-MCP-Capability-Token': this.capabilityToken
|
|
202
|
+
}
|
|
203
|
+
: undefined;
|
|
204
|
+
|
|
193
205
|
const socket = new WebSocket(url, protocols, {
|
|
194
|
-
headers
|
|
206
|
+
headers,
|
|
195
207
|
perMessageDeflate: false
|
|
196
208
|
});
|
|
197
209
|
|
|
@@ -233,10 +245,69 @@ export class AutomationBridge extends EventEmitter {
|
|
|
233
245
|
protocol: socket.protocol || null
|
|
234
246
|
});
|
|
235
247
|
|
|
236
|
-
|
|
248
|
+
const getRawDataByteLength = (data: unknown): number => {
|
|
249
|
+
if (typeof data === 'string') {
|
|
250
|
+
return Buffer.byteLength(data, 'utf8');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (Buffer.isBuffer(data)) {
|
|
254
|
+
return data.length;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (Array.isArray(data)) {
|
|
258
|
+
return data.reduce((total, item) => total + (Buffer.isBuffer(item) ? item.length : 0), 0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (data instanceof ArrayBuffer) {
|
|
262
|
+
return data.byteLength;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (ArrayBuffer.isView(data)) {
|
|
266
|
+
return data.byteLength;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return 0;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const rawDataToUtf8String = (data: unknown, byteLengthHint?: number): string => {
|
|
273
|
+
if (typeof data === 'string') {
|
|
274
|
+
return data;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (Buffer.isBuffer(data)) {
|
|
278
|
+
return data.toString('utf8');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (Array.isArray(data)) {
|
|
282
|
+
const buffers = data.filter((item): item is Buffer => Buffer.isBuffer(item));
|
|
283
|
+
const totalLength = typeof byteLengthHint === 'number'
|
|
284
|
+
? byteLengthHint
|
|
285
|
+
: buffers.reduce((total, item) => total + item.length, 0);
|
|
286
|
+
return Buffer.concat(buffers, totalLength).toString('utf8');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (data instanceof ArrayBuffer) {
|
|
290
|
+
return Buffer.from(data).toString('utf8');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (ArrayBuffer.isView(data)) {
|
|
294
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return '';
|
|
298
|
+
};
|
|
299
|
+
|
|
237
300
|
socket.on('message', (data) => {
|
|
238
301
|
try {
|
|
239
|
-
const
|
|
302
|
+
const byteLength = getRawDataByteLength(data);
|
|
303
|
+
if (byteLength > MAX_WS_MESSAGE_SIZE_BYTES) {
|
|
304
|
+
this.log.error(
|
|
305
|
+
`Received oversized message (${byteLength} bytes, max: ${MAX_WS_MESSAGE_SIZE_BYTES}). Dropping.`
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const text = rawDataToUtf8String(data, byteLength);
|
|
240
311
|
this.log.debug(`[AutomationBridge Client] Received message: ${text.substring(0, 1000)}`);
|
|
241
312
|
const parsed = JSON.parse(text) as AutomationBridgeMessage;
|
|
242
313
|
this.connectionManager.updateLastMessageTime();
|
|
@@ -431,11 +502,10 @@ export class AutomationBridge extends EventEmitter {
|
|
|
431
502
|
throw new Error('Automation bridge not connected');
|
|
432
503
|
}
|
|
433
504
|
|
|
434
|
-
// Check if we need to queue (unless it's a priority request which standard ones are not)
|
|
435
|
-
// We use requestTracker directly to check limit as it's the source of truth
|
|
436
|
-
// Note: requestTracker exposes maxPendingRequests via constructor but generic check logic isn't public
|
|
437
|
-
// We assumed getPendingCount() is available
|
|
438
505
|
if (this.requestTracker.getPendingCount() >= this.requestTracker.getMaxPendingRequests()) {
|
|
506
|
+
if (this.queuedRequestItems.length >= this.maxQueuedRequests) {
|
|
507
|
+
throw new Error(`Automation bridge request queue is full (max: ${this.maxQueuedRequests}). Please retry later.`);
|
|
508
|
+
}
|
|
439
509
|
return new Promise<T>((resolve, reject) => {
|
|
440
510
|
this.queuedRequestItems.push({
|
|
441
511
|
resolve,
|
package/src/automation/types.ts
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared configuration for Unreal Engine class names and aliases.
|
|
3
|
+
* Centralizes class mappings to avoid duplication across handlers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* User-friendly class name aliases to full Unreal class paths.
|
|
8
|
+
* These allow users to specify simpler names like 'PointLight' instead of '/Script/Engine.PointLight'.
|
|
9
|
+
*/
|
|
10
|
+
export const ACTOR_CLASS_ALIASES: Record<string, string> = {
|
|
11
|
+
// Special cases that need component addition
|
|
12
|
+
'SplineActor': '/Script/Engine.Actor',
|
|
13
|
+
'Spline': '/Script/Engine.Actor',
|
|
14
|
+
|
|
15
|
+
// Light actors
|
|
16
|
+
'PointLight': '/Script/Engine.PointLight',
|
|
17
|
+
'SpotLight': '/Script/Engine.SpotLight',
|
|
18
|
+
'DirectionalLight': '/Script/Engine.DirectionalLight',
|
|
19
|
+
'RectLight': '/Script/Engine.RectLight',
|
|
20
|
+
|
|
21
|
+
// Camera actors
|
|
22
|
+
'Camera': '/Script/Engine.CameraActor',
|
|
23
|
+
'CameraActor': '/Script/Engine.CameraActor',
|
|
24
|
+
|
|
25
|
+
// Mesh actors
|
|
26
|
+
'StaticMeshActor': '/Script/Engine.StaticMeshActor',
|
|
27
|
+
'SkeletalMeshActor': '/Script/Engine.SkeletalMeshActor',
|
|
28
|
+
|
|
29
|
+
// Gameplay actors
|
|
30
|
+
'PlayerStart': '/Script/Engine.PlayerStart',
|
|
31
|
+
'Pawn': '/Script/Engine.Pawn',
|
|
32
|
+
'Character': '/Script/Engine.Character',
|
|
33
|
+
'Actor': '/Script/Engine.Actor',
|
|
34
|
+
|
|
35
|
+
// Trigger volumes
|
|
36
|
+
'TriggerBox': '/Script/Engine.TriggerBox',
|
|
37
|
+
'TriggerSphere': '/Script/Engine.TriggerSphere',
|
|
38
|
+
'BlockingVolume': '/Script/Engine.BlockingVolume',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Class aliases that require a component to be auto-added after spawning.
|
|
43
|
+
*/
|
|
44
|
+
export const CLASSES_REQUIRING_COMPONENT: Record<string, string> = {
|
|
45
|
+
'SplineActor': 'SplineComponent',
|
|
46
|
+
'Spline': 'SplineComponent',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a class name alias to its full Unreal class path.
|
|
51
|
+
* @param classNameOrPath - The class name or alias to resolve
|
|
52
|
+
* @returns The full class path, or the original if not an alias
|
|
53
|
+
*/
|
|
54
|
+
export function resolveClassAlias(classNameOrPath: string): string {
|
|
55
|
+
return ACTOR_CLASS_ALIASES[classNameOrPath] || classNameOrPath;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a class alias requires an auto-added component.
|
|
60
|
+
* @param className - The original class name or alias
|
|
61
|
+
* @returns The component name to add, or undefined
|
|
62
|
+
*/
|
|
63
|
+
export function getRequiredComponent(className: string): string | undefined {
|
|
64
|
+
return CLASSES_REQUIRING_COMPONENT[className];
|
|
65
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -6,6 +6,7 @@ export const DEFAULT_NEGOTIATED_PROTOCOLS = ['mcp-automation'];
|
|
|
6
6
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 10000;
|
|
7
7
|
export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5000;
|
|
8
8
|
export const DEFAULT_MAX_PENDING_REQUESTS = 25;
|
|
9
|
+
export const DEFAULT_MAX_QUEUED_REQUESTS = 100;
|
|
9
10
|
export const DEFAULT_TIME_OF_DAY = 9;
|
|
10
11
|
export const DEFAULT_SUN_INTENSITY = 10000;
|
|
11
12
|
export const DEFAULT_SKYLIGHT_INTENSITY = 1;
|
|
@@ -17,3 +18,12 @@ export const DEFAULT_OPERATION_TIMEOUT_MS = 30000;
|
|
|
17
18
|
export const DEFAULT_ASSET_OP_TIMEOUT_MS = 60000;
|
|
18
19
|
export const EXTENDED_ASSET_OP_TIMEOUT_MS = 120000;
|
|
19
20
|
export const LONG_RUNNING_OP_TIMEOUT_MS = 300000;
|
|
21
|
+
|
|
22
|
+
// Command-specific timeouts
|
|
23
|
+
export const CONSOLE_COMMAND_TIMEOUT_MS = 30000;
|
|
24
|
+
export const ENGINE_QUERY_TIMEOUT_MS = 15000;
|
|
25
|
+
export const CONNECTION_TIMEOUT_MS = 15000;
|
|
26
|
+
|
|
27
|
+
// Message size limits
|
|
28
|
+
export const MAX_WS_MESSAGE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
29
|
+
|
package/src/graphql/server.ts
CHANGED
|
@@ -50,11 +50,32 @@ export class GraphQLServer {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
const isLoopback = this.config.host === '127.0.0.1' ||
|
|
54
|
+
this.config.host === '::1' ||
|
|
55
|
+
this.config.host.toLowerCase() === 'localhost';
|
|
56
|
+
|
|
57
|
+
const allowRemote = process.env.GRAPHQL_ALLOW_REMOTE === 'true';
|
|
58
|
+
|
|
59
|
+
if (!isLoopback && !allowRemote) {
|
|
60
|
+
this.log.warn(
|
|
61
|
+
`GraphQL server is configured to bind to non-loopback host '${this.config.host}'. GraphQL is for local debugging only. ` +
|
|
62
|
+
'To allow remote binding, set GRAPHQL_ALLOW_REMOTE=true. Aborting start.'
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!isLoopback && allowRemote) {
|
|
68
|
+
if (this.config.cors.origin === '*') {
|
|
69
|
+
this.log.warn(
|
|
70
|
+
"GraphQL server is binding to a remote host with permissive CORS origin '*'. " +
|
|
71
|
+
'Set GRAPHQL_CORS_ORIGIN to specific origins for production. Using permissive CORS for now.'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
try {
|
|
54
|
-
// Create GraphQL schema
|
|
55
77
|
const schema = createGraphQLSchema(this.bridge, this.automationBridge);
|
|
56
78
|
|
|
57
|
-
// Create Yoga server
|
|
58
79
|
const yoga = createYoga({
|
|
59
80
|
schema,
|
|
60
81
|
graphqlEndpoint: this.config.path,
|
|
@@ -77,12 +98,10 @@ export class GraphQLServer {
|
|
|
77
98
|
}
|
|
78
99
|
});
|
|
79
100
|
|
|
80
|
-
// Create HTTP server with Yoga's request handler
|
|
81
101
|
this.server = createServer(
|
|
82
102
|
yoga as any
|
|
83
103
|
);
|
|
84
104
|
|
|
85
|
-
// Start server
|
|
86
105
|
await new Promise<void>((resolve, reject) => {
|
|
87
106
|
if (!this.server) {
|
|
88
107
|
reject(new Error('Server not initialized'));
|
|
@@ -102,9 +121,6 @@ export class GraphQLServer {
|
|
|
102
121
|
resolve();
|
|
103
122
|
});
|
|
104
123
|
});
|
|
105
|
-
|
|
106
|
-
// Setup graceful shutdown
|
|
107
|
-
this.setupShutdown();
|
|
108
124
|
} catch (error) {
|
|
109
125
|
this.log.error('Failed to start GraphQL server:', error);
|
|
110
126
|
throw error;
|
|
@@ -130,22 +146,6 @@ export class GraphQLServer {
|
|
|
130
146
|
});
|
|
131
147
|
}
|
|
132
148
|
|
|
133
|
-
private setupShutdown(): void {
|
|
134
|
-
const gracefulShutdown = async (signal: string) => {
|
|
135
|
-
this.log.info(`Received ${signal}, shutting down GraphQL server...`);
|
|
136
|
-
try {
|
|
137
|
-
await this.stop();
|
|
138
|
-
process.exit(0);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
this.log.error('Error during GraphQL server shutdown:', error);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
146
|
-
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
149
|
getConfig() {
|
|
150
150
|
return this.config;
|
|
151
151
|
}
|
package/src/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ const DEFAULT_SERVER_NAME = typeof packageInfo.name === 'string' && packageInfo.
|
|
|
30
30
|
: 'unreal-engine-mcp';
|
|
31
31
|
const DEFAULT_SERVER_VERSION = typeof packageInfo.version === 'string' && packageInfo.version.trim().length > 0
|
|
32
32
|
? packageInfo.version
|
|
33
|
-
: '0.
|
|
33
|
+
: '0.5.4';
|
|
34
34
|
|
|
35
35
|
function routeStdoutLogsToStderr(): void {
|
|
36
36
|
if (!config.MCP_ROUTE_STDOUT_LOGS) {
|