@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.
Files changed (3) hide show
  1. package/README.md +56 -0
  2. package/dist/index.mjs +2037 -31
  3. 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 { access, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
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
- runProcess: overrides.runProcess ?? runProcess
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 engineVersion = options.engineVersion?.trim() || await readEngineAssociation(options.uprojectPath, context);
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: await resolveUnrealBuildTool(engineRoot, context)
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, "dotnet");
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
- //#endregion
3017
- //#region src/mcpTools.ts
3018
- const EDITOR_WORK_TIMEOUT_MS = 1e4;
3019
- const getProjectIdInputSchema = { projectPath: z.string().trim().min(1) };
3020
- const pingProjectInputSchema = { projectId: projectIdSchema };
3021
- const startPieInputSchema = {
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
- const stopPieInputSchema = {
3034
- pieSessionId: z.string().trim().min(1).max(512).optional().describe("Optional pieSessionId returned by start_pie; omit to stop the editor current PIE session."),
3035
- projectId: projectIdSchema
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
- const listLaunchDevicesInputSchema = { projectId: projectIdSchema };
3038
- const launchOnDeviceInputSchema = {
3039
- deviceId: z.string().trim().min(1).max(512).describe("Target device id from list_launch_devices (e.g. IOS@MyiPhone, Android@serial)."),
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.1.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");