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.
Files changed (98) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/README.md +9 -6
  3. package/dist/automation/bridge.d.ts +1 -0
  4. package/dist/automation/bridge.js +62 -4
  5. package/dist/automation/types.d.ts +1 -0
  6. package/dist/config/class-aliases.d.ts +5 -0
  7. package/dist/config/class-aliases.js +30 -0
  8. package/dist/constants.d.ts +5 -0
  9. package/dist/constants.js +5 -0
  10. package/dist/graphql/server.d.ts +0 -1
  11. package/dist/graphql/server.js +15 -16
  12. package/dist/index.js +1 -1
  13. package/dist/services/metrics-server.d.ts +2 -1
  14. package/dist/services/metrics-server.js +29 -4
  15. package/dist/tools/consolidated-tool-definitions.js +3 -3
  16. package/dist/tools/debug.d.ts +5 -0
  17. package/dist/tools/debug.js +7 -0
  18. package/dist/tools/handlers/actor-handlers.js +4 -27
  19. package/dist/tools/handlers/asset-handlers.js +13 -1
  20. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -1
  21. package/dist/tools/handlers/common-handlers.d.ts +11 -11
  22. package/dist/tools/handlers/common-handlers.js +6 -4
  23. package/dist/tools/handlers/editor-handlers.d.ts +2 -1
  24. package/dist/tools/handlers/editor-handlers.js +6 -6
  25. package/dist/tools/handlers/effect-handlers.js +3 -0
  26. package/dist/tools/handlers/graph-handlers.d.ts +2 -1
  27. package/dist/tools/handlers/graph-handlers.js +1 -1
  28. package/dist/tools/handlers/input-handlers.d.ts +5 -1
  29. package/dist/tools/handlers/level-handlers.d.ts +2 -1
  30. package/dist/tools/handlers/level-handlers.js +3 -3
  31. package/dist/tools/handlers/lighting-handlers.d.ts +2 -1
  32. package/dist/tools/handlers/lighting-handlers.js +3 -0
  33. package/dist/tools/handlers/pipeline-handlers.d.ts +2 -1
  34. package/dist/tools/handlers/pipeline-handlers.js +64 -10
  35. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  36. package/dist/tools/handlers/system-handlers.d.ts +1 -1
  37. package/dist/tools/input.d.ts +5 -1
  38. package/dist/tools/input.js +37 -1
  39. package/dist/tools/lighting.d.ts +1 -0
  40. package/dist/tools/lighting.js +7 -0
  41. package/dist/tools/physics.d.ts +1 -1
  42. package/dist/tools/sequence.d.ts +1 -0
  43. package/dist/tools/sequence.js +7 -0
  44. package/dist/types/handler-types.d.ts +343 -0
  45. package/dist/types/handler-types.js +2 -0
  46. package/dist/unreal-bridge.d.ts +1 -1
  47. package/dist/unreal-bridge.js +8 -6
  48. package/dist/utils/command-validator.d.ts +1 -0
  49. package/dist/utils/command-validator.js +11 -1
  50. package/dist/utils/error-handler.js +3 -1
  51. package/dist/utils/response-validator.js +2 -2
  52. package/dist/utils/safe-json.d.ts +1 -1
  53. package/dist/utils/safe-json.js +3 -6
  54. package/dist/utils/unreal-command-queue.js +1 -1
  55. package/dist/utils/validation.js +6 -2
  56. package/docs/handler-mapping.md +6 -1
  57. package/package.json +2 -2
  58. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +25 -1
  59. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +40 -58
  60. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +27 -46
  61. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +16 -1
  62. package/server.json +2 -2
  63. package/src/automation/bridge.ts +80 -10
  64. package/src/automation/types.ts +1 -0
  65. package/src/config/class-aliases.ts +65 -0
  66. package/src/constants.ts +10 -0
  67. package/src/graphql/server.ts +23 -23
  68. package/src/index.ts +1 -1
  69. package/src/services/metrics-server.ts +40 -6
  70. package/src/tools/consolidated-tool-definitions.ts +3 -3
  71. package/src/tools/debug.ts +8 -0
  72. package/src/tools/handlers/actor-handlers.ts +5 -31
  73. package/src/tools/handlers/asset-handlers.ts +19 -1
  74. package/src/tools/handlers/blueprint-handlers.ts +1 -1
  75. package/src/tools/handlers/common-handlers.ts +32 -11
  76. package/src/tools/handlers/editor-handlers.ts +8 -7
  77. package/src/tools/handlers/effect-handlers.ts +4 -0
  78. package/src/tools/handlers/graph-handlers.ts +7 -6
  79. package/src/tools/handlers/level-handlers.ts +5 -4
  80. package/src/tools/handlers/lighting-handlers.ts +5 -1
  81. package/src/tools/handlers/pipeline-handlers.ts +83 -16
  82. package/src/tools/input.ts +60 -1
  83. package/src/tools/lighting.ts +11 -0
  84. package/src/tools/physics.ts +1 -1
  85. package/src/tools/sequence.ts +11 -0
  86. package/src/types/handler-types.ts +442 -0
  87. package/src/unreal-bridge.ts +8 -6
  88. package/src/utils/command-validator.ts +23 -1
  89. package/src/utils/error-handler.ts +4 -1
  90. package/src/utils/response-validator.ts +7 -9
  91. package/src/utils/safe-json.ts +20 -15
  92. package/src/utils/unreal-command-queue.ts +3 -1
  93. package/src/utils/validation.test.ts +3 -3
  94. package/src/utils/validation.ts +36 -26
  95. package/tests/test-console-command.mjs +1 -1
  96. package/tests/test-runner.mjs +63 -3
  97. package/tests/run-unreal-tool-tests.mjs +0 -948
  98. 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
- UClass *LightClass = nullptr;
111
- if (LightClassStr == TEXT("DirectionalLight"))
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
- // Validate the resolved class is actually a light actor
130
- if (!LightClass || !LightClass->IsChildOf(ALight::StaticClass())) {
131
- SendAutomationError(
132
- RequestingSocket, RequestId,
133
- FString::Printf(
134
- TEXT("Light class not found or not a light type: %s"),
135
- *LightClassStr),
136
- TEXT("INVALID_ARGUMENT"));
137
- return true;
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> *LocObj;
143
- if (Payload->TryGetObjectField(TEXT("location"), LocObj)) {
144
- Location.X = (*LocObj)->GetNumberField(TEXT("x"));
145
- Location.Y = (*LocObj)->GetNumberField(TEXT("y"));
146
- Location.Z = (*LocObj)->GetNumberField(TEXT("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> *RotObj;
151
- if (Payload->TryGetObjectField(TEXT("rotation"), RotObj)) {
152
- Rotation.Pitch = (*RotObj)->GetNumberField(TEXT("pitch"));
153
- Rotation.Yaw = (*RotObj)->GetNumberField(TEXT("yaw"));
154
- Rotation.Roll = (*RotObj)->GetNumberField(TEXT("roll"));
155
- }
156
-
157
- AActor *NewLight = nullptr;
158
- UWorld *TargetWorld = (GEditor->PlayWorld) ? GEditor->PlayWorld : nullptr;
159
-
160
- if (TargetWorld) {
161
- // PIE Path
162
- FActorSpawnParameters SpawnParams;
163
- SpawnParams.SpawnCollisionHandlingOverride =
164
- ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
165
- NewLight = TargetWorld->SpawnActor(LightClass, &Location, &Rotation,
166
- SpawnParams);
167
- } else {
168
- // Editor Path
169
- NewLight = ActorSS->SpawnActorFromClass(LightClass, Location, Rotation);
170
- // Explicitly set location/rotation
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
- #if __has_include("Tracks/MovieScene3DTransformTrack.h")
2490
- if (TrackTypeLower == TEXT("transform") ||
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
- if (TrackTypeLower == TEXT("audio")) {
2500
- if (BindingGuid.IsValid()) {
2501
- NewTrack = MovieScene->AddTrack(UMovieSceneAudioTrack::StaticClass(),
2502
- BindingGuid);
2503
- } else {
2504
- NewTrack = MovieScene->AddTrack(UMovieSceneAudioTrack::StaticClass());
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
- if (TrackTypeLower == TEXT("event")) {
2506
+ // Validate it's actually a track class
2507
+ if (TrackClass && TrackClass->IsChildOf(UMovieSceneTrack::StaticClass())) {
2509
2508
  if (BindingGuid.IsValid()) {
2510
- NewTrack = MovieScene->AddTrack(UMovieSceneEventTrack::StaticClass(),
2511
- BindingGuid);
2509
+ NewTrack = MovieScene->AddTrack(TrackClass, BindingGuid);
2512
2510
  } else {
2513
- NewTrack = MovieScene->AddTrack(UMovieSceneEventTrack::StaticClass());
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) {
@@ -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
- ListenAddr->SetAnyAddress();
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.2",
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.2",
11
+ "version": "0.5.4",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  },
@@ -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
- // Compatibility fix: If only one protocol, pass as string to ensure ws/plugin compatibility
189
- const protocols = 'mcp-automation';
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: this.capabilityToken ? { 'X-MCP-Capability': this.capabilityToken } : undefined,
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
- // Set up message handling for the authenticated socket
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 text = typeof data === 'string' ? data : data.toString('utf8');
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,
@@ -12,6 +12,7 @@ export interface AutomationBridgeOptions {
12
12
  heartbeatIntervalMs?: number;
13
13
  maxPendingRequests?: number;
14
14
  maxConcurrentConnections?: number;
15
+ maxQueuedRequests?: number;
15
16
  clientMode?: boolean;
16
17
  clientHost?: string;
17
18
  clientPort?: number;
@@ -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
+
@@ -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.0.0';
33
+ : '0.5.4';
34
34
 
35
35
  function routeStdoutLogsToStderr(): void {
36
36
  if (!config.MCP_ROUTE_STDOUT_LOGS) {