@uemcp/server 0.1.0 → 0.3.0
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/README.md +56 -0
- package/dist/index.mjs +2037 -31
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import os from "node:os";
|
|
2
|
+
import os, { tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -10,7 +10,9 @@ import Fastify from "fastify";
|
|
|
10
10
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
11
11
|
import pino from "pino";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
|
-
import {
|
|
13
|
+
import { createReadStream } from "node:fs";
|
|
14
|
+
import { access, mkdir, mkdtemp, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
15
|
+
import { createInterface } from "node:readline";
|
|
14
16
|
//#region src/errors.ts
|
|
15
17
|
var McpServerError = class extends Error {
|
|
16
18
|
constructor(message, options) {
|
|
@@ -228,6 +230,75 @@ const BRIDGE_GET_CREATE_CLASS_STATUS_REQUEST_TYPE = "get_create_class_status";
|
|
|
228
230
|
const BRIDGE_FIND_ASSETS_REQUEST_TYPE = "find_assets";
|
|
229
231
|
const BRIDGE_GET_ASSET_DEPENDENCIES_REQUEST_TYPE = "get_asset_dependencies";
|
|
230
232
|
const BRIDGE_FIND_BLUEPRINT_REFERENCES_REQUEST_TYPE = "find_blueprint_references";
|
|
233
|
+
const BRIDGE_GET_BLUEPRINT_SUMMARY_REQUEST_TYPE = "get_blueprint_summary";
|
|
234
|
+
const BRIDGE_GET_BLUEPRINT_GRAPH_REQUEST_TYPE = "get_blueprint_graph";
|
|
235
|
+
const BRIDGE_GET_BLUEPRINT_COMPILE_ERRORS_REQUEST_TYPE = "get_blueprint_compile_errors";
|
|
236
|
+
const BRIDGE_COMPILE_BLUEPRINT_REQUEST_TYPE = "compile_blueprint";
|
|
237
|
+
const BRIDGE_GET_BLUEPRINT_COMPILE_STATUS_REQUEST_TYPE = "get_blueprint_compile_status";
|
|
238
|
+
const BRIDGE_GET_CONFIG_VALUE_REQUEST_TYPE = "get_config_value";
|
|
239
|
+
const BRIDGE_SET_CONFIG_VALUE_REQUEST_TYPE = "set_config_value";
|
|
240
|
+
const BRIDGE_LIST_CONFIG_SECTION_REQUEST_TYPE = "list_config_section";
|
|
241
|
+
const BRIDGE_CONFIG_FILES = [
|
|
242
|
+
"Engine",
|
|
243
|
+
"Game",
|
|
244
|
+
"Input",
|
|
245
|
+
"Editor",
|
|
246
|
+
"EditorPerProjectUserSettings"
|
|
247
|
+
];
|
|
248
|
+
const BRIDGE_CONFIG_WRITE_SCOPES = ["project_default", "saved_user"];
|
|
249
|
+
const BRIDGE_CONFIG_RESOLVED_SCOPES = [
|
|
250
|
+
"engine_base",
|
|
251
|
+
"project_default",
|
|
252
|
+
"saved_user",
|
|
253
|
+
"unknown"
|
|
254
|
+
];
|
|
255
|
+
const BRIDGE_CONFIG_MAX_VALUE_CHARS = 4096;
|
|
256
|
+
const BRIDGE_BLUEPRINT_KINDS = [
|
|
257
|
+
"normal",
|
|
258
|
+
"const",
|
|
259
|
+
"macro_library",
|
|
260
|
+
"interface",
|
|
261
|
+
"level_script",
|
|
262
|
+
"function_library",
|
|
263
|
+
"unknown"
|
|
264
|
+
];
|
|
265
|
+
const BRIDGE_BLUEPRINT_GRAPH_KINDS = [
|
|
266
|
+
"event",
|
|
267
|
+
"function",
|
|
268
|
+
"macro",
|
|
269
|
+
"interface",
|
|
270
|
+
"construction_script",
|
|
271
|
+
"delegate",
|
|
272
|
+
"unknown"
|
|
273
|
+
];
|
|
274
|
+
const BRIDGE_BLUEPRINT_MEMBER_KINDS = [
|
|
275
|
+
"function",
|
|
276
|
+
"event",
|
|
277
|
+
"macro",
|
|
278
|
+
"interface"
|
|
279
|
+
];
|
|
280
|
+
const BRIDGE_BLUEPRINT_PIN_DIRECTIONS = ["input", "output"];
|
|
281
|
+
const BRIDGE_BLUEPRINT_REPLICATION_MODES = [
|
|
282
|
+
"none",
|
|
283
|
+
"replicated",
|
|
284
|
+
"rep_notify"
|
|
285
|
+
];
|
|
286
|
+
const BRIDGE_BLUEPRINT_STATUSES = [
|
|
287
|
+
"up_to_date",
|
|
288
|
+
"warnings",
|
|
289
|
+
"error",
|
|
290
|
+
"dirty",
|
|
291
|
+
"unknown",
|
|
292
|
+
"not_loaded",
|
|
293
|
+
"not_found"
|
|
294
|
+
];
|
|
295
|
+
const BRIDGE_COMPILE_BLUEPRINT_STATES = ["compiling", "unsupported"];
|
|
296
|
+
const BRIDGE_BLUEPRINT_COMPILE_STATUS_STATES = [
|
|
297
|
+
"compiling",
|
|
298
|
+
"succeeded",
|
|
299
|
+
"failed",
|
|
300
|
+
"unknown_job"
|
|
301
|
+
];
|
|
231
302
|
const BRIDGE_CREATE_CLASS_STATES = ["creating", "unsupported"];
|
|
232
303
|
const BRIDGE_CREATE_CLASS_STATUS_STATES = [
|
|
233
304
|
"creating",
|
|
@@ -391,6 +462,26 @@ const assetPathArraySchema = z.array(assetPathSchema).max(32);
|
|
|
391
462
|
const assetDependencyCategorySchema = z.enum(BRIDGE_ASSET_DEPENDENCY_CATEGORIES);
|
|
392
463
|
const assetDependencyLinkageSchema = z.enum(BRIDGE_ASSET_DEPENDENCY_LINKAGES);
|
|
393
464
|
const blueprintReferenceKindSchema = z.enum(BRIDGE_BLUEPRINT_REFERENCE_KINDS);
|
|
465
|
+
const blueprintKindSchema = z.enum(BRIDGE_BLUEPRINT_KINDS);
|
|
466
|
+
const blueprintGraphKindSchema = z.enum(BRIDGE_BLUEPRINT_GRAPH_KINDS);
|
|
467
|
+
const blueprintMemberKindSchema = z.enum(BRIDGE_BLUEPRINT_MEMBER_KINDS);
|
|
468
|
+
const blueprintPinDirectionSchema = z.enum(BRIDGE_BLUEPRINT_PIN_DIRECTIONS);
|
|
469
|
+
const blueprintReplicationSchema = z.enum(BRIDGE_BLUEPRINT_REPLICATION_MODES);
|
|
470
|
+
const blueprintStatusSchema = z.enum(BRIDGE_BLUEPRINT_STATUSES);
|
|
471
|
+
const compileBlueprintStateSchema = z.enum(BRIDGE_COMPILE_BLUEPRINT_STATES);
|
|
472
|
+
const blueprintCompileStatusStateSchema = z.enum(BRIDGE_BLUEPRINT_COMPILE_STATUS_STATES);
|
|
473
|
+
const blueprintCompileJobIdSchema = z.string().trim().min(1);
|
|
474
|
+
const blueprintGraphNameSchema = z.string().trim().min(1).max(256);
|
|
475
|
+
const blueprintMemberLimitSchema = z.number().int().min(1).max(200);
|
|
476
|
+
const blueprintNodeLimitSchema = z.number().int().min(1).max(200);
|
|
477
|
+
const blueprintPathArraySchema = z.array(assetPathSchema).min(1).max(32);
|
|
478
|
+
const configFileSchema = z.enum(BRIDGE_CONFIG_FILES);
|
|
479
|
+
const configWriteScopeSchema = z.enum(BRIDGE_CONFIG_WRITE_SCOPES);
|
|
480
|
+
const configResolvedScopeSchema = z.enum(BRIDGE_CONFIG_RESOLVED_SCOPES);
|
|
481
|
+
const configSectionSchema = z.string().trim().min(1).max(256);
|
|
482
|
+
const configKeySchema = z.string().trim().min(1).max(256);
|
|
483
|
+
const configValueSchema = z.string().max(BRIDGE_CONFIG_MAX_VALUE_CHARS);
|
|
484
|
+
const configSectionMaxResultsSchema = z.number().int().min(1).max(200);
|
|
394
485
|
const createClassStateSchema = z.enum(BRIDGE_CREATE_CLASS_STATES);
|
|
395
486
|
const createClassStatusStateSchema = z.enum(BRIDGE_CREATE_CLASS_STATUS_STATES);
|
|
396
487
|
const createClassJobIdSchema = z.string().trim().min(1);
|
|
@@ -694,6 +785,77 @@ const bridgeFindBlueprintReferencesRequestSchema = z.object({
|
|
|
694
785
|
type: z.literal(BRIDGE_FIND_BLUEPRINT_REFERENCES_REQUEST_TYPE),
|
|
695
786
|
version: z.literal(1)
|
|
696
787
|
});
|
|
788
|
+
const bridgeGetBlueprintSummaryRequestSchema = z.object({
|
|
789
|
+
blueprintPath: assetPathSchema,
|
|
790
|
+
componentCursor: assetCursorSchema.default(0),
|
|
791
|
+
functionCursor: assetCursorSchema.default(0),
|
|
792
|
+
graphCursor: assetCursorSchema.default(0),
|
|
793
|
+
loadIfNeeded: z.boolean().default(false),
|
|
794
|
+
maxResults: blueprintMemberLimitSchema,
|
|
795
|
+
requestId: requestIdSchema,
|
|
796
|
+
type: z.literal(BRIDGE_GET_BLUEPRINT_SUMMARY_REQUEST_TYPE),
|
|
797
|
+
variableCursor: assetCursorSchema.default(0),
|
|
798
|
+
version: z.literal(1)
|
|
799
|
+
});
|
|
800
|
+
const bridgeGetBlueprintGraphRequestSchema = z.object({
|
|
801
|
+
blueprintPath: assetPathSchema,
|
|
802
|
+
graphName: blueprintGraphNameSchema,
|
|
803
|
+
loadIfNeeded: z.boolean().default(false),
|
|
804
|
+
maxNodes: blueprintNodeLimitSchema,
|
|
805
|
+
nodeCursor: assetCursorSchema.default(0),
|
|
806
|
+
requestId: requestIdSchema,
|
|
807
|
+
type: z.literal(BRIDGE_GET_BLUEPRINT_GRAPH_REQUEST_TYPE),
|
|
808
|
+
version: z.literal(1)
|
|
809
|
+
});
|
|
810
|
+
const bridgeGetBlueprintCompileErrorsRequestSchema = z.object({
|
|
811
|
+
blueprintPaths: blueprintPathArraySchema,
|
|
812
|
+
cursor: assetCursorSchema.default(0),
|
|
813
|
+
maxResults: assetMaxResultsSchema,
|
|
814
|
+
requestId: requestIdSchema,
|
|
815
|
+
type: z.literal(BRIDGE_GET_BLUEPRINT_COMPILE_ERRORS_REQUEST_TYPE),
|
|
816
|
+
version: z.literal(1)
|
|
817
|
+
});
|
|
818
|
+
const bridgeCompileBlueprintRequestSchema = z.object({
|
|
819
|
+
blueprintPaths: blueprintPathArraySchema,
|
|
820
|
+
requestId: requestIdSchema,
|
|
821
|
+
type: z.literal(BRIDGE_COMPILE_BLUEPRINT_REQUEST_TYPE),
|
|
822
|
+
version: z.literal(1)
|
|
823
|
+
});
|
|
824
|
+
const bridgeGetBlueprintCompileStatusRequestSchema = z.object({
|
|
825
|
+
compileJobId: blueprintCompileJobIdSchema,
|
|
826
|
+
cursor: assetCursorSchema.default(0),
|
|
827
|
+
maxResults: assetMaxResultsSchema,
|
|
828
|
+
requestId: requestIdSchema,
|
|
829
|
+
type: z.literal(BRIDGE_GET_BLUEPRINT_COMPILE_STATUS_REQUEST_TYPE),
|
|
830
|
+
version: z.literal(1)
|
|
831
|
+
});
|
|
832
|
+
const bridgeGetConfigValueRequestSchema = z.object({
|
|
833
|
+
file: configFileSchema,
|
|
834
|
+
key: configKeySchema,
|
|
835
|
+
requestId: requestIdSchema,
|
|
836
|
+
section: configSectionSchema,
|
|
837
|
+
type: z.literal(BRIDGE_GET_CONFIG_VALUE_REQUEST_TYPE),
|
|
838
|
+
version: z.literal(1)
|
|
839
|
+
});
|
|
840
|
+
const bridgeSetConfigValueRequestSchema = z.object({
|
|
841
|
+
file: configFileSchema,
|
|
842
|
+
key: configKeySchema,
|
|
843
|
+
requestId: requestIdSchema,
|
|
844
|
+
scope: configWriteScopeSchema.default("project_default"),
|
|
845
|
+
section: configSectionSchema,
|
|
846
|
+
type: z.literal(BRIDGE_SET_CONFIG_VALUE_REQUEST_TYPE),
|
|
847
|
+
value: configValueSchema,
|
|
848
|
+
version: z.literal(1)
|
|
849
|
+
});
|
|
850
|
+
const bridgeListConfigSectionRequestSchema = z.object({
|
|
851
|
+
cursor: assetCursorSchema.default(0),
|
|
852
|
+
file: configFileSchema,
|
|
853
|
+
maxResults: configSectionMaxResultsSchema.default(100),
|
|
854
|
+
requestId: requestIdSchema,
|
|
855
|
+
section: configSectionSchema,
|
|
856
|
+
type: z.literal(BRIDGE_LIST_CONFIG_SECTION_REQUEST_TYPE),
|
|
857
|
+
version: z.literal(1)
|
|
858
|
+
});
|
|
697
859
|
const bridgePingSuccessResponseSchema = z.object({
|
|
698
860
|
ok: z.literal(true),
|
|
699
861
|
port: z.number().int().min(1).max(65535),
|
|
@@ -1198,6 +1360,197 @@ const bridgeFindBlueprintReferencesSuccessResponseSchema = z.object({
|
|
|
1198
1360
|
type: z.literal(BRIDGE_FIND_BLUEPRINT_REFERENCES_REQUEST_TYPE),
|
|
1199
1361
|
version: z.literal(1)
|
|
1200
1362
|
});
|
|
1363
|
+
const bridgeBlueprintComponentSchema = z.object({
|
|
1364
|
+
className: z.string(),
|
|
1365
|
+
isInherited: z.boolean(),
|
|
1366
|
+
isRoot: z.boolean(),
|
|
1367
|
+
name: z.string(),
|
|
1368
|
+
parentName: z.string().optional()
|
|
1369
|
+
});
|
|
1370
|
+
const bridgeBlueprintVariableSchema = z.object({
|
|
1371
|
+
category: z.string().optional(),
|
|
1372
|
+
defaultValue: z.string().optional(),
|
|
1373
|
+
name: z.string(),
|
|
1374
|
+
replication: blueprintReplicationSchema,
|
|
1375
|
+
type: z.string()
|
|
1376
|
+
});
|
|
1377
|
+
const bridgeBlueprintMemberSchema = z.object({
|
|
1378
|
+
graphName: z.string().optional(),
|
|
1379
|
+
kind: blueprintMemberKindSchema,
|
|
1380
|
+
name: z.string(),
|
|
1381
|
+
parameters: z.array(bridgeReflectionParameterSchema)
|
|
1382
|
+
});
|
|
1383
|
+
const bridgeBlueprintGraphSummarySchema = z.object({
|
|
1384
|
+
kind: blueprintGraphKindSchema,
|
|
1385
|
+
name: z.string(),
|
|
1386
|
+
nodeCount: z.number().int().min(0)
|
|
1387
|
+
});
|
|
1388
|
+
const bridgeGetBlueprintSummarySuccessResponseSchema = z.object({
|
|
1389
|
+
blueprintName: z.string(),
|
|
1390
|
+
blueprintPath: z.string(),
|
|
1391
|
+
componentCount: z.number().int().min(0),
|
|
1392
|
+
components: z.array(bridgeBlueprintComponentSchema),
|
|
1393
|
+
functionCount: z.number().int().min(0),
|
|
1394
|
+
functions: z.array(bridgeBlueprintMemberSchema),
|
|
1395
|
+
generatedClassPath: z.string().optional(),
|
|
1396
|
+
graphCount: z.number().int().min(0),
|
|
1397
|
+
graphs: z.array(bridgeBlueprintGraphSummarySchema),
|
|
1398
|
+
interfaces: z.array(z.string()),
|
|
1399
|
+
kind: blueprintKindSchema,
|
|
1400
|
+
loaded: z.boolean(),
|
|
1401
|
+
nextComponentCursor: assetCursorSchema.optional(),
|
|
1402
|
+
nextFunctionCursor: assetCursorSchema.optional(),
|
|
1403
|
+
nextGraphCursor: assetCursorSchema.optional(),
|
|
1404
|
+
nextVariableCursor: assetCursorSchema.optional(),
|
|
1405
|
+
ok: z.literal(true),
|
|
1406
|
+
parentClass: z.string().optional(),
|
|
1407
|
+
parentClassPath: z.string().optional(),
|
|
1408
|
+
requestId: requestIdSchema,
|
|
1409
|
+
scanning: z.boolean(),
|
|
1410
|
+
truncated: z.boolean(),
|
|
1411
|
+
type: z.literal(BRIDGE_GET_BLUEPRINT_SUMMARY_REQUEST_TYPE),
|
|
1412
|
+
variableCount: z.number().int().min(0),
|
|
1413
|
+
variables: z.array(bridgeBlueprintVariableSchema),
|
|
1414
|
+
version: z.literal(1)
|
|
1415
|
+
});
|
|
1416
|
+
const bridgeBlueprintPinLinkSchema = z.object({
|
|
1417
|
+
nodeId: z.string(),
|
|
1418
|
+
pinId: z.string()
|
|
1419
|
+
});
|
|
1420
|
+
const bridgeBlueprintPinSchema = z.object({
|
|
1421
|
+
defaultValue: z.string().optional(),
|
|
1422
|
+
direction: blueprintPinDirectionSchema,
|
|
1423
|
+
linkedTo: z.array(bridgeBlueprintPinLinkSchema),
|
|
1424
|
+
linkedToCount: z.number().int().min(0),
|
|
1425
|
+
name: z.string(),
|
|
1426
|
+
pinId: z.string(),
|
|
1427
|
+
type: z.string()
|
|
1428
|
+
});
|
|
1429
|
+
const bridgeBlueprintNodeSchema = z.object({
|
|
1430
|
+
comment: z.string().optional(),
|
|
1431
|
+
nodeClass: z.string(),
|
|
1432
|
+
nodeId: z.string(),
|
|
1433
|
+
pinCount: z.number().int().min(0),
|
|
1434
|
+
pins: z.array(bridgeBlueprintPinSchema),
|
|
1435
|
+
title: z.string()
|
|
1436
|
+
});
|
|
1437
|
+
const bridgeGetBlueprintGraphSuccessResponseSchema = z.object({
|
|
1438
|
+
blueprintPath: z.string(),
|
|
1439
|
+
graphKind: blueprintGraphKindSchema,
|
|
1440
|
+
graphName: z.string(),
|
|
1441
|
+
nodeCount: z.number().int().min(0),
|
|
1442
|
+
nodes: z.array(bridgeBlueprintNodeSchema),
|
|
1443
|
+
nextNodeCursor: assetCursorSchema.optional(),
|
|
1444
|
+
ok: z.literal(true),
|
|
1445
|
+
requestId: requestIdSchema,
|
|
1446
|
+
truncated: z.boolean(),
|
|
1447
|
+
type: z.literal(BRIDGE_GET_BLUEPRINT_GRAPH_REQUEST_TYPE),
|
|
1448
|
+
version: z.literal(1)
|
|
1449
|
+
});
|
|
1450
|
+
const bridgeBlueprintCompileDiagnosticSchema = bridgeCompileDiagnosticSchema.extend({
|
|
1451
|
+
graphName: z.string().optional(),
|
|
1452
|
+
nodeGuid: z.string().optional(),
|
|
1453
|
+
nodeName: z.string().optional()
|
|
1454
|
+
});
|
|
1455
|
+
const bridgeBlueprintCompileResultSchema = z.object({
|
|
1456
|
+
blueprintPath: z.string(),
|
|
1457
|
+
compiledAt: nonEmptyStringSchema.optional(),
|
|
1458
|
+
errorCount: z.number().int().min(0),
|
|
1459
|
+
errors: z.array(bridgeBlueprintCompileDiagnosticSchema),
|
|
1460
|
+
hasDiagnostics: z.boolean(),
|
|
1461
|
+
status: blueprintStatusSchema,
|
|
1462
|
+
warningCount: z.number().int().min(0),
|
|
1463
|
+
warnings: z.array(bridgeBlueprintCompileDiagnosticSchema)
|
|
1464
|
+
});
|
|
1465
|
+
const bridgeGetBlueprintCompileErrorsSuccessResponseSchema = z.object({
|
|
1466
|
+
nextCursor: assetCursorSchema.optional(),
|
|
1467
|
+
ok: z.literal(true),
|
|
1468
|
+
requestId: requestIdSchema,
|
|
1469
|
+
results: z.array(bridgeBlueprintCompileResultSchema),
|
|
1470
|
+
totalCount: z.number().int().min(0),
|
|
1471
|
+
truncated: z.boolean(),
|
|
1472
|
+
type: z.literal(BRIDGE_GET_BLUEPRINT_COMPILE_ERRORS_REQUEST_TYPE),
|
|
1473
|
+
version: z.literal(1)
|
|
1474
|
+
});
|
|
1475
|
+
const bridgeCompileBlueprintSuccessResponseSchema = z.object({
|
|
1476
|
+
compileJobId: blueprintCompileJobIdSchema.optional(),
|
|
1477
|
+
message: nonEmptyStringSchema.optional(),
|
|
1478
|
+
ok: z.literal(true),
|
|
1479
|
+
requestId: requestIdSchema,
|
|
1480
|
+
state: compileBlueprintStateSchema,
|
|
1481
|
+
type: z.literal(BRIDGE_COMPILE_BLUEPRINT_REQUEST_TYPE),
|
|
1482
|
+
version: z.literal(1)
|
|
1483
|
+
}).refine((value) => value.state === "compiling" === (value.compileJobId !== void 0), {
|
|
1484
|
+
message: "A compile_blueprint response must carry compileJobId exactly when state is 'compiling'.",
|
|
1485
|
+
path: ["compileJobId"]
|
|
1486
|
+
});
|
|
1487
|
+
const bridgeGetBlueprintCompileStatusSuccessResponseSchema = z.object({
|
|
1488
|
+
compileJobId: blueprintCompileJobIdSchema,
|
|
1489
|
+
durationMs: z.number().int().min(0).optional(),
|
|
1490
|
+
finishedAt: nonEmptyStringSchema.optional(),
|
|
1491
|
+
nextCursor: assetCursorSchema.optional(),
|
|
1492
|
+
ok: z.literal(true),
|
|
1493
|
+
requestId: requestIdSchema,
|
|
1494
|
+
results: z.array(bridgeBlueprintCompileResultSchema),
|
|
1495
|
+
state: blueprintCompileStatusStateSchema,
|
|
1496
|
+
totalCount: z.number().int().min(0),
|
|
1497
|
+
truncated: z.boolean(),
|
|
1498
|
+
type: z.literal(BRIDGE_GET_BLUEPRINT_COMPILE_STATUS_REQUEST_TYPE),
|
|
1499
|
+
version: z.literal(1)
|
|
1500
|
+
}).superRefine((value, ctx) => {
|
|
1501
|
+
if (value.state === "succeeded" || value.state === "failed") return;
|
|
1502
|
+
for (const field of ["durationMs", "finishedAt"]) if (value[field] !== void 0) ctx.addIssue({
|
|
1503
|
+
code: z.ZodIssueCode.custom,
|
|
1504
|
+
message: `${field} must be absent unless state is succeeded or failed`,
|
|
1505
|
+
path: [field]
|
|
1506
|
+
});
|
|
1507
|
+
});
|
|
1508
|
+
const bridgeConfigEntrySchema = z.object({
|
|
1509
|
+
key: configKeySchema,
|
|
1510
|
+
value: configValueSchema,
|
|
1511
|
+
valueTruncated: z.boolean().optional()
|
|
1512
|
+
});
|
|
1513
|
+
const bridgeGetConfigValueSuccessResponseSchema = z.object({
|
|
1514
|
+
file: configFileSchema,
|
|
1515
|
+
filePath: z.string().trim().min(1).optional(),
|
|
1516
|
+
key: configKeySchema,
|
|
1517
|
+
ok: z.literal(true),
|
|
1518
|
+
present: z.boolean(),
|
|
1519
|
+
requestId: requestIdSchema,
|
|
1520
|
+
resolvedFrom: configResolvedScopeSchema,
|
|
1521
|
+
section: configSectionSchema,
|
|
1522
|
+
type: z.literal(BRIDGE_GET_CONFIG_VALUE_REQUEST_TYPE),
|
|
1523
|
+
value: configValueSchema.optional(),
|
|
1524
|
+
valueTruncated: z.boolean().optional(),
|
|
1525
|
+
version: z.literal(1)
|
|
1526
|
+
});
|
|
1527
|
+
const bridgeSetConfigValueSuccessResponseSchema = z.object({
|
|
1528
|
+
file: configFileSchema,
|
|
1529
|
+
filePath: z.string().trim().min(1),
|
|
1530
|
+
key: configKeySchema,
|
|
1531
|
+
ok: z.literal(true),
|
|
1532
|
+
previousPresent: z.boolean(),
|
|
1533
|
+
previousValue: configValueSchema.optional(),
|
|
1534
|
+
previousValueTruncated: z.boolean().optional(),
|
|
1535
|
+
requestId: requestIdSchema,
|
|
1536
|
+
scope: configWriteScopeSchema,
|
|
1537
|
+
section: configSectionSchema,
|
|
1538
|
+
type: z.literal(BRIDGE_SET_CONFIG_VALUE_REQUEST_TYPE),
|
|
1539
|
+
value: configValueSchema,
|
|
1540
|
+
version: z.literal(1)
|
|
1541
|
+
});
|
|
1542
|
+
const bridgeListConfigSectionSuccessResponseSchema = z.object({
|
|
1543
|
+
entries: z.array(bridgeConfigEntrySchema).max(200),
|
|
1544
|
+
file: configFileSchema,
|
|
1545
|
+
nextCursor: assetCursorSchema.optional(),
|
|
1546
|
+
ok: z.literal(true),
|
|
1547
|
+
requestId: requestIdSchema,
|
|
1548
|
+
section: configSectionSchema,
|
|
1549
|
+
totalCount: z.number().int().min(0),
|
|
1550
|
+
truncated: z.boolean(),
|
|
1551
|
+
type: z.literal(BRIDGE_LIST_CONFIG_SECTION_REQUEST_TYPE),
|
|
1552
|
+
version: z.literal(1)
|
|
1553
|
+
});
|
|
1201
1554
|
const bridgeErrorResponseSchema = z.object({
|
|
1202
1555
|
error: bridgeErrorSchema,
|
|
1203
1556
|
ok: z.literal(false),
|
|
@@ -1235,6 +1588,14 @@ const bridgeGetCreateClassStatusResponseSchema = z.union([bridgeGetCreateClassSt
|
|
|
1235
1588
|
const bridgeFindAssetsResponseSchema = z.union([bridgeFindAssetsSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1236
1589
|
const bridgeGetAssetDependenciesResponseSchema = z.union([bridgeGetAssetDependenciesSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1237
1590
|
const bridgeFindBlueprintReferencesResponseSchema = z.union([bridgeFindBlueprintReferencesSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1591
|
+
const bridgeGetBlueprintSummaryResponseSchema = z.union([bridgeGetBlueprintSummarySuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1592
|
+
const bridgeGetBlueprintGraphResponseSchema = z.union([bridgeGetBlueprintGraphSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1593
|
+
const bridgeGetBlueprintCompileErrorsResponseSchema = z.union([bridgeGetBlueprintCompileErrorsSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1594
|
+
const bridgeCompileBlueprintResponseSchema = z.union([bridgeCompileBlueprintSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1595
|
+
const bridgeGetBlueprintCompileStatusResponseSchema = z.union([bridgeGetBlueprintCompileStatusSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1596
|
+
const bridgeGetConfigValueResponseSchema = z.union([bridgeGetConfigValueSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1597
|
+
const bridgeSetConfigValueResponseSchema = z.union([bridgeSetConfigValueSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1598
|
+
const bridgeListConfigSectionResponseSchema = z.union([bridgeListConfigSectionSuccessResponseSchema, bridgeErrorResponseSchema]);
|
|
1238
1599
|
function createBridgeFindAssetsRequest(options = {}, requestId = randomUUID()) {
|
|
1239
1600
|
return bridgeFindAssetsRequestSchema.parse({
|
|
1240
1601
|
classNames: options.classNames,
|
|
@@ -1273,6 +1634,93 @@ function createBridgeFindBlueprintReferencesRequest(className, options = {}, req
|
|
|
1273
1634
|
version: 1
|
|
1274
1635
|
});
|
|
1275
1636
|
}
|
|
1637
|
+
function createBridgeGetBlueprintSummaryRequest(blueprintPath, options = {}, requestId = randomUUID()) {
|
|
1638
|
+
return bridgeGetBlueprintSummaryRequestSchema.parse({
|
|
1639
|
+
blueprintPath,
|
|
1640
|
+
componentCursor: options.componentCursor,
|
|
1641
|
+
functionCursor: options.functionCursor,
|
|
1642
|
+
graphCursor: options.graphCursor,
|
|
1643
|
+
loadIfNeeded: options.loadIfNeeded,
|
|
1644
|
+
maxResults: options.maxResults ?? 25,
|
|
1645
|
+
requestId,
|
|
1646
|
+
type: BRIDGE_GET_BLUEPRINT_SUMMARY_REQUEST_TYPE,
|
|
1647
|
+
variableCursor: options.variableCursor,
|
|
1648
|
+
version: 1
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
function createBridgeGetBlueprintGraphRequest(blueprintPath, graphName, options = {}, requestId = randomUUID()) {
|
|
1652
|
+
return bridgeGetBlueprintGraphRequestSchema.parse({
|
|
1653
|
+
blueprintPath,
|
|
1654
|
+
graphName,
|
|
1655
|
+
loadIfNeeded: options.loadIfNeeded,
|
|
1656
|
+
maxNodes: options.maxNodes ?? 25,
|
|
1657
|
+
nodeCursor: options.nodeCursor,
|
|
1658
|
+
requestId,
|
|
1659
|
+
type: BRIDGE_GET_BLUEPRINT_GRAPH_REQUEST_TYPE,
|
|
1660
|
+
version: 1
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
function createBridgeGetBlueprintCompileErrorsRequest(blueprintPaths, options = {}, requestId = randomUUID()) {
|
|
1664
|
+
return bridgeGetBlueprintCompileErrorsRequestSchema.parse({
|
|
1665
|
+
blueprintPaths,
|
|
1666
|
+
cursor: options.cursor,
|
|
1667
|
+
maxResults: options.maxResults ?? 50,
|
|
1668
|
+
requestId,
|
|
1669
|
+
type: BRIDGE_GET_BLUEPRINT_COMPILE_ERRORS_REQUEST_TYPE,
|
|
1670
|
+
version: 1
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
function createBridgeCompileBlueprintRequest(blueprintPaths, requestId = randomUUID()) {
|
|
1674
|
+
return bridgeCompileBlueprintRequestSchema.parse({
|
|
1675
|
+
blueprintPaths,
|
|
1676
|
+
requestId,
|
|
1677
|
+
type: BRIDGE_COMPILE_BLUEPRINT_REQUEST_TYPE,
|
|
1678
|
+
version: 1
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
function createBridgeGetBlueprintCompileStatusRequest(compileJobId, options = {}, requestId = randomUUID()) {
|
|
1682
|
+
return bridgeGetBlueprintCompileStatusRequestSchema.parse({
|
|
1683
|
+
compileJobId,
|
|
1684
|
+
cursor: options.cursor,
|
|
1685
|
+
maxResults: options.maxResults ?? 50,
|
|
1686
|
+
requestId,
|
|
1687
|
+
type: BRIDGE_GET_BLUEPRINT_COMPILE_STATUS_REQUEST_TYPE,
|
|
1688
|
+
version: 1
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function createBridgeGetConfigValueRequest(input, requestId = randomUUID()) {
|
|
1692
|
+
return bridgeGetConfigValueRequestSchema.parse({
|
|
1693
|
+
file: input.file,
|
|
1694
|
+
key: input.key,
|
|
1695
|
+
requestId,
|
|
1696
|
+
section: input.section,
|
|
1697
|
+
type: BRIDGE_GET_CONFIG_VALUE_REQUEST_TYPE,
|
|
1698
|
+
version: 1
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
function createBridgeSetConfigValueRequest(input, requestId = randomUUID()) {
|
|
1702
|
+
return bridgeSetConfigValueRequestSchema.parse({
|
|
1703
|
+
file: input.file,
|
|
1704
|
+
key: input.key,
|
|
1705
|
+
requestId,
|
|
1706
|
+
scope: input.scope,
|
|
1707
|
+
section: input.section,
|
|
1708
|
+
type: BRIDGE_SET_CONFIG_VALUE_REQUEST_TYPE,
|
|
1709
|
+
value: input.value,
|
|
1710
|
+
version: 1
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
function createBridgeListConfigSectionRequest(input, requestId = randomUUID()) {
|
|
1714
|
+
return bridgeListConfigSectionRequestSchema.parse({
|
|
1715
|
+
cursor: input.cursor,
|
|
1716
|
+
file: input.file,
|
|
1717
|
+
maxResults: input.maxResults,
|
|
1718
|
+
requestId,
|
|
1719
|
+
section: input.section,
|
|
1720
|
+
type: BRIDGE_LIST_CONFIG_SECTION_REQUEST_TYPE,
|
|
1721
|
+
version: 1
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1276
1724
|
function createBridgePingRequest(requestId = randomUUID()) {
|
|
1277
1725
|
return {
|
|
1278
1726
|
requestId,
|
|
@@ -1587,6 +2035,15 @@ async function sendBridgeGetCdoDefaultsRequest(request, options) {
|
|
|
1587
2035
|
async function sendBridgeExecConsoleCommandRequest(request, options) {
|
|
1588
2036
|
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeExecConsoleCommandResponseSchema, request.requestId);
|
|
1589
2037
|
}
|
|
2038
|
+
async function sendBridgeGetConfigValueRequest(request, options) {
|
|
2039
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetConfigValueResponseSchema, request.requestId);
|
|
2040
|
+
}
|
|
2041
|
+
async function sendBridgeSetConfigValueRequest(request, options) {
|
|
2042
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeSetConfigValueResponseSchema, request.requestId);
|
|
2043
|
+
}
|
|
2044
|
+
async function sendBridgeListConfigSectionRequest(request, options) {
|
|
2045
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeListConfigSectionResponseSchema, request.requestId);
|
|
2046
|
+
}
|
|
1590
2047
|
async function sendBridgeStatSnapshotRequest(request, options) {
|
|
1591
2048
|
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeStatSnapshotResponseSchema, request.requestId);
|
|
1592
2049
|
}
|
|
@@ -1605,6 +2062,21 @@ async function sendBridgeGetAssetDependenciesRequest(request, options) {
|
|
|
1605
2062
|
async function sendBridgeFindBlueprintReferencesRequest(request, options) {
|
|
1606
2063
|
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeFindBlueprintReferencesResponseSchema, request.requestId);
|
|
1607
2064
|
}
|
|
2065
|
+
async function sendBridgeGetBlueprintSummaryRequest(request, options) {
|
|
2066
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetBlueprintSummaryResponseSchema, request.requestId);
|
|
2067
|
+
}
|
|
2068
|
+
async function sendBridgeGetBlueprintGraphRequest(request, options) {
|
|
2069
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetBlueprintGraphResponseSchema, request.requestId);
|
|
2070
|
+
}
|
|
2071
|
+
async function sendBridgeGetBlueprintCompileErrorsRequest(request, options) {
|
|
2072
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetBlueprintCompileErrorsResponseSchema, request.requestId);
|
|
2073
|
+
}
|
|
2074
|
+
async function sendBridgeCompileBlueprintRequest(request, options) {
|
|
2075
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeCompileBlueprintResponseSchema, request.requestId);
|
|
2076
|
+
}
|
|
2077
|
+
async function sendBridgeGetBlueprintCompileStatusRequest(request, options) {
|
|
2078
|
+
return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetBlueprintCompileStatusResponseSchema, request.requestId);
|
|
2079
|
+
}
|
|
1608
2080
|
function encodeBridgeFrame(payload) {
|
|
1609
2081
|
const payloadBuffer = Buffer.from(JSON.stringify(payload), "utf8");
|
|
1610
2082
|
if (payloadBuffer.length === 0 || payloadBuffer.length > 65536) throw new BridgeSocketError("invalid_frame", `Bridge payload size ${String(payloadBuffer.length)} is outside the allowed range.`);
|
|
@@ -1684,6 +2156,7 @@ var BridgeSessionClient = class {
|
|
|
1684
2156
|
port: this.options.port
|
|
1685
2157
|
});
|
|
1686
2158
|
this.connectingSocket = socket;
|
|
2159
|
+
socket.on("error", ignoreSocketError);
|
|
1687
2160
|
let settled = false;
|
|
1688
2161
|
const connectTimeout = setTimeout(() => {
|
|
1689
2162
|
socket.destroy(new BridgeSocketError("connect_timeout", `Timed out connecting to localhost bridge on port ${String(this.options.port)}.`));
|
|
@@ -1817,6 +2290,7 @@ function parseBridgePayload(responsePayload) {
|
|
|
1817
2290
|
throw new BridgeSocketError("invalid_frame", `Bridge response contained invalid JSON: ${error.message}. Payload bytes: ${String(Buffer.byteLength(responsePayload, "utf8"))}.`, { cause: error });
|
|
1818
2291
|
}
|
|
1819
2292
|
}
|
|
2293
|
+
function ignoreSocketError() {}
|
|
1820
2294
|
function exchangeBridgeMessage(frame, options) {
|
|
1821
2295
|
return new Promise((resolve, reject) => {
|
|
1822
2296
|
const socket = net.createConnection({
|
|
@@ -1846,6 +2320,7 @@ function exchangeBridgeMessage(frame, options) {
|
|
|
1846
2320
|
settled = true;
|
|
1847
2321
|
clearTimers();
|
|
1848
2322
|
socket.removeAllListeners();
|
|
2323
|
+
socket.on("error", ignoreSocketError);
|
|
1849
2324
|
handler();
|
|
1850
2325
|
};
|
|
1851
2326
|
const refreshReadTimeout = () => {
|
|
@@ -2501,9 +2976,80 @@ function isLogLevel(value) {
|
|
|
2501
2976
|
const CLANG_DIAGNOSTIC_PATTERN = /^(?<file>.+?):(?<line>\d+):(?:(?<column>\d+):)?\s+(?<severity>fatal error|error|warning):\s+(?<message>.*)$/u;
|
|
2502
2977
|
const MSVC_DIAGNOSTIC_PATTERN = /^(?<file>.+?)\((?<line>\d+)(?:,\d+)?\):\s+(?<severity>fatal error|error|warning)\s+(?<code>[A-Z]+\d+):\s+(?<message>.*)$/u;
|
|
2503
2978
|
const CLANG_TIDY_PATTERN = /^(?<file>.+?):(?<line>\d+):(?<column>\d+):\s+(?<severity>error|warning):\s+(?<message>.*?)(?:\s+\[(?<check>[^\]]+)\])?$/u;
|
|
2979
|
+
const COOK_LINE_PREFIX_PATTERN = /^(?:\[[^\]]*\]\s*)*(?:[A-Za-z]+Helper:\s*[^:]*:\s*)?/u;
|
|
2980
|
+
const COOK_DIAGNOSTIC_PATTERN = /^(?<category>[A-Za-z_][A-Za-z0-9_]*):\s+(?<severity>Error|Warning)(?::)?\s+(?<message>.+)$/u;
|
|
2981
|
+
const UAT_DIAGNOSTIC_PATTERN = /^(?<severity>ERROR|WARNING):\s+(?<message>.+)$/u;
|
|
2504
2982
|
function diagnosticKey(diagnostic) {
|
|
2505
2983
|
return `${diagnostic.file ?? ""}|${diagnostic.line ?? ""}|${diagnostic.message}`;
|
|
2506
2984
|
}
|
|
2985
|
+
function matchCookDiagnostic(rawLine) {
|
|
2986
|
+
const line = rawLine.trim().replace(COOK_LINE_PREFIX_PATTERN, "").trim();
|
|
2987
|
+
if (line.length === 0) return null;
|
|
2988
|
+
const categoryMatch = COOK_DIAGNOSTIC_PATTERN.exec(line);
|
|
2989
|
+
if (categoryMatch?.groups) {
|
|
2990
|
+
const { category, message, severity } = categoryMatch.groups;
|
|
2991
|
+
if (category === void 0 || message === void 0 || severity === void 0) return null;
|
|
2992
|
+
return {
|
|
2993
|
+
isError: severity === "Error",
|
|
2994
|
+
message: `${category}: ${message.trim()}`
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
const uatMatch = UAT_DIAGNOSTIC_PATTERN.exec(line);
|
|
2998
|
+
if (!uatMatch?.groups) return null;
|
|
2999
|
+
const { message, severity } = uatMatch.groups;
|
|
3000
|
+
if (message === void 0 || severity === void 0) return null;
|
|
3001
|
+
return {
|
|
3002
|
+
isError: severity === "ERROR",
|
|
3003
|
+
message: message.trim()
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
var CookDiagnosticCollector = class {
|
|
3007
|
+
maxEntriesPerSeverity;
|
|
3008
|
+
errors = [];
|
|
3009
|
+
warnings = [];
|
|
3010
|
+
seenErrors = /* @__PURE__ */ new Set();
|
|
3011
|
+
seenWarnings = /* @__PURE__ */ new Set();
|
|
3012
|
+
pending = "";
|
|
3013
|
+
truncatedFlag = false;
|
|
3014
|
+
constructor(maxEntriesPerSeverity) {
|
|
3015
|
+
this.maxEntriesPerSeverity = maxEntriesPerSeverity;
|
|
3016
|
+
}
|
|
3017
|
+
push(chunk) {
|
|
3018
|
+
const lines = (this.pending + chunk).split(/\r?\n/u);
|
|
3019
|
+
this.pending = lines.pop() ?? "";
|
|
3020
|
+
for (const line of lines) this.pushLine(line);
|
|
3021
|
+
}
|
|
3022
|
+
flush() {
|
|
3023
|
+
if (this.pending.length === 0) return;
|
|
3024
|
+
const remaining = this.pending;
|
|
3025
|
+
this.pending = "";
|
|
3026
|
+
this.pushLine(remaining);
|
|
3027
|
+
}
|
|
3028
|
+
snapshot() {
|
|
3029
|
+
return {
|
|
3030
|
+
errorCount: this.errors.length,
|
|
3031
|
+
errors: [...this.errors],
|
|
3032
|
+
truncated: this.truncatedFlag,
|
|
3033
|
+
warningCount: this.warnings.length,
|
|
3034
|
+
warnings: [...this.warnings]
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
pushLine(rawLine) {
|
|
3038
|
+
const matched = matchCookDiagnostic(rawLine);
|
|
3039
|
+
if (!matched) return;
|
|
3040
|
+
const diagnostic = { message: matched.message };
|
|
3041
|
+
const target = matched.isError ? this.errors : this.warnings;
|
|
3042
|
+
const seen = matched.isError ? this.seenErrors : this.seenWarnings;
|
|
3043
|
+
const key = diagnosticKey(diagnostic);
|
|
3044
|
+
if (seen.has(key)) return;
|
|
3045
|
+
seen.add(key);
|
|
3046
|
+
if (target.length >= this.maxEntriesPerSeverity) {
|
|
3047
|
+
this.truncatedFlag = true;
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
target.push(diagnostic);
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
2507
3053
|
function parseBuildDiagnostics(output) {
|
|
2508
3054
|
const errors = [];
|
|
2509
3055
|
const warnings = [];
|
|
@@ -2577,8 +3123,23 @@ const UNREAL_TARGET_PLATFORMS = [
|
|
|
2577
3123
|
"Mac",
|
|
2578
3124
|
"Linux"
|
|
2579
3125
|
];
|
|
3126
|
+
const UNREAL_COOK_TARGET_PLATFORMS = [
|
|
3127
|
+
"Win64",
|
|
3128
|
+
"Mac",
|
|
3129
|
+
"Linux",
|
|
3130
|
+
"LinuxArm64",
|
|
3131
|
+
"Android",
|
|
3132
|
+
"IOS",
|
|
3133
|
+
"TVOS",
|
|
3134
|
+
"VisionOS"
|
|
3135
|
+
];
|
|
3136
|
+
const COOK_COMMANDLET_PLATFORM_NAMES = { Win64: "Windows" };
|
|
3137
|
+
function cookCommandletTargetPlatform(platform) {
|
|
3138
|
+
return COOK_COMMANDLET_PLATFORM_NAMES[platform] ?? platform;
|
|
3139
|
+
}
|
|
2580
3140
|
const DEFAULT_PROCESS_TIMEOUT_MS = 18e5;
|
|
2581
3141
|
const MAX_PROCESS_TIMEOUT_MS = 72e5;
|
|
3142
|
+
const DEFAULT_PACKAGE_TIMEOUT_MS = 54e5;
|
|
2582
3143
|
async function defaultFileExists(filePath) {
|
|
2583
3144
|
try {
|
|
2584
3145
|
await access(filePath);
|
|
@@ -2593,6 +3154,26 @@ async function defaultListDirectory(dirPath) {
|
|
|
2593
3154
|
async function defaultReadFile(filePath) {
|
|
2594
3155
|
return await readFile(filePath, "utf8");
|
|
2595
3156
|
}
|
|
3157
|
+
async function defaultWriteFile(filePath, contents) {
|
|
3158
|
+
await writeFile(filePath, contents, "utf8");
|
|
3159
|
+
}
|
|
3160
|
+
function defaultReadTextLines(filePath) {
|
|
3161
|
+
return createInterface({
|
|
3162
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
3163
|
+
input: createReadStream(filePath, "utf8")
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
async function defaultCreateTempDirectory(prefix) {
|
|
3167
|
+
return await mkdtemp(path.join(tmpdir(), prefix));
|
|
3168
|
+
}
|
|
3169
|
+
async function defaultRemoveDirectory(dirPath) {
|
|
3170
|
+
await rm(dirPath, {
|
|
3171
|
+
force: true,
|
|
3172
|
+
maxRetries: 3,
|
|
3173
|
+
recursive: true,
|
|
3174
|
+
retryDelay: 50
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
2596
3177
|
var RollingTextBuffer = class {
|
|
2597
3178
|
maxBytes;
|
|
2598
3179
|
chunks = [];
|
|
@@ -2633,7 +3214,11 @@ const runProcess = (options) => {
|
|
|
2633
3214
|
return new Promise((resolve, reject) => {
|
|
2634
3215
|
const child = spawn(options.command, options.args, {
|
|
2635
3216
|
cwd: options.cwd,
|
|
2636
|
-
env: options.env
|
|
3217
|
+
env: options.env,
|
|
3218
|
+
...options.windowsVerbatimArguments === true && process.platform === "win32" ? {
|
|
3219
|
+
argv0: `"${options.command}"`,
|
|
3220
|
+
windowsVerbatimArguments: true
|
|
3221
|
+
} : {}
|
|
2637
3222
|
});
|
|
2638
3223
|
const stdoutBuffer = new RollingTextBuffer(maxOutputBytes);
|
|
2639
3224
|
const stderrBuffer = new RollingTextBuffer(maxOutputBytes);
|
|
@@ -2643,7 +3228,13 @@ const runProcess = (options) => {
|
|
|
2643
3228
|
const append = (buffer, text) => {
|
|
2644
3229
|
buffer.push(text);
|
|
2645
3230
|
combinedBuffer.push(text);
|
|
3231
|
+
options.onOutput?.(text);
|
|
3232
|
+
};
|
|
3233
|
+
const abortListener = () => {
|
|
3234
|
+
child.kill("SIGKILL");
|
|
2646
3235
|
};
|
|
3236
|
+
if (options.signal?.aborted === true) child.kill("SIGKILL");
|
|
3237
|
+
else options.signal?.addEventListener("abort", abortListener, { once: true });
|
|
2647
3238
|
child.stdout.setEncoding("utf8");
|
|
2648
3239
|
child.stderr.setEncoding("utf8");
|
|
2649
3240
|
child.stdout.on("data", (chunk) => {
|
|
@@ -2660,12 +3251,14 @@ const runProcess = (options) => {
|
|
|
2660
3251
|
if (settled) return;
|
|
2661
3252
|
settled = true;
|
|
2662
3253
|
clearTimeout(timeoutHandle);
|
|
3254
|
+
options.signal?.removeEventListener("abort", abortListener);
|
|
2663
3255
|
reject(new ProcessExecutionError(options.command, error.message, { cause: error }));
|
|
2664
3256
|
});
|
|
2665
3257
|
child.on("close", (code, signal) => {
|
|
2666
3258
|
if (settled) return;
|
|
2667
3259
|
settled = true;
|
|
2668
3260
|
clearTimeout(timeoutHandle);
|
|
3261
|
+
options.signal?.removeEventListener("abort", abortListener);
|
|
2669
3262
|
resolve({
|
|
2670
3263
|
combinedOutput: combinedBuffer.toString(),
|
|
2671
3264
|
durationMs: Date.now() - startedAt,
|
|
@@ -2682,12 +3275,16 @@ const runProcess = (options) => {
|
|
|
2682
3275
|
function createEngineToolchainContext(overrides = {}) {
|
|
2683
3276
|
return {
|
|
2684
3277
|
arch: overrides.arch ?? process.arch,
|
|
3278
|
+
createTempDirectory: overrides.createTempDirectory ?? defaultCreateTempDirectory,
|
|
2685
3279
|
env: overrides.env ?? process.env,
|
|
2686
3280
|
fileExists: overrides.fileExists ?? defaultFileExists,
|
|
2687
3281
|
listDirectory: overrides.listDirectory ?? defaultListDirectory,
|
|
2688
3282
|
platform: overrides.platform ?? process.platform,
|
|
2689
3283
|
readFile: overrides.readFile ?? defaultReadFile,
|
|
2690
|
-
|
|
3284
|
+
readTextLines: overrides.readTextLines ?? defaultReadTextLines,
|
|
3285
|
+
removeDirectory: overrides.removeDirectory ?? defaultRemoveDirectory,
|
|
3286
|
+
runProcess: overrides.runProcess ?? runProcess,
|
|
3287
|
+
writeFile: overrides.writeFile ?? defaultWriteFile
|
|
2691
3288
|
};
|
|
2692
3289
|
}
|
|
2693
3290
|
function defaultTargetPlatform(platform) {
|
|
@@ -2744,14 +3341,20 @@ async function resolveEngine(options) {
|
|
|
2744
3341
|
const { context } = options;
|
|
2745
3342
|
const ueInstallsDir = context.env.UE_INSTALLS_DIR?.trim();
|
|
2746
3343
|
if (!ueInstallsDir) throw new EngineToolchainError("UE_INSTALLS_DIR is not set; define it in .env (see .env.example).");
|
|
2747
|
-
const
|
|
3344
|
+
const explicitEngineVersion = options.engineVersion?.trim();
|
|
3345
|
+
const { uprojectPath } = options;
|
|
3346
|
+
let engineVersion;
|
|
3347
|
+
if (explicitEngineVersion) engineVersion = explicitEngineVersion;
|
|
3348
|
+
else if (uprojectPath === void 0) throw new EngineToolchainError("Engine resolution needs either an engineVersion or a project path.");
|
|
3349
|
+
else engineVersion = await readEngineAssociation(uprojectPath, context);
|
|
2748
3350
|
if (!/^\d+(?:\.\d+)*$/u.test(engineVersion)) throw new EngineToolchainError(`EngineAssociation "${engineVersion}" is not a numeric version; pass engineVersion explicitly (e.g. "5.7").`);
|
|
2749
3351
|
const engineRoot = path.join(ueInstallsDir, `UE_${engineVersion}`);
|
|
2750
3352
|
if (!await context.fileExists(engineRoot)) throw new EngineToolchainError(`Engine directory not found for UE version ${engineVersion}: ${engineRoot}`);
|
|
3353
|
+
const unrealBuildTool = await resolveUnrealBuildTool(engineRoot, context);
|
|
2751
3354
|
return {
|
|
2752
3355
|
engineRoot,
|
|
2753
3356
|
engineVersion,
|
|
2754
|
-
unrealBuildTool
|
|
3357
|
+
unrealBuildTool
|
|
2755
3358
|
};
|
|
2756
3359
|
}
|
|
2757
3360
|
async function resolveUnrealBuildTool(engineRoot, context) {
|
|
@@ -2771,8 +3374,72 @@ async function resolveUnrealBuildTool(engineRoot, context) {
|
|
|
2771
3374
|
command: bundledDotnet ?? "dotnet"
|
|
2772
3375
|
};
|
|
2773
3376
|
}
|
|
3377
|
+
async function resolveAutomationTool(engineRoot, context) {
|
|
3378
|
+
if (context.platform !== "win32") {
|
|
3379
|
+
const scriptPath = path.join(engineRoot, "Engine", "Build", "BatchFiles", "RunUAT.sh");
|
|
3380
|
+
if (!await context.fileExists(scriptPath)) throw new EngineToolchainError(`RunUAT.sh not found at ${scriptPath}`);
|
|
3381
|
+
return {
|
|
3382
|
+
args: [],
|
|
3383
|
+
command: scriptPath
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
const automationToolDirectory = path.join(engineRoot, "Engine", "Binaries", "DotNET", "AutomationTool");
|
|
3387
|
+
const automationToolDll = path.join(automationToolDirectory, "AutomationTool.dll");
|
|
3388
|
+
if (!await context.fileExists(automationToolDll)) throw new EngineToolchainError(`AutomationTool.dll not found at ${automationToolDll}`);
|
|
3389
|
+
const bundledDotnet = await resolveBundledDotnet(engineRoot, context);
|
|
3390
|
+
if (bundledDotnet === void 0) return {
|
|
3391
|
+
args: [automationToolDll],
|
|
3392
|
+
command: "dotnet",
|
|
3393
|
+
cwd: automationToolDirectory,
|
|
3394
|
+
env: buildAutomationToolEnvironment(context.env)
|
|
3395
|
+
};
|
|
3396
|
+
return {
|
|
3397
|
+
args: [automationToolDll],
|
|
3398
|
+
command: bundledDotnet,
|
|
3399
|
+
cwd: automationToolDirectory,
|
|
3400
|
+
env: buildAutomationToolEnvironment(context.env, path.dirname(bundledDotnet))
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
const VISUAL_STUDIO_ENVIRONMENT_KEYS = [
|
|
3404
|
+
"PkgDefApplicationConfigFile",
|
|
3405
|
+
"VSAPPIDDIR",
|
|
3406
|
+
"VSSKUEDITION",
|
|
3407
|
+
"VisualStudioDir",
|
|
3408
|
+
"VisualStudioEdition",
|
|
3409
|
+
"VisualStudioVersion"
|
|
3410
|
+
];
|
|
3411
|
+
function buildAutomationToolEnvironment(parentEnvironment, dotnetDirectory) {
|
|
3412
|
+
const environment = { ...parentEnvironment };
|
|
3413
|
+
const clearedKeys = new Set(VISUAL_STUDIO_ENVIRONMENT_KEYS.map((key) => key.toUpperCase()));
|
|
3414
|
+
for (const key of Object.keys(environment)) if (clearedKeys.has(key.toUpperCase())) delete environment[key];
|
|
3415
|
+
if (dotnetDirectory === void 0) return environment;
|
|
3416
|
+
const pathKey = Object.keys(environment).find((key) => key.toUpperCase() === "PATH") ?? "PATH";
|
|
3417
|
+
const inheritedPath = environment[pathKey];
|
|
3418
|
+
environment[pathKey] = inheritedPath === void 0 || inheritedPath.length === 0 ? dotnetDirectory : `${dotnetDirectory}${path.delimiter}${inheritedPath}`;
|
|
3419
|
+
environment.DOTNET_MULTILEVEL_LOOKUP = "0";
|
|
3420
|
+
environment.DOTNET_ROLL_FORWARD = "LatestMajor";
|
|
3421
|
+
environment.DOTNET_ROOT = dotnetDirectory;
|
|
3422
|
+
return environment;
|
|
3423
|
+
}
|
|
3424
|
+
function editorBinariesDirs(platform, arch) {
|
|
3425
|
+
if (platform === "win32") return ["Win64"];
|
|
3426
|
+
if (platform === "darwin") return ["Mac"];
|
|
3427
|
+
return arch === "arm64" ? ["LinuxArm64", "Linux"] : ["Linux"];
|
|
3428
|
+
}
|
|
3429
|
+
async function resolveUnrealEditorCmd(engineRoot, context) {
|
|
3430
|
+
const binaryName = context.platform === "win32" ? "UnrealEditor-Cmd.exe" : "UnrealEditor-Cmd";
|
|
3431
|
+
const candidates = editorBinariesDirs(context.platform, context.arch).map((binariesDir) => path.join(engineRoot, "Engine", "Binaries", binariesDir, binaryName));
|
|
3432
|
+
for (const candidate of candidates) if (await context.fileExists(candidate)) return candidate;
|
|
3433
|
+
throw new EngineToolchainError(`${binaryName} not found under ${path.join(engineRoot, "Engine", "Binaries")}`);
|
|
3434
|
+
}
|
|
3435
|
+
async function resolveUnrealInsights(engineRoot, context) {
|
|
3436
|
+
const binariesRoot = path.join(engineRoot, "Engine", "Binaries");
|
|
3437
|
+
const candidates = context.platform === "win32" ? [path.join(binariesRoot, "Win64", "UnrealInsights.exe")] : context.platform === "darwin" ? [path.join(binariesRoot, "Mac", "UnrealInsights.app", "Contents", "MacOS", "UnrealInsights"), path.join(binariesRoot, "Mac", "UnrealInsights")] : editorBinariesDirs(context.platform, context.arch).map((binariesDir) => path.join(binariesRoot, binariesDir, "UnrealInsights"));
|
|
3438
|
+
for (const candidate of candidates) if (await context.fileExists(candidate)) return candidate;
|
|
3439
|
+
return null;
|
|
3440
|
+
}
|
|
2774
3441
|
function dotnetHostPlatformDirs(platform, arch) {
|
|
2775
|
-
const osPrefix = platform === "darwin" ? "mac" : platform === "linux" ? "linux" : void 0;
|
|
3442
|
+
const osPrefix = platform === "darwin" ? "mac" : platform === "linux" ? "linux" : platform === "win32" ? "win" : void 0;
|
|
2776
3443
|
if (!osPrefix) return [];
|
|
2777
3444
|
const archDir = arch === "arm64" ? "arm64" : "x64";
|
|
2778
3445
|
const otherArchDir = archDir === "arm64" ? "x64" : "arm64";
|
|
@@ -2788,8 +3455,9 @@ async function resolveBundledDotnet(engineRoot, context) {
|
|
|
2788
3455
|
} catch {
|
|
2789
3456
|
return;
|
|
2790
3457
|
}
|
|
3458
|
+
const executableName = context.platform === "win32" ? "dotnet.exe" : "dotnet";
|
|
2791
3459
|
for (const versionDir of versionDirs) for (const platformDir of platformDirs) {
|
|
2792
|
-
const candidate = path.join(dotnetRoot, versionDir, platformDir,
|
|
3460
|
+
const candidate = path.join(dotnetRoot, versionDir, platformDir, executableName);
|
|
2793
3461
|
if (await context.fileExists(candidate)) return candidate;
|
|
2794
3462
|
}
|
|
2795
3463
|
}
|
|
@@ -3013,30 +3681,412 @@ function isPlainObject(value) {
|
|
|
3013
3681
|
function getErrorCode(error) {
|
|
3014
3682
|
if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
|
|
3015
3683
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
map: z.string().trim().min(1).max(512).optional().describe("Optional level/map path to load before play (e.g. /Game/Maps/MyLevel)."),
|
|
3023
|
-
mode: z.enum(BRIDGE_PIE_MODES).optional().describe("Play-in-Editor mode: 'selected_viewport', 'new_window' (default), or 'standalone'."),
|
|
3024
|
-
netMode: z.enum(BRIDGE_PIE_NET_MODES).optional().describe("Multiplayer net mode: 'standalone', 'listen_server', 'client', or 'dedicated_server'. Omit for single-player editor defaults."),
|
|
3025
|
-
numClients: z.number().int().min(1).max(16).optional().describe(`Number of client windows to launch (1-${String(16)}). With a server net mode, launches 1 server + N-1 clients.`),
|
|
3026
|
-
projectId: projectIdSchema,
|
|
3027
|
-
runUnderOneProcess: z.boolean().optional().describe("Run all multiplayer instances under a single process instead of separate processes.")
|
|
3028
|
-
};
|
|
3029
|
-
const getPieStatusInputSchema = {
|
|
3030
|
-
pieSessionId: z.string().trim().min(1).max(512).optional().describe("Optional pieSessionId returned by start_pie; omit to query the editor current PIE session."),
|
|
3031
|
-
projectId: projectIdSchema
|
|
3684
|
+
const PACKAGE_JOB_MAX_OUTPUT_BYTES = 2097152;
|
|
3685
|
+
var PackageJobCapacityError = class extends Error {
|
|
3686
|
+
constructor(message) {
|
|
3687
|
+
super(message);
|
|
3688
|
+
this.name = "PackageJobCapacityError";
|
|
3689
|
+
}
|
|
3032
3690
|
};
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3691
|
+
var PackageJobRegistry = class {
|
|
3692
|
+
jobs = /* @__PURE__ */ new Map();
|
|
3693
|
+
start(options) {
|
|
3694
|
+
this.evictFinishedJobs();
|
|
3695
|
+
const projectKey = projectComparisonKey(options.projectPath);
|
|
3696
|
+
this.assertCapacity(projectKey, options.projectPath);
|
|
3697
|
+
const record = {
|
|
3698
|
+
abortController: new AbortController(),
|
|
3699
|
+
args: [...options.args],
|
|
3700
|
+
cancelled: false,
|
|
3701
|
+
collector: new CookDiagnosticCollector(200),
|
|
3702
|
+
command: options.command,
|
|
3703
|
+
kind: options.kind,
|
|
3704
|
+
outputTail: "",
|
|
3705
|
+
outputTruncated: false,
|
|
3706
|
+
packageJobId: randomUUID().replaceAll("-", ""),
|
|
3707
|
+
projectKey,
|
|
3708
|
+
projectPath: options.projectPath,
|
|
3709
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3710
|
+
state: "running",
|
|
3711
|
+
timedOut: false
|
|
3712
|
+
};
|
|
3713
|
+
if (options.platform !== void 0) record.platform = options.platform;
|
|
3714
|
+
this.jobs.set(record.packageJobId, record);
|
|
3715
|
+
this.run(record, options);
|
|
3716
|
+
return toPackageJobStatus(record);
|
|
3717
|
+
}
|
|
3718
|
+
get(packageJobId) {
|
|
3719
|
+
const record = this.jobs.get(packageJobId);
|
|
3720
|
+
return record ? toPackageJobStatus(record) : null;
|
|
3721
|
+
}
|
|
3722
|
+
cancel(packageJobId) {
|
|
3723
|
+
const record = this.jobs.get(packageJobId);
|
|
3724
|
+
if (!record) return {
|
|
3725
|
+
cancelled: false,
|
|
3726
|
+
state: "unknown_job"
|
|
3727
|
+
};
|
|
3728
|
+
if (record.state !== "running") return {
|
|
3729
|
+
cancelled: false,
|
|
3730
|
+
state: record.state
|
|
3731
|
+
};
|
|
3732
|
+
record.cancelled = true;
|
|
3733
|
+
record.abortController.abort();
|
|
3734
|
+
return {
|
|
3735
|
+
cancelled: true,
|
|
3736
|
+
state: record.state
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
async run(record, options) {
|
|
3740
|
+
try {
|
|
3741
|
+
const result = await options.runProcess({
|
|
3742
|
+
args: options.args,
|
|
3743
|
+
command: options.command,
|
|
3744
|
+
cwd: options.cwd,
|
|
3745
|
+
...options.env ? { env: options.env } : {},
|
|
3746
|
+
maxOutputBytes: PACKAGE_JOB_MAX_OUTPUT_BYTES,
|
|
3747
|
+
onOutput: (chunk) => {
|
|
3748
|
+
record.collector.push(chunk);
|
|
3749
|
+
appendOutputTail(record, chunk);
|
|
3750
|
+
},
|
|
3751
|
+
signal: record.abortController.signal,
|
|
3752
|
+
timeoutMs: options.timeoutMs
|
|
3753
|
+
});
|
|
3754
|
+
record.collector.flush();
|
|
3755
|
+
record.durationMs = result.durationMs;
|
|
3756
|
+
record.exitCode = result.exitCode;
|
|
3757
|
+
record.outputTail = result.combinedOutput.slice(-8e3);
|
|
3758
|
+
record.outputTruncated = record.outputTruncated || result.truncated || result.combinedOutput.length > 8e3;
|
|
3759
|
+
record.timedOut = result.timedOut;
|
|
3760
|
+
record.state = resolveTerminalState(record, result.exitCode, result.timedOut);
|
|
3761
|
+
} catch (error) {
|
|
3762
|
+
record.collector.flush();
|
|
3763
|
+
record.failureReason = error instanceof Error ? error.message : String(error);
|
|
3764
|
+
record.state = record.cancelled ? "cancelled" : "failed";
|
|
3765
|
+
options.logger.error({
|
|
3766
|
+
error,
|
|
3767
|
+
kind: record.kind,
|
|
3768
|
+
packageJobId: record.packageJobId,
|
|
3769
|
+
projectPath: record.projectPath
|
|
3770
|
+
}, "Package job process failed to run");
|
|
3771
|
+
} finally {
|
|
3772
|
+
record.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
assertCapacity(projectKey, projectPath) {
|
|
3776
|
+
let activeCount = 0;
|
|
3777
|
+
let activeForProject = 0;
|
|
3778
|
+
for (const record of this.jobs.values()) {
|
|
3779
|
+
if (record.state !== "running") continue;
|
|
3780
|
+
activeCount += 1;
|
|
3781
|
+
if (record.projectKey === projectKey) activeForProject += 1;
|
|
3782
|
+
}
|
|
3783
|
+
if (activeForProject >= 1) throw new PackageJobCapacityError(`A cook, package or commandlet job is already running for '${projectPath}'. Wait for it, or cancel it with cancel_package_job.`);
|
|
3784
|
+
if (activeCount >= 4) throw new PackageJobCapacityError(`${String(4)} cook, package or commandlet jobs are already running. Wait for one to finish, or cancel one with cancel_package_job.`);
|
|
3785
|
+
}
|
|
3786
|
+
evictFinishedJobs() {
|
|
3787
|
+
if (this.jobs.size < 20) return;
|
|
3788
|
+
for (const [packageJobId, record] of this.jobs) {
|
|
3789
|
+
if (this.jobs.size < 20) return;
|
|
3790
|
+
if (record.state !== "running") this.jobs.delete(packageJobId);
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3036
3793
|
};
|
|
3037
|
-
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3794
|
+
function projectComparisonKey(projectPath) {
|
|
3795
|
+
const resolved = path.resolve(projectPath.trim());
|
|
3796
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
3797
|
+
}
|
|
3798
|
+
function appendOutputTail(record, chunk) {
|
|
3799
|
+
const outputTail = record.outputTail + chunk;
|
|
3800
|
+
if (outputTail.length > 8e3) record.outputTruncated = true;
|
|
3801
|
+
record.outputTail = outputTail.slice(-8e3);
|
|
3802
|
+
}
|
|
3803
|
+
function resolveTerminalState(record, exitCode, timedOut) {
|
|
3804
|
+
if (record.cancelled) return "cancelled";
|
|
3805
|
+
return exitCode === 0 && !timedOut ? "succeeded" : "failed";
|
|
3806
|
+
}
|
|
3807
|
+
function toPackageJobStatus(record) {
|
|
3808
|
+
const diagnostics = record.collector.snapshot();
|
|
3809
|
+
const status = {
|
|
3810
|
+
args: [...record.args],
|
|
3811
|
+
command: record.command,
|
|
3812
|
+
diagnosticsTruncated: diagnostics.truncated,
|
|
3813
|
+
errorCount: diagnostics.errorCount,
|
|
3814
|
+
errors: diagnostics.errors,
|
|
3815
|
+
kind: record.kind,
|
|
3816
|
+
outputTail: record.outputTail,
|
|
3817
|
+
outputTruncated: record.outputTruncated,
|
|
3818
|
+
packageJobId: record.packageJobId,
|
|
3819
|
+
projectPath: record.projectPath,
|
|
3820
|
+
startedAt: record.startedAt,
|
|
3821
|
+
state: record.state,
|
|
3822
|
+
timedOut: record.timedOut,
|
|
3823
|
+
warningCount: diagnostics.warningCount,
|
|
3824
|
+
warnings: diagnostics.warnings
|
|
3825
|
+
};
|
|
3826
|
+
if (record.durationMs !== void 0) status.durationMs = record.durationMs;
|
|
3827
|
+
if (record.exitCode !== void 0) status.exitCode = record.exitCode;
|
|
3828
|
+
if (record.failureReason !== void 0) status.failureReason = record.failureReason;
|
|
3829
|
+
if (record.finishedAt !== void 0) status.finishedAt = record.finishedAt;
|
|
3830
|
+
if (record.platform !== void 0) status.platform = record.platform;
|
|
3831
|
+
return status;
|
|
3832
|
+
}
|
|
3833
|
+
//#endregion
|
|
3834
|
+
//#region src/traceAnalysis.ts
|
|
3835
|
+
const SECONDS_TO_MS = 1e3;
|
|
3836
|
+
const FRAME_ROOT_TIMER_NAMES = /* @__PURE__ */ new Set(["FEngineLoop::Tick"]);
|
|
3837
|
+
function parseCsvRows(text) {
|
|
3838
|
+
const [headerLine, ...dataLines] = splitCsvLines(text);
|
|
3839
|
+
if (headerLine === void 0) return [];
|
|
3840
|
+
const headers = parseCsvLine(headerLine);
|
|
3841
|
+
return dataLines.map((line) => {
|
|
3842
|
+
const values = parseCsvLine(line);
|
|
3843
|
+
const row = {};
|
|
3844
|
+
headers.forEach((header, index) => {
|
|
3845
|
+
row[header] = values[index] ?? "";
|
|
3846
|
+
});
|
|
3847
|
+
return row;
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
function splitCsvLines(text) {
|
|
3851
|
+
return text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
3852
|
+
}
|
|
3853
|
+
function parseCsvLine(line) {
|
|
3854
|
+
const values = [];
|
|
3855
|
+
let current = "";
|
|
3856
|
+
let inQuotes = false;
|
|
3857
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
3858
|
+
const character = line[index];
|
|
3859
|
+
if (character === "\"") {
|
|
3860
|
+
if (inQuotes && line[index + 1] === "\"") {
|
|
3861
|
+
current += "\"";
|
|
3862
|
+
index += 1;
|
|
3863
|
+
continue;
|
|
3864
|
+
}
|
|
3865
|
+
inQuotes = !inQuotes;
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
if (character === "," && !inQuotes) {
|
|
3869
|
+
values.push(current.trim());
|
|
3870
|
+
current = "";
|
|
3871
|
+
continue;
|
|
3872
|
+
}
|
|
3873
|
+
current += character ?? "";
|
|
3874
|
+
}
|
|
3875
|
+
values.push(current.trim());
|
|
3876
|
+
return values;
|
|
3877
|
+
}
|
|
3878
|
+
function findColumn(row, ...candidates) {
|
|
3879
|
+
const headers = Object.keys(row);
|
|
3880
|
+
for (const candidate of candidates) {
|
|
3881
|
+
const match = headers.find((header) => header.toLowerCase() === candidate.toLowerCase());
|
|
3882
|
+
if (match !== void 0) return match;
|
|
3883
|
+
}
|
|
3884
|
+
for (const candidate of candidates) {
|
|
3885
|
+
const match = headers.find((header) => header.toLowerCase().includes(candidate.toLowerCase()));
|
|
3886
|
+
if (match !== void 0) return match;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
function readNumber(value) {
|
|
3890
|
+
if (value === void 0 || value.length === 0) return;
|
|
3891
|
+
const parsed = Number.parseFloat(value);
|
|
3892
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3893
|
+
}
|
|
3894
|
+
function summarizeTimerStatistics(rows, maxScopes) {
|
|
3895
|
+
const [firstRow] = rows;
|
|
3896
|
+
if (firstRow === void 0) return {
|
|
3897
|
+
byExclusiveTime: [],
|
|
3898
|
+
byInclusiveTime: [],
|
|
3899
|
+
totalScopeCount: 0,
|
|
3900
|
+
truncated: false
|
|
3901
|
+
};
|
|
3902
|
+
const nameColumn = findColumn(firstRow, "Name", "TimerName");
|
|
3903
|
+
const inclusiveColumn = findColumn(firstRow, "Incl", "TotalInclusiveTime", "InclusiveTime");
|
|
3904
|
+
const exclusiveColumn = findColumn(firstRow, "Excl", "TotalExclusiveTime", "ExclusiveTime");
|
|
3905
|
+
const countColumn = findColumn(firstRow, "Count", "InstanceCount");
|
|
3906
|
+
if (nameColumn === void 0 || inclusiveColumn === void 0 || exclusiveColumn === void 0) return {
|
|
3907
|
+
byExclusiveTime: [],
|
|
3908
|
+
byInclusiveTime: [],
|
|
3909
|
+
totalScopeCount: 0,
|
|
3910
|
+
truncated: false
|
|
3911
|
+
};
|
|
3912
|
+
const scopes = [];
|
|
3913
|
+
for (const row of rows) {
|
|
3914
|
+
const name = row[nameColumn];
|
|
3915
|
+
const inclusive = readNumber(row[inclusiveColumn]);
|
|
3916
|
+
const exclusive = readNumber(row[exclusiveColumn]);
|
|
3917
|
+
if (name === void 0 || name.length === 0 || inclusive === void 0 || exclusive === void 0) continue;
|
|
3918
|
+
const scope = {
|
|
3919
|
+
exclusiveTimeMs: exclusive * SECONDS_TO_MS,
|
|
3920
|
+
inclusiveTimeMs: inclusive * SECONDS_TO_MS,
|
|
3921
|
+
name
|
|
3922
|
+
};
|
|
3923
|
+
const count = countColumn === void 0 ? void 0 : readNumber(row[countColumn]);
|
|
3924
|
+
if (count !== void 0) scope.count = count;
|
|
3925
|
+
scopes.push(scope);
|
|
3926
|
+
}
|
|
3927
|
+
const byInclusiveTime = [...scopes].sort((left, right) => right.inclusiveTimeMs - left.inclusiveTimeMs);
|
|
3928
|
+
return {
|
|
3929
|
+
byExclusiveTime: [...scopes].sort((left, right) => right.exclusiveTimeMs - left.exclusiveTimeMs).slice(0, maxScopes),
|
|
3930
|
+
byInclusiveTime: byInclusiveTime.slice(0, maxScopes),
|
|
3931
|
+
totalScopeCount: scopes.length,
|
|
3932
|
+
truncated: scopes.length > maxScopes
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
function isGpuThread(threadName) {
|
|
3936
|
+
return threadName.toUpperCase().includes("GPU");
|
|
3937
|
+
}
|
|
3938
|
+
async function summarizeTimingEventStream(lines, options) {
|
|
3939
|
+
let headers;
|
|
3940
|
+
let threadIndex = -1;
|
|
3941
|
+
let timerIndex = -1;
|
|
3942
|
+
let durationIndex = -1;
|
|
3943
|
+
let startIndex = -1;
|
|
3944
|
+
let depthIndex = -1;
|
|
3945
|
+
let cpuTimeMs = 0;
|
|
3946
|
+
let gpuTimeMs = 0;
|
|
3947
|
+
const frameDurationsMs = [];
|
|
3948
|
+
const worstHitches = [];
|
|
3949
|
+
let hitchCount = 0;
|
|
3950
|
+
for await (const rawLine of lines) {
|
|
3951
|
+
const line = rawLine.trim();
|
|
3952
|
+
if (line.length === 0) continue;
|
|
3953
|
+
const values = parseCsvLine(line);
|
|
3954
|
+
if (headers === void 0) {
|
|
3955
|
+
headers = values;
|
|
3956
|
+
const headerRow = Object.fromEntries(headers.map((header) => [header, ""]));
|
|
3957
|
+
threadIndex = columnIndex(headers, findColumn(headerRow, "ThreadName", "Thread", "TrackName"));
|
|
3958
|
+
timerIndex = columnIndex(headers, findColumn(headerRow, "TimerName"));
|
|
3959
|
+
durationIndex = columnIndex(headers, findColumn(headerRow, "Duration", "InclusiveTime"));
|
|
3960
|
+
startIndex = columnIndex(headers, findColumn(headerRow, "StartTime", "Start"));
|
|
3961
|
+
depthIndex = columnIndex(headers, findColumn(headerRow, "Depth"));
|
|
3962
|
+
if (threadIndex < 0 || durationIndex < 0) break;
|
|
3963
|
+
continue;
|
|
3964
|
+
}
|
|
3965
|
+
const duration = readNumber(values[durationIndex]);
|
|
3966
|
+
const threadName = values[threadIndex];
|
|
3967
|
+
if (duration === void 0 || threadName === void 0) continue;
|
|
3968
|
+
if ((depthIndex < 0 ? 0 : readNumber(values[depthIndex]) ?? 0) !== 0) continue;
|
|
3969
|
+
const durationMs = duration * SECONDS_TO_MS;
|
|
3970
|
+
if (isGpuThread(threadName)) {
|
|
3971
|
+
gpuTimeMs += durationMs;
|
|
3972
|
+
continue;
|
|
3973
|
+
}
|
|
3974
|
+
cpuTimeMs += durationMs;
|
|
3975
|
+
if (timerIndex < 0 || !FRAME_ROOT_TIMER_NAMES.has(values[timerIndex] ?? "")) continue;
|
|
3976
|
+
frameDurationsMs.push(durationMs);
|
|
3977
|
+
if (durationMs < options.hitchThresholdMs) continue;
|
|
3978
|
+
hitchCount += 1;
|
|
3979
|
+
retainWorstHitch(worstHitches, options.maxHitches, {
|
|
3980
|
+
durationMs,
|
|
3981
|
+
startTimeMs: (startIndex < 0 ? 0 : readNumber(values[startIndex]) ?? 0) * SECONDS_TO_MS
|
|
3982
|
+
});
|
|
3983
|
+
}
|
|
3984
|
+
return {
|
|
3985
|
+
frames: summarizeFrameDurations(frameDurationsMs, worstHitches, hitchCount, options),
|
|
3986
|
+
threads: {
|
|
3987
|
+
cpuTimeMs,
|
|
3988
|
+
gpuTimeMs
|
|
3989
|
+
}
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
function retainWorstHitch(worstHitches, maxHitches, hitch) {
|
|
3993
|
+
if (worstHitches.length >= maxHitches) {
|
|
3994
|
+
const weakest = worstHitches[worstHitches.length - 1];
|
|
3995
|
+
if (weakest === void 0 || hitch.durationMs <= weakest.durationMs) return;
|
|
3996
|
+
worstHitches.pop();
|
|
3997
|
+
}
|
|
3998
|
+
const insertAt = worstHitches.findIndex((candidate) => hitch.durationMs > candidate.durationMs);
|
|
3999
|
+
worstHitches.splice(insertAt < 0 ? worstHitches.length : insertAt, 0, hitch);
|
|
4000
|
+
}
|
|
4001
|
+
function columnIndex(headers, header) {
|
|
4002
|
+
return header === void 0 ? -1 : headers.indexOf(header);
|
|
4003
|
+
}
|
|
4004
|
+
function summarizeFrameDurations(frameDurationsMs, worstHitches, hitchCount, options) {
|
|
4005
|
+
if (frameDurationsMs.length === 0) return {
|
|
4006
|
+
frameCount: 0,
|
|
4007
|
+
hitches: [],
|
|
4008
|
+
hitchesTruncated: false
|
|
4009
|
+
};
|
|
4010
|
+
const sortedDurations = [...frameDurationsMs].sort((left, right) => left - right);
|
|
4011
|
+
return {
|
|
4012
|
+
frameCount: frameDurationsMs.length,
|
|
4013
|
+
hitches: worstHitches,
|
|
4014
|
+
hitchesTruncated: hitchCount > options.maxHitches,
|
|
4015
|
+
percentilesMs: {
|
|
4016
|
+
p50: percentile(sortedDurations, .5),
|
|
4017
|
+
p90: percentile(sortedDurations, .9),
|
|
4018
|
+
p99: percentile(sortedDurations, .99)
|
|
4019
|
+
},
|
|
4020
|
+
worstFrameMs: sortedDurations[sortedDurations.length - 1]
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
function percentile(sortedValues, fraction) {
|
|
4024
|
+
if (sortedValues.length === 0) return 0;
|
|
4025
|
+
return sortedValues[Math.min(sortedValues.length - 1, Math.max(0, Math.ceil(fraction * sortedValues.length) - 1))] ?? 0;
|
|
4026
|
+
}
|
|
4027
|
+
//#endregion
|
|
4028
|
+
//#region src/mcpTools.ts
|
|
4029
|
+
const EDITOR_WORK_TIMEOUT_MS = 1e4;
|
|
4030
|
+
const WAIT_FOR_JOB_KINDS = [
|
|
4031
|
+
"compile",
|
|
4032
|
+
"create_class",
|
|
4033
|
+
"pie",
|
|
4034
|
+
"test_run"
|
|
4035
|
+
];
|
|
4036
|
+
const WAIT_FOR_JOB_MAX_TIMEOUT_MS = 6e5;
|
|
4037
|
+
const WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS = {
|
|
4038
|
+
compile: 12e4,
|
|
4039
|
+
create_class: 6e4,
|
|
4040
|
+
pie: 3e4,
|
|
4041
|
+
test_run: 3e5
|
|
4042
|
+
};
|
|
4043
|
+
const WAIT_FOR_JOB_POLL_CADENCE_MS = {
|
|
4044
|
+
compile: {
|
|
4045
|
+
capMs: 2e3,
|
|
4046
|
+
initialMs: 250
|
|
4047
|
+
},
|
|
4048
|
+
create_class: {
|
|
4049
|
+
capMs: 2e3,
|
|
4050
|
+
initialMs: 250
|
|
4051
|
+
},
|
|
4052
|
+
pie: {
|
|
4053
|
+
capMs: 2e3,
|
|
4054
|
+
initialMs: 250
|
|
4055
|
+
},
|
|
4056
|
+
test_run: {
|
|
4057
|
+
capMs: 5e3,
|
|
4058
|
+
initialMs: 1e3
|
|
4059
|
+
}
|
|
4060
|
+
};
|
|
4061
|
+
const ANALYZE_TRACE_DEFAULT_TIMEOUT_MS = 6e5;
|
|
4062
|
+
const ANALYZE_TRACE_DEFAULT_HITCH_THRESHOLD_MS = 33.4;
|
|
4063
|
+
const ANALYZE_TRACE_MAX_RESPONSE_BYTES = 32768;
|
|
4064
|
+
const BRIDGE_LOOPBACK_HOST = "127.0.0.1";
|
|
4065
|
+
const getProjectIdInputSchema = { projectPath: z.string().trim().min(1) };
|
|
4066
|
+
const pingProjectInputSchema = { projectId: projectIdSchema };
|
|
4067
|
+
const listRegisteredProjectsInputSchema = {
|
|
4068
|
+
maxResults: z.number().int().min(1).max(200).optional().describe(`Maximum registrations to return (1-${String(200)}, default ${String(50)}). Sets truncated when more exist.`),
|
|
4069
|
+
verify: z.boolean().optional().describe("Ping every returned registration once, concurrently, and annotate it with reachable/reason. Costs a bridge round trip per entry; omit for the cheap registry-only listing.")
|
|
4070
|
+
};
|
|
4071
|
+
const startPieInputSchema = {
|
|
4072
|
+
map: z.string().trim().min(1).max(512).optional().describe("Optional level/map path to load before play (e.g. /Game/Maps/MyLevel)."),
|
|
4073
|
+
mode: z.enum(BRIDGE_PIE_MODES).optional().describe("Play-in-Editor mode: 'selected_viewport', 'new_window' (default), or 'standalone'."),
|
|
4074
|
+
netMode: z.enum(BRIDGE_PIE_NET_MODES).optional().describe("Multiplayer net mode: 'standalone', 'listen_server', 'client', or 'dedicated_server'. Omit for single-player editor defaults."),
|
|
4075
|
+
numClients: z.number().int().min(1).max(16).optional().describe(`Number of client windows to launch (1-${String(16)}). With a server net mode, launches 1 server + N-1 clients.`),
|
|
4076
|
+
projectId: projectIdSchema,
|
|
4077
|
+
runUnderOneProcess: z.boolean().optional().describe("Run all multiplayer instances under a single process instead of separate processes.")
|
|
4078
|
+
};
|
|
4079
|
+
const getPieStatusInputSchema = {
|
|
4080
|
+
pieSessionId: z.string().trim().min(1).max(512).optional().describe("Optional pieSessionId returned by start_pie; omit to query the editor current PIE session."),
|
|
4081
|
+
projectId: projectIdSchema
|
|
4082
|
+
};
|
|
4083
|
+
const stopPieInputSchema = {
|
|
4084
|
+
pieSessionId: z.string().trim().min(1).max(512).optional().describe("Optional pieSessionId returned by start_pie; omit to stop the editor current PIE session."),
|
|
4085
|
+
projectId: projectIdSchema
|
|
4086
|
+
};
|
|
4087
|
+
const listLaunchDevicesInputSchema = { projectId: projectIdSchema };
|
|
4088
|
+
const launchOnDeviceInputSchema = {
|
|
4089
|
+
deviceId: z.string().trim().min(1).max(512).describe("Target device id from list_launch_devices (e.g. IOS@MyiPhone, Android@serial)."),
|
|
3040
4090
|
map: z.string().trim().min(1).max(512).optional().describe("Optional level/map path to load on the device before play (e.g. /Game/Maps/MyLevel)."),
|
|
3041
4091
|
projectId: projectIdSchema
|
|
3042
4092
|
};
|
|
@@ -3106,6 +4156,12 @@ const getTestRunStatusInputSchema = {
|
|
|
3106
4156
|
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the automation run to poll."),
|
|
3107
4157
|
testRunId: z.string().trim().min(1).describe("The testRunId returned by run_automation_tests for the run to poll.")
|
|
3108
4158
|
};
|
|
4159
|
+
const waitForJobInputSchema = {
|
|
4160
|
+
jobId: z.string().trim().min(1).max(512).describe("Job identifier to wait on: compileJobId (compile), createClassJobId (create_class), testRunId (test_run), or pieSessionId (pie)."),
|
|
4161
|
+
kind: z.enum(WAIT_FOR_JOB_KINDS).describe("Which async job the id belongs to. Required — job ids are bare GUIDs and carry no type tag, so the kind cannot be inferred."),
|
|
4162
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the job."),
|
|
4163
|
+
timeoutMs: z.number().int().min(1).optional().describe(`Wall-clock budget for the wait, clamped to ${String(WAIT_FOR_JOB_MAX_TIMEOUT_MS)}ms. Defaults per kind: compile ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.compile)}ms, create_class ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.create_class)}ms, pie ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.pie)}ms, test_run ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.test_run)}ms.`)
|
|
4164
|
+
};
|
|
3109
4165
|
const getEditorStateInputSchema = { projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should report its state.") };
|
|
3110
4166
|
const saveDirtyPackagesInputSchema = {
|
|
3111
4167
|
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should save its changes."),
|
|
@@ -3147,6 +4203,45 @@ const cleanBuildInputSchema = {
|
|
|
3147
4203
|
target: targetSchema,
|
|
3148
4204
|
timeoutMs: buildTimeoutSchema
|
|
3149
4205
|
};
|
|
4206
|
+
const packageTimeoutSchema = z.number().int().min(1e3).max(MAX_PROCESS_TIMEOUT_MS).optional().describe(`Hard timeout in milliseconds before the cook/package process is killed (default ${String(DEFAULT_PACKAGE_TIMEOUT_MS)}, max ${String(MAX_PROCESS_TIMEOUT_MS)}).`);
|
|
4207
|
+
const cookPlatformSchema = z.enum(UNREAL_COOK_TARGET_PLATFORMS).describe("Cook/package target platform (Win64, Mac, Linux, LinuxArm64, Android, IOS, TVOS, VisionOS).");
|
|
4208
|
+
const packageForceSchema = z.boolean().default(false).describe("Run even if this project's UnrealEditor is registered and running. Default false refuses, because a second editor process contends for the same Saved/, Intermediate/ and DDC paths.");
|
|
4209
|
+
const cookContentInputSchema = {
|
|
4210
|
+
engineVersion: engineVersionSchema,
|
|
4211
|
+
force: packageForceSchema,
|
|
4212
|
+
maps: z.array(z.string().trim().min(1).max(512)).max(64).optional().describe("Explicit maps to cook (e.g. /Game/Maps/MyLevel). Omit to cook everything (-CookAll)."),
|
|
4213
|
+
platform: cookPlatformSchema,
|
|
4214
|
+
projectPath: projectPathSchema,
|
|
4215
|
+
timeoutMs: packageTimeoutSchema
|
|
4216
|
+
};
|
|
4217
|
+
const packageProjectInputSchema = {
|
|
4218
|
+
archiveDirectory: z.string().trim().min(1).describe("Directory the staged build is archived into (-archivedirectory)."),
|
|
4219
|
+
configuration: configurationSchema,
|
|
4220
|
+
engineVersion: engineVersionSchema,
|
|
4221
|
+
force: packageForceSchema,
|
|
4222
|
+
platform: cookPlatformSchema,
|
|
4223
|
+
projectPath: projectPathSchema,
|
|
4224
|
+
timeoutMs: packageTimeoutSchema
|
|
4225
|
+
};
|
|
4226
|
+
const runEditorCommandletInputSchema = {
|
|
4227
|
+
args: z.array(z.string().trim().min(1).max(512)).max(64).optional().describe("Commandlet arguments as separate argv entries (never one string to be split). -run= and -Project= are set by the tool and are rejected here."),
|
|
4228
|
+
commandletName: z.string().trim().min(1).max(128).describe("Commandlet class name, identifier characters only (e.g. ResavePackages, FixupRedirects, DataValidation)."),
|
|
4229
|
+
engineVersion: engineVersionSchema,
|
|
4230
|
+
force: packageForceSchema,
|
|
4231
|
+
projectPath: projectPathSchema,
|
|
4232
|
+
timeoutMs: packageTimeoutSchema
|
|
4233
|
+
};
|
|
4234
|
+
const analyzeTraceInputSchema = {
|
|
4235
|
+
engineVersion: engineVersionSchema,
|
|
4236
|
+
hitchThresholdMs: z.number().min(1).max(1e4).optional().describe(`Frame duration at or above which a frame counts as a hitch (default ${String(ANALYZE_TRACE_DEFAULT_HITCH_THRESHOLD_MS)}ms, about two frames at 60Hz).`),
|
|
4237
|
+
maxHitches: z.number().int().min(1).max(50).optional().describe(`Maximum worst-hitch entries to return (default ${String(10)}, max ${String(50)}).`),
|
|
4238
|
+
maxScopes: z.number().int().min(1).max(100).optional().describe(`Maximum timing scopes to return per ranking (default ${String(20)}, max ${String(100)}).`),
|
|
4239
|
+
projectPath: z.string().trim().min(1).optional().describe("Project whose EngineAssociation selects the analyzer engine. Omit when engineVersion is given."),
|
|
4240
|
+
timeoutMs: z.number().int().min(1e3).max(MAX_PROCESS_TIMEOUT_MS).optional().describe(`Hard timeout in milliseconds for the analyzer run (default ${String(ANALYZE_TRACE_DEFAULT_TIMEOUT_MS)}).`),
|
|
4241
|
+
tracePath: z.string().trim().min(1).describe("Path to the .utrace file produced by stop_insights_trace.")
|
|
4242
|
+
};
|
|
4243
|
+
const getPackageStatusInputSchema = { packageJobId: z.string().trim().min(1).max(512).describe("packageJobId returned by cook_content, package_project, or run_editor_commandlet.") };
|
|
4244
|
+
const cancelPackageJobInputSchema = { packageJobId: z.string().trim().min(1).max(512).describe("packageJobId of the in-flight job to abort.") };
|
|
3150
4245
|
const runClangTidyInputSchema = {
|
|
3151
4246
|
checks: z.string().trim().min(1).optional().describe("clang-tidy --checks expression (e.g. \"-*,bugprone-*\"); omit to use the .clang-tidy config."),
|
|
3152
4247
|
compileCommandsDir: z.string().trim().min(1).describe("Directory containing the compile_commands.json produced by the compile-commands sync."),
|
|
@@ -3219,6 +4314,313 @@ async function buildTarget(input, dependencies, context = createEngineToolchainC
|
|
|
3219
4314
|
warnings: diagnostics.warnings
|
|
3220
4315
|
};
|
|
3221
4316
|
}
|
|
4317
|
+
async function analyzeTrace(input, dependencies, context = createEngineToolchainContext()) {
|
|
4318
|
+
const tracePath = path.resolve(input.tracePath);
|
|
4319
|
+
if (!await context.fileExists(tracePath)) throw new EngineToolchainError(`Trace file not found or unreadable: ${tracePath}`);
|
|
4320
|
+
const engine = await resolveEngineForTrace(input, context);
|
|
4321
|
+
const analyzerPath = await resolveUnrealInsights(engine.engineRoot, context);
|
|
4322
|
+
const hitchThresholdMs = input.hitchThresholdMs ?? 33.4;
|
|
4323
|
+
if (!analyzerPath) return {
|
|
4324
|
+
durationMs: 0,
|
|
4325
|
+
engineVersion: engine.engineVersion,
|
|
4326
|
+
hitchThresholdMs,
|
|
4327
|
+
hitches: [],
|
|
4328
|
+
reason: `UnrealInsights was not found under ${path.join(engine.engineRoot, "Engine", "Binaries")}; the installed UE ${engine.engineVersion} does not ship the trace analyzer for this host.`,
|
|
4329
|
+
state: "unsupported",
|
|
4330
|
+
topScopesByExclusiveTime: [],
|
|
4331
|
+
topScopesByInclusiveTime: [],
|
|
4332
|
+
tracePath,
|
|
4333
|
+
truncated: []
|
|
4334
|
+
};
|
|
4335
|
+
const timeoutMs = input.timeoutMs ?? 6e5;
|
|
4336
|
+
const exportDirectory = await context.createTempDirectory("uemcp-trace-");
|
|
4337
|
+
const timerStatisticsPath = path.join(exportDirectory, "timer-statistics.csv");
|
|
4338
|
+
const timingEventsPath = path.join(exportDirectory, "timing-events.csv");
|
|
4339
|
+
const commandFilePath = path.join(exportDirectory, "export.rsp");
|
|
4340
|
+
try {
|
|
4341
|
+
await context.writeFile(commandFilePath, [`TimingInsights.ExportTimerStatistics ${quoteConsoleArgument(timerStatisticsPath)}`, `TimingInsights.ExportTimingEvents ${quoteConsoleArgument(timingEventsPath)} -columns=ThreadName,TimerName,StartTime,Duration,Depth`].join("\n"));
|
|
4342
|
+
dependencies.logger.info({
|
|
4343
|
+
engineVersion: engine.engineVersion,
|
|
4344
|
+
tracePath
|
|
4345
|
+
}, "Running Unreal Insights trace analysis");
|
|
4346
|
+
const isWindowsHost = context.platform === "win32";
|
|
4347
|
+
const quoteValue = (value) => isWindowsHost ? `"${value}"` : value;
|
|
4348
|
+
const run = await context.runProcess({
|
|
4349
|
+
args: [
|
|
4350
|
+
`-OpenTraceFile=${quoteValue(tracePath)}`,
|
|
4351
|
+
`-ExecOnAnalysisCompleteCmd=${quoteValue(`@=${commandFilePath}`)}`,
|
|
4352
|
+
"-AutoQuit",
|
|
4353
|
+
"-NoUI"
|
|
4354
|
+
],
|
|
4355
|
+
command: analyzerPath,
|
|
4356
|
+
timeoutMs,
|
|
4357
|
+
windowsVerbatimArguments: isWindowsHost
|
|
4358
|
+
});
|
|
4359
|
+
if (run.timedOut) throw new EngineToolchainError(`Unreal Insights did not finish analyzing ${tracePath} within ${String(timeoutMs)}ms; raise timeoutMs or trim the trace.`);
|
|
4360
|
+
if (run.exitCode !== 0) throw new EngineToolchainError(`Unreal Insights exited with code ${String(run.exitCode)} while analyzing ${tracePath}: ${run.combinedOutput.slice(-1e3).trim()}`);
|
|
4361
|
+
const scopeSummaries = summarizeTimerStatistics(await readCsvRows(timerStatisticsPath, context), input.maxScopes ?? 20);
|
|
4362
|
+
const timingEvents = await summarizeTimingEventStream(await context.fileExists(timingEventsPath) ? context.readTextLines(timingEventsPath) : emptyTextLines(), {
|
|
4363
|
+
hitchThresholdMs,
|
|
4364
|
+
maxHitches: input.maxHitches ?? 10
|
|
4365
|
+
});
|
|
4366
|
+
const frames = timingEvents.frames;
|
|
4367
|
+
const threadTimes = timingEvents.threads;
|
|
4368
|
+
const truncated = [];
|
|
4369
|
+
if (scopeSummaries.truncated) truncated.push("topScopes");
|
|
4370
|
+
if (frames.hitchesTruncated) truncated.push("hitches");
|
|
4371
|
+
const result = {
|
|
4372
|
+
analyzerPath,
|
|
4373
|
+
cpuTimeMs: threadTimes.cpuTimeMs,
|
|
4374
|
+
durationMs: run.durationMs,
|
|
4375
|
+
engineVersion: engine.engineVersion,
|
|
4376
|
+
exitCode: run.exitCode,
|
|
4377
|
+
frameCount: frames.frameCount,
|
|
4378
|
+
gpuTimeMs: threadTimes.gpuTimeMs,
|
|
4379
|
+
hitchThresholdMs,
|
|
4380
|
+
hitches: frames.hitches,
|
|
4381
|
+
state: "analyzed",
|
|
4382
|
+
topScopesByExclusiveTime: scopeSummaries.byExclusiveTime,
|
|
4383
|
+
topScopesByInclusiveTime: scopeSummaries.byInclusiveTime,
|
|
4384
|
+
totalScopeCount: scopeSummaries.totalScopeCount,
|
|
4385
|
+
tracePath,
|
|
4386
|
+
truncated
|
|
4387
|
+
};
|
|
4388
|
+
if (frames.percentilesMs) result.framePercentilesMs = frames.percentilesMs;
|
|
4389
|
+
if (frames.worstFrameMs !== void 0) result.worstFrameMs = frames.worstFrameMs;
|
|
4390
|
+
if (frames.frameCount === 0) result.reason = "The trace contains no FEngineLoop::Tick scopes, so frame percentiles and hitches are unavailable; capture with the cpu channel while the engine is ticking.";
|
|
4391
|
+
return boundAnalyzeTraceResult(result);
|
|
4392
|
+
} finally {
|
|
4393
|
+
try {
|
|
4394
|
+
await context.removeDirectory(exportDirectory);
|
|
4395
|
+
} catch (error) {
|
|
4396
|
+
dependencies.logger.warn({
|
|
4397
|
+
error,
|
|
4398
|
+
exportDirectory
|
|
4399
|
+
}, "Failed to remove the Unreal Insights export directory");
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
async function resolveEngineForTrace(input, context) {
|
|
4404
|
+
if (input.projectPath === void 0) return await resolveEngine({
|
|
4405
|
+
context,
|
|
4406
|
+
engineVersion: input.engineVersion
|
|
4407
|
+
});
|
|
4408
|
+
const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
|
|
4409
|
+
return await resolveEngine({
|
|
4410
|
+
context,
|
|
4411
|
+
engineVersion: input.engineVersion,
|
|
4412
|
+
uprojectPath
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
function quoteConsoleArgument(value) {
|
|
4416
|
+
return `"${value.replace(/\\/gu, "\\\\")}"`;
|
|
4417
|
+
}
|
|
4418
|
+
async function* emptyTextLines() {}
|
|
4419
|
+
async function readCsvRows(filePath, context) {
|
|
4420
|
+
if (!await context.fileExists(filePath)) return [];
|
|
4421
|
+
return parseCsvRows(await context.readFile(filePath));
|
|
4422
|
+
}
|
|
4423
|
+
function boundAnalyzeTraceResult(result) {
|
|
4424
|
+
const bounded = {
|
|
4425
|
+
...result,
|
|
4426
|
+
truncated: [...result.truncated]
|
|
4427
|
+
};
|
|
4428
|
+
while (JSON.stringify(bounded).length > ANALYZE_TRACE_MAX_RESPONSE_BYTES) {
|
|
4429
|
+
const longestListLength = Math.max(bounded.topScopesByInclusiveTime.length, bounded.topScopesByExclusiveTime.length, bounded.hitches.length);
|
|
4430
|
+
if (longestListLength <= 1) {
|
|
4431
|
+
addTruncationReason(bounded, "responseBytesExceeded");
|
|
4432
|
+
return bounded;
|
|
4433
|
+
}
|
|
4434
|
+
if (bounded.topScopesByInclusiveTime.length === longestListLength) {
|
|
4435
|
+
bounded.topScopesByInclusiveTime = bounded.topScopesByInclusiveTime.slice(0, longestListLength - 1);
|
|
4436
|
+
bounded.topScopesByExclusiveTime = bounded.topScopesByExclusiveTime.slice(0, longestListLength - 1);
|
|
4437
|
+
addTruncationReason(bounded, "topScopes");
|
|
4438
|
+
continue;
|
|
4439
|
+
}
|
|
4440
|
+
bounded.hitches = bounded.hitches.slice(0, bounded.hitches.length - 1);
|
|
4441
|
+
addTruncationReason(bounded, "hitches");
|
|
4442
|
+
}
|
|
4443
|
+
return bounded;
|
|
4444
|
+
}
|
|
4445
|
+
function addTruncationReason(result, reason) {
|
|
4446
|
+
if (!result.truncated.includes(reason)) result.truncated.push(reason);
|
|
4447
|
+
}
|
|
4448
|
+
const COMMANDLET_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/u;
|
|
4449
|
+
const ENGINE_CONTROLLED_COMMANDLET_SWITCHES = ["-run=", "-project="];
|
|
4450
|
+
const defaultPackageJobRegistry = new PackageJobRegistry();
|
|
4451
|
+
function resolvePackageJobRegistry(dependencies) {
|
|
4452
|
+
return dependencies.packageJobRegistry ?? defaultPackageJobRegistry;
|
|
4453
|
+
}
|
|
4454
|
+
async function cookContent(input, dependencies, context = createEngineToolchainContext()) {
|
|
4455
|
+
if (!input.force) await assertEditorNotRunning(input.projectPath, dependencies);
|
|
4456
|
+
const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
|
|
4457
|
+
const engine = await resolveEngine({
|
|
4458
|
+
context,
|
|
4459
|
+
engineVersion: input.engineVersion,
|
|
4460
|
+
uprojectPath
|
|
4461
|
+
});
|
|
4462
|
+
const editorCmd = await resolveUnrealEditorCmd(engine.engineRoot, context);
|
|
4463
|
+
const mapArgs = input.maps && input.maps.length > 0 ? [`-Map=${input.maps.join("+")}`] : ["-CookAll"];
|
|
4464
|
+
const args = [
|
|
4465
|
+
uprojectPath,
|
|
4466
|
+
"-run=Cook",
|
|
4467
|
+
`-TargetPlatform=${cookCommandletTargetPlatform(input.platform)}`,
|
|
4468
|
+
...mapArgs,
|
|
4469
|
+
"-Unversioned",
|
|
4470
|
+
"-unattended",
|
|
4471
|
+
"-nopause",
|
|
4472
|
+
"-nosplash",
|
|
4473
|
+
"-stdout"
|
|
4474
|
+
];
|
|
4475
|
+
dependencies.logger.info({
|
|
4476
|
+
engineVersion: engine.engineVersion,
|
|
4477
|
+
platform: input.platform,
|
|
4478
|
+
projectPath: uprojectPath
|
|
4479
|
+
}, "Starting Unreal cook");
|
|
4480
|
+
return toStartPackageJobResult(resolvePackageJobRegistry(dependencies).start({
|
|
4481
|
+
args,
|
|
4482
|
+
command: editorCmd,
|
|
4483
|
+
cwd: engine.engineRoot,
|
|
4484
|
+
kind: "cook",
|
|
4485
|
+
logger: dependencies.logger,
|
|
4486
|
+
platform: input.platform,
|
|
4487
|
+
projectPath: uprojectPath,
|
|
4488
|
+
runProcess: context.runProcess,
|
|
4489
|
+
timeoutMs: input.timeoutMs ?? 54e5
|
|
4490
|
+
}), engine.engineVersion);
|
|
4491
|
+
}
|
|
4492
|
+
async function packageProject(input, dependencies, context = createEngineToolchainContext()) {
|
|
4493
|
+
if (!input.force) await assertEditorNotRunning(input.projectPath, dependencies);
|
|
4494
|
+
const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
|
|
4495
|
+
const engine = await resolveEngine({
|
|
4496
|
+
context,
|
|
4497
|
+
engineVersion: input.engineVersion,
|
|
4498
|
+
uprojectPath
|
|
4499
|
+
});
|
|
4500
|
+
const automationTool = await resolveAutomationTool(engine.engineRoot, context);
|
|
4501
|
+
const args = [
|
|
4502
|
+
...automationTool.args,
|
|
4503
|
+
"BuildCookRun",
|
|
4504
|
+
`-project=${uprojectPath}`,
|
|
4505
|
+
"-noP4",
|
|
4506
|
+
"-utf8output",
|
|
4507
|
+
"-nocompileeditor",
|
|
4508
|
+
"-unattended",
|
|
4509
|
+
`-platform=${input.platform}`,
|
|
4510
|
+
`-clientconfig=${input.configuration}`,
|
|
4511
|
+
"-build",
|
|
4512
|
+
"-cook",
|
|
4513
|
+
"-stage",
|
|
4514
|
+
"-pak",
|
|
4515
|
+
"-archive",
|
|
4516
|
+
`-archivedirectory=${path.resolve(input.archiveDirectory)}`
|
|
4517
|
+
];
|
|
4518
|
+
dependencies.logger.info({
|
|
4519
|
+
configuration: input.configuration,
|
|
4520
|
+
engineVersion: engine.engineVersion,
|
|
4521
|
+
platform: input.platform,
|
|
4522
|
+
projectPath: uprojectPath
|
|
4523
|
+
}, "Starting AutomationTool BuildCookRun");
|
|
4524
|
+
return toStartPackageJobResult(resolvePackageJobRegistry(dependencies).start({
|
|
4525
|
+
args,
|
|
4526
|
+
command: automationTool.command,
|
|
4527
|
+
cwd: automationTool.cwd ?? engine.engineRoot,
|
|
4528
|
+
...automationTool.env ? { env: automationTool.env } : {},
|
|
4529
|
+
kind: "package",
|
|
4530
|
+
logger: dependencies.logger,
|
|
4531
|
+
platform: input.platform,
|
|
4532
|
+
projectPath: uprojectPath,
|
|
4533
|
+
runProcess: context.runProcess,
|
|
4534
|
+
timeoutMs: input.timeoutMs ?? 54e5
|
|
4535
|
+
}), engine.engineVersion);
|
|
4536
|
+
}
|
|
4537
|
+
async function runEditorCommandlet(input, dependencies, context = createEngineToolchainContext()) {
|
|
4538
|
+
assertValidCommandletName(input.commandletName);
|
|
4539
|
+
const commandletArgs = input.args ?? [];
|
|
4540
|
+
for (const argument of commandletArgs) assertCommandletArgumentAllowed(argument);
|
|
4541
|
+
if (!input.force) await assertEditorNotRunning(input.projectPath, dependencies);
|
|
4542
|
+
const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
|
|
4543
|
+
const engine = await resolveEngine({
|
|
4544
|
+
context,
|
|
4545
|
+
engineVersion: input.engineVersion,
|
|
4546
|
+
uprojectPath
|
|
4547
|
+
});
|
|
4548
|
+
const editorCmd = await resolveUnrealEditorCmd(engine.engineRoot, context);
|
|
4549
|
+
const args = [
|
|
4550
|
+
uprojectPath,
|
|
4551
|
+
`-run=${input.commandletName}`,
|
|
4552
|
+
...commandletArgs,
|
|
4553
|
+
"-unattended",
|
|
4554
|
+
"-nopause",
|
|
4555
|
+
"-nosplash",
|
|
4556
|
+
"-stdout"
|
|
4557
|
+
];
|
|
4558
|
+
dependencies.logger.info({
|
|
4559
|
+
commandletName: input.commandletName,
|
|
4560
|
+
engineVersion: engine.engineVersion,
|
|
4561
|
+
projectPath: uprojectPath
|
|
4562
|
+
}, "Starting Unreal editor commandlet");
|
|
4563
|
+
return toStartPackageJobResult(resolvePackageJobRegistry(dependencies).start({
|
|
4564
|
+
args,
|
|
4565
|
+
command: editorCmd,
|
|
4566
|
+
cwd: engine.engineRoot,
|
|
4567
|
+
kind: "commandlet",
|
|
4568
|
+
logger: dependencies.logger,
|
|
4569
|
+
projectPath: uprojectPath,
|
|
4570
|
+
runProcess: context.runProcess,
|
|
4571
|
+
timeoutMs: input.timeoutMs ?? 54e5
|
|
4572
|
+
}), engine.engineVersion);
|
|
4573
|
+
}
|
|
4574
|
+
function getPackageStatus(input, dependencies) {
|
|
4575
|
+
const status = resolvePackageJobRegistry(dependencies).get(input.packageJobId);
|
|
4576
|
+
if (status) return status;
|
|
4577
|
+
return {
|
|
4578
|
+
args: [],
|
|
4579
|
+
command: "",
|
|
4580
|
+
diagnosticsTruncated: false,
|
|
4581
|
+
errorCount: 0,
|
|
4582
|
+
errors: [],
|
|
4583
|
+
outputTail: "",
|
|
4584
|
+
outputTruncated: false,
|
|
4585
|
+
packageJobId: input.packageJobId,
|
|
4586
|
+
projectPath: "",
|
|
4587
|
+
startedAt: "",
|
|
4588
|
+
state: "unknown_job",
|
|
4589
|
+
timedOut: false,
|
|
4590
|
+
warningCount: 0,
|
|
4591
|
+
warnings: []
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
function cancelPackageJob(input, dependencies) {
|
|
4595
|
+
const outcome = resolvePackageJobRegistry(dependencies).cancel(input.packageJobId);
|
|
4596
|
+
return {
|
|
4597
|
+
cancelled: outcome.cancelled,
|
|
4598
|
+
packageJobId: input.packageJobId,
|
|
4599
|
+
state: outcome.state
|
|
4600
|
+
};
|
|
4601
|
+
}
|
|
4602
|
+
function assertValidCommandletName(commandletName) {
|
|
4603
|
+
if (!COMMANDLET_NAME_PATTERN.test(commandletName)) throw new EngineToolchainError(`Commandlet name '${commandletName}' is invalid; use identifier characters only (e.g. ResavePackages).`);
|
|
4604
|
+
}
|
|
4605
|
+
function assertCommandletArgumentAllowed(argument) {
|
|
4606
|
+
const normalized = argument.toLowerCase();
|
|
4607
|
+
for (const reserved of ENGINE_CONTROLLED_COMMANDLET_SWITCHES) if (normalized.startsWith(reserved)) throw new EngineToolchainError(`Commandlet argument '${argument}' overrides the engine switch ${reserved} that run_editor_commandlet sets itself.`);
|
|
4608
|
+
if (/[\0\r\n]/u.test(argument)) throw new EngineToolchainError("Commandlet arguments cannot contain control characters.");
|
|
4609
|
+
}
|
|
4610
|
+
function toStartPackageJobResult(status, engineVersion) {
|
|
4611
|
+
const result = {
|
|
4612
|
+
args: status.args,
|
|
4613
|
+
command: status.command,
|
|
4614
|
+
engineVersion,
|
|
4615
|
+
kind: status.kind,
|
|
4616
|
+
packageJobId: status.packageJobId,
|
|
4617
|
+
projectPath: status.projectPath,
|
|
4618
|
+
startedAt: status.startedAt,
|
|
4619
|
+
state: status.state
|
|
4620
|
+
};
|
|
4621
|
+
if (status.platform !== void 0) result.platform = status.platform;
|
|
4622
|
+
return result;
|
|
4623
|
+
}
|
|
3222
4624
|
async function generateProjectFiles(input, dependencies, context = createEngineToolchainContext()) {
|
|
3223
4625
|
const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
|
|
3224
4626
|
const engine = await resolveEngine({
|
|
@@ -3420,6 +4822,30 @@ const execConsoleCommandInputSchema = {
|
|
|
3420
4822
|
command: z.string().trim().min(1).max(BRIDGE_EXEC_CONSOLE_MAX_COMMAND_CHARS).describe("Console command line to execute, exactly as it would be typed into the editor console (e.g. stat fps)."),
|
|
3421
4823
|
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should execute the console command.")
|
|
3422
4824
|
};
|
|
4825
|
+
const configFileInputSchema = z.enum(BRIDGE_CONFIG_FILES).describe("Which engine ini to read or write: Engine (DefaultEngine.ini), Game, Input, Editor, or EditorPerProjectUserSettings.");
|
|
4826
|
+
const configSectionInputSchema = bridgeGetConfigValueRequestSchema.shape.section.describe("Ini section without brackets, e.g. /Script/Engine.RendererSettings or SystemSettings.");
|
|
4827
|
+
const configKeyInputSchema = bridgeGetConfigValueRequestSchema.shape.key.describe("Key inside the section.");
|
|
4828
|
+
const getConfigValueInputSchema = {
|
|
4829
|
+
file: configFileInputSchema,
|
|
4830
|
+
key: configKeyInputSchema,
|
|
4831
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the config."),
|
|
4832
|
+
section: configSectionInputSchema
|
|
4833
|
+
};
|
|
4834
|
+
const setConfigValueInputSchema = {
|
|
4835
|
+
file: configFileInputSchema,
|
|
4836
|
+
key: configKeyInputSchema,
|
|
4837
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should write the config."),
|
|
4838
|
+
scope: z.enum(BRIDGE_CONFIG_WRITE_SCOPES).optional().describe("Where to write: 'project_default' (Config/Default<File>.ini, the default and the checked-in setting) or 'saved_user' (Saved/Config user override)."),
|
|
4839
|
+
section: configSectionInputSchema,
|
|
4840
|
+
value: bridgeSetConfigValueRequestSchema.shape.value.describe("Value to write verbatim. An empty string writes a present-but-empty value, which is distinct from removing the key.")
|
|
4841
|
+
};
|
|
4842
|
+
const listConfigSectionInputSchema = {
|
|
4843
|
+
cursor: bridgeListConfigSectionRequestSchema.shape.cursor.describe("Offset into the stable-ordered key list (default 0). Pass the previous response nextCursor for the next page."),
|
|
4844
|
+
file: configFileInputSchema,
|
|
4845
|
+
maxResults: bridgeListConfigSectionRequestSchema.shape.maxResults.describe(`Maximum key/value pairs per page (1-${String(200)}, default ${String(100)}).`),
|
|
4846
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the config."),
|
|
4847
|
+
section: configSectionInputSchema
|
|
4848
|
+
};
|
|
3423
4849
|
const getStatSnapshotInputSchema = { projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should sample its performance counters.") };
|
|
3424
4850
|
const startInsightsTraceInputSchema = {
|
|
3425
4851
|
channels: z.array(z.string().trim().min(1).max(64)).max(32).optional().describe("Unreal Insights channels to record (e.g. cpu, gpu, frame, loadtime). Omit to record the engine's default channel set."),
|
|
@@ -3453,6 +4879,40 @@ const findBlueprintReferencesInputSchema = {
|
|
|
3453
4879
|
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should query its Asset Registry."),
|
|
3454
4880
|
referenceKinds: bridgeFindBlueprintReferencesRequestSchema.shape.referenceKinds.describe("Restrict results to 'subclass' and/or 'module_dependency'; omit to return both.")
|
|
3455
4881
|
};
|
|
4882
|
+
const getBlueprintSummaryInputSchema = {
|
|
4883
|
+
blueprintPath: bridgeGetBlueprintSummaryRequestSchema.shape.blueprintPath.describe("Package name or object path of the Blueprint (e.g. /Game/BP/BP_Player or /Game/BP/BP_Player.BP_Player)."),
|
|
4884
|
+
componentCursor: bridgeGetBlueprintSummaryRequestSchema.shape.componentCursor.describe("Offset into the component list (default 0). Pass the previous response nextComponentCursor to fetch the next page."),
|
|
4885
|
+
functionCursor: bridgeGetBlueprintSummaryRequestSchema.shape.functionCursor.describe("Offset into the function/event/macro list (default 0). Pass the previous response nextFunctionCursor to fetch the next page."),
|
|
4886
|
+
graphCursor: bridgeGetBlueprintSummaryRequestSchema.shape.graphCursor.describe("Offset into the graph inventory (default 0). Pass the previous response nextGraphCursor to fetch the next page."),
|
|
4887
|
+
loadIfNeeded: bridgeGetBlueprintSummaryRequestSchema.shape.loadIfNeeded.describe("Load the Blueprint if it is not already in memory (default false). With false, an unloaded asset answers from Asset Registry metadata only — parent class and generated class, with loaded=false and empty member lists."),
|
|
4888
|
+
maxResults: bridgeGetBlueprintSummaryRequestSchema.shape.maxResults.default(25).describe("Maximum entries per list per page (1-200, default 25). The *Count fields stay authoritative."),
|
|
4889
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should read the Blueprint."),
|
|
4890
|
+
variableCursor: bridgeGetBlueprintSummaryRequestSchema.shape.variableCursor.describe("Offset into the variable list (default 0). Pass the previous response nextVariableCursor to fetch the next page.")
|
|
4891
|
+
};
|
|
4892
|
+
const getBlueprintGraphInputSchema = {
|
|
4893
|
+
blueprintPath: bridgeGetBlueprintGraphRequestSchema.shape.blueprintPath.describe("Package name or object path of the Blueprint that owns the graph."),
|
|
4894
|
+
graphName: bridgeGetBlueprintGraphRequestSchema.shape.graphName.describe("Name of the graph to dump, as reported by get_blueprint_summary (e.g. 'EventGraph', 'UserConstructionScript', a function name)."),
|
|
4895
|
+
loadIfNeeded: bridgeGetBlueprintGraphRequestSchema.shape.loadIfNeeded.describe("Load the Blueprint if it is not already in memory (default false). Graph contents require the asset in memory, so an unloaded Blueprint fails with blueprint_not_loaded unless this is true."),
|
|
4896
|
+
maxNodes: bridgeGetBlueprintGraphRequestSchema.shape.maxNodes.default(25).describe("Maximum nodes to return per page (1-200, default 25). nodeCount still reports the whole graph."),
|
|
4897
|
+
nodeCursor: bridgeGetBlueprintGraphRequestSchema.shape.nodeCursor.describe("Offset into the GUID-ordered node list (default 0). Pass the previous response nextNodeCursor to fetch the next page."),
|
|
4898
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should read the Blueprint.")
|
|
4899
|
+
};
|
|
4900
|
+
const getBlueprintCompileErrorsInputSchema = {
|
|
4901
|
+
blueprintPaths: bridgeGetBlueprintCompileErrorsRequestSchema.shape.blueprintPaths.describe("Blueprints to report on, as package names or object paths (1-32 entries)."),
|
|
4902
|
+
cursor: bridgeGetBlueprintCompileErrorsRequestSchema.shape.cursor.describe("Offset into the path-ordered result list (default 0). Pass the previous response nextCursor to fetch the next page."),
|
|
4903
|
+
maxResults: bridgeGetBlueprintCompileErrorsRequestSchema.shape.maxResults.default(50).describe("Maximum results to return per page (1-100, default 50)."),
|
|
4904
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should report Blueprint status.")
|
|
4905
|
+
};
|
|
4906
|
+
const compileBlueprintInputSchema = {
|
|
4907
|
+
blueprintPaths: bridgeCompileBlueprintRequestSchema.shape.blueprintPaths.describe("Blueprints to compile, as package names or object paths (1-32 entries). Each is loaded if needed."),
|
|
4908
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should compile the Blueprints.")
|
|
4909
|
+
};
|
|
4910
|
+
const getBlueprintCompileStatusInputSchema = {
|
|
4911
|
+
compileJobId: bridgeGetBlueprintCompileStatusRequestSchema.shape.compileJobId.describe("Job id returned by compile_blueprint."),
|
|
4912
|
+
cursor: bridgeGetBlueprintCompileStatusRequestSchema.shape.cursor.describe("Offset into the result list (default 0). Pass the previous response nextCursor to fetch the next page."),
|
|
4913
|
+
maxResults: bridgeGetBlueprintCompileStatusRequestSchema.shape.maxResults.default(50).describe("Maximum results to return per page (1-100, default 50)."),
|
|
4914
|
+
projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the compile job.")
|
|
4915
|
+
};
|
|
3456
4916
|
function registerMcpTools(server, dependencies) {
|
|
3457
4917
|
server.registerTool("get_project_id", {
|
|
3458
4918
|
description: "Resolve the canonical Unreal ProjectID from a project path by reading Config/DefaultGame.ini.",
|
|
@@ -3482,6 +4942,16 @@ function registerMcpTools(server, dependencies) {
|
|
|
3482
4942
|
}, async ({ projectId }) => {
|
|
3483
4943
|
return toToolSuccess(await pingProject(projectId, dependencies));
|
|
3484
4944
|
});
|
|
4945
|
+
server.registerTool("list_registered_projects", {
|
|
4946
|
+
description: "List every Unreal Editor currently registered with this server (projectId, projectName, projectPath, host, port, pid, registeredAt, lastSeenAt). Call this first to discover which editors are running and which projectId to pass to the other tools. Reads the registry only and performs no network I/O unless verify is true, which pings each entry once and adds reachable/reason. Never evicts a registration — use ping_project for that.",
|
|
4947
|
+
inputSchema: listRegisteredProjectsInputSchema,
|
|
4948
|
+
title: "List Registered Projects"
|
|
4949
|
+
}, async ({ maxResults, verify }) => {
|
|
4950
|
+
return toToolSuccess(await listRegisteredProjects({
|
|
4951
|
+
maxResults,
|
|
4952
|
+
verify
|
|
4953
|
+
}, dependencies));
|
|
4954
|
+
});
|
|
3485
4955
|
server.registerTool("trigger_live_compile", {
|
|
3486
4956
|
description: "Trigger an Unreal Live Coding recompile of patched modules for a projectId; tail stream_output_log (LogLiveCoding) for build output.",
|
|
3487
4957
|
inputSchema: triggerLiveCompileInputSchema,
|
|
@@ -3538,6 +5008,48 @@ function registerMcpTools(server, dependencies) {
|
|
|
3538
5008
|
}, async (input) => {
|
|
3539
5009
|
return toToolSuccess(await cleanBuild(input, dependencies));
|
|
3540
5010
|
});
|
|
5011
|
+
server.registerTool("cook_content", {
|
|
5012
|
+
description: "Start a full offline cook (UnrealEditor-Cmd -run=Cook) for a target platform and return a packageJobId immediately; poll it with get_package_status. Unlike launch_on_device (cook-on-the-fly) this proves the content actually cooks, surfacing missing references, unfixed redirectors and platform shader failures. Runs for minutes to tens of minutes. Refuses while the project editor is running unless force is true. Server-side; no running editor required.",
|
|
5013
|
+
inputSchema: cookContentInputSchema,
|
|
5014
|
+
title: "Cook Content"
|
|
5015
|
+
}, async (input) => {
|
|
5016
|
+
return toToolSuccess(await cookContent(input, dependencies));
|
|
5017
|
+
});
|
|
5018
|
+
server.registerTool("package_project", {
|
|
5019
|
+
description: "Start a full package via RunUAT BuildCookRun (build + cook + stage + pak + archive) for a platform, configuration and archive directory, returning a packageJobId immediately; poll it with get_package_status. Runs for minutes to tens of minutes. Refuses while the project editor is running unless force is true. Server-side; no running editor required.",
|
|
5020
|
+
inputSchema: packageProjectInputSchema,
|
|
5021
|
+
title: "Package Project"
|
|
5022
|
+
}, async (input) => {
|
|
5023
|
+
return toToolSuccess(await packageProject(input, dependencies));
|
|
5024
|
+
});
|
|
5025
|
+
server.registerTool("run_editor_commandlet", {
|
|
5026
|
+
description: "Start an editor commandlet (UnrealEditor-Cmd -run=<name>) with an argv array and return a packageJobId; poll it with get_package_status. Unlocks the maintenance commandlets you reach for after a failed cook — ResavePackages, FixupRedirects, DataValidation. The commandlet name must be identifier characters only and arguments are passed as separate argv entries without a shell; -run= and -Project= are set by the tool and rejected as arguments. A commandlet can modify or delete project assets, so it refuses to run while the project editor is running unless force is true.",
|
|
5027
|
+
inputSchema: runEditorCommandletInputSchema,
|
|
5028
|
+
title: "Run Editor Commandlet"
|
|
5029
|
+
}, async (input) => {
|
|
5030
|
+
return toToolSuccess(await runEditorCommandlet(input, dependencies));
|
|
5031
|
+
});
|
|
5032
|
+
server.registerTool("analyze_trace", {
|
|
5033
|
+
description: "Analyze a completed .utrace (the path stop_insights_trace returns) headlessly with the engine's Unreal Insights analyzer and return a bounded, readable summary: frame-time p50/p90/p99, the worst N hitches with timestamps, a CPU vs GPU time split, and top-N timing scopes by inclusive and exclusive time. Memory and LLM-tag totals are deliberately not reported — they need a separate analysis pass. Resolve the engine with engineVersion or projectPath. Every list is top-N and the response is byte-capped, with truncated naming what was shortened. Returns state 'unsupported' when the installed engine ships no Insights analyzer for this host.",
|
|
5034
|
+
inputSchema: analyzeTraceInputSchema,
|
|
5035
|
+
title: "Analyze Trace"
|
|
5036
|
+
}, async (input) => {
|
|
5037
|
+
return toToolSuccess(await analyzeTrace(input, dependencies));
|
|
5038
|
+
});
|
|
5039
|
+
server.registerTool("get_package_status", {
|
|
5040
|
+
description: "Poll a job started by cook_content, package_project, or run_editor_commandlet. Returns state ('running', 'succeeded', 'failed', 'cancelled', or 'unknown_job' for an unknown/evicted id), structured cook/UAT error and warning diagnostics, truncation flags, exit code, and timing once finished.",
|
|
5041
|
+
inputSchema: getPackageStatusInputSchema,
|
|
5042
|
+
title: "Get Package Status"
|
|
5043
|
+
}, async ({ packageJobId }) => {
|
|
5044
|
+
return toToolSuccess(getPackageStatus({ packageJobId }, dependencies));
|
|
5045
|
+
});
|
|
5046
|
+
server.registerTool("cancel_package_job", {
|
|
5047
|
+
description: "Abort an in-flight cook, package, or commandlet job by its packageJobId. Returns cancelled=true (state cancelled) when a live job was killed, or cancelled=false with the job current state (or unknown_job) otherwise.",
|
|
5048
|
+
inputSchema: cancelPackageJobInputSchema,
|
|
5049
|
+
title: "Cancel Package Job"
|
|
5050
|
+
}, async ({ packageJobId }) => {
|
|
5051
|
+
return toToolSuccess(cancelPackageJob({ packageJobId }, dependencies));
|
|
5052
|
+
});
|
|
3541
5053
|
server.registerTool("run_clang_tidy", {
|
|
3542
5054
|
description: "Run clang-tidy over a project's synced compile_commands.json and return structured findings (file, line, column, check, severity, message), catching issues before a compile. Report only; does not apply fixes. Server-side; does not require a running editor.",
|
|
3543
5055
|
inputSchema: runClangTidyInputSchema,
|
|
@@ -3718,6 +5230,18 @@ function registerMcpTools(server, dependencies) {
|
|
|
3718
5230
|
testRunId
|
|
3719
5231
|
}, dependencies));
|
|
3720
5232
|
});
|
|
5233
|
+
server.registerTool("wait_for_job", {
|
|
5234
|
+
description: `Block server-side until an async job reaches a terminal state, then return the payload its get_*_status tool would have returned under 'status'. Use instead of a poll loop after trigger_live_compile, trigger_hot_reload (kind 'compile'), create_cpp_class ('create_class'), run_automation_tests ('test_run'), or start_pie ('pie'). kind is required — job ids carry no type tag. outcome is 'completed', 'timed_out' (the last observed status is still returned; call again or poll), or 'unknown_job' (unknown/expired/cancelled id, returned immediately). timeoutMs defaults per kind (compile ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.compile)}ms, create_class ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.create_class)}ms, pie ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.pie)}ms, test_run ${String(WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS.test_run)}ms) and is clamped to ${String(WAIT_FOR_JOB_MAX_TIMEOUT_MS)}ms, so a long automation run may need more than one call. For kind 'test_run' the returned status.results is the first page only — page the rest with get_test_run_status and nextCursor. For kind 'pie' only 'starting' keeps waiting; 'running', 'stopped' and 'none' are all terminal.`,
|
|
5235
|
+
inputSchema: waitForJobInputSchema,
|
|
5236
|
+
title: "Wait For Job"
|
|
5237
|
+
}, async ({ jobId, kind, projectId, timeoutMs }) => {
|
|
5238
|
+
return toToolSuccess(await waitForJob({
|
|
5239
|
+
jobId,
|
|
5240
|
+
kind,
|
|
5241
|
+
projectId,
|
|
5242
|
+
timeoutMs
|
|
5243
|
+
}, dependencies));
|
|
5244
|
+
});
|
|
3721
5245
|
server.registerTool("cancel_test_run", {
|
|
3722
5246
|
description: "Abort an in-progress Unreal automation run for a project by its testRunId. Returns cancelled=true (state cancelled) when a live run was aborted, or cancelled=false (state not_found) when the run is unknown or already finished.",
|
|
3723
5247
|
inputSchema: cancelTestRunInputSchema,
|
|
@@ -3745,6 +5269,45 @@ function registerMcpTools(server, dependencies) {
|
|
|
3745
5269
|
projectId
|
|
3746
5270
|
}, dependencies));
|
|
3747
5271
|
});
|
|
5272
|
+
server.registerTool("get_config_value", {
|
|
5273
|
+
description: "Read one project config value through the running editor's config system, which resolves the full Base*/Default*/Saved hierarchy. Returns the resolved value, the ini it came from, and resolvedFrom ('engine_base', 'project_default', 'saved_user'), and reports present=false for an unset key so an absent value is distinguishable from a present-but-empty one. file is a closed set of known ini names, not a path.",
|
|
5274
|
+
inputSchema: getConfigValueInputSchema,
|
|
5275
|
+
title: "Get Config Value"
|
|
5276
|
+
}, async ({ file, key, projectId, section }) => {
|
|
5277
|
+
return toToolSuccess(await getConfigValue({
|
|
5278
|
+
file,
|
|
5279
|
+
key,
|
|
5280
|
+
projectId,
|
|
5281
|
+
section
|
|
5282
|
+
}, dependencies));
|
|
5283
|
+
});
|
|
5284
|
+
server.registerTool("set_config_value", {
|
|
5285
|
+
description: "Write one project config value through the running editor's config system and flush it to disk, so the ini is normalised the way the engine expects and the live editor picks the change up without a restart. Writes Config/Default<File>.ini by default, or the Saved/Config user override with scope 'saved_user'. Returns the previous value as the editor resolved it across the whole hierarchy, which is not necessarily what the target layer held: if it came from an engine or default layer, writing it back leaves the key materialised in the layer this tool wrote. Requires a registered editor: a raw file write behind a running editor is clobbered when the editor saves its own config.",
|
|
5286
|
+
inputSchema: setConfigValueInputSchema,
|
|
5287
|
+
title: "Set Config Value"
|
|
5288
|
+
}, async ({ file, key, projectId, scope, section, value }) => {
|
|
5289
|
+
return toToolSuccess(await setConfigValue({
|
|
5290
|
+
file,
|
|
5291
|
+
key,
|
|
5292
|
+
projectId,
|
|
5293
|
+
scope,
|
|
5294
|
+
section,
|
|
5295
|
+
value
|
|
5296
|
+
}, dependencies));
|
|
5297
|
+
});
|
|
5298
|
+
server.registerTool("list_config_section", {
|
|
5299
|
+
description: "Dump one config section as key/value pairs as the running editor resolves it. Paged: pass the previous response nextCursor for the next page; totalCount reports the full key count.",
|
|
5300
|
+
inputSchema: listConfigSectionInputSchema,
|
|
5301
|
+
title: "List Config Section"
|
|
5302
|
+
}, async ({ cursor, file, maxResults, projectId, section }) => {
|
|
5303
|
+
return toToolSuccess(await listConfigSection({
|
|
5304
|
+
cursor,
|
|
5305
|
+
file,
|
|
5306
|
+
maxResults,
|
|
5307
|
+
projectId,
|
|
5308
|
+
section
|
|
5309
|
+
}, dependencies));
|
|
5310
|
+
});
|
|
3748
5311
|
server.registerTool("get_class_reflection", {
|
|
3749
5312
|
description: "Reflect a UCLASS or USTRUCT in a project's running Unreal Editor: parent type, implemented interfaces, class flags, and the declared properties (name, C++ type, UPROPERTY specifiers, metadata) and functions (UFUNCTION specifiers, parameters, metadata). Use it to learn a type's API without reading headers. Properties and functions page independently — pass the previous response nextPropertyCursor/nextFunctionCursor to continue.",
|
|
3750
5313
|
inputSchema: getClassReflectionInputSchema,
|
|
@@ -3861,6 +5424,70 @@ function registerMcpTools(server, dependencies) {
|
|
|
3861
5424
|
referenceKinds
|
|
3862
5425
|
}, dependencies));
|
|
3863
5426
|
});
|
|
5427
|
+
server.registerTool("get_blueprint_summary", {
|
|
5428
|
+
description: "Describe what is inside a Blueprint asset: parent class, implemented interfaces, its component hierarchy from the Simple Construction Script, its variables (type, default, category, replication), its functions/events/macros with signatures, and an inventory of its graphs. Use the returned graph names with get_blueprint_graph. By default nothing is loaded — an unloaded Blueprint answers from Asset Registry metadata (parent and generated class only) and reports loaded=false; pass loadIfNeeded to read the full contents. Each list pages independently.",
|
|
5429
|
+
inputSchema: getBlueprintSummaryInputSchema,
|
|
5430
|
+
title: "Get Blueprint Summary"
|
|
5431
|
+
}, async ({ blueprintPath, componentCursor, functionCursor, graphCursor, loadIfNeeded, maxResults, projectId, variableCursor }) => {
|
|
5432
|
+
return toToolSuccess(await getBlueprintSummary({
|
|
5433
|
+
blueprintPath,
|
|
5434
|
+
componentCursor,
|
|
5435
|
+
functionCursor,
|
|
5436
|
+
graphCursor,
|
|
5437
|
+
loadIfNeeded,
|
|
5438
|
+
maxResults,
|
|
5439
|
+
projectId,
|
|
5440
|
+
variableCursor
|
|
5441
|
+
}, dependencies));
|
|
5442
|
+
});
|
|
5443
|
+
server.registerTool("get_blueprint_graph", {
|
|
5444
|
+
description: "Dump one Blueprint graph as nodes and connections: each node reports its title, node class and pins, and each pin reports its name, direction, type, default value and the node/pin ids it links to. Get valid graph names from get_blueprint_summary first. Reading a graph requires the Blueprint in memory, so pass loadIfNeeded for an unloaded asset. Nodes are ordered by their stable GUID and paged via nextNodeCursor.",
|
|
5445
|
+
inputSchema: getBlueprintGraphInputSchema,
|
|
5446
|
+
title: "Get Blueprint Graph"
|
|
5447
|
+
}, async ({ blueprintPath, graphName, loadIfNeeded, maxNodes, nodeCursor, projectId }) => {
|
|
5448
|
+
return toToolSuccess(await getBlueprintGraph({
|
|
5449
|
+
blueprintPath,
|
|
5450
|
+
graphName,
|
|
5451
|
+
loadIfNeeded,
|
|
5452
|
+
maxNodes,
|
|
5453
|
+
nodeCursor,
|
|
5454
|
+
projectId
|
|
5455
|
+
}, dependencies));
|
|
5456
|
+
});
|
|
5457
|
+
server.registerTool("get_blueprint_compile_errors", {
|
|
5458
|
+
description: "Report each Blueprint's compile status (up_to_date, warnings, error, dirty, unknown, not_loaded, not_found) plus the errors and warnings from the last compile this bridge ran, including the node each message was attached to. hasDiagnostics=false means this bridge has not compiled that Blueprint in this editor session, so empty lists do not mean it is clean — run compile_blueprint first to get diagnostics.",
|
|
5459
|
+
inputSchema: getBlueprintCompileErrorsInputSchema,
|
|
5460
|
+
title: "Get Blueprint Compile Errors"
|
|
5461
|
+
}, async ({ blueprintPaths, cursor, maxResults, projectId }) => {
|
|
5462
|
+
return toToolSuccess(await getBlueprintCompileErrors({
|
|
5463
|
+
blueprintPaths,
|
|
5464
|
+
cursor,
|
|
5465
|
+
maxResults,
|
|
5466
|
+
projectId
|
|
5467
|
+
}, dependencies));
|
|
5468
|
+
});
|
|
5469
|
+
server.registerTool("compile_blueprint", {
|
|
5470
|
+
description: "Compile one or more Blueprints in the running editor so content breakage is caught after a C++ change that altered a base class or a UFUNCTION signature. Compiling is asynchronous: this returns a compileJobId to poll with get_blueprint_compile_status, which reports the per-Blueprint diagnostics. Blueprints are loaded as needed and their assets are modified in memory — save them with save_dirty_packages if you want the result on disk.",
|
|
5471
|
+
inputSchema: compileBlueprintInputSchema,
|
|
5472
|
+
title: "Compile Blueprint"
|
|
5473
|
+
}, async ({ blueprintPaths, projectId }) => {
|
|
5474
|
+
return toToolSuccess(await compileBlueprint({
|
|
5475
|
+
blueprintPaths,
|
|
5476
|
+
projectId
|
|
5477
|
+
}, dependencies));
|
|
5478
|
+
});
|
|
5479
|
+
server.registerTool("get_blueprint_compile_status", {
|
|
5480
|
+
description: "Poll a compile_blueprint job. Reports 'compiling' while it runs, then 'succeeded' or 'failed' with per-Blueprint errors and warnings (each carrying the node the compiler blamed); 'unknown_job' means the id was never issued or has been evicted.",
|
|
5481
|
+
inputSchema: getBlueprintCompileStatusInputSchema,
|
|
5482
|
+
title: "Get Blueprint Compile Status"
|
|
5483
|
+
}, async ({ compileJobId, cursor, maxResults, projectId }) => {
|
|
5484
|
+
return toToolSuccess(await getBlueprintCompileStatus({
|
|
5485
|
+
compileJobId,
|
|
5486
|
+
cursor,
|
|
5487
|
+
maxResults,
|
|
5488
|
+
projectId
|
|
5489
|
+
}, dependencies));
|
|
5490
|
+
});
|
|
3864
5491
|
}
|
|
3865
5492
|
async function findAssets(input, dependencies) {
|
|
3866
5493
|
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
@@ -3946,6 +5573,142 @@ async function findBlueprintReferences(input, dependencies) {
|
|
|
3946
5573
|
truncated: response.truncated
|
|
3947
5574
|
};
|
|
3948
5575
|
}
|
|
5576
|
+
async function getBlueprintSummary(input, dependencies) {
|
|
5577
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
5578
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
5579
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
5580
|
+
const response = await sendBridgeGetBlueprintSummaryRequest(createBridgeGetBlueprintSummaryRequest(input.blueprintPath, {
|
|
5581
|
+
componentCursor: input.componentCursor,
|
|
5582
|
+
functionCursor: input.functionCursor,
|
|
5583
|
+
graphCursor: input.graphCursor,
|
|
5584
|
+
loadIfNeeded: input.loadIfNeeded,
|
|
5585
|
+
maxResults: input.maxResults,
|
|
5586
|
+
variableCursor: input.variableCursor
|
|
5587
|
+
}), {
|
|
5588
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
5589
|
+
host: "127.0.0.1",
|
|
5590
|
+
port: entry.port,
|
|
5591
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
5592
|
+
});
|
|
5593
|
+
if (!response.ok) throw new Error(`Bridge get_blueprint_summary failed (${response.error.code}): ${response.error.message}`);
|
|
5594
|
+
return {
|
|
5595
|
+
blueprintName: response.blueprintName,
|
|
5596
|
+
blueprintPath: response.blueprintPath,
|
|
5597
|
+
componentCount: response.componentCount,
|
|
5598
|
+
components: response.components,
|
|
5599
|
+
functionCount: response.functionCount,
|
|
5600
|
+
functions: response.functions,
|
|
5601
|
+
generatedClassPath: response.generatedClassPath,
|
|
5602
|
+
graphCount: response.graphCount,
|
|
5603
|
+
graphs: response.graphs,
|
|
5604
|
+
interfaces: response.interfaces,
|
|
5605
|
+
kind: response.kind,
|
|
5606
|
+
loaded: response.loaded,
|
|
5607
|
+
nextComponentCursor: response.nextComponentCursor,
|
|
5608
|
+
nextFunctionCursor: response.nextFunctionCursor,
|
|
5609
|
+
nextGraphCursor: response.nextGraphCursor,
|
|
5610
|
+
nextVariableCursor: response.nextVariableCursor,
|
|
5611
|
+
parentClass: response.parentClass,
|
|
5612
|
+
parentClassPath: response.parentClassPath,
|
|
5613
|
+
projectId: entry.projectId,
|
|
5614
|
+
scanning: response.scanning,
|
|
5615
|
+
truncated: response.truncated,
|
|
5616
|
+
variableCount: response.variableCount,
|
|
5617
|
+
variables: response.variables
|
|
5618
|
+
};
|
|
5619
|
+
}
|
|
5620
|
+
async function getBlueprintGraph(input, dependencies) {
|
|
5621
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
5622
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
5623
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
5624
|
+
const response = await sendBridgeGetBlueprintGraphRequest(createBridgeGetBlueprintGraphRequest(input.blueprintPath, input.graphName, {
|
|
5625
|
+
loadIfNeeded: input.loadIfNeeded,
|
|
5626
|
+
maxNodes: input.maxNodes,
|
|
5627
|
+
nodeCursor: input.nodeCursor
|
|
5628
|
+
}), {
|
|
5629
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
5630
|
+
host: "127.0.0.1",
|
|
5631
|
+
port: entry.port,
|
|
5632
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
5633
|
+
});
|
|
5634
|
+
if (!response.ok) throw new Error(`Bridge get_blueprint_graph failed (${response.error.code}): ${response.error.message}`);
|
|
5635
|
+
return {
|
|
5636
|
+
blueprintPath: response.blueprintPath,
|
|
5637
|
+
graphKind: response.graphKind,
|
|
5638
|
+
graphName: response.graphName,
|
|
5639
|
+
nextNodeCursor: response.nextNodeCursor,
|
|
5640
|
+
nodeCount: response.nodeCount,
|
|
5641
|
+
nodes: response.nodes,
|
|
5642
|
+
projectId: entry.projectId,
|
|
5643
|
+
truncated: response.truncated
|
|
5644
|
+
};
|
|
5645
|
+
}
|
|
5646
|
+
async function getBlueprintCompileErrors(input, dependencies) {
|
|
5647
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
5648
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
5649
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
5650
|
+
const response = await sendBridgeGetBlueprintCompileErrorsRequest(createBridgeGetBlueprintCompileErrorsRequest(input.blueprintPaths, {
|
|
5651
|
+
cursor: input.cursor,
|
|
5652
|
+
maxResults: input.maxResults
|
|
5653
|
+
}), {
|
|
5654
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
5655
|
+
host: "127.0.0.1",
|
|
5656
|
+
port: entry.port,
|
|
5657
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
5658
|
+
});
|
|
5659
|
+
if (!response.ok) throw new Error(`Bridge get_blueprint_compile_errors failed (${response.error.code}): ${response.error.message}`);
|
|
5660
|
+
return {
|
|
5661
|
+
nextCursor: response.nextCursor,
|
|
5662
|
+
projectId: entry.projectId,
|
|
5663
|
+
results: response.results,
|
|
5664
|
+
totalCount: response.totalCount,
|
|
5665
|
+
truncated: response.truncated
|
|
5666
|
+
};
|
|
5667
|
+
}
|
|
5668
|
+
async function compileBlueprint(input, dependencies) {
|
|
5669
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
5670
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
5671
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
5672
|
+
const response = await sendBridgeCompileBlueprintRequest(createBridgeCompileBlueprintRequest(input.blueprintPaths), {
|
|
5673
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
5674
|
+
host: "127.0.0.1",
|
|
5675
|
+
port: entry.port,
|
|
5676
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
5677
|
+
});
|
|
5678
|
+
if (!response.ok) throw new Error(`Bridge compile_blueprint failed (${response.error.code}): ${response.error.message}`);
|
|
5679
|
+
return {
|
|
5680
|
+
compileJobId: response.compileJobId,
|
|
5681
|
+
message: response.message,
|
|
5682
|
+
projectId: entry.projectId,
|
|
5683
|
+
state: response.state
|
|
5684
|
+
};
|
|
5685
|
+
}
|
|
5686
|
+
async function getBlueprintCompileStatus(input, dependencies) {
|
|
5687
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
5688
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
5689
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
5690
|
+
const response = await sendBridgeGetBlueprintCompileStatusRequest(createBridgeGetBlueprintCompileStatusRequest(input.compileJobId, {
|
|
5691
|
+
cursor: input.cursor,
|
|
5692
|
+
maxResults: input.maxResults
|
|
5693
|
+
}), {
|
|
5694
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
5695
|
+
host: "127.0.0.1",
|
|
5696
|
+
port: entry.port,
|
|
5697
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
5698
|
+
});
|
|
5699
|
+
if (!response.ok) throw new Error(`Bridge get_blueprint_compile_status failed (${response.error.code}): ${response.error.message}`);
|
|
5700
|
+
return {
|
|
5701
|
+
compileJobId: response.compileJobId,
|
|
5702
|
+
durationMs: response.durationMs,
|
|
5703
|
+
finishedAt: response.finishedAt,
|
|
5704
|
+
nextCursor: response.nextCursor,
|
|
5705
|
+
projectId: entry.projectId,
|
|
5706
|
+
results: response.results,
|
|
5707
|
+
state: response.state,
|
|
5708
|
+
totalCount: response.totalCount,
|
|
5709
|
+
truncated: response.truncated
|
|
5710
|
+
};
|
|
5711
|
+
}
|
|
3949
5712
|
async function getClassReflection(input, dependencies) {
|
|
3950
5713
|
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
3951
5714
|
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
@@ -4234,6 +5997,71 @@ async function pingProject(projectId, dependencies) {
|
|
|
4234
5997
|
registrationRemoved: true
|
|
4235
5998
|
};
|
|
4236
5999
|
}
|
|
6000
|
+
async function listRegisteredProjects(input, dependencies) {
|
|
6001
|
+
const maxResults = input.maxResults ?? 50;
|
|
6002
|
+
const entries = dependencies.registry.snapshot().sort(compareRegistryEntries);
|
|
6003
|
+
const page = entries.slice(0, maxResults);
|
|
6004
|
+
const verified = input.verify === true;
|
|
6005
|
+
return {
|
|
6006
|
+
projects: verified ? await verifyRegisteredProjects(page, dependencies) : page.map(toRegisteredProjectSummary),
|
|
6007
|
+
totalCount: entries.length,
|
|
6008
|
+
truncated: entries.length > page.length,
|
|
6009
|
+
verified
|
|
6010
|
+
};
|
|
6011
|
+
}
|
|
6012
|
+
async function verifyRegisteredProjects(entries, dependencies) {
|
|
6013
|
+
const budgetMs = deriveVerifySweepBudgetMs(dependencies.pingTimeoutMs);
|
|
6014
|
+
let budgetTimer;
|
|
6015
|
+
const budgetExpiry = new Promise((resolve) => {
|
|
6016
|
+
budgetTimer = setTimeout(() => {
|
|
6017
|
+
resolve({
|
|
6018
|
+
alive: false,
|
|
6019
|
+
reason: `Verification budget of ${String(budgetMs)}ms expired before the editor answered.`
|
|
6020
|
+
});
|
|
6021
|
+
}, budgetMs);
|
|
6022
|
+
});
|
|
6023
|
+
try {
|
|
6024
|
+
return await Promise.all(entries.map(async (entry) => {
|
|
6025
|
+
const pingResult = await Promise.race([pingRegistryEntry$1(entry, {
|
|
6026
|
+
logger: dependencies.logger,
|
|
6027
|
+
timeoutMs: dependencies.pingTimeoutMs
|
|
6028
|
+
}), budgetExpiry]);
|
|
6029
|
+
const summary = toRegisteredProjectSummary(entry);
|
|
6030
|
+
if (pingResult.alive) return {
|
|
6031
|
+
...summary,
|
|
6032
|
+
reachable: true
|
|
6033
|
+
};
|
|
6034
|
+
return {
|
|
6035
|
+
...summary,
|
|
6036
|
+
reachable: false,
|
|
6037
|
+
reason: pingResult.reason ?? "Unknown ping failure."
|
|
6038
|
+
};
|
|
6039
|
+
}));
|
|
6040
|
+
} finally {
|
|
6041
|
+
clearTimeout(budgetTimer);
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
function toRegisteredProjectSummary(entry) {
|
|
6045
|
+
return {
|
|
6046
|
+
host: BRIDGE_LOOPBACK_HOST,
|
|
6047
|
+
lastSeenAt: entry.lastSeenAt,
|
|
6048
|
+
pid: entry.pid,
|
|
6049
|
+
port: entry.port,
|
|
6050
|
+
projectId: entry.projectId,
|
|
6051
|
+
projectName: entry.projectName,
|
|
6052
|
+
projectPath: entry.projectPath,
|
|
6053
|
+
registeredAt: entry.registeredAt
|
|
6054
|
+
};
|
|
6055
|
+
}
|
|
6056
|
+
function compareRegistryEntries(left, right) {
|
|
6057
|
+
if (left.projectName !== right.projectName) return left.projectName < right.projectName ? -1 : 1;
|
|
6058
|
+
if (left.projectPath !== right.projectPath) return left.projectPath < right.projectPath ? -1 : 1;
|
|
6059
|
+
if (left.projectId === right.projectId) return 0;
|
|
6060
|
+
return left.projectId < right.projectId ? -1 : 1;
|
|
6061
|
+
}
|
|
6062
|
+
function deriveVerifySweepBudgetMs(pingTimeoutMs) {
|
|
6063
|
+
return Math.max(1, Math.round(pingTimeoutMs * 2));
|
|
6064
|
+
}
|
|
4237
6065
|
async function startPie(input, dependencies) {
|
|
4238
6066
|
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
4239
6067
|
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
@@ -4512,6 +6340,92 @@ async function execConsoleCommand(input, dependencies) {
|
|
|
4512
6340
|
truncated: response.truncated
|
|
4513
6341
|
};
|
|
4514
6342
|
}
|
|
6343
|
+
async function getConfigValue(input, dependencies) {
|
|
6344
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
6345
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
6346
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
6347
|
+
const response = await sendBridgeGetConfigValueRequest(createBridgeGetConfigValueRequest({
|
|
6348
|
+
file: input.file,
|
|
6349
|
+
key: input.key,
|
|
6350
|
+
section: input.section
|
|
6351
|
+
}), {
|
|
6352
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
6353
|
+
host: BRIDGE_LOOPBACK_HOST,
|
|
6354
|
+
port: entry.port,
|
|
6355
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
6356
|
+
});
|
|
6357
|
+
if (!response.ok) throw new Error(`Bridge get_config_value failed (${response.error.code}): ${response.error.message}`);
|
|
6358
|
+
const result = {
|
|
6359
|
+
file: response.file,
|
|
6360
|
+
key: response.key,
|
|
6361
|
+
present: response.present,
|
|
6362
|
+
projectId: entry.projectId,
|
|
6363
|
+
resolvedFrom: response.resolvedFrom,
|
|
6364
|
+
section: response.section
|
|
6365
|
+
};
|
|
6366
|
+
if (response.filePath !== void 0) result.filePath = response.filePath;
|
|
6367
|
+
if (response.value !== void 0) result.value = response.value;
|
|
6368
|
+
if (response.valueTruncated === true) result.valueTruncated = true;
|
|
6369
|
+
return result;
|
|
6370
|
+
}
|
|
6371
|
+
async function setConfigValue(input, dependencies) {
|
|
6372
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
6373
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
6374
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'. Config writes go through the running editor so the engine normalises the ini and the live editor observes the change; start the editor, or edit the ini by hand.`);
|
|
6375
|
+
const response = await sendBridgeSetConfigValueRequest(createBridgeSetConfigValueRequest({
|
|
6376
|
+
file: input.file,
|
|
6377
|
+
key: input.key,
|
|
6378
|
+
scope: input.scope,
|
|
6379
|
+
section: input.section,
|
|
6380
|
+
value: input.value
|
|
6381
|
+
}), {
|
|
6382
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
6383
|
+
host: BRIDGE_LOOPBACK_HOST,
|
|
6384
|
+
port: entry.port,
|
|
6385
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
6386
|
+
});
|
|
6387
|
+
if (!response.ok) throw new Error(`Bridge set_config_value failed (${response.error.code}): ${response.error.message}`);
|
|
6388
|
+
const result = {
|
|
6389
|
+
file: response.file,
|
|
6390
|
+
filePath: response.filePath,
|
|
6391
|
+
key: response.key,
|
|
6392
|
+
previousPresent: response.previousPresent,
|
|
6393
|
+
projectId: entry.projectId,
|
|
6394
|
+
scope: response.scope,
|
|
6395
|
+
section: response.section,
|
|
6396
|
+
value: response.value
|
|
6397
|
+
};
|
|
6398
|
+
if (response.previousValue !== void 0) result.previousValue = response.previousValue;
|
|
6399
|
+
if (response.previousValueTruncated === true) result.previousValueTruncated = true;
|
|
6400
|
+
return result;
|
|
6401
|
+
}
|
|
6402
|
+
async function listConfigSection(input, dependencies) {
|
|
6403
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
6404
|
+
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
6405
|
+
if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
6406
|
+
const response = await sendBridgeListConfigSectionRequest(createBridgeListConfigSectionRequest({
|
|
6407
|
+
cursor: input.cursor,
|
|
6408
|
+
file: input.file,
|
|
6409
|
+
maxResults: input.maxResults,
|
|
6410
|
+
section: input.section
|
|
6411
|
+
}), {
|
|
6412
|
+
connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
|
|
6413
|
+
host: BRIDGE_LOOPBACK_HOST,
|
|
6414
|
+
port: entry.port,
|
|
6415
|
+
readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
|
|
6416
|
+
});
|
|
6417
|
+
if (!response.ok) throw new Error(`Bridge list_config_section failed (${response.error.code}): ${response.error.message}`);
|
|
6418
|
+
const result = {
|
|
6419
|
+
entries: response.entries,
|
|
6420
|
+
file: response.file,
|
|
6421
|
+
projectId: entry.projectId,
|
|
6422
|
+
section: response.section,
|
|
6423
|
+
totalCount: response.totalCount,
|
|
6424
|
+
truncated: response.truncated
|
|
6425
|
+
};
|
|
6426
|
+
if (response.nextCursor !== void 0) result.nextCursor = response.nextCursor;
|
|
6427
|
+
return result;
|
|
6428
|
+
}
|
|
4515
6429
|
async function getStatSnapshot(projectId, dependencies) {
|
|
4516
6430
|
const normalizedProjectId = projectIdSchema.parse(projectId);
|
|
4517
6431
|
const entry = dependencies.registry.getProject(normalizedProjectId);
|
|
@@ -4629,6 +6543,98 @@ function buildGetCreateClassStatusResult(response, projectId) {
|
|
|
4629
6543
|
if (response.sourcePath !== void 0) result.sourcePath = response.sourcePath;
|
|
4630
6544
|
return result;
|
|
4631
6545
|
}
|
|
6546
|
+
async function waitForJob(input, dependencies) {
|
|
6547
|
+
const normalizedProjectId = projectIdSchema.parse(input.projectId);
|
|
6548
|
+
if (!dependencies.registry.getProject(normalizedProjectId)) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
|
|
6549
|
+
const timeoutMs = clampWaitForJobTimeoutMs(input.kind, input.timeoutMs);
|
|
6550
|
+
const cadence = WAIT_FOR_JOB_POLL_CADENCE_MS[input.kind];
|
|
6551
|
+
const startedAtMs = Date.now();
|
|
6552
|
+
const deadlineMs = startedAtMs + timeoutMs;
|
|
6553
|
+
let delayMs = cadence.initialMs;
|
|
6554
|
+
let pollCount = 0;
|
|
6555
|
+
let lastStatus;
|
|
6556
|
+
let lastTransientError;
|
|
6557
|
+
for (;;) {
|
|
6558
|
+
pollCount += 1;
|
|
6559
|
+
try {
|
|
6560
|
+
const status = await pollJobStatus(input.kind, input.jobId, normalizedProjectId, dependencies);
|
|
6561
|
+
lastStatus = status;
|
|
6562
|
+
lastTransientError = void 0;
|
|
6563
|
+
const progress = classifyJobStatus(input.kind, status);
|
|
6564
|
+
if (progress !== "pending") return {
|
|
6565
|
+
jobId: input.jobId,
|
|
6566
|
+
kind: input.kind,
|
|
6567
|
+
outcome: progress,
|
|
6568
|
+
pollCount,
|
|
6569
|
+
projectId: normalizedProjectId,
|
|
6570
|
+
status,
|
|
6571
|
+
timeoutMs,
|
|
6572
|
+
waitedMs: Date.now() - startedAtMs
|
|
6573
|
+
};
|
|
6574
|
+
} catch (error) {
|
|
6575
|
+
if (!isTransientBridgeError(error)) throw error;
|
|
6576
|
+
lastTransientError = error;
|
|
6577
|
+
dependencies.logger.warn({
|
|
6578
|
+
error,
|
|
6579
|
+
jobId: input.jobId,
|
|
6580
|
+
kind: input.kind,
|
|
6581
|
+
projectId: normalizedProjectId
|
|
6582
|
+
}, "Retrying wait_for_job poll after a transient bridge failure");
|
|
6583
|
+
}
|
|
6584
|
+
const remainingMs = deadlineMs - Date.now();
|
|
6585
|
+
if (remainingMs <= 0) break;
|
|
6586
|
+
await sleep(Math.min(delayMs, remainingMs));
|
|
6587
|
+
delayMs = Math.min(cadence.capMs, delayMs * 2);
|
|
6588
|
+
}
|
|
6589
|
+
if (!lastStatus) throw lastTransientError ?? /* @__PURE__ */ new Error(`wait_for_job could not read the ${input.kind} status of job '${input.jobId}' for projectId '${normalizedProjectId}' before the ${String(timeoutMs)}ms budget expired.`);
|
|
6590
|
+
return {
|
|
6591
|
+
jobId: input.jobId,
|
|
6592
|
+
kind: input.kind,
|
|
6593
|
+
outcome: "timed_out",
|
|
6594
|
+
pollCount,
|
|
6595
|
+
projectId: normalizedProjectId,
|
|
6596
|
+
status: lastStatus,
|
|
6597
|
+
timeoutMs,
|
|
6598
|
+
waitedMs: Date.now() - startedAtMs
|
|
6599
|
+
};
|
|
6600
|
+
}
|
|
6601
|
+
function clampWaitForJobTimeoutMs(kind, timeoutMs) {
|
|
6602
|
+
return Math.min(timeoutMs ?? WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS[kind], WAIT_FOR_JOB_MAX_TIMEOUT_MS);
|
|
6603
|
+
}
|
|
6604
|
+
async function pollJobStatus(kind, jobId, projectId, dependencies) {
|
|
6605
|
+
switch (kind) {
|
|
6606
|
+
case "compile": return await getCompileStatus({
|
|
6607
|
+
compileJobId: jobId,
|
|
6608
|
+
projectId
|
|
6609
|
+
}, dependencies);
|
|
6610
|
+
case "create_class": return await getCreateClassStatus({
|
|
6611
|
+
createClassJobId: jobId,
|
|
6612
|
+
projectId
|
|
6613
|
+
}, dependencies);
|
|
6614
|
+
case "pie": return await getPieStatus({
|
|
6615
|
+
pieSessionId: jobId,
|
|
6616
|
+
projectId
|
|
6617
|
+
}, dependencies);
|
|
6618
|
+
case "test_run": return await getTestRunStatus({
|
|
6619
|
+
projectId,
|
|
6620
|
+
testRunId: jobId
|
|
6621
|
+
}, dependencies);
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
function classifyJobStatus(kind, status) {
|
|
6625
|
+
switch (kind) {
|
|
6626
|
+
case "compile": return status.state === "compiling" ? "pending" : toTerminalOutcome(status.state === "unknown_job");
|
|
6627
|
+
case "create_class": return status.state === "creating" ? "pending" : toTerminalOutcome(status.state === "unknown_job");
|
|
6628
|
+
case "pie": return status.state === "starting" ? "pending" : "completed";
|
|
6629
|
+
case "test_run": return status.state === "running" ? "pending" : toTerminalOutcome(status.state === "unknown_run");
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
function toTerminalOutcome(isUnknownJob) {
|
|
6633
|
+
return isUnknownJob ? "unknown_job" : "completed";
|
|
6634
|
+
}
|
|
6635
|
+
function isTransientBridgeError(error) {
|
|
6636
|
+
return error instanceof BridgeSocketError && (error.code === "read_timeout" || error.code === "connection_closed");
|
|
6637
|
+
}
|
|
4632
6638
|
async function runQueuedRegistryWrite(dependencies, operation) {
|
|
4633
6639
|
return await (dependencies.registryWriteQueue ?? new RegistryWriteQueue()).run(operation);
|
|
4634
6640
|
}
|
|
@@ -4876,7 +6882,7 @@ function isMissingFileError(error) {
|
|
|
4876
6882
|
//#endregion
|
|
4877
6883
|
//#region src/server.ts
|
|
4878
6884
|
const SERVER_NAME = "ue-mcp-server";
|
|
4879
|
-
const SERVER_VERSION = "0.
|
|
6885
|
+
const SERVER_VERSION = "0.3.0";
|
|
4880
6886
|
const logger = createModuleLogger("bootstrap");
|
|
4881
6887
|
const enginePingLogger = createModuleLogger("engine-ping");
|
|
4882
6888
|
const httpApiLogger = createModuleLogger("http-api");
|