unreal-engine-mcp-server 0.5.3 โ†’ 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 CHANGED
@@ -7,6 +7,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## ๐Ÿท๏ธ [0.5.4] - 2025-12-27
11
+
12
+ > [!IMPORTANT]
13
+ > ### ๐Ÿ›ก๏ธ Security Release
14
+ > This release focuses on **security hardening** and **defensive improvements** across the entire stack, including command injection prevention, network isolation, and resource management.
15
+
16
+ ### ๐Ÿ›ก๏ธ Security & Command Hardening
17
+
18
+ <details>
19
+ <summary><b>UBT Validation & Safe Execution</b></summary>
20
+
21
+ | Feature | Description |
22
+ |---------|-------------|
23
+ | **UBT Argument Validation** | Added `validateUbtArgumentsString` and `tokenizeArgs` to block dangerous characters (`;`, `|`, backticks) |
24
+ | **Safe Process Spawning** | Updated child process spawning to use `shell: false`, preventing shell injection attacks |
25
+ | **Console Command Validation** | Implemented strict input validation for the Unreal Automation Bridge to block chained or multi-line commands |
26
+ | **Argument Quoting** | Improved logging and execution logic to correctly quote arguments containing spaces |
27
+
28
+ </details>
29
+
30
+ ### ๐ŸŒ Network & Host Binding
31
+
32
+ <details>
33
+ <summary><b>Localhost Default & Remote Configuration</b></summary>
34
+
35
+ | Feature | Description |
36
+ |---------|-------------|
37
+ | **Localhost Default** | WebSocket, Metrics, and GraphQL servers now bind to `127.0.0.1` by default |
38
+ | **Remote Exposure Prevention** | Prevents accidental remote exposure of services |
39
+ | **GRAPHQL_ALLOW_REMOTE** | Added environment variable check for explicit remote binding configuration |
40
+ | **Security Warnings** | Warnings logged for unsafe/permissive network settings |
41
+
42
+ </details>
43
+
44
+ ### ๐Ÿšฆ Resource Management
45
+
46
+ <details>
47
+ <summary><b>Rate Limiting & Queue Management</b></summary>
48
+
49
+ | Feature | Description |
50
+ |---------|-------------|
51
+ | **IP-Based Rate Limiting** | Implemented rate limiting on the metrics server |
52
+ | **Queue Limits** | Introduced `maxQueuedRequests` to automation bridge to prevent memory exhaustion |
53
+ | **Message Size Enforcement** | Enforced `MAX_WS_MESSAGE_SIZE_BYTES` for WebSocket connections to reject oversized payloads |
54
+
55
+ </details>
56
+
57
+ ### ๐Ÿงช Testing & Cleanup
58
+
59
+ <details>
60
+ <summary><b>Test Updates & File Cleanup</b></summary>
61
+
62
+ | Change | Description |
63
+ |--------|-------------|
64
+ | **Path Sanitization Tests** | Modified validation tests to verify path sanitization and expect errors for traversal attempts |
65
+ | **Removed Legacy Tests** | Removed outdated test files (`run-unreal-tool-tests.mjs`, `test-asset-errors.mjs`) |
66
+ | **Response Logging** | Implemented better response logging in the test runner |
67
+
68
+ </details>
69
+
70
+ ### ๐Ÿ”„ Dependencies
71
+
72
+ - **dependencies group**: Bumped 2 updates via @dependabot ([#33](https://github.com/ChiR24/Unreal_mcp/pull/33))
73
+
74
+ ---
75
+
10
76
  ## ๐Ÿท๏ธ [0.5.3] - 2025-12-21
11
77
 
12
78
  > [!IMPORTANT]
@@ -13,6 +13,7 @@ export declare class AutomationBridge extends EventEmitter {
13
13
  private readonly clientPort;
14
14
  private readonly serverLegacyEnabled;
15
15
  private readonly maxConcurrentConnections;
16
+ private readonly maxQueuedRequests;
16
17
  private connectionManager;
17
18
  private requestTracker;
18
19
  private handshakeHandler;
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { WebSocket } from 'ws';
3
3
  import { Logger } from '../utils/logger.js';
4
- import { DEFAULT_AUTOMATION_HOST, DEFAULT_AUTOMATION_PORT, DEFAULT_NEGOTIATED_PROTOCOLS, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_MAX_PENDING_REQUESTS } from '../constants.js';
4
+ import { DEFAULT_AUTOMATION_HOST, DEFAULT_AUTOMATION_PORT, DEFAULT_NEGOTIATED_PROTOCOLS, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_MAX_PENDING_REQUESTS, DEFAULT_MAX_QUEUED_REQUESTS, MAX_WS_MESSAGE_SIZE_BYTES } from '../constants.js';
5
5
  import { createRequire } from 'node:module';
6
6
  import { ConnectionManager } from './connection-manager.js';
7
7
  import { RequestTracker } from './request-tracker.js';
@@ -31,6 +31,7 @@ export class AutomationBridge extends EventEmitter {
31
31
  clientPort;
32
32
  serverLegacyEnabled;
33
33
  maxConcurrentConnections;
34
+ maxQueuedRequests;
34
35
  connectionManager;
35
36
  requestTracker;
36
37
  handshakeHandler;
@@ -107,6 +108,7 @@ export class AutomationBridge extends EventEmitter {
107
108
  : 0;
108
109
  const maxPendingRequests = Math.max(1, options.maxPendingRequests ?? DEFAULT_MAX_PENDING_REQUESTS);
109
110
  const maxConcurrentConnections = Math.max(1, options.maxConcurrentConnections ?? 10);
111
+ this.maxQueuedRequests = Math.max(0, options.maxQueuedRequests ?? DEFAULT_MAX_QUEUED_REQUESTS);
110
112
  this.clientHost = options.clientHost ?? process.env.MCP_AUTOMATION_CLIENT_HOST ?? DEFAULT_AUTOMATION_HOST;
111
113
  this.clientPort = options.clientPort ?? sanitizePort(process.env.MCP_AUTOMATION_CLIENT_PORT) ?? DEFAULT_AUTOMATION_PORT;
112
114
  this.maxConcurrentConnections = maxConcurrentConnections;
@@ -137,10 +139,18 @@ export class AutomationBridge extends EventEmitter {
137
139
  const url = `ws://${this.clientHost}:${this.clientPort}`;
138
140
  this.log.info(`Connecting to Unreal Engine automation server at ${url}`);
139
141
  this.log.debug(`Negotiated protocols: ${JSON.stringify(this.negotiatedProtocols)}`);
140
- const protocols = 'mcp-automation';
142
+ const protocols = this.negotiatedProtocols.length === 1
143
+ ? this.negotiatedProtocols[0]
144
+ : this.negotiatedProtocols;
141
145
  this.log.debug(`Using WebSocket protocols arg: ${JSON.stringify(protocols)}`);
146
+ const headers = this.capabilityToken
147
+ ? {
148
+ 'X-MCP-Capability': this.capabilityToken,
149
+ 'X-MCP-Capability-Token': this.capabilityToken
150
+ }
151
+ : undefined;
142
152
  const socket = new WebSocket(url, protocols, {
143
- headers: this.capabilityToken ? { 'X-MCP-Capability': this.capabilityToken } : undefined,
153
+ headers,
144
154
  perMessageDeflate: false
145
155
  });
146
156
  this.handleClientConnection(socket);
@@ -174,9 +184,54 @@ export class AutomationBridge extends EventEmitter {
174
184
  port: this.clientPort,
175
185
  protocol: socket.protocol || null
176
186
  });
187
+ const getRawDataByteLength = (data) => {
188
+ if (typeof data === 'string') {
189
+ return Buffer.byteLength(data, 'utf8');
190
+ }
191
+ if (Buffer.isBuffer(data)) {
192
+ return data.length;
193
+ }
194
+ if (Array.isArray(data)) {
195
+ return data.reduce((total, item) => total + (Buffer.isBuffer(item) ? item.length : 0), 0);
196
+ }
197
+ if (data instanceof ArrayBuffer) {
198
+ return data.byteLength;
199
+ }
200
+ if (ArrayBuffer.isView(data)) {
201
+ return data.byteLength;
202
+ }
203
+ return 0;
204
+ };
205
+ const rawDataToUtf8String = (data, byteLengthHint) => {
206
+ if (typeof data === 'string') {
207
+ return data;
208
+ }
209
+ if (Buffer.isBuffer(data)) {
210
+ return data.toString('utf8');
211
+ }
212
+ if (Array.isArray(data)) {
213
+ const buffers = data.filter((item) => Buffer.isBuffer(item));
214
+ const totalLength = typeof byteLengthHint === 'number'
215
+ ? byteLengthHint
216
+ : buffers.reduce((total, item) => total + item.length, 0);
217
+ return Buffer.concat(buffers, totalLength).toString('utf8');
218
+ }
219
+ if (data instanceof ArrayBuffer) {
220
+ return Buffer.from(data).toString('utf8');
221
+ }
222
+ if (ArrayBuffer.isView(data)) {
223
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8');
224
+ }
225
+ return '';
226
+ };
177
227
  socket.on('message', (data) => {
178
228
  try {
179
- const text = typeof data === 'string' ? data : data.toString('utf8');
229
+ const byteLength = getRawDataByteLength(data);
230
+ if (byteLength > MAX_WS_MESSAGE_SIZE_BYTES) {
231
+ this.log.error(`Received oversized message (${byteLength} bytes, max: ${MAX_WS_MESSAGE_SIZE_BYTES}). Dropping.`);
232
+ return;
233
+ }
234
+ const text = rawDataToUtf8String(data, byteLength);
180
235
  this.log.debug(`[AutomationBridge Client] Received message: ${text.substring(0, 1000)}`);
181
236
  const parsed = JSON.parse(text);
182
237
  this.connectionManager.updateLastMessageTime();
@@ -350,6 +405,9 @@ export class AutomationBridge extends EventEmitter {
350
405
  throw new Error('Automation bridge not connected');
351
406
  }
352
407
  if (this.requestTracker.getPendingCount() >= this.requestTracker.getMaxPendingRequests()) {
408
+ if (this.queuedRequestItems.length >= this.maxQueuedRequests) {
409
+ throw new Error(`Automation bridge request queue is full (max: ${this.maxQueuedRequests}). Please retry later.`);
410
+ }
353
411
  return new Promise((resolve, reject) => {
354
412
  this.queuedRequestItems.push({
355
413
  resolve,
@@ -11,6 +11,7 @@ export interface AutomationBridgeOptions {
11
11
  heartbeatIntervalMs?: number;
12
12
  maxPendingRequests?: number;
13
13
  maxConcurrentConnections?: number;
14
+ maxQueuedRequests?: number;
14
15
  clientMode?: boolean;
15
16
  clientHost?: string;
16
17
  clientPort?: number;
@@ -5,6 +5,7 @@ export declare const DEFAULT_NEGOTIATED_PROTOCOLS: string[];
5
5
  export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 10000;
6
6
  export declare const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5000;
7
7
  export declare const DEFAULT_MAX_PENDING_REQUESTS = 25;
8
+ export declare const DEFAULT_MAX_QUEUED_REQUESTS = 100;
8
9
  export declare const DEFAULT_TIME_OF_DAY = 9;
9
10
  export declare const DEFAULT_SUN_INTENSITY = 10000;
10
11
  export declare const DEFAULT_SKYLIGHT_INTENSITY = 1;
@@ -16,4 +17,5 @@ export declare const LONG_RUNNING_OP_TIMEOUT_MS = 300000;
16
17
  export declare const CONSOLE_COMMAND_TIMEOUT_MS = 30000;
17
18
  export declare const ENGINE_QUERY_TIMEOUT_MS = 15000;
18
19
  export declare const CONNECTION_TIMEOUT_MS = 15000;
20
+ export declare const MAX_WS_MESSAGE_SIZE_BYTES: number;
19
21
  //# sourceMappingURL=constants.d.ts.map
package/dist/constants.js CHANGED
@@ -5,6 +5,7 @@ export const DEFAULT_NEGOTIATED_PROTOCOLS = ['mcp-automation'];
5
5
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 10000;
6
6
  export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5000;
7
7
  export const DEFAULT_MAX_PENDING_REQUESTS = 25;
8
+ export const DEFAULT_MAX_QUEUED_REQUESTS = 100;
8
9
  export const DEFAULT_TIME_OF_DAY = 9;
9
10
  export const DEFAULT_SUN_INTENSITY = 10000;
10
11
  export const DEFAULT_SKYLIGHT_INTENSITY = 1;
@@ -16,4 +17,5 @@ export const LONG_RUNNING_OP_TIMEOUT_MS = 300000;
16
17
  export const CONSOLE_COMMAND_TIMEOUT_MS = 30000;
17
18
  export const ENGINE_QUERY_TIMEOUT_MS = 15000;
18
19
  export const CONNECTION_TIMEOUT_MS = 15000;
20
+ export const MAX_WS_MESSAGE_SIZE_BYTES = 5 * 1024 * 1024;
19
21
  //# sourceMappingURL=constants.js.map
@@ -19,7 +19,6 @@ export declare class GraphQLServer {
19
19
  constructor(bridge: UnrealBridge, automationBridge: AutomationBridge, config?: GraphQLServerConfig);
20
20
  start(): Promise<void>;
21
21
  stop(): Promise<void>;
22
- private setupShutdown;
23
22
  getConfig(): Required<GraphQLServerConfig>;
24
23
  isRunning(): boolean;
25
24
  }
@@ -28,6 +28,21 @@ export class GraphQLServer {
28
28
  this.log.info('GraphQL server is disabled');
29
29
  return;
30
30
  }
31
+ const isLoopback = this.config.host === '127.0.0.1' ||
32
+ this.config.host === '::1' ||
33
+ this.config.host.toLowerCase() === 'localhost';
34
+ const allowRemote = process.env.GRAPHQL_ALLOW_REMOTE === 'true';
35
+ if (!isLoopback && !allowRemote) {
36
+ this.log.warn(`GraphQL server is configured to bind to non-loopback host '${this.config.host}'. GraphQL is for local debugging only. ` +
37
+ 'To allow remote binding, set GRAPHQL_ALLOW_REMOTE=true. Aborting start.');
38
+ return;
39
+ }
40
+ if (!isLoopback && allowRemote) {
41
+ if (this.config.cors.origin === '*') {
42
+ this.log.warn("GraphQL server is binding to a remote host with permissive CORS origin '*'. " +
43
+ 'Set GRAPHQL_CORS_ORIGIN to specific origins for production. Using permissive CORS for now.');
44
+ }
45
+ }
31
46
  try {
32
47
  const schema = createGraphQLSchema(this.bridge, this.automationBridge);
33
48
  const yoga = createYoga({
@@ -67,7 +82,6 @@ export class GraphQLServer {
67
82
  resolve();
68
83
  });
69
84
  });
70
- this.setupShutdown();
71
85
  }
72
86
  catch (error) {
73
87
  this.log.error('Failed to start GraphQL server:', error);
@@ -92,21 +106,6 @@ export class GraphQLServer {
92
106
  });
93
107
  });
94
108
  }
95
- setupShutdown() {
96
- const gracefulShutdown = async (signal) => {
97
- this.log.info(`Received ${signal}, shutting down GraphQL server...`);
98
- try {
99
- await this.stop();
100
- process.exit(0);
101
- }
102
- catch (error) {
103
- this.log.error('Error during GraphQL server shutdown:', error);
104
- process.exit(1);
105
- }
106
- };
107
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
108
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
109
- }
110
109
  getConfig() {
111
110
  return this.config;
112
111
  }
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ const DEFAULT_SERVER_NAME = typeof packageInfo.name === 'string' && packageInfo.
29
29
  : 'unreal-engine-mcp';
30
30
  const DEFAULT_SERVER_VERSION = typeof packageInfo.version === 'string' && packageInfo.version.trim().length > 0
31
31
  ? packageInfo.version
32
- : '0.5.3';
32
+ : '0.5.4';
33
33
  function routeStdoutLogsToStderr() {
34
34
  if (!config.MCP_ROUTE_STDOUT_LOGS) {
35
35
  return;
@@ -1,6 +1,5 @@
1
1
  import http from 'http';
2
2
  import { wasmIntegration } from '../wasm/index.js';
3
- import { DEFAULT_AUTOMATION_HOST } from '../constants.js';
4
3
  function formatPrometheusMetrics(options) {
5
4
  const { healthMonitor, automationBridge } = options;
6
5
  const m = healthMonitor.metrics;
@@ -61,6 +60,7 @@ export function startMetricsServer(options) {
61
60
  logger.debug('Metrics server disabled (set MCP_METRICS_PORT to enable Prometheus /metrics endpoint).');
62
61
  return null;
63
62
  }
63
+ const host = process.env.MCP_METRICS_HOST || '127.0.0.1';
64
64
  const RATE_LIMIT_WINDOW_MS = 60000;
65
65
  const RATE_LIMIT_MAX_REQUESTS = 60;
66
66
  const requestCounts = new Map();
@@ -114,8 +114,8 @@ export function startMetricsServer(options) {
114
114
  res.end('Internal Server Error');
115
115
  }
116
116
  });
117
- server.listen(port, () => {
118
- logger.info(`Prometheus metrics server listening on http://${DEFAULT_AUTOMATION_HOST}:${port}/metrics`);
117
+ server.listen(port, host, () => {
118
+ logger.info(`Prometheus metrics server listening on http://${host}:${port}/metrics`);
119
119
  });
120
120
  server.on('error', (err) => {
121
121
  logger.warn('Metrics server error', err);
@@ -3,6 +3,55 @@ import { executeAutomationRequest } from './common-handlers.js';
3
3
  import { spawn } from 'child_process';
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
+ function validateUbtArgumentsString(extraArgs) {
7
+ if (!extraArgs || typeof extraArgs !== 'string') {
8
+ return;
9
+ }
10
+ const forbiddenChars = ['\n', '\r', ';', '|', '`', '&&', '||', '>', '<'];
11
+ for (const char of forbiddenChars) {
12
+ if (extraArgs.includes(char)) {
13
+ throw new Error(`UBT arguments contain forbidden character(s) and are blocked for safety. Blocked: ${JSON.stringify(char)}.`);
14
+ }
15
+ }
16
+ }
17
+ function tokenizeArgs(extraArgs) {
18
+ if (!extraArgs) {
19
+ return [];
20
+ }
21
+ const args = [];
22
+ let current = '';
23
+ let inQuotes = false;
24
+ let escapeNext = false;
25
+ for (let i = 0; i < extraArgs.length; i++) {
26
+ const ch = extraArgs[i];
27
+ if (escapeNext) {
28
+ current += ch;
29
+ escapeNext = false;
30
+ continue;
31
+ }
32
+ if (ch === '\\') {
33
+ escapeNext = true;
34
+ continue;
35
+ }
36
+ if (ch === '"') {
37
+ inQuotes = !inQuotes;
38
+ continue;
39
+ }
40
+ if (!inQuotes && /\s/.test(ch)) {
41
+ if (current.length > 0) {
42
+ args.push(current);
43
+ current = '';
44
+ }
45
+ }
46
+ else {
47
+ current += ch;
48
+ }
49
+ }
50
+ if (current.length > 0) {
51
+ args.push(current);
52
+ }
53
+ return args;
54
+ }
6
55
  export async function handlePipelineTools(action, args, tools) {
7
56
  switch (action) {
8
57
  case 'run_ubt': {
@@ -13,6 +62,7 @@ export async function handlePipelineTools(action, args, tools) {
13
62
  if (!target) {
14
63
  throw new Error('Target is required for run_ubt');
15
64
  }
65
+ validateUbtArgumentsString(extraArgs);
16
66
  let ubtPath = 'UnrealBuildTool';
17
67
  const enginePath = process.env.UE_ENGINE_PATH || process.env.UNREAL_ENGINE_PATH;
18
68
  if (enginePath) {
@@ -41,15 +91,17 @@ export async function handlePipelineTools(action, args, tools) {
41
91
  throw new Error(`Could not read project directory: ${projectPath}`);
42
92
  }
43
93
  }
94
+ const projectArg = `-Project="${uprojectFile}"`;
95
+ const extraTokens = tokenizeArgs(extraArgs);
44
96
  const cmdArgs = [
45
97
  target,
46
98
  platform,
47
99
  configuration,
48
- `-Project="${uprojectFile}"`,
49
- extraArgs
50
- ].filter(Boolean);
100
+ projectArg,
101
+ ...extraTokens
102
+ ];
51
103
  return new Promise((resolve) => {
52
- const child = spawn(ubtPath, cmdArgs, { shell: true });
104
+ const child = spawn(ubtPath, cmdArgs, { shell: false });
53
105
  const MAX_OUTPUT_SIZE = 20 * 1024;
54
106
  let stdout = '';
55
107
  let stderr = '';
@@ -73,12 +125,13 @@ export async function handlePipelineTools(action, args, tools) {
73
125
  const truncatedNote = (stdout.length >= MAX_OUTPUT_SIZE || stderr.length >= MAX_OUTPUT_SIZE)
74
126
  ? '\n[Output truncated for response payload]'
75
127
  : '';
128
+ const quotedArgs = cmdArgs.map(arg => arg.includes(' ') ? `"${arg}"` : arg);
76
129
  if (code === 0) {
77
130
  resolve({
78
131
  success: true,
79
132
  message: 'UnrealBuildTool finished successfully',
80
133
  output: stdout + truncatedNote,
81
- command: `${ubtPath} ${cmdArgs.join(' ')}`
134
+ command: `${ubtPath} ${quotedArgs.join(' ')}`
82
135
  });
83
136
  }
84
137
  else {
@@ -88,16 +141,17 @@ export async function handlePipelineTools(action, args, tools) {
88
141
  message: `UnrealBuildTool failed with code ${code}`,
89
142
  output: stdout + truncatedNote,
90
143
  errorOutput: stderr + truncatedNote,
91
- command: `${ubtPath} ${cmdArgs.join(' ')}`
144
+ command: `${ubtPath} ${quotedArgs.join(' ')}`
92
145
  });
93
146
  }
94
147
  });
95
148
  child.on('error', (err) => {
149
+ const quotedArgs = cmdArgs.map(arg => arg.includes(' ') ? `"${arg}"` : arg);
96
150
  resolve({
97
151
  success: false,
98
152
  error: 'SPAWN_FAILED',
99
153
  message: `Failed to spawn UnrealBuildTool: ${err.message}`,
100
- command: `${ubtPath} ${cmdArgs.join(' ')}`
154
+ command: `${ubtPath} ${quotedArgs.join(' ')}`
101
155
  });
102
156
  });
103
157
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unreal-engine-mcp-server",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "mcpName": "io.github.ChiR24/unreal-engine-mcp",
5
5
  "description": "A comprehensive Model Context Protocol (MCP) server that enables AI assistants to control Unreal Engine via native automation bridge. Built with TypeScript and designed for game development automation.",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- ๏ปฟ#include "McpAutomationBridgeGlobals.h"
1
+ #include "McpAutomationBridgeGlobals.h"
2
2
  #include "McpAutomationBridgeHelpers.h"
3
3
  #include "McpAutomationBridgeSubsystem.h"
4
4
 
@@ -1024,6 +1024,30 @@ bool UMcpAutomationBridgeSubsystem::HandleConsoleCommandAction(
1024
1024
  return true;
1025
1025
  }
1026
1026
 
1027
+ // 4. Block line breaks
1028
+ if (LowerCommand.Contains(TEXT("\n")) || LowerCommand.Contains(TEXT("\r"))) {
1029
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1030
+ TEXT("Multi-line commands are blocked for safety"),
1031
+ nullptr, TEXT("COMMAND_BLOCKED"));
1032
+ return true;
1033
+ }
1034
+
1035
+ // 5. Block semicolon and pipe
1036
+ if (LowerCommand.Contains(TEXT(";")) || LowerCommand.Contains(TEXT("|"))) {
1037
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1038
+ TEXT("Command chaining with semicolon or pipe is blocked for safety"),
1039
+ nullptr, TEXT("COMMAND_BLOCKED"));
1040
+ return true;
1041
+ }
1042
+
1043
+ // 6. Block backticks
1044
+ if (LowerCommand.Contains(TEXT("`"))) {
1045
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1046
+ TEXT("Commands containing backticks are blocked for safety"),
1047
+ nullptr, TEXT("COMMAND_BLOCKED"));
1048
+ return true;
1049
+ }
1050
+
1027
1051
  // Execute the command
1028
1052
  try {
1029
1053
  UWorld *TargetWorld = nullptr;
@@ -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.3",
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.3",
11
+ "version": "0.5.4",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  },