unreal-engine-mcp-server 0.4.6 → 0.4.7

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/.env.production CHANGED
@@ -19,7 +19,7 @@ LOG_LEVEL=info
19
19
 
20
20
  # Server Settings
21
21
  SERVER_NAME=unreal-engine-mcp
22
- SERVER_VERSION=0.4.6
22
+ SERVER_VERSION=0.4.7
23
23
 
24
24
  # Connection Settings
25
25
  MAX_RETRY_ATTEMPTS=3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.4.7] - 2025-11-16
6
+ ### Added
7
+ - Output Log reading via `system_control` tool with `read_log` action. Supports filtering by category (comma-separated or array), log level (Error, Warning, Log, Verbose, VeryVerbose, All), line count (up to 2000), specific log path, include prefixes, and exclude categories. Automatically resolves the latest project log under Saved/Logs.
8
+ - New `src/tools/logs.ts` implementing robust log tailing, parsing (timestamp/category/level/message), and UE-specific internal entry filtering (e.g., excludes LogPython RESULT: blocks unless requested).
9
+
10
+ ### Changed
11
+ - `system_control` tool schema: Added `read_log` action with full filter parameters to inputSchema; extended outputSchema with `logPath`, `entries` array, and `filteredCount`.
12
+ - Updated `src/tools/consolidated-tool-handlers.ts` to route `read_log` to LogTools without requiring UE connection (file-based).
13
+ - `src/index.ts`: Instantiates and passes LogTools to consolidated handler.
14
+ - Version bumped to 0.4.7 in package.json, package-lock.json, server.json, .env.production, and runtime config.
15
+
5
16
  ## [0.4.6] - 2025-10-04
6
17
  ### Fixed
7
18
  - Fixed duplicate response output issue where tool responses were being displayed twice in MCP content
package/README.md CHANGED
@@ -5,7 +5,6 @@
5
5
  [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-TypeScript-blue)](https://github.com/modelcontextprotocol/sdk)
6
6
  [![Unreal Engine](https://img.shields.io/badge/Unreal%20Engine-5.0--5.6-orange)](https://www.unrealengine.com/)
7
7
  [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-Published-green)](https://registry.modelcontextprotocol.io/)
8
- [![smithery badge](https://smithery.ai/badge/@ChiR24/unreal_mcp)](https://smithery.ai/server/@ChiR24/unreal_mcp)
9
8
 
10
9
  A comprehensive Model Context Protocol (MCP) server that enables AI assistants to control Unreal Engine via Remote Control API. Built with TypeScript and designed for game development automation.
11
10
 
@@ -145,7 +144,7 @@ Then enable Python execution in: Edit > Project Settings > Plugins > Remote Cont
145
144
  | `create_effect` | Particles, Niagara, debug shapes |
146
145
  | `manage_blueprint` | Create blueprints, add components |
147
146
  | `build_environment` | Landscapes, terrain, foliage |
148
- | `system_control` | Profiling, quality, UI, screenshots |
147
+ | `system_control` | Profiling, quality, UI, screenshots, Output Log reading |
149
148
  | `console_command` | Direct console command execution |
150
149
  | `manage_rc` | Remote Control presets |
151
150
  | `manage_sequence` | Sequencer/cinematics |
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ import { SequenceTools } from './tools/sequence.js';
28
28
  import { IntrospectionTools } from './tools/introspection.js';
29
29
  import { VisualTools } from './tools/visual.js';
30
30
  import { EngineTools } from './tools/engine.js';
31
+ import { LogTools } from './tools/logs.js';
31
32
  import { consolidatedToolDefinitions } from './tools/consolidated-tool-definitions.js';
32
33
  import { handleConsolidatedToolCall } from './tools/consolidated-tool-handlers.js';
33
34
  import { prompts } from './prompts/index.js';
@@ -63,7 +64,7 @@ const CONFIG = {
63
64
  RETRY_DELAY_MS: 2000,
64
65
  // Server info
65
66
  SERVER_NAME: 'unreal-engine-mcp',
66
- SERVER_VERSION: '0.4.6',
67
+ SERVER_VERSION: '0.4.7',
67
68
  // Monitoring
68
69
  HEALTH_CHECK_INTERVAL_MS: 30000 // 30 seconds
69
70
  };
@@ -206,6 +207,7 @@ export function createServer() {
206
207
  const introspectionTools = new IntrospectionTools(bridge);
207
208
  const visualTools = new VisualTools(bridge);
208
209
  const engineTools = new EngineTools(bridge);
210
+ const logTools = new LogTools(bridge);
209
211
  const server = new Server({
210
212
  name: CONFIG.SERVER_NAME,
211
213
  version: CONFIG.SERVER_VERSION
@@ -430,11 +432,23 @@ export function createServer() {
430
432
  const { name } = request.params;
431
433
  let args = request.params.arguments || {};
432
434
  const startTime = Date.now();
433
- // Ensure connection only when needed, with 3 attempts
434
- const connected = await ensureConnectedOnDemand();
435
- if (!connected) {
436
- trackPerformance(startTime, false);
437
- return createNotConnectedResponse(name);
435
+ let requiresEngine = true;
436
+ try {
437
+ const n = String(name);
438
+ if (n === 'system_control') {
439
+ const action = String((args || {}).action || '').trim();
440
+ if (action === 'read_log') {
441
+ requiresEngine = false;
442
+ }
443
+ }
444
+ }
445
+ catch { }
446
+ if (requiresEngine) {
447
+ const connected = await ensureConnectedOnDemand();
448
+ if (!connected) {
449
+ trackPerformance(startTime, false);
450
+ return createNotConnectedResponse(name);
451
+ }
438
452
  }
439
453
  // Create tools object for handler
440
454
  const tools = {
@@ -460,6 +474,7 @@ export function createServer() {
460
474
  introspectionTools,
461
475
  visualTools,
462
476
  engineTools,
477
+ logTools,
463
478
  // Elicitation (client-optional)
464
479
  elicit: elicitation.elicit,
465
480
  supportsElicitation: elicitation.supports,
@@ -90,6 +90,12 @@ export declare const consolidatedToolDefinitions: ({
90
90
  resolution?: undefined;
91
91
  projectPath?: undefined;
92
92
  editorExe?: undefined;
93
+ filter_category?: undefined;
94
+ filter_level?: undefined;
95
+ lines?: undefined;
96
+ log_path?: undefined;
97
+ include_prefixes?: undefined;
98
+ exclude_categories?: undefined;
93
99
  command?: undefined;
94
100
  presetPath?: undefined;
95
101
  objectPath?: undefined;
@@ -186,6 +192,9 @@ export declare const consolidatedToolDefinitions: ({
186
192
  imagePath?: undefined;
187
193
  imageBase64?: undefined;
188
194
  pid?: undefined;
195
+ logPath?: undefined;
196
+ entries?: undefined;
197
+ filteredCount?: undefined;
189
198
  command?: undefined;
190
199
  result?: undefined;
191
200
  info?: undefined;
@@ -340,6 +349,12 @@ export declare const consolidatedToolDefinitions: ({
340
349
  resolution?: undefined;
341
350
  projectPath?: undefined;
342
351
  editorExe?: undefined;
352
+ filter_category?: undefined;
353
+ filter_level?: undefined;
354
+ lines?: undefined;
355
+ log_path?: undefined;
356
+ include_prefixes?: undefined;
357
+ exclude_categories?: undefined;
343
358
  command?: undefined;
344
359
  presetPath?: undefined;
345
360
  objectPath?: undefined;
@@ -416,6 +431,9 @@ export declare const consolidatedToolDefinitions: ({
416
431
  imagePath?: undefined;
417
432
  imageBase64?: undefined;
418
433
  pid?: undefined;
434
+ logPath?: undefined;
435
+ entries?: undefined;
436
+ filteredCount?: undefined;
419
437
  command?: undefined;
420
438
  result?: undefined;
421
439
  info?: undefined;
@@ -550,6 +568,12 @@ export declare const consolidatedToolDefinitions: ({
550
568
  resolution?: undefined;
551
569
  projectPath?: undefined;
552
570
  editorExe?: undefined;
571
+ filter_category?: undefined;
572
+ filter_level?: undefined;
573
+ lines?: undefined;
574
+ log_path?: undefined;
575
+ include_prefixes?: undefined;
576
+ exclude_categories?: undefined;
553
577
  command?: undefined;
554
578
  presetPath?: undefined;
555
579
  objectPath?: undefined;
@@ -632,6 +656,9 @@ export declare const consolidatedToolDefinitions: ({
632
656
  imagePath?: undefined;
633
657
  imageBase64?: undefined;
634
658
  pid?: undefined;
659
+ logPath?: undefined;
660
+ entries?: undefined;
661
+ filteredCount?: undefined;
635
662
  command?: undefined;
636
663
  result?: undefined;
637
664
  info?: undefined;
@@ -775,6 +802,12 @@ export declare const consolidatedToolDefinitions: ({
775
802
  resolution?: undefined;
776
803
  projectPath?: undefined;
777
804
  editorExe?: undefined;
805
+ filter_category?: undefined;
806
+ filter_level?: undefined;
807
+ lines?: undefined;
808
+ log_path?: undefined;
809
+ include_prefixes?: undefined;
810
+ exclude_categories?: undefined;
778
811
  command?: undefined;
779
812
  presetPath?: undefined;
780
813
  objectPath?: undefined;
@@ -854,6 +887,9 @@ export declare const consolidatedToolDefinitions: ({
854
887
  imagePath?: undefined;
855
888
  imageBase64?: undefined;
856
889
  pid?: undefined;
890
+ logPath?: undefined;
891
+ entries?: undefined;
892
+ filteredCount?: undefined;
857
893
  command?: undefined;
858
894
  result?: undefined;
859
895
  info?: undefined;
@@ -978,6 +1014,12 @@ export declare const consolidatedToolDefinitions: ({
978
1014
  resolution?: undefined;
979
1015
  projectPath?: undefined;
980
1016
  editorExe?: undefined;
1017
+ filter_category?: undefined;
1018
+ filter_level?: undefined;
1019
+ lines?: undefined;
1020
+ log_path?: undefined;
1021
+ include_prefixes?: undefined;
1022
+ exclude_categories?: undefined;
981
1023
  command?: undefined;
982
1024
  presetPath?: undefined;
983
1025
  objectPath?: undefined;
@@ -1054,6 +1096,9 @@ export declare const consolidatedToolDefinitions: ({
1054
1096
  imagePath?: undefined;
1055
1097
  imageBase64?: undefined;
1056
1098
  pid?: undefined;
1099
+ logPath?: undefined;
1100
+ entries?: undefined;
1101
+ filteredCount?: undefined;
1057
1102
  command?: undefined;
1058
1103
  result?: undefined;
1059
1104
  info?: undefined;
@@ -1195,6 +1240,12 @@ export declare const consolidatedToolDefinitions: ({
1195
1240
  resolution?: undefined;
1196
1241
  projectPath?: undefined;
1197
1242
  editorExe?: undefined;
1243
+ filter_category?: undefined;
1244
+ filter_level?: undefined;
1245
+ lines?: undefined;
1246
+ log_path?: undefined;
1247
+ include_prefixes?: undefined;
1248
+ exclude_categories?: undefined;
1198
1249
  command?: undefined;
1199
1250
  presetPath?: undefined;
1200
1251
  objectPath?: undefined;
@@ -1274,6 +1325,9 @@ export declare const consolidatedToolDefinitions: ({
1274
1325
  imagePath?: undefined;
1275
1326
  imageBase64?: undefined;
1276
1327
  pid?: undefined;
1328
+ logPath?: undefined;
1329
+ entries?: undefined;
1330
+ filteredCount?: undefined;
1277
1331
  command?: undefined;
1278
1332
  result?: undefined;
1279
1333
  info?: undefined;
@@ -1386,6 +1440,12 @@ export declare const consolidatedToolDefinitions: ({
1386
1440
  resolution?: undefined;
1387
1441
  projectPath?: undefined;
1388
1442
  editorExe?: undefined;
1443
+ filter_category?: undefined;
1444
+ filter_level?: undefined;
1445
+ lines?: undefined;
1446
+ log_path?: undefined;
1447
+ include_prefixes?: undefined;
1448
+ exclude_categories?: undefined;
1389
1449
  command?: undefined;
1390
1450
  presetPath?: undefined;
1391
1451
  objectPath?: undefined;
@@ -1459,6 +1519,9 @@ export declare const consolidatedToolDefinitions: ({
1459
1519
  imagePath?: undefined;
1460
1520
  imageBase64?: undefined;
1461
1521
  pid?: undefined;
1522
+ logPath?: undefined;
1523
+ entries?: undefined;
1524
+ filteredCount?: undefined;
1462
1525
  command?: undefined;
1463
1526
  result?: undefined;
1464
1527
  info?: undefined;
@@ -1733,6 +1796,12 @@ export declare const consolidatedToolDefinitions: ({
1733
1796
  resolution?: undefined;
1734
1797
  projectPath?: undefined;
1735
1798
  editorExe?: undefined;
1799
+ filter_category?: undefined;
1800
+ filter_level?: undefined;
1801
+ lines?: undefined;
1802
+ log_path?: undefined;
1803
+ include_prefixes?: undefined;
1804
+ exclude_categories?: undefined;
1736
1805
  command?: undefined;
1737
1806
  presetPath?: undefined;
1738
1807
  objectPath?: undefined;
@@ -1806,6 +1875,9 @@ export declare const consolidatedToolDefinitions: ({
1806
1875
  imagePath?: undefined;
1807
1876
  imageBase64?: undefined;
1808
1877
  pid?: undefined;
1878
+ logPath?: undefined;
1879
+ entries?: undefined;
1880
+ filteredCount?: undefined;
1809
1881
  command?: undefined;
1810
1882
  result?: undefined;
1811
1883
  info?: undefined;
@@ -1911,6 +1983,36 @@ export declare const consolidatedToolDefinitions: ({
1911
1983
  type: string;
1912
1984
  description: string;
1913
1985
  };
1986
+ filter_category: {
1987
+ description: string;
1988
+ };
1989
+ filter_level: {
1990
+ type: string;
1991
+ enum: string[];
1992
+ description: string;
1993
+ };
1994
+ lines: {
1995
+ type: string;
1996
+ description: string;
1997
+ };
1998
+ log_path: {
1999
+ type: string;
2000
+ description: string;
2001
+ };
2002
+ include_prefixes: {
2003
+ type: string;
2004
+ items: {
2005
+ type: string;
2006
+ };
2007
+ description: string;
2008
+ };
2009
+ exclude_categories: {
2010
+ type: string;
2011
+ items: {
2012
+ type: string;
2013
+ };
2014
+ description: string;
2015
+ };
1914
2016
  directory?: undefined;
1915
2017
  sourcePath?: undefined;
1916
2018
  destinationPath?: undefined;
@@ -2030,6 +2132,35 @@ export declare const consolidatedToolDefinitions: ({
2030
2132
  type: string;
2031
2133
  description: string;
2032
2134
  };
2135
+ logPath: {
2136
+ type: string;
2137
+ description: string;
2138
+ };
2139
+ entries: {
2140
+ type: string;
2141
+ items: {
2142
+ type: string;
2143
+ properties: {
2144
+ timestamp: {
2145
+ type: string;
2146
+ };
2147
+ category: {
2148
+ type: string;
2149
+ };
2150
+ level: {
2151
+ type: string;
2152
+ };
2153
+ message: {
2154
+ type: string;
2155
+ };
2156
+ };
2157
+ };
2158
+ description: string;
2159
+ };
2160
+ filteredCount: {
2161
+ type: string;
2162
+ description: string;
2163
+ };
2033
2164
  assets?: undefined;
2034
2165
  paths?: undefined;
2035
2166
  materialPath?: undefined;
@@ -2153,6 +2284,12 @@ export declare const consolidatedToolDefinitions: ({
2153
2284
  resolution?: undefined;
2154
2285
  projectPath?: undefined;
2155
2286
  editorExe?: undefined;
2287
+ filter_category?: undefined;
2288
+ filter_level?: undefined;
2289
+ lines?: undefined;
2290
+ log_path?: undefined;
2291
+ include_prefixes?: undefined;
2292
+ exclude_categories?: undefined;
2156
2293
  presetPath?: undefined;
2157
2294
  objectPath?: undefined;
2158
2295
  propertyName?: undefined;
@@ -2231,6 +2368,9 @@ export declare const consolidatedToolDefinitions: ({
2231
2368
  imagePath?: undefined;
2232
2369
  imageBase64?: undefined;
2233
2370
  pid?: undefined;
2371
+ logPath?: undefined;
2372
+ entries?: undefined;
2373
+ filteredCount?: undefined;
2234
2374
  presetPath?: undefined;
2235
2375
  fields?: undefined;
2236
2376
  value?: undefined;
@@ -2349,6 +2489,12 @@ export declare const consolidatedToolDefinitions: ({
2349
2489
  resolution?: undefined;
2350
2490
  projectPath?: undefined;
2351
2491
  editorExe?: undefined;
2492
+ filter_category?: undefined;
2493
+ filter_level?: undefined;
2494
+ lines?: undefined;
2495
+ log_path?: undefined;
2496
+ include_prefixes?: undefined;
2497
+ exclude_categories?: undefined;
2352
2498
  command?: undefined;
2353
2499
  actorNames?: undefined;
2354
2500
  className?: undefined;
@@ -2422,6 +2568,9 @@ export declare const consolidatedToolDefinitions: ({
2422
2568
  imagePath?: undefined;
2423
2569
  imageBase64?: undefined;
2424
2570
  pid?: undefined;
2571
+ logPath?: undefined;
2572
+ entries?: undefined;
2573
+ filteredCount?: undefined;
2425
2574
  command?: undefined;
2426
2575
  result?: undefined;
2427
2576
  info?: undefined;
@@ -2565,6 +2714,12 @@ export declare const consolidatedToolDefinitions: ({
2565
2714
  resolution?: undefined;
2566
2715
  projectPath?: undefined;
2567
2716
  editorExe?: undefined;
2717
+ filter_category?: undefined;
2718
+ filter_level?: undefined;
2719
+ lines?: undefined;
2720
+ log_path?: undefined;
2721
+ include_prefixes?: undefined;
2722
+ exclude_categories?: undefined;
2568
2723
  command?: undefined;
2569
2724
  presetPath?: undefined;
2570
2725
  objectPath?: undefined;
@@ -2671,6 +2826,9 @@ export declare const consolidatedToolDefinitions: ({
2671
2826
  imagePath?: undefined;
2672
2827
  imageBase64?: undefined;
2673
2828
  pid?: undefined;
2829
+ logPath?: undefined;
2830
+ entries?: undefined;
2831
+ filteredCount?: undefined;
2674
2832
  command?: undefined;
2675
2833
  result?: undefined;
2676
2834
  info?: undefined;
@@ -2767,6 +2925,12 @@ export declare const consolidatedToolDefinitions: ({
2767
2925
  resolution?: undefined;
2768
2926
  projectPath?: undefined;
2769
2927
  editorExe?: undefined;
2928
+ filter_category?: undefined;
2929
+ filter_level?: undefined;
2930
+ lines?: undefined;
2931
+ log_path?: undefined;
2932
+ include_prefixes?: undefined;
2933
+ exclude_categories?: undefined;
2770
2934
  command?: undefined;
2771
2935
  presetPath?: undefined;
2772
2936
  actorNames?: undefined;
@@ -2835,6 +2999,9 @@ export declare const consolidatedToolDefinitions: ({
2835
2999
  imagePath?: undefined;
2836
3000
  imageBase64?: undefined;
2837
3001
  pid?: undefined;
3002
+ logPath?: undefined;
3003
+ entries?: undefined;
3004
+ filteredCount?: undefined;
2838
3005
  command?: undefined;
2839
3006
  result?: undefined;
2840
3007
  presetPath?: undefined;
@@ -528,7 +528,7 @@ Supported actions: profile, show_fps, set_quality, play_sound, create_widget, sh
528
528
  properties: {
529
529
  action: {
530
530
  type: 'string',
531
- enum: ['profile', 'show_fps', 'set_quality', 'play_sound', 'create_widget', 'show_widget', 'screenshot', 'engine_start', 'engine_quit'],
531
+ enum: ['profile', 'show_fps', 'set_quality', 'play_sound', 'create_widget', 'show_widget', 'screenshot', 'engine_start', 'engine_quit', 'read_log'],
532
532
  description: 'System action'
533
533
  },
534
534
  // Performance
@@ -567,7 +567,14 @@ Supported actions: profile, show_fps, set_quality, play_sound, create_widget, sh
567
567
  resolution: { type: 'string', description: 'Screenshot resolution in WIDTHxHEIGHT format (e.g., "1920x1080", "3840x2160"). Optional for screenshot action, uses viewport size if not specified.' },
568
568
  // Engine lifecycle
569
569
  projectPath: { type: 'string', description: 'Absolute path to .uproject file (e.g., "C:/Projects/MyGame/MyGame.uproject"). Required for engine_start unless UE_PROJECT_PATH environment variable is set.' },
570
- editorExe: { type: 'string', description: 'Absolute path to Unreal Editor executable (e.g., "C:/UnrealEngine/Engine/Binaries/Win64/UnrealEditor.exe"). Required for engine_start unless UE_EDITOR_EXE environment variable is set.' }
570
+ editorExe: { type: 'string', description: 'Absolute path to Unreal Editor executable (e.g., "C:/UnrealEngine/Engine/Binaries/Win64/UnrealEditor.exe"). Required for engine_start unless UE_EDITOR_EXE environment variable is set.' },
571
+ // Log reading
572
+ filter_category: { description: 'Category filter as string or array; comma-separated or array values' },
573
+ filter_level: { type: 'string', enum: ['Error', 'Warning', 'Log', 'Verbose', 'VeryVerbose', 'All'], description: 'Log level filter' },
574
+ lines: { type: 'number', description: 'Number of lines to read from tail' },
575
+ log_path: { type: 'string', description: 'Absolute path to a specific .log file to read' },
576
+ include_prefixes: { type: 'array', items: { type: 'string' }, description: 'Only include categories starting with any of these prefixes' },
577
+ exclude_categories: { type: 'array', items: { type: 'string' }, description: 'Categories to exclude' }
571
578
  },
572
579
  required: ['action']
573
580
  },
@@ -585,7 +592,14 @@ Supported actions: profile, show_fps, set_quality, play_sound, create_widget, sh
585
592
  imageBase64: { type: 'string', description: 'Screenshot image base64 (truncated)' },
586
593
  pid: { type: 'number', description: 'Process ID for launched editor' },
587
594
  message: { type: 'string', description: 'Status message' },
588
- error: { type: 'string', description: 'Error message if failed' }
595
+ error: { type: 'string', description: 'Error message if failed' },
596
+ logPath: { type: 'string', description: 'Log file path used for read_log' },
597
+ entries: {
598
+ type: 'array',
599
+ items: { type: 'object', properties: { timestamp: { type: 'string' }, category: { type: 'string' }, level: { type: 'string' }, message: { type: 'string' } } },
600
+ description: 'Parsed Output Log entries'
601
+ },
602
+ filteredCount: { type: 'number', description: 'Count of entries after filtering' }
589
603
  }
590
604
  }
591
605
  },
@@ -705,6 +705,23 @@ print('RESULT:' + json.dumps({'success': exists, 'exists': exists, 'path': path}
705
705
  // 9. SYSTEM CONTROL
706
706
  case 'system_control':
707
707
  switch (requireAction(args)) {
708
+ case 'read_log': {
709
+ const filterCategoryRaw = args.filter_category;
710
+ const filterCategory = Array.isArray(filterCategoryRaw)
711
+ ? filterCategoryRaw
712
+ : typeof filterCategoryRaw === 'string' && filterCategoryRaw.trim() !== ''
713
+ ? filterCategoryRaw.split(',').map((s) => s.trim()).filter(Boolean)
714
+ : undefined;
715
+ const res = await tools.logTools.readOutputLog({
716
+ filterCategory,
717
+ filterLevel: args.filter_level,
718
+ lines: typeof args.lines === 'number' ? args.lines : undefined,
719
+ logPath: typeof args.log_path === 'string' ? args.log_path : undefined,
720
+ includePrefixes: Array.isArray(args.include_prefixes) ? args.include_prefixes : undefined,
721
+ excludeCategories: Array.isArray(args.exclude_categories) ? args.exclude_categories : undefined
722
+ });
723
+ return cleanObject(res);
724
+ }
708
725
  case 'profile': {
709
726
  const res = await tools.performanceTools.startProfiling({ type: args.profileType, duration: args.duration });
710
727
  return cleanObject(res);
@@ -0,0 +1,45 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+ type ReadParams = {
3
+ filterCategory?: string[];
4
+ filterLevel?: 'Error' | 'Warning' | 'Log' | 'Verbose' | 'VeryVerbose' | 'All';
5
+ lines?: number;
6
+ logPath?: string;
7
+ includePrefixes?: string[];
8
+ excludeCategories?: string[];
9
+ };
10
+ type Entry = {
11
+ timestamp?: string;
12
+ category?: string;
13
+ level?: string;
14
+ message: string;
15
+ };
16
+ export declare class LogTools {
17
+ private bridge;
18
+ private env;
19
+ private log;
20
+ private cachedLogPath?;
21
+ constructor(bridge: UnrealBridge);
22
+ readOutputLog(params: ReadParams): Promise<{
23
+ success: boolean;
24
+ error: string;
25
+ logPath?: undefined;
26
+ entries?: undefined;
27
+ filteredCount?: undefined;
28
+ } | {
29
+ success: boolean;
30
+ logPath: string;
31
+ entries: Entry[];
32
+ filteredCount: number;
33
+ error?: undefined;
34
+ }>;
35
+ private resolveLogPath;
36
+ private resolveFromProjectEnv;
37
+ private findLatestLogInDir;
38
+ private fileExists;
39
+ private cacheLogPath;
40
+ private tailFile;
41
+ private parseLine;
42
+ private isInternalLogEntry;
43
+ }
44
+ export {};
45
+ //# sourceMappingURL=logs.d.ts.map
@@ -0,0 +1,262 @@
1
+ import { loadEnv } from '../types/env.js';
2
+ import { Logger } from '../utils/logger.js';
3
+ import { promises as fs } from 'fs';
4
+ import path from 'path';
5
+ export class LogTools {
6
+ bridge;
7
+ env = loadEnv();
8
+ log = new Logger('LogTools');
9
+ cachedLogPath;
10
+ constructor(bridge) {
11
+ this.bridge = bridge;
12
+ }
13
+ async readOutputLog(params) {
14
+ const target = await this.resolveLogPath(params.logPath);
15
+ if (!target) {
16
+ return { success: false, error: 'Log file not found' };
17
+ }
18
+ const maxLines = typeof params.lines === 'number' && params.lines > 0 ? Math.min(params.lines, 2000) : 200;
19
+ let text = '';
20
+ try {
21
+ text = await this.tailFile(target, maxLines);
22
+ }
23
+ catch (err) {
24
+ return { success: false, error: String(err?.message || err) };
25
+ }
26
+ const rawLines = text.split(/\r?\n/).filter(l => l.length > 0);
27
+ const parsed = rawLines.map(l => this.parseLine(l));
28
+ const mappedLevel = params.filterLevel || 'All';
29
+ const includeCats = Array.isArray(params.filterCategory) && params.filterCategory.length ? new Set(params.filterCategory) : undefined;
30
+ const includePrefixes = Array.isArray(params.includePrefixes) && params.includePrefixes.length ? params.includePrefixes : undefined;
31
+ const excludeCats = Array.isArray(params.excludeCategories) && params.excludeCategories.length ? new Set(params.excludeCategories) : undefined;
32
+ const filtered = parsed.filter(e => {
33
+ if (!e)
34
+ return false;
35
+ if (mappedLevel && mappedLevel !== 'All') {
36
+ const lv = (e.level || 'Log');
37
+ if (lv === 'Display') {
38
+ if (mappedLevel !== 'Log')
39
+ return false;
40
+ }
41
+ else if (lv !== mappedLevel) {
42
+ return false;
43
+ }
44
+ }
45
+ if (includeCats && e.category && !includeCats.has(e.category))
46
+ return false;
47
+ if (includePrefixes && includePrefixes.length && e.category) {
48
+ if (!includePrefixes.some(p => (e.category ?? '').startsWith(p)))
49
+ return false;
50
+ }
51
+ if (excludeCats && e.category && excludeCats.has(e.category))
52
+ return false;
53
+ return true;
54
+ });
55
+ const includeInternal = Boolean((includeCats && includeCats.has('LogPython')) ||
56
+ (includePrefixes && includePrefixes.some(p => 'LogPython'.startsWith(p))));
57
+ const sanitized = includeInternal ? filtered : filtered.filter(entry => !this.isInternalLogEntry(entry));
58
+ return { success: true, logPath: target.replace(/\\/g, '/'), entries: sanitized, filteredCount: sanitized.length };
59
+ }
60
+ async resolveLogPath(override) {
61
+ if (override && typeof override === 'string' && override.trim()) {
62
+ try {
63
+ const st = await fs.stat(override);
64
+ if (st.isFile()) {
65
+ return this.cacheLogPath(path.resolve(override));
66
+ }
67
+ }
68
+ catch { }
69
+ }
70
+ if (this.cachedLogPath && (await this.fileExists(this.cachedLogPath))) {
71
+ return this.cachedLogPath;
72
+ }
73
+ const envLog = await this.resolveFromProjectEnv();
74
+ if (envLog) {
75
+ return envLog;
76
+ }
77
+ if (this.bridge.isConnected) {
78
+ try {
79
+ const script = `
80
+ import unreal, json, os
81
+ paths = []
82
+ try:
83
+ d = unreal.Paths.project_log_dir()
84
+ if d:
85
+ paths.append(os.path.abspath(d))
86
+ except Exception:
87
+ pass
88
+ try:
89
+ sd = unreal.Paths.project_saved_dir()
90
+ if sd:
91
+ p = os.path.join(sd, 'Logs')
92
+ paths.append(os.path.abspath(p))
93
+ except Exception:
94
+ pass
95
+ try:
96
+ pf = unreal.Paths.get_project_file_path()
97
+ if pf:
98
+ pd = os.path.dirname(pf)
99
+ p = os.path.join(pd, 'Saved', 'Logs')
100
+ paths.append(os.path.abspath(p))
101
+ except Exception:
102
+ pass
103
+ all_logs = []
104
+ for base in paths:
105
+ try:
106
+ if os.path.isdir(base):
107
+ for name in os.listdir(base):
108
+ if name.lower().endswith('.log'):
109
+ fp = os.path.join(base, name)
110
+ try:
111
+ m = os.path.getmtime(fp)
112
+ all_logs.append({'p': fp, 'm': m})
113
+ except Exception:
114
+ pass
115
+ except Exception:
116
+ pass
117
+ all_logs.sort(key=lambda x: x['m'], reverse=True)
118
+ print('RESULT:' + json.dumps({'dirs': paths, 'logs': all_logs}))
119
+ `.trim();
120
+ const res = await this.bridge.executePythonWithResult(script);
121
+ const logs = Array.isArray(res?.logs) ? res.logs : [];
122
+ for (const entry of logs) {
123
+ const p = typeof entry?.p === 'string' ? entry.p : undefined;
124
+ if (p && p.trim())
125
+ return this.cacheLogPath(p);
126
+ }
127
+ }
128
+ catch { }
129
+ }
130
+ const fallback = await this.findLatestLogInDir(path.join(process.cwd(), 'Saved', 'Logs'));
131
+ if (fallback) {
132
+ return fallback;
133
+ }
134
+ return undefined;
135
+ }
136
+ async resolveFromProjectEnv() {
137
+ const projectPath = this.env.UE_PROJECT_PATH;
138
+ if (projectPath && typeof projectPath === 'string' && projectPath.trim()) {
139
+ const projectDir = path.dirname(projectPath);
140
+ const logsDir = path.join(projectDir, 'Saved', 'Logs');
141
+ const envLog = await this.findLatestLogInDir(logsDir);
142
+ if (envLog) {
143
+ return envLog;
144
+ }
145
+ }
146
+ return undefined;
147
+ }
148
+ async findLatestLogInDir(dir) {
149
+ if (!dir)
150
+ return undefined;
151
+ try {
152
+ const entries = await fs.readdir(dir);
153
+ const candidates = [];
154
+ for (const name of entries) {
155
+ if (!name.toLowerCase().endsWith('.log'))
156
+ continue;
157
+ const fp = path.join(dir, name);
158
+ try {
159
+ const st = await fs.stat(fp);
160
+ candidates.push({ p: fp, m: st.mtimeMs });
161
+ }
162
+ catch { }
163
+ }
164
+ if (candidates.length) {
165
+ candidates.sort((a, b) => b.m - a.m);
166
+ return this.cacheLogPath(candidates[0].p);
167
+ }
168
+ }
169
+ catch { }
170
+ return undefined;
171
+ }
172
+ async fileExists(filePath) {
173
+ try {
174
+ const st = await fs.stat(filePath);
175
+ return st.isFile();
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ }
181
+ cacheLogPath(p) {
182
+ this.cachedLogPath = p;
183
+ return p;
184
+ }
185
+ async tailFile(filePath, maxLines) {
186
+ const handle = await fs.open(filePath, 'r');
187
+ try {
188
+ const stat = await handle.stat();
189
+ const chunkSize = 128 * 1024;
190
+ let position = stat.size;
191
+ let remaining = '';
192
+ const lines = [];
193
+ while (position > 0 && lines.length < maxLines) {
194
+ const readSize = Math.min(chunkSize, position);
195
+ position -= readSize;
196
+ const buf = Buffer.alloc(readSize);
197
+ await handle.read(buf, 0, readSize, position);
198
+ remaining = buf.toString('utf8') + remaining;
199
+ const parts = remaining.split(/\r?\n/);
200
+ remaining = parts.shift() || '';
201
+ while (parts.length) {
202
+ const line = parts.pop();
203
+ if (line === undefined)
204
+ break;
205
+ if (line.length === 0)
206
+ continue;
207
+ lines.unshift(line);
208
+ if (lines.length >= maxLines)
209
+ break;
210
+ }
211
+ }
212
+ if (lines.length < maxLines && remaining) {
213
+ lines.unshift(remaining);
214
+ }
215
+ return lines.slice(0, maxLines).join('\n');
216
+ }
217
+ finally {
218
+ try {
219
+ await handle.close();
220
+ }
221
+ catch { }
222
+ }
223
+ }
224
+ parseLine(line) {
225
+ const m1 = line.match(/^\[?(\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}\.\d{2}:\d+)\]?\s*\[(.*?)\]\s*(.*)$/);
226
+ if (m1) {
227
+ const rest = m1[3];
228
+ const m2 = rest.match(/^(\w+):\s*(Error|Warning|Display|Log|Verbose|VeryVerbose):\s*(.*)$/);
229
+ if (m2) {
230
+ return { timestamp: m1[1], category: m2[1], level: m2[2] === 'Display' ? 'Log' : m2[2], message: m2[3] };
231
+ }
232
+ const m3 = rest.match(/^(\w+):\s*(.*)$/);
233
+ if (m3) {
234
+ return { timestamp: m1[1], category: m3[1], level: 'Log', message: m3[2] };
235
+ }
236
+ return { timestamp: m1[1], message: rest };
237
+ }
238
+ const m = line.match(/^(\w+):\s*(Error|Warning|Display|Log|Verbose|VeryVerbose):\s*(.*)$/);
239
+ if (m) {
240
+ return { category: m[1], level: m[2] === 'Display' ? 'Log' : m[2], message: m[3] };
241
+ }
242
+ const mAlt = line.match(/^(\w+):\s*(.*)$/);
243
+ if (mAlt) {
244
+ return { category: mAlt[1], level: 'Log', message: mAlt[2] };
245
+ }
246
+ return { message: line };
247
+ }
248
+ isInternalLogEntry(entry) {
249
+ if (!entry)
250
+ return false;
251
+ const category = entry.category?.toLowerCase() || '';
252
+ const message = entry.message?.trim() || '';
253
+ if (category === 'logpython' && message.startsWith('RESULT:')) {
254
+ return true;
255
+ }
256
+ if (!entry.category && message.startsWith('[') && message.includes('LogPython: RESULT:')) {
257
+ return true;
258
+ }
259
+ return false;
260
+ }
261
+ }
262
+ //# sourceMappingURL=logs.js.map
@@ -75,6 +75,17 @@ export interface SystemControlResponse extends BaseToolResponse {
75
75
  soundPlaying?: boolean;
76
76
  widgetPath?: string;
77
77
  widgetVisible?: boolean;
78
+ imagePath?: string;
79
+ imageBase64?: string;
80
+ pid?: number;
81
+ logPath?: string;
82
+ entries?: Array<{
83
+ timestamp?: string;
84
+ category?: string;
85
+ level?: string;
86
+ message: string;
87
+ }>;
88
+ filteredCount?: number;
78
89
  }
79
90
  export interface ConsoleCommandResponse extends BaseToolResponse {
80
91
  command?: string;
@@ -131,7 +142,7 @@ export type AnimationAction = 'create_animation_bp' | 'play_montage' | 'setup_ra
131
142
  export type EffectAction = 'particle' | 'niagara' | 'debug_shape';
132
143
  export type BlueprintAction = 'create' | 'add_component';
133
144
  export type EnvironmentAction = 'create_landscape' | 'sculpt' | 'add_foliage' | 'paint_foliage';
134
- export type SystemAction = 'profile' | 'show_fps' | 'set_quality' | 'play_sound' | 'create_widget' | 'show_widget';
145
+ export type SystemAction = 'profile' | 'show_fps' | 'set_quality' | 'play_sound' | 'create_widget' | 'show_widget' | 'screenshot' | 'engine_start' | 'engine_quit' | 'read_log';
135
146
  export type VerificationAction = 'foliage_type_exists' | 'foliage_instances_near' | 'landscape_exists' | 'quality_level';
136
147
  export interface ConsolidatedToolParams {
137
148
  manage_asset: {
@@ -229,6 +240,15 @@ export interface ConsolidatedToolParams {
229
240
  widgetName?: string;
230
241
  widgetType?: string;
231
242
  visible?: boolean;
243
+ resolution?: string;
244
+ projectPath?: string;
245
+ editorExe?: string;
246
+ filter_category?: string | string[];
247
+ filter_level?: 'Error' | 'Warning' | 'Log' | 'Verbose' | 'VeryVerbose' | 'All';
248
+ lines?: number;
249
+ log_path?: string;
250
+ include_prefixes?: string[];
251
+ exclude_categories?: string[];
232
252
  };
233
253
  console_command: {
234
254
  command: string;
@@ -196,6 +196,8 @@ Additional advanced blueprint scenarios remain documented in the legacy matrix f
196
196
  | 2 | Play sound with missing asset fails | `{"action":"play_sound","soundPath":"/Game/MCP/Audio/Missing"}` | Error message reports that the sound asset could not be found. |
197
197
  | 3 | Play sound at location missing asset fails | `{"action":"play_sound","soundPath":"/Game/MCP/Audio/Missing","location":{"x":0,"y":0,"z":0}}` | Error message reports that the sound asset could not be found for the world location playback. |
198
198
  | 4 | Play sound with empty path fails validation | `{"action":"play_sound","soundPath":"","volume":1.0}` | Error message explains that the sound asset path could not be resolved. |
199
+ | 5 | Read Output Log tail with defaults | `{"action":"read_log","lines":50,"filter_level":"All"}` | Success response returns recent log entries and the logPath used. |
200
+ | 6 | Read Output Log filtered by custom categories | `{"action":"read_log","filter_category":"LogMyCategory,LogOtherLog","filter_level":"Error","lines":100}` | Success response returns only matching categories at Error level. |
199
201
 
200
202
  ---
201
203
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unreal-engine-mcp-server",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
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 Remote Control API. Built with TypeScript and designed for game development automation.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -2,13 +2,13 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json",
3
3
  "name": "io.github.ChiR24/unreal-engine-mcp",
4
4
  "description": "MCP server for Unreal Engine 5 with 13 tools for game development automation.",
5
- "version": "0.4.6",
5
+ "version": "0.4.7",
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.4.6",
11
+ "version": "0.4.7",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  },
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ import { SequenceTools } from './tools/sequence.js';
28
28
  import { IntrospectionTools } from './tools/introspection.js';
29
29
  import { VisualTools } from './tools/visual.js';
30
30
  import { EngineTools } from './tools/engine.js';
31
+ import { LogTools } from './tools/logs.js';
31
32
  import { consolidatedToolDefinitions } from './tools/consolidated-tool-definitions.js';
32
33
  import { handleConsolidatedToolCall } from './tools/consolidated-tool-handlers.js';
33
34
  import { prompts } from './prompts/index.js';
@@ -88,7 +89,7 @@ const CONFIG = {
88
89
  RETRY_DELAY_MS: 2000,
89
90
  // Server info
90
91
  SERVER_NAME: 'unreal-engine-mcp',
91
- SERVER_VERSION: '0.4.6',
92
+ SERVER_VERSION: '0.4.7',
92
93
  // Monitoring
93
94
  HEALTH_CHECK_INTERVAL_MS: 30000 // 30 seconds
94
95
  };
@@ -237,6 +238,7 @@ export function createServer() {
237
238
  const introspectionTools = new IntrospectionTools(bridge);
238
239
  const visualTools = new VisualTools(bridge);
239
240
  const engineTools = new EngineTools(bridge);
241
+ const logTools = new LogTools(bridge);
240
242
 
241
243
  const server = new Server(
242
244
  {
@@ -474,11 +476,22 @@ export function createServer() {
474
476
  let args: any = request.params.arguments || {};
475
477
  const startTime = Date.now();
476
478
 
477
- // Ensure connection only when needed, with 3 attempts
478
- const connected = await ensureConnectedOnDemand();
479
- if (!connected) {
480
- trackPerformance(startTime, false);
481
- return createNotConnectedResponse(name);
479
+ let requiresEngine = true;
480
+ try {
481
+ const n = String(name);
482
+ if (n === 'system_control') {
483
+ const action = String((args || {}).action || '').trim();
484
+ if (action === 'read_log') {
485
+ requiresEngine = false;
486
+ }
487
+ }
488
+ } catch {}
489
+ if (requiresEngine) {
490
+ const connected = await ensureConnectedOnDemand();
491
+ if (!connected) {
492
+ trackPerformance(startTime, false);
493
+ return createNotConnectedResponse(name);
494
+ }
482
495
  }
483
496
 
484
497
  // Create tools object for handler
@@ -505,6 +518,7 @@ export function createServer() {
505
518
  introspectionTools,
506
519
  visualTools,
507
520
  engineTools,
521
+ logTools,
508
522
  // Elicitation (client-optional)
509
523
  elicit: elicitation.elicit,
510
524
  supportsElicitation: elicitation.supports,
@@ -537,7 +537,7 @@ Supported actions: profile, show_fps, set_quality, play_sound, create_widget, sh
537
537
  properties: {
538
538
  action: {
539
539
  type: 'string',
540
- enum: ['profile', 'show_fps', 'set_quality', 'play_sound', 'create_widget', 'show_widget', 'screenshot', 'engine_start', 'engine_quit'],
540
+ enum: ['profile', 'show_fps', 'set_quality', 'play_sound', 'create_widget', 'show_widget', 'screenshot', 'engine_start', 'engine_quit', 'read_log'],
541
541
  description: 'System action'
542
542
  },
543
543
  // Performance
@@ -577,6 +577,14 @@ Supported actions: profile, show_fps, set_quality, play_sound, create_widget, sh
577
577
  // Engine lifecycle
578
578
  projectPath: { type: 'string', description: 'Absolute path to .uproject file (e.g., "C:/Projects/MyGame/MyGame.uproject"). Required for engine_start unless UE_PROJECT_PATH environment variable is set.' },
579
579
  editorExe: { type: 'string', description: 'Absolute path to Unreal Editor executable (e.g., "C:/UnrealEngine/Engine/Binaries/Win64/UnrealEditor.exe"). Required for engine_start unless UE_EDITOR_EXE environment variable is set.' }
580
+ ,
581
+ // Log reading
582
+ filter_category: { description: 'Category filter as string or array; comma-separated or array values' },
583
+ filter_level: { type: 'string', enum: ['Error','Warning','Log','Verbose','VeryVerbose','All'], description: 'Log level filter' },
584
+ lines: { type: 'number', description: 'Number of lines to read from tail' },
585
+ log_path: { type: 'string', description: 'Absolute path to a specific .log file to read' },
586
+ include_prefixes: { type: 'array', items: { type: 'string' }, description: 'Only include categories starting with any of these prefixes' },
587
+ exclude_categories: { type: 'array', items: { type: 'string' }, description: 'Categories to exclude' }
580
588
  },
581
589
  required: ['action']
582
590
  },
@@ -594,7 +602,14 @@ Supported actions: profile, show_fps, set_quality, play_sound, create_widget, sh
594
602
  imageBase64: { type: 'string', description: 'Screenshot image base64 (truncated)' },
595
603
  pid: { type: 'number', description: 'Process ID for launched editor' },
596
604
  message: { type: 'string', description: 'Status message' },
597
- error: { type: 'string', description: 'Error message if failed' }
605
+ error: { type: 'string', description: 'Error message if failed' },
606
+ logPath: { type: 'string', description: 'Log file path used for read_log' },
607
+ entries: {
608
+ type: 'array',
609
+ items: { type: 'object', properties: { timestamp: { type: 'string' }, category: { type: 'string' }, level: { type: 'string' }, message: { type: 'string' } } },
610
+ description: 'Parsed Output Log entries'
611
+ },
612
+ filteredCount: { type: 'number', description: 'Count of entries after filtering' }
598
613
  }
599
614
  }
600
615
  },
@@ -823,8 +823,25 @@ print('RESULT:' + json.dumps({'success': exists, 'exists': exists, 'path': path}
823
823
  }
824
824
 
825
825
  // 9. SYSTEM CONTROL
826
- case 'system_control':
826
+ case 'system_control':
827
827
  switch (requireAction(args)) {
828
+ case 'read_log': {
829
+ const filterCategoryRaw = args.filter_category;
830
+ const filterCategory = Array.isArray(filterCategoryRaw)
831
+ ? filterCategoryRaw
832
+ : typeof filterCategoryRaw === 'string' && filterCategoryRaw.trim() !== ''
833
+ ? filterCategoryRaw.split(',').map((s: string) => s.trim()).filter(Boolean)
834
+ : undefined;
835
+ const res = await tools.logTools.readOutputLog({
836
+ filterCategory,
837
+ filterLevel: args.filter_level,
838
+ lines: typeof args.lines === 'number' ? args.lines : undefined,
839
+ logPath: typeof args.log_path === 'string' ? args.log_path : undefined,
840
+ includePrefixes: Array.isArray(args.include_prefixes) ? args.include_prefixes : undefined,
841
+ excludeCategories: Array.isArray(args.exclude_categories) ? args.exclude_categories : undefined
842
+ });
843
+ return cleanObject(res);
844
+ }
828
845
  case 'profile': {
829
846
  const res = await tools.performanceTools.startProfiling({ type: args.profileType, duration: args.duration });
830
847
  return cleanObject(res);
@@ -0,0 +1,267 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { loadEnv } from '../types/env.js';
3
+ import { Logger } from '../utils/logger.js';
4
+ import { promises as fs } from 'fs';
5
+ import path from 'path';
6
+
7
+ type ReadParams = {
8
+ filterCategory?: string[]
9
+ filterLevel?: 'Error' | 'Warning' | 'Log' | 'Verbose' | 'VeryVerbose' | 'All'
10
+ lines?: number
11
+ logPath?: string
12
+ includePrefixes?: string[]
13
+ excludeCategories?: string[]
14
+ }
15
+
16
+ type Entry = {
17
+ timestamp?: string
18
+ category?: string
19
+ level?: string
20
+ message: string
21
+ }
22
+
23
+ export class LogTools {
24
+ private env = loadEnv();
25
+ private log = new Logger('LogTools');
26
+ private cachedLogPath?: string;
27
+ constructor(private bridge: UnrealBridge) {}
28
+
29
+ async readOutputLog(params: ReadParams) {
30
+ const target = await this.resolveLogPath(params.logPath);
31
+ if (!target) {
32
+ return { success: false, error: 'Log file not found' };
33
+ }
34
+ const maxLines = typeof params.lines === 'number' && params.lines > 0 ? Math.min(params.lines, 2000) : 200;
35
+ let text = '';
36
+ try {
37
+ text = await this.tailFile(target, maxLines);
38
+ } catch (err: any) {
39
+ return { success: false, error: String(err?.message || err) };
40
+ }
41
+ const rawLines = text.split(/\r?\n/).filter(l => l.length > 0);
42
+ const parsed: Entry[] = rawLines.map(l => this.parseLine(l));
43
+ const mappedLevel = params.filterLevel || 'All';
44
+ const includeCats = Array.isArray(params.filterCategory) && params.filterCategory.length ? new Set(params.filterCategory) : undefined;
45
+ const includePrefixes = Array.isArray(params.includePrefixes) && params.includePrefixes.length ? params.includePrefixes : undefined;
46
+ const excludeCats = Array.isArray(params.excludeCategories) && params.excludeCategories.length ? new Set(params.excludeCategories) : undefined;
47
+ const filtered = parsed.filter(e => {
48
+ if (!e) return false;
49
+ if (mappedLevel && mappedLevel !== 'All') {
50
+ const lv = (e.level || 'Log');
51
+ if (lv === 'Display') {
52
+ if (mappedLevel !== 'Log') return false;
53
+ } else if (lv !== mappedLevel) {
54
+ return false;
55
+ }
56
+ }
57
+ if (includeCats && e.category && !includeCats.has(e.category)) return false;
58
+ if (includePrefixes && includePrefixes.length && e.category) {
59
+ if (!includePrefixes.some(p => (e.category ?? '').startsWith(p))) return false;
60
+ }
61
+ if (excludeCats && e.category && excludeCats.has(e.category)) return false;
62
+ return true;
63
+ });
64
+ const includeInternal = Boolean(
65
+ (includeCats && includeCats.has('LogPython')) ||
66
+ (includePrefixes && includePrefixes.some(p => 'LogPython'.startsWith(p)))
67
+ );
68
+ const sanitized = includeInternal ? filtered : filtered.filter(entry => !this.isInternalLogEntry(entry));
69
+ return { success: true, logPath: target.replace(/\\/g, '/'), entries: sanitized, filteredCount: sanitized.length };
70
+ }
71
+
72
+ private async resolveLogPath(override?: string): Promise<string | undefined> {
73
+ if (override && typeof override === 'string' && override.trim()) {
74
+ try {
75
+ const st = await fs.stat(override);
76
+ if (st.isFile()) {
77
+ return this.cacheLogPath(path.resolve(override));
78
+ }
79
+ } catch {}
80
+ }
81
+
82
+ if (this.cachedLogPath && (await this.fileExists(this.cachedLogPath))) {
83
+ return this.cachedLogPath;
84
+ }
85
+
86
+ const envLog = await this.resolveFromProjectEnv();
87
+ if (envLog) {
88
+ return envLog;
89
+ }
90
+
91
+ if (this.bridge.isConnected) {
92
+ try {
93
+ const script = `
94
+ import unreal, json, os
95
+ paths = []
96
+ try:
97
+ d = unreal.Paths.project_log_dir()
98
+ if d:
99
+ paths.append(os.path.abspath(d))
100
+ except Exception:
101
+ pass
102
+ try:
103
+ sd = unreal.Paths.project_saved_dir()
104
+ if sd:
105
+ p = os.path.join(sd, 'Logs')
106
+ paths.append(os.path.abspath(p))
107
+ except Exception:
108
+ pass
109
+ try:
110
+ pf = unreal.Paths.get_project_file_path()
111
+ if pf:
112
+ pd = os.path.dirname(pf)
113
+ p = os.path.join(pd, 'Saved', 'Logs')
114
+ paths.append(os.path.abspath(p))
115
+ except Exception:
116
+ pass
117
+ all_logs = []
118
+ for base in paths:
119
+ try:
120
+ if os.path.isdir(base):
121
+ for name in os.listdir(base):
122
+ if name.lower().endswith('.log'):
123
+ fp = os.path.join(base, name)
124
+ try:
125
+ m = os.path.getmtime(fp)
126
+ all_logs.append({'p': fp, 'm': m})
127
+ except Exception:
128
+ pass
129
+ except Exception:
130
+ pass
131
+ all_logs.sort(key=lambda x: x['m'], reverse=True)
132
+ print('RESULT:' + json.dumps({'dirs': paths, 'logs': all_logs}))
133
+ `.trim();
134
+ const res = await this.bridge.executePythonWithResult(script);
135
+ const logs = Array.isArray(res?.logs) ? res.logs : [];
136
+ for (const entry of logs) {
137
+ const p = typeof entry?.p === 'string' ? entry.p : undefined;
138
+ if (p && p.trim()) return this.cacheLogPath(p);
139
+ }
140
+ } catch {}
141
+ }
142
+ const fallback = await this.findLatestLogInDir(path.join(process.cwd(), 'Saved', 'Logs'));
143
+ if (fallback) {
144
+ return fallback;
145
+ }
146
+ return undefined;
147
+ }
148
+
149
+ private async resolveFromProjectEnv(): Promise<string | undefined> {
150
+ const projectPath = this.env.UE_PROJECT_PATH;
151
+ if (projectPath && typeof projectPath === 'string' && projectPath.trim()) {
152
+ const projectDir = path.dirname(projectPath);
153
+ const logsDir = path.join(projectDir, 'Saved', 'Logs');
154
+ const envLog = await this.findLatestLogInDir(logsDir);
155
+ if (envLog) {
156
+ return envLog;
157
+ }
158
+ }
159
+ return undefined;
160
+ }
161
+
162
+ private async findLatestLogInDir(dir: string): Promise<string | undefined> {
163
+ if (!dir) return undefined;
164
+ try {
165
+ const entries = await fs.readdir(dir);
166
+ const candidates: { p: string; m: number }[] = [];
167
+ for (const name of entries) {
168
+ if (!name.toLowerCase().endsWith('.log')) continue;
169
+ const fp = path.join(dir, name);
170
+ try {
171
+ const st = await fs.stat(fp);
172
+ candidates.push({ p: fp, m: st.mtimeMs });
173
+ } catch {}
174
+ }
175
+ if (candidates.length) {
176
+ candidates.sort((a, b) => b.m - a.m);
177
+ return this.cacheLogPath(candidates[0].p);
178
+ }
179
+ } catch {}
180
+ return undefined;
181
+ }
182
+
183
+ private async fileExists(filePath: string): Promise<boolean> {
184
+ try {
185
+ const st = await fs.stat(filePath);
186
+ return st.isFile();
187
+ } catch {
188
+ return false;
189
+ }
190
+ }
191
+
192
+ private cacheLogPath(p: string): string {
193
+ this.cachedLogPath = p;
194
+ return p;
195
+ }
196
+
197
+ private async tailFile(filePath: string, maxLines: number): Promise<string> {
198
+ const handle = await fs.open(filePath, 'r');
199
+ try {
200
+ const stat = await handle.stat();
201
+ const chunkSize = 128 * 1024;
202
+ let position = stat.size;
203
+ let remaining = '';
204
+ const lines: string[] = [];
205
+ while (position > 0 && lines.length < maxLines) {
206
+ const readSize = Math.min(chunkSize, position);
207
+ position -= readSize;
208
+ const buf = Buffer.alloc(readSize);
209
+ await handle.read(buf, 0, readSize, position);
210
+ remaining = buf.toString('utf8') + remaining;
211
+ const parts = remaining.split(/\r?\n/);
212
+ remaining = parts.shift() || '';
213
+ while (parts.length) {
214
+ const line = parts.pop() as string;
215
+ if (line === undefined) break;
216
+ if (line.length === 0) continue;
217
+ lines.unshift(line);
218
+ if (lines.length >= maxLines) break;
219
+ }
220
+ }
221
+ if (lines.length < maxLines && remaining) {
222
+ lines.unshift(remaining);
223
+ }
224
+ return lines.slice(0, maxLines).join('\n');
225
+ } finally {
226
+ try { await handle.close(); } catch {}
227
+ }
228
+ }
229
+
230
+ private parseLine(line: string): Entry {
231
+ const m1 = line.match(/^\[?(\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}\.\d{2}:\d+)\]?\s*\[(.*?)\]\s*(.*)$/);
232
+ if (m1) {
233
+ const rest = m1[3];
234
+ const m2 = rest.match(/^(\w+):\s*(Error|Warning|Display|Log|Verbose|VeryVerbose):\s*(.*)$/);
235
+ if (m2) {
236
+ return { timestamp: m1[1], category: m2[1], level: m2[2] === 'Display' ? 'Log' : m2[2], message: m2[3] };
237
+ }
238
+ const m3 = rest.match(/^(\w+):\s*(.*)$/);
239
+ if (m3) {
240
+ return { timestamp: m1[1], category: m3[1], level: 'Log', message: m3[2] };
241
+ }
242
+ return { timestamp: m1[1], message: rest };
243
+ }
244
+ const m = line.match(/^(\w+):\s*(Error|Warning|Display|Log|Verbose|VeryVerbose):\s*(.*)$/);
245
+ if (m) {
246
+ return { category: m[1], level: m[2] === 'Display' ? 'Log' : m[2], message: m[3] };
247
+ }
248
+ const mAlt = line.match(/^(\w+):\s*(.*)$/);
249
+ if (mAlt) {
250
+ return { category: mAlt[1], level: 'Log', message: mAlt[2] };
251
+ }
252
+ return { message: line };
253
+ }
254
+
255
+ private isInternalLogEntry(entry: Entry): boolean {
256
+ if (!entry) return false;
257
+ const category = entry.category?.toLowerCase() || '';
258
+ const message = entry.message?.trim() || '';
259
+ if (category === 'logpython' && message.startsWith('RESULT:')) {
260
+ return true;
261
+ }
262
+ if (!entry.category && message.startsWith('[') && message.includes('LogPython: RESULT:')) {
263
+ return true;
264
+ }
265
+ return false;
266
+ }
267
+ }
@@ -98,6 +98,12 @@ export interface SystemControlResponse extends BaseToolResponse {
98
98
  soundPlaying?: boolean;
99
99
  widgetPath?: string;
100
100
  widgetVisible?: boolean;
101
+ imagePath?: string;
102
+ imageBase64?: string;
103
+ pid?: number;
104
+ logPath?: string;
105
+ entries?: Array<{ timestamp?: string; category?: string; level?: string; message: string }>;
106
+ filteredCount?: number;
101
107
  }
102
108
 
103
109
  // Console Command Types
@@ -174,7 +180,7 @@ export type AnimationAction = 'create_animation_bp' | 'play_montage' | 'setup_ra
174
180
  export type EffectAction = 'particle' | 'niagara' | 'debug_shape';
175
181
  export type BlueprintAction = 'create' | 'add_component';
176
182
  export type EnvironmentAction = 'create_landscape' | 'sculpt' | 'add_foliage' | 'paint_foliage';
177
- export type SystemAction = 'profile' | 'show_fps' | 'set_quality' | 'play_sound' | 'create_widget' | 'show_widget';
183
+ export type SystemAction = 'profile' | 'show_fps' | 'set_quality' | 'play_sound' | 'create_widget' | 'show_widget' | 'screenshot' | 'engine_start' | 'engine_quit' | 'read_log';
178
184
  export type VerificationAction = 'foliage_type_exists' | 'foliage_instances_near' | 'landscape_exists' | 'quality_level';
179
185
 
180
186
  // Consolidated tool parameter types
@@ -282,6 +288,15 @@ export interface ConsolidatedToolParams {
282
288
  widgetName?: string;
283
289
  widgetType?: string;
284
290
  visible?: boolean;
291
+ resolution?: string;
292
+ projectPath?: string;
293
+ editorExe?: string;
294
+ filter_category?: string | string[];
295
+ filter_level?: 'Error' | 'Warning' | 'Log' | 'Verbose' | 'VeryVerbose' | 'All';
296
+ lines?: number;
297
+ log_path?: string;
298
+ include_prefixes?: string[];
299
+ exclude_categories?: string[];
285
300
  };
286
301
 
287
302
  console_command: {