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.
- package/.env.production +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +1 -2
- package/dist/index.js +21 -6
- package/dist/tools/consolidated-tool-definitions.d.ts +167 -0
- package/dist/tools/consolidated-tool-definitions.js +17 -3
- package/dist/tools/consolidated-tool-handlers.js +17 -0
- package/dist/tools/logs.d.ts +45 -0
- package/dist/tools/logs.js +262 -0
- package/dist/types/tool-types.d.ts +21 -1
- package/dist/utils/response-validator.js +64 -9
- package/docs/unreal-tool-test-cases.md +2 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/index.ts +20 -6
- package/src/tools/consolidated-tool-definitions.ts +17 -2
- package/src/tools/consolidated-tool-handlers.ts +18 -1
- package/src/tools/logs.ts +267 -0
- package/src/types/tool-types.ts +16 -1
- package/src/utils/response-validator.ts +67 -8
|
@@ -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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
text =
|
|
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.
|
|
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
|
+
"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.
|
|
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.
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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);
|