blueprint-extractor-mcp 8.1.1 → 8.2.1
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.
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
const REGISTRY_ENV = 'BLUEPRINT_EXTRACTOR_EDITOR_REGISTRY_DIR';
|
|
6
6
|
const DEFAULT_STALE_TTL_MS = 15_000;
|
|
7
|
+
const WSL_WINDOWS_USERS_ROOT = '/mnt/c/Users';
|
|
8
|
+
const WSL_WINDOWS_REGISTRY_SUFFIX = ['AppData', 'Local', 'Temp', 'BlueprintExtractor', 'EditorRegistry'];
|
|
7
9
|
const editorInstanceSchema = z.object({
|
|
8
10
|
instanceId: z.string(),
|
|
9
11
|
projectName: z.string().optional(),
|
|
@@ -29,45 +31,82 @@ export function getEditorRegistryDir() {
|
|
|
29
31
|
? path.resolve(process.env[REGISTRY_ENV])
|
|
30
32
|
: path.join(tmpdir(), 'BlueprintExtractor', 'EditorRegistry');
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
34
|
+
async function getEditorRegistryDirs() {
|
|
35
|
+
const overrideDir = process.env[REGISTRY_ENV];
|
|
36
|
+
if (overrideDir) {
|
|
37
|
+
return [path.resolve(overrideDir)];
|
|
38
|
+
}
|
|
39
|
+
const dirs = new Set([getEditorRegistryDir()]);
|
|
40
|
+
if (process.platform !== 'linux') {
|
|
41
|
+
return Array.from(dirs);
|
|
42
|
+
}
|
|
35
43
|
try {
|
|
36
|
-
|
|
44
|
+
const userEntries = await readdir(WSL_WINDOWS_USERS_ROOT, { withFileTypes: true });
|
|
45
|
+
for (const entry of userEntries) {
|
|
46
|
+
if (!entry.isDirectory()) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
dirs.add(path.join(WSL_WINDOWS_USERS_ROOT, entry.name, ...WSL_WINDOWS_REGISTRY_SUFFIX));
|
|
50
|
+
}
|
|
37
51
|
}
|
|
38
52
|
catch {
|
|
39
|
-
|
|
40
|
-
editors: [],
|
|
41
|
-
registryDir,
|
|
42
|
-
staleEntryCount: 0,
|
|
43
|
-
};
|
|
53
|
+
// Ignore missing /mnt/c/Users on non-WSL or restricted hosts.
|
|
44
54
|
}
|
|
45
|
-
|
|
55
|
+
return Array.from(dirs);
|
|
56
|
+
}
|
|
57
|
+
export async function listRegisteredEditors(staleTtlMs = DEFAULT_STALE_TTL_MS) {
|
|
58
|
+
const registryDirs = await getEditorRegistryDirs();
|
|
59
|
+
const editorsByInstanceId = new Map();
|
|
46
60
|
let staleEntryCount = 0;
|
|
47
|
-
for (const
|
|
48
|
-
|
|
61
|
+
for (const registryDir of registryDirs) {
|
|
62
|
+
let fileNames = [];
|
|
63
|
+
try {
|
|
64
|
+
fileNames = await readdir(registryDir);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
49
67
|
continue;
|
|
50
68
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
for (const fileName of fileNames) {
|
|
70
|
+
if (!fileName.toLowerCase().endsWith('.json')) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const fullPath = path.join(registryDir, fileName);
|
|
74
|
+
try {
|
|
75
|
+
const [raw, fileStat] = await Promise.all([
|
|
76
|
+
readFile(fullPath, 'utf8'),
|
|
77
|
+
stat(fullPath),
|
|
78
|
+
]);
|
|
79
|
+
const parsed = editorInstanceSchema.parse(JSON.parse(raw));
|
|
80
|
+
const lastSeenAt = parseLastSeenAt(parsed.lastSeenAt) ?? fileStat.mtimeMs;
|
|
81
|
+
if ((Date.now() - lastSeenAt) > staleTtlMs) {
|
|
82
|
+
staleEntryCount += 1;
|
|
83
|
+
await rm(fullPath, { force: true }).catch(() => undefined);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Verify the process is actually alive — registry files can outlive the editor
|
|
87
|
+
// process (e.g. after a crash, force-kill, or external build restart).
|
|
88
|
+
if (typeof parsed.processId === 'number') {
|
|
89
|
+
try {
|
|
90
|
+
process.kill(parsed.processId, 0);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
staleEntryCount += 1;
|
|
94
|
+
await rm(fullPath, { force: true }).catch(() => undefined);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const existing = editorsByInstanceId.get(parsed.instanceId);
|
|
99
|
+
if (!existing || lastSeenAt >= existing.lastSeenAt) {
|
|
100
|
+
editorsByInstanceId.set(parsed.instanceId, { snapshot: parsed, lastSeenAt });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
60
104
|
staleEntryCount += 1;
|
|
61
105
|
await rm(fullPath, { force: true }).catch(() => undefined);
|
|
62
|
-
continue;
|
|
63
106
|
}
|
|
64
|
-
editors.push(parsed);
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
staleEntryCount += 1;
|
|
68
|
-
await rm(fullPath, { force: true }).catch(() => undefined);
|
|
69
107
|
}
|
|
70
108
|
}
|
|
109
|
+
const editors = Array.from(editorsByInstanceId.values(), (entry) => entry.snapshot);
|
|
71
110
|
editors.sort((left, right) => {
|
|
72
111
|
const projectCompare = String(left.projectName ?? left.projectFilePath)
|
|
73
112
|
.localeCompare(String(right.projectName ?? right.projectFilePath));
|
|
@@ -78,7 +117,7 @@ export async function listRegisteredEditors(staleTtlMs = DEFAULT_STALE_TTL_MS) {
|
|
|
78
117
|
});
|
|
79
118
|
return {
|
|
80
119
|
editors,
|
|
81
|
-
registryDir,
|
|
120
|
+
registryDir: registryDirs[0] ?? getEditorRegistryDir(),
|
|
82
121
|
staleEntryCount,
|
|
83
122
|
};
|
|
84
123
|
}
|
|
@@ -48,6 +48,11 @@ export function normalizeFilesystemPath(input) {
|
|
|
48
48
|
if (isWindowsStylePath(trimmed)) {
|
|
49
49
|
return path.win32.normalize(trimmed).replaceAll('\\', '/').toLowerCase();
|
|
50
50
|
}
|
|
51
|
+
// Canonicalize WSL-mounted Windows paths to the same normalized form as native Windows paths
|
|
52
|
+
// so registry entries like D:/Project/MyGame.uproject match /mnt/d/Project/MyGame.uproject.
|
|
53
|
+
if (isWslMountedWindowsPath(trimmed)) {
|
|
54
|
+
return toWindowsStylePath(trimmed).replaceAll('\\', '/').toLowerCase();
|
|
55
|
+
}
|
|
51
56
|
return path.posix.normalize(trimmed);
|
|
52
57
|
}
|
|
53
58
|
export function filesystemPathsEqual(left, right) {
|
|
@@ -7,7 +7,14 @@ const DEFAULT_BUILD_TIMEOUT_MS = 30 * 60 * 1000;
|
|
|
7
7
|
const DEFAULT_DISCONNECT_TIMEOUT_MS = 60 * 1000;
|
|
8
8
|
const DEFAULT_RECONNECT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
9
9
|
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
10
|
-
const DEFAULT_EDITOR_LAUNCH_ARGS = ['-RCWebControlEnable', '-RCWebInterfaceEnable'];
|
|
10
|
+
const DEFAULT_EDITOR_LAUNCH_ARGS = ['-RCWebControlEnable', '-RCWebInterfaceEnable', '-WebControl.EnableServerOnStartup=1'];
|
|
11
|
+
function hasCommandLineArg(args, arg) {
|
|
12
|
+
const normalizedTarget = arg.toLowerCase();
|
|
13
|
+
return args.some((candidate) => {
|
|
14
|
+
const normalizedCandidate = candidate.toLowerCase();
|
|
15
|
+
return normalizedCandidate === normalizedTarget || normalizedCandidate.startsWith(`${normalizedTarget}=`);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
11
18
|
function defaultSleep(ms) {
|
|
12
19
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
13
20
|
}
|
|
@@ -504,9 +511,13 @@ export class ProjectController {
|
|
|
504
511
|
const hostExecutable = toHostFilesystemPath(executable, executionPlatform, this.platform);
|
|
505
512
|
const requestedArgs = request.additionalArgs ?? [];
|
|
506
513
|
const commandProjectPath = normalizeFilesystemPathForCommand(projectPath, executionPlatform);
|
|
514
|
+
const defaultArgs = DEFAULT_EDITOR_LAUNCH_ARGS.filter((arg) => {
|
|
515
|
+
const argName = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
|
|
516
|
+
return !hasCommandLineArg(requestedArgs, argName);
|
|
517
|
+
});
|
|
507
518
|
const args = [
|
|
508
519
|
commandProjectPath,
|
|
509
|
-
...
|
|
520
|
+
...defaultArgs,
|
|
510
521
|
...requestedArgs,
|
|
511
522
|
];
|
|
512
523
|
const invocation = resolveCommandInvocation(hostExecutable, args, executionPlatform, this.env);
|
|
@@ -1324,4 +1324,87 @@ export function registerProjectControlTools({ server, client, projectController,
|
|
|
1324
1324
|
return jsonToolSuccess(failurePayload);
|
|
1325
1325
|
}
|
|
1326
1326
|
});
|
|
1327
|
+
// ============================================================
|
|
1328
|
+
// StateTree Debugger
|
|
1329
|
+
// ============================================================
|
|
1330
|
+
server.registerTool('start_statetree_debugger', {
|
|
1331
|
+
title: 'Start StateTree Debugger',
|
|
1332
|
+
description: 'Start recording StateTree debug traces. Requires PIE to be running. Optionally filter to a specific StateTree asset.',
|
|
1333
|
+
inputSchema: {
|
|
1334
|
+
asset_path: z.string().default('').describe('UE content path of the StateTree asset to filter on (e.g. /Game/AI/ST_Character). Empty = record all.'),
|
|
1335
|
+
},
|
|
1336
|
+
annotations: {
|
|
1337
|
+
title: 'Start StateTree Debugger',
|
|
1338
|
+
readOnlyHint: false,
|
|
1339
|
+
destructiveHint: false,
|
|
1340
|
+
idempotentHint: false,
|
|
1341
|
+
openWorldHint: false,
|
|
1342
|
+
},
|
|
1343
|
+
}, async ({ asset_path }) => {
|
|
1344
|
+
try {
|
|
1345
|
+
const parsed = await callSubsystemJson('StartStateTreeDebugger', {
|
|
1346
|
+
AssetPath: asset_path,
|
|
1347
|
+
});
|
|
1348
|
+
return jsonToolSuccess(parsed);
|
|
1349
|
+
}
|
|
1350
|
+
catch (error) {
|
|
1351
|
+
return jsonToolError(error);
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
server.registerTool('stop_statetree_debugger', {
|
|
1355
|
+
title: 'Stop StateTree Debugger',
|
|
1356
|
+
description: 'Stop the active StateTree debugger session and discard trace data.',
|
|
1357
|
+
inputSchema: {},
|
|
1358
|
+
annotations: {
|
|
1359
|
+
title: 'Stop StateTree Debugger',
|
|
1360
|
+
readOnlyHint: false,
|
|
1361
|
+
destructiveHint: false,
|
|
1362
|
+
idempotentHint: false,
|
|
1363
|
+
openWorldHint: false,
|
|
1364
|
+
},
|
|
1365
|
+
}, async () => {
|
|
1366
|
+
try {
|
|
1367
|
+
const parsed = await callSubsystemJson('StopStateTreeDebugger', {});
|
|
1368
|
+
return jsonToolSuccess(parsed);
|
|
1369
|
+
}
|
|
1370
|
+
catch (error) {
|
|
1371
|
+
return jsonToolError(error);
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
server.registerTool('read_statetree_debugger', {
|
|
1375
|
+
title: 'Read StateTree Debugger',
|
|
1376
|
+
description: 'Read current StateTree debugger data: instances, events, active states, transitions, conditions.',
|
|
1377
|
+
inputSchema: {
|
|
1378
|
+
instance_id: z.string().default('').describe('Filter to a specific instance ID (from a previous read). Empty = list all instances without events.'),
|
|
1379
|
+
max_events: z.number().int().positive().default(500).describe('Maximum events to return per instance.'),
|
|
1380
|
+
scrub_time: z.number().default(-1).describe('Set the scrub time before reading. -1 = use current position.'),
|
|
1381
|
+
},
|
|
1382
|
+
annotations: {
|
|
1383
|
+
title: 'Read StateTree Debugger',
|
|
1384
|
+
readOnlyHint: true,
|
|
1385
|
+
destructiveHint: false,
|
|
1386
|
+
idempotentHint: true,
|
|
1387
|
+
openWorldHint: false,
|
|
1388
|
+
},
|
|
1389
|
+
}, async ({ instance_id, max_events, scrub_time }) => {
|
|
1390
|
+
try {
|
|
1391
|
+
const payload = {};
|
|
1392
|
+
if (instance_id) {
|
|
1393
|
+
payload.instanceId = instance_id;
|
|
1394
|
+
}
|
|
1395
|
+
if (max_events !== 500) {
|
|
1396
|
+
payload.maxEvents = max_events;
|
|
1397
|
+
}
|
|
1398
|
+
if (scrub_time >= 0) {
|
|
1399
|
+
payload.scrubTime = scrub_time;
|
|
1400
|
+
}
|
|
1401
|
+
const parsed = await callSubsystemJson('ReadStateTreeDebugger', {
|
|
1402
|
+
PayloadJson: JSON.stringify(payload),
|
|
1403
|
+
});
|
|
1404
|
+
return jsonToolSuccess(parsed);
|
|
1405
|
+
}
|
|
1406
|
+
catch (error) {
|
|
1407
|
+
return jsonToolError(error);
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1327
1410
|
}
|