unreal-engine-mcp-server 0.4.5 → 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.
@@ -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;
@@ -2,6 +2,66 @@ import Ajv from 'ajv';
2
2
  import { Logger } from './logger.js';
3
3
  import { cleanObject } from './safe-json.js';
4
4
  const log = new Logger('ResponseValidator');
5
+ function isRecord(value) {
6
+ return !!value && typeof value === 'object' && !Array.isArray(value);
7
+ }
8
+ function normalizeText(text) {
9
+ return text.replace(/\s+/g, ' ').trim();
10
+ }
11
+ function buildSummaryText(toolName, payload) {
12
+ if (typeof payload === 'string') {
13
+ const normalized = payload.trim();
14
+ return normalized || `${toolName} responded`;
15
+ }
16
+ if (typeof payload === 'number' || typeof payload === 'bigint' || typeof payload === 'boolean') {
17
+ return `${toolName} responded: ${payload}`;
18
+ }
19
+ if (!isRecord(payload)) {
20
+ return `${toolName} responded`;
21
+ }
22
+ const parts = [];
23
+ const message = typeof payload.message === 'string' ? normalizeText(payload.message) : '';
24
+ const error = typeof payload.error === 'string' ? normalizeText(payload.error) : '';
25
+ const success = typeof payload.success === 'boolean' ? (payload.success ? 'success' : 'failed') : '';
26
+ const path = typeof payload.path === 'string' ? payload.path : '';
27
+ const name = typeof payload.name === 'string' ? payload.name : '';
28
+ const warningCount = Array.isArray(payload.warnings) ? payload.warnings.length : 0;
29
+ if (message)
30
+ parts.push(message);
31
+ if (error && (!message || !message.includes(error)))
32
+ parts.push(`error: ${error}`);
33
+ if (success)
34
+ parts.push(success);
35
+ if (path)
36
+ parts.push(`path: ${path}`);
37
+ if (name)
38
+ parts.push(`name: ${name}`);
39
+ if (warningCount > 0)
40
+ parts.push(`warnings: ${warningCount}`);
41
+ const summary = isRecord(payload.summary) ? payload.summary : undefined;
42
+ if (summary) {
43
+ const summaryParts = [];
44
+ for (const [key, value] of Object.entries(summary)) {
45
+ if (typeof value === 'number' || typeof value === 'string') {
46
+ summaryParts.push(`${key}: ${value}`);
47
+ }
48
+ if (summaryParts.length >= 3)
49
+ break;
50
+ }
51
+ if (summaryParts.length) {
52
+ parts.push(`summary(${summaryParts.join(', ')})`);
53
+ }
54
+ }
55
+ if (parts.length === 0) {
56
+ const keys = Object.keys(payload).slice(0, 3);
57
+ if (keys.length) {
58
+ return `${toolName} responded (${keys.join(', ')})`;
59
+ }
60
+ }
61
+ return parts.length > 0
62
+ ? parts.join(' | ')
63
+ : `${toolName} responded`;
64
+ }
5
65
  /**
6
66
  * Response Validator for MCP Tool Outputs
7
67
  * Validates tool responses against their defined output schemas
@@ -127,15 +187,10 @@ export class ResponseValidator {
127
187
  return response;
128
188
  }
129
189
  // Otherwise, wrap structured result into MCP content
130
- let text;
131
- try {
132
- // Pretty-print small objects for readability
133
- text = typeof response === 'string'
134
- ? response
135
- : JSON.stringify(response ?? { success: true }, null, 2);
136
- }
137
- catch (_e) {
138
- text = String(response);
190
+ const summarySource = structuredPayload !== undefined ? structuredPayload : response;
191
+ let text = buildSummaryText(toolName, summarySource);
192
+ if (!text || !text.trim()) {
193
+ text = buildSummaryText(toolName, response);
139
194
  }
140
195
  const wrapped = {
141
196
  content: [
@@ -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.5",
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.5",
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.5",
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.5',
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);