@uemcp/server 0.1.0 → 0.4.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 +86 -0
  2. package/dist/index.mjs +2435 -83
  3. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
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
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { z } from "zod";
@@ -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 = () => {
@@ -1954,6 +2429,13 @@ var RegistryWriteQueue = class {
1954
2429
  function createHttpApi(options) {
1955
2430
  const app = Fastify({ logger: false });
1956
2431
  const registryWriteQueue = options.registryWriteQueue ?? new RegistryWriteQueue();
2432
+ const notifyRegistryChanged = () => {
2433
+ try {
2434
+ options.onRegistryChanged?.();
2435
+ } catch (error) {
2436
+ options.logger.warn({ error }, "Failed to notify listeners that the registry changed");
2437
+ }
2438
+ };
1957
2439
  app.setErrorHandler((error, _request, reply) => {
1958
2440
  if (error instanceof HttpError) {
1959
2441
  reply.code(error.statusCode).send({
@@ -2038,6 +2520,7 @@ function createHttpApi(options) {
2038
2520
  projectName: entry.projectName,
2039
2521
  projectPath: entry.projectPath
2040
2522
  }, "Upserted Unreal Editor project registration");
2523
+ notifyRegistryChanged();
2041
2524
  return {
2042
2525
  projectId: entry.projectId,
2043
2526
  stored: true
@@ -2068,6 +2551,7 @@ function createHttpApi(options) {
2068
2551
  existed,
2069
2552
  projectId
2070
2553
  }, "Removed Unreal Editor project registration");
2554
+ if (existed) notifyRegistryChanged();
2071
2555
  return {
2072
2556
  projectId,
2073
2557
  removed: true
@@ -2497,73 +2981,6 @@ function isLogLevel(value) {
2497
2981
  return value === "fatal" || value === "error" || value === "warn" || value === "info" || value === "debug" || value === "trace" || value === "silent";
2498
2982
  }
2499
2983
  //#endregion
2500
- //#region src/buildDiagnostics.ts
2501
- const CLANG_DIAGNOSTIC_PATTERN = /^(?<file>.+?):(?<line>\d+):(?:(?<column>\d+):)?\s+(?<severity>fatal error|error|warning):\s+(?<message>.*)$/u;
2502
- const MSVC_DIAGNOSTIC_PATTERN = /^(?<file>.+?)\((?<line>\d+)(?:,\d+)?\):\s+(?<severity>fatal error|error|warning)\s+(?<code>[A-Z]+\d+):\s+(?<message>.*)$/u;
2503
- const CLANG_TIDY_PATTERN = /^(?<file>.+?):(?<line>\d+):(?<column>\d+):\s+(?<severity>error|warning):\s+(?<message>.*?)(?:\s+\[(?<check>[^\]]+)\])?$/u;
2504
- function diagnosticKey(diagnostic) {
2505
- return `${diagnostic.file ?? ""}|${diagnostic.line ?? ""}|${diagnostic.message}`;
2506
- }
2507
- function parseBuildDiagnostics(output) {
2508
- const errors = [];
2509
- const warnings = [];
2510
- const seenErrors = /* @__PURE__ */ new Set();
2511
- const seenWarnings = /* @__PURE__ */ new Set();
2512
- for (const rawLine of output.split(/\r?\n/u)) {
2513
- const line = rawLine.trim();
2514
- if (line.length === 0) continue;
2515
- const match = CLANG_DIAGNOSTIC_PATTERN.exec(line) ?? MSVC_DIAGNOSTIC_PATTERN.exec(line);
2516
- if (!match?.groups) continue;
2517
- const groups = match.groups;
2518
- const { file, line: lineText, message, severity } = groups;
2519
- if (file === void 0 || lineText === void 0 || message === void 0 || severity === void 0) continue;
2520
- const code = typeof groups.code === "string" ? `${groups.code}: ` : "";
2521
- const diagnostic = {
2522
- file,
2523
- line: Number.parseInt(lineText, 10),
2524
- message: `${code}${message}`.trim()
2525
- };
2526
- const isError = severity === "error" || severity === "fatal error";
2527
- const target = isError ? errors : warnings;
2528
- const seen = isError ? seenErrors : seenWarnings;
2529
- const key = diagnosticKey(diagnostic);
2530
- if (seen.has(key)) continue;
2531
- seen.add(key);
2532
- target.push(diagnostic);
2533
- }
2534
- return {
2535
- errorCount: errors.length,
2536
- errors,
2537
- warningCount: warnings.length,
2538
- warnings
2539
- };
2540
- }
2541
- function parseClangTidyFindings(output) {
2542
- const findings = [];
2543
- const seen = /* @__PURE__ */ new Set();
2544
- for (const rawLine of output.split(/\r?\n/u)) {
2545
- const match = CLANG_TIDY_PATTERN.exec(rawLine.trim());
2546
- if (!match?.groups) continue;
2547
- const groups = match.groups;
2548
- const { file, line, message } = groups;
2549
- if (file === void 0 || line === void 0 || message === void 0) continue;
2550
- const severity = groups.severity === "error" ? "error" : "warning";
2551
- const finding = {
2552
- check: groups.check ?? "unknown",
2553
- file,
2554
- line: Number.parseInt(line, 10),
2555
- message: message.trim(),
2556
- severity
2557
- };
2558
- if (typeof groups.column === "string") finding.column = Number.parseInt(groups.column, 10);
2559
- const key = `${finding.file}|${finding.line}|${finding.column ?? ""}|${finding.check}|${finding.message}`;
2560
- if (seen.has(key)) continue;
2561
- seen.add(key);
2562
- findings.push(finding);
2563
- }
2564
- return findings;
2565
- }
2566
- //#endregion
2567
2984
  //#region src/engineToolchain.ts
2568
2985
  const UNREAL_BUILD_CONFIGURATIONS = [
2569
2986
  "Debug",
@@ -2577,8 +2994,24 @@ const UNREAL_TARGET_PLATFORMS = [
2577
2994
  "Mac",
2578
2995
  "Linux"
2579
2996
  ];
2997
+ const UNREAL_COOK_TARGET_PLATFORMS = [
2998
+ "Win64",
2999
+ "Mac",
3000
+ "Linux",
3001
+ "LinuxArm64",
3002
+ "Android",
3003
+ "IOS",
3004
+ "TVOS",
3005
+ "VisionOS"
3006
+ ];
3007
+ const COOK_COMMANDLET_PLATFORM_NAMES = { Win64: "Windows" };
3008
+ function cookCommandletTargetPlatform(platform) {
3009
+ return COOK_COMMANDLET_PLATFORM_NAMES[platform] ?? platform;
3010
+ }
2580
3011
  const DEFAULT_PROCESS_TIMEOUT_MS = 18e5;
2581
3012
  const MAX_PROCESS_TIMEOUT_MS = 72e5;
3013
+ const DEFAULT_PACKAGE_TIMEOUT_MS = 54e5;
3014
+ const ENGINE_INSTALL_DIRECTORY_PATTERN = /^UE_(\d+(?:\.\d+)*)$/u;
2582
3015
  async function defaultFileExists(filePath) {
2583
3016
  try {
2584
3017
  await access(filePath);
@@ -2593,6 +3026,26 @@ async function defaultListDirectory(dirPath) {
2593
3026
  async function defaultReadFile(filePath) {
2594
3027
  return await readFile(filePath, "utf8");
2595
3028
  }
3029
+ async function defaultWriteFile(filePath, contents) {
3030
+ await writeFile(filePath, contents, "utf8");
3031
+ }
3032
+ function defaultReadTextLines(filePath) {
3033
+ return createInterface({
3034
+ crlfDelay: Number.POSITIVE_INFINITY,
3035
+ input: createReadStream(filePath, "utf8")
3036
+ });
3037
+ }
3038
+ async function defaultCreateTempDirectory(prefix) {
3039
+ return await mkdtemp(path.join(tmpdir(), prefix));
3040
+ }
3041
+ async function defaultRemoveDirectory(dirPath) {
3042
+ await rm(dirPath, {
3043
+ force: true,
3044
+ maxRetries: 3,
3045
+ recursive: true,
3046
+ retryDelay: 50
3047
+ });
3048
+ }
2596
3049
  var RollingTextBuffer = class {
2597
3050
  maxBytes;
2598
3051
  chunks = [];
@@ -2633,7 +3086,11 @@ const runProcess = (options) => {
2633
3086
  return new Promise((resolve, reject) => {
2634
3087
  const child = spawn(options.command, options.args, {
2635
3088
  cwd: options.cwd,
2636
- env: options.env
3089
+ env: options.env,
3090
+ ...options.windowsVerbatimArguments === true && process.platform === "win32" ? {
3091
+ argv0: `"${options.command}"`,
3092
+ windowsVerbatimArguments: true
3093
+ } : {}
2637
3094
  });
2638
3095
  const stdoutBuffer = new RollingTextBuffer(maxOutputBytes);
2639
3096
  const stderrBuffer = new RollingTextBuffer(maxOutputBytes);
@@ -2643,7 +3100,13 @@ const runProcess = (options) => {
2643
3100
  const append = (buffer, text) => {
2644
3101
  buffer.push(text);
2645
3102
  combinedBuffer.push(text);
3103
+ options.onOutput?.(text);
3104
+ };
3105
+ const abortListener = () => {
3106
+ child.kill("SIGKILL");
2646
3107
  };
3108
+ if (options.signal?.aborted === true) child.kill("SIGKILL");
3109
+ else options.signal?.addEventListener("abort", abortListener, { once: true });
2647
3110
  child.stdout.setEncoding("utf8");
2648
3111
  child.stderr.setEncoding("utf8");
2649
3112
  child.stdout.on("data", (chunk) => {
@@ -2660,12 +3123,14 @@ const runProcess = (options) => {
2660
3123
  if (settled) return;
2661
3124
  settled = true;
2662
3125
  clearTimeout(timeoutHandle);
3126
+ options.signal?.removeEventListener("abort", abortListener);
2663
3127
  reject(new ProcessExecutionError(options.command, error.message, { cause: error }));
2664
3128
  });
2665
3129
  child.on("close", (code, signal) => {
2666
3130
  if (settled) return;
2667
3131
  settled = true;
2668
3132
  clearTimeout(timeoutHandle);
3133
+ options.signal?.removeEventListener("abort", abortListener);
2669
3134
  resolve({
2670
3135
  combinedOutput: combinedBuffer.toString(),
2671
3136
  durationMs: Date.now() - startedAt,
@@ -2682,12 +3147,16 @@ const runProcess = (options) => {
2682
3147
  function createEngineToolchainContext(overrides = {}) {
2683
3148
  return {
2684
3149
  arch: overrides.arch ?? process.arch,
3150
+ createTempDirectory: overrides.createTempDirectory ?? defaultCreateTempDirectory,
2685
3151
  env: overrides.env ?? process.env,
2686
3152
  fileExists: overrides.fileExists ?? defaultFileExists,
2687
3153
  listDirectory: overrides.listDirectory ?? defaultListDirectory,
2688
3154
  platform: overrides.platform ?? process.platform,
2689
3155
  readFile: overrides.readFile ?? defaultReadFile,
2690
- runProcess: overrides.runProcess ?? runProcess
3156
+ readTextLines: overrides.readTextLines ?? defaultReadTextLines,
3157
+ removeDirectory: overrides.removeDirectory ?? defaultRemoveDirectory,
3158
+ runProcess: overrides.runProcess ?? runProcess,
3159
+ writeFile: overrides.writeFile ?? defaultWriteFile
2691
3160
  };
2692
3161
  }
2693
3162
  function defaultTargetPlatform(platform) {
@@ -2744,20 +3213,67 @@ async function resolveEngine(options) {
2744
3213
  const { context } = options;
2745
3214
  const ueInstallsDir = context.env.UE_INSTALLS_DIR?.trim();
2746
3215
  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);
3216
+ const explicitEngineVersion = options.engineVersion?.trim();
3217
+ const { uprojectPath } = options;
3218
+ let engineVersion;
3219
+ if (explicitEngineVersion) engineVersion = explicitEngineVersion;
3220
+ else if (uprojectPath === void 0) throw new EngineToolchainError("Engine resolution needs either an engineVersion or a project path.");
3221
+ else engineVersion = await readEngineAssociation(uprojectPath, context);
2748
3222
  if (!/^\d+(?:\.\d+)*$/u.test(engineVersion)) throw new EngineToolchainError(`EngineAssociation "${engineVersion}" is not a numeric version; pass engineVersion explicitly (e.g. "5.7").`);
2749
3223
  const engineRoot = path.join(ueInstallsDir, `UE_${engineVersion}`);
2750
3224
  if (!await context.fileExists(engineRoot)) throw new EngineToolchainError(`Engine directory not found for UE version ${engineVersion}: ${engineRoot}`);
3225
+ const unrealBuildTool = await resolveUnrealBuildTool(engineRoot, context);
2751
3226
  return {
2752
3227
  engineRoot,
2753
3228
  engineVersion,
2754
- unrealBuildTool: await resolveUnrealBuildTool(engineRoot, context)
3229
+ unrealBuildTool
2755
3230
  };
2756
3231
  }
2757
- async function resolveUnrealBuildTool(engineRoot, context) {
2758
- if (context.platform === "win32") {
2759
- const ubtExe = path.join(engineRoot, "Engine", "Binaries", "DotNET", "UnrealBuildTool", "UnrealBuildTool.exe");
2760
- if (!await context.fileExists(ubtExe)) throw new EngineToolchainError(`UnrealBuildTool.exe not found at ${ubtExe}`);
3232
+ async function listEngineInstalls(context) {
3233
+ const ueInstallsDir = context.env.UE_INSTALLS_DIR?.trim();
3234
+ if (!ueInstallsDir) throw new EngineToolchainError("UE_INSTALLS_DIR is not set; define it in .env (see .env.example).");
3235
+ let entries;
3236
+ try {
3237
+ entries = await context.listDirectory(ueInstallsDir);
3238
+ } catch (error) {
3239
+ throw new EngineToolchainError(`Failed to read UE_INSTALLS_DIR ${ueInstallsDir}: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : void 0 });
3240
+ }
3241
+ const installs = [];
3242
+ for (const entry of entries) {
3243
+ const engineVersion = ENGINE_INSTALL_DIRECTORY_PATTERN.exec(entry)?.[1];
3244
+ if (engineVersion === void 0) continue;
3245
+ const engineRoot = path.join(ueInstallsDir, entry);
3246
+ installs.push({
3247
+ engineRoot,
3248
+ engineVersion,
3249
+ unrealBuildToolResolved: await isUnrealBuildToolResolvable(engineRoot, context)
3250
+ });
3251
+ }
3252
+ return installs.sort((left, right) => compareEngineVersions(left.engineVersion, right.engineVersion));
3253
+ }
3254
+ async function isUnrealBuildToolResolvable(engineRoot, context) {
3255
+ try {
3256
+ await resolveUnrealBuildTool(engineRoot, context);
3257
+ return true;
3258
+ } catch (error) {
3259
+ if (error instanceof EngineToolchainError) return false;
3260
+ throw error;
3261
+ }
3262
+ }
3263
+ function compareEngineVersions(left, right) {
3264
+ const leftSegments = left.split(".");
3265
+ const rightSegments = right.split(".");
3266
+ const segmentCount = Math.max(leftSegments.length, rightSegments.length);
3267
+ for (let index = 0; index < segmentCount; index += 1) {
3268
+ const difference = Number(leftSegments[index] ?? 0) - Number(rightSegments[index] ?? 0);
3269
+ if (difference !== 0) return difference;
3270
+ }
3271
+ return 0;
3272
+ }
3273
+ async function resolveUnrealBuildTool(engineRoot, context) {
3274
+ if (context.platform === "win32") {
3275
+ const ubtExe = path.join(engineRoot, "Engine", "Binaries", "DotNET", "UnrealBuildTool", "UnrealBuildTool.exe");
3276
+ if (!await context.fileExists(ubtExe)) throw new EngineToolchainError(`UnrealBuildTool.exe not found at ${ubtExe}`);
2761
3277
  return {
2762
3278
  args: [],
2763
3279
  command: ubtExe
@@ -2771,8 +3287,72 @@ async function resolveUnrealBuildTool(engineRoot, context) {
2771
3287
  command: bundledDotnet ?? "dotnet"
2772
3288
  };
2773
3289
  }
3290
+ async function resolveAutomationTool(engineRoot, context) {
3291
+ if (context.platform !== "win32") {
3292
+ const scriptPath = path.join(engineRoot, "Engine", "Build", "BatchFiles", "RunUAT.sh");
3293
+ if (!await context.fileExists(scriptPath)) throw new EngineToolchainError(`RunUAT.sh not found at ${scriptPath}`);
3294
+ return {
3295
+ args: [],
3296
+ command: scriptPath
3297
+ };
3298
+ }
3299
+ const automationToolDirectory = path.join(engineRoot, "Engine", "Binaries", "DotNET", "AutomationTool");
3300
+ const automationToolDll = path.join(automationToolDirectory, "AutomationTool.dll");
3301
+ if (!await context.fileExists(automationToolDll)) throw new EngineToolchainError(`AutomationTool.dll not found at ${automationToolDll}`);
3302
+ const bundledDotnet = await resolveBundledDotnet(engineRoot, context);
3303
+ if (bundledDotnet === void 0) return {
3304
+ args: [automationToolDll],
3305
+ command: "dotnet",
3306
+ cwd: automationToolDirectory,
3307
+ env: buildAutomationToolEnvironment(context.env)
3308
+ };
3309
+ return {
3310
+ args: [automationToolDll],
3311
+ command: bundledDotnet,
3312
+ cwd: automationToolDirectory,
3313
+ env: buildAutomationToolEnvironment(context.env, path.dirname(bundledDotnet))
3314
+ };
3315
+ }
3316
+ const VISUAL_STUDIO_ENVIRONMENT_KEYS = [
3317
+ "PkgDefApplicationConfigFile",
3318
+ "VSAPPIDDIR",
3319
+ "VSSKUEDITION",
3320
+ "VisualStudioDir",
3321
+ "VisualStudioEdition",
3322
+ "VisualStudioVersion"
3323
+ ];
3324
+ function buildAutomationToolEnvironment(parentEnvironment, dotnetDirectory) {
3325
+ const environment = { ...parentEnvironment };
3326
+ const clearedKeys = new Set(VISUAL_STUDIO_ENVIRONMENT_KEYS.map((key) => key.toUpperCase()));
3327
+ for (const key of Object.keys(environment)) if (clearedKeys.has(key.toUpperCase())) delete environment[key];
3328
+ if (dotnetDirectory === void 0) return environment;
3329
+ const pathKey = Object.keys(environment).find((key) => key.toUpperCase() === "PATH") ?? "PATH";
3330
+ const inheritedPath = environment[pathKey];
3331
+ environment[pathKey] = inheritedPath === void 0 || inheritedPath.length === 0 ? dotnetDirectory : `${dotnetDirectory}${path.delimiter}${inheritedPath}`;
3332
+ environment.DOTNET_MULTILEVEL_LOOKUP = "0";
3333
+ environment.DOTNET_ROLL_FORWARD = "LatestMajor";
3334
+ environment.DOTNET_ROOT = dotnetDirectory;
3335
+ return environment;
3336
+ }
3337
+ function editorBinariesDirs(platform, arch) {
3338
+ if (platform === "win32") return ["Win64"];
3339
+ if (platform === "darwin") return ["Mac"];
3340
+ return arch === "arm64" ? ["LinuxArm64", "Linux"] : ["Linux"];
3341
+ }
3342
+ async function resolveUnrealEditorCmd(engineRoot, context) {
3343
+ const binaryName = context.platform === "win32" ? "UnrealEditor-Cmd.exe" : "UnrealEditor-Cmd";
3344
+ const candidates = editorBinariesDirs(context.platform, context.arch).map((binariesDir) => path.join(engineRoot, "Engine", "Binaries", binariesDir, binaryName));
3345
+ for (const candidate of candidates) if (await context.fileExists(candidate)) return candidate;
3346
+ throw new EngineToolchainError(`${binaryName} not found under ${path.join(engineRoot, "Engine", "Binaries")}`);
3347
+ }
3348
+ async function resolveUnrealInsights(engineRoot, context) {
3349
+ const binariesRoot = path.join(engineRoot, "Engine", "Binaries");
3350
+ 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"));
3351
+ for (const candidate of candidates) if (await context.fileExists(candidate)) return candidate;
3352
+ return null;
3353
+ }
2774
3354
  function dotnetHostPlatformDirs(platform, arch) {
2775
- const osPrefix = platform === "darwin" ? "mac" : platform === "linux" ? "linux" : void 0;
3355
+ const osPrefix = platform === "darwin" ? "mac" : platform === "linux" ? "linux" : platform === "win32" ? "win" : void 0;
2776
3356
  if (!osPrefix) return [];
2777
3357
  const archDir = arch === "arm64" ? "arm64" : "x64";
2778
3358
  const otherArchDir = archDir === "arm64" ? "x64" : "arm64";
@@ -2788,8 +3368,9 @@ async function resolveBundledDotnet(engineRoot, context) {
2788
3368
  } catch {
2789
3369
  return;
2790
3370
  }
3371
+ const executableName = context.platform === "win32" ? "dotnet.exe" : "dotnet";
2791
3372
  for (const versionDir of versionDirs) for (const platformDir of platformDirs) {
2792
- const candidate = path.join(dotnetRoot, versionDir, platformDir, "dotnet");
3373
+ const candidate = path.join(dotnetRoot, versionDir, platformDir, executableName);
2793
3374
  if (await context.fileExists(candidate)) return candidate;
2794
3375
  }
2795
3376
  }
@@ -2799,6 +3380,425 @@ function buildUnrealBuildToolInvocation(engine, extraArgs) {
2799
3380
  command: engine.unrealBuildTool.command
2800
3381
  };
2801
3382
  }
3383
+ const RESOURCE_MIME_TYPE = "application/json";
3384
+ const REGISTRY_RESOURCE_URI = "uemcp://registry";
3385
+ const REGISTRY_PROJECT_RESOURCE_URI_TEMPLATE = "uemcp://registry/{projectId}";
3386
+ const ENGINES_RESOURCE_URI = "uemcp://engines";
3387
+ const WORKSPACE_PROJECT_RESOURCE_URI = "uemcp://workspace/project";
3388
+ function readRegistryResource(dependencies) {
3389
+ const entries = dependencies.registry.snapshot();
3390
+ return clampResourceList(entries, (visibleEntries, truncated) => ({
3391
+ entries: visibleEntries,
3392
+ totalCount: entries.length,
3393
+ truncated
3394
+ }));
3395
+ }
3396
+ function readRegistryProjectResource(projectId, dependencies) {
3397
+ const parsedProjectId = projectIdSchema.safeParse(projectId);
3398
+ if (!parsedProjectId.success) return {
3399
+ entry: null,
3400
+ found: false,
3401
+ projectId,
3402
+ reason: "The requested projectId is not a canonical 32-character Unreal ProjectID.",
3403
+ truncated: false
3404
+ };
3405
+ const entry = dependencies.registry.getProject(parsedProjectId.data);
3406
+ if (!entry) return {
3407
+ entry: null,
3408
+ found: false,
3409
+ projectId: parsedProjectId.data,
3410
+ reason: "No Unreal Editor is registered with this server for that projectId.",
3411
+ truncated: false
3412
+ };
3413
+ return clampResourcePayload({
3414
+ entry,
3415
+ found: true,
3416
+ projectId: entry.projectId,
3417
+ truncated: false
3418
+ }, () => ({
3419
+ entry: null,
3420
+ found: true,
3421
+ projectId: parsedProjectId.data,
3422
+ reason: "The registry entry exceeded the resource payload budget.",
3423
+ truncated: true
3424
+ }));
3425
+ }
3426
+ async function readEnginesResource(dependencies) {
3427
+ const context = dependencies.engineToolchainContext ?? createEngineToolchainContext();
3428
+ const installsDirectory = context.env.UE_INSTALLS_DIR?.trim() ?? null;
3429
+ let engineInstalls;
3430
+ try {
3431
+ engineInstalls = await listEngineInstalls(context);
3432
+ } catch (error) {
3433
+ if (!(error instanceof EngineToolchainError)) throw error;
3434
+ dependencies.logger.warn({ error }, "Failed to enumerate installed Unreal engines for the engines resource");
3435
+ return {
3436
+ engineInstalls: [],
3437
+ installsDirectory: installsDirectory && installsDirectory.length > 0 ? installsDirectory : null,
3438
+ reason: error.message,
3439
+ totalCount: 0,
3440
+ truncated: false
3441
+ };
3442
+ }
3443
+ return clampResourceList(engineInstalls, (visibleInstalls, truncated) => ({
3444
+ engineInstalls: visibleInstalls,
3445
+ installsDirectory: installsDirectory && installsDirectory.length > 0 ? installsDirectory : null,
3446
+ totalCount: engineInstalls.length,
3447
+ truncated
3448
+ }));
3449
+ }
3450
+ async function readWorkspaceProjectResource(dependencies) {
3451
+ const workingDirectory = dependencies.workingDirectory ?? process.cwd();
3452
+ try {
3453
+ const resolved = await dependencies.projectIdResolver.resolveProjectIdFromProjectPath(workingDirectory);
3454
+ return clampResourcePayload({
3455
+ configPath: resolved.configPath,
3456
+ projectId: resolved.projectId,
3457
+ projectPath: resolved.projectPath,
3458
+ resolved: true,
3459
+ truncated: false,
3460
+ workingDirectory
3461
+ }, () => ({
3462
+ reason: "The resolved workspace project exceeded the resource payload budget.",
3463
+ resolved: false,
3464
+ truncated: true,
3465
+ workingDirectory
3466
+ }));
3467
+ } catch (error) {
3468
+ if (!(error instanceof ProjectIdResolutionError)) throw error;
3469
+ return {
3470
+ configPath: error.filePath,
3471
+ reason: error.reason,
3472
+ resolved: false,
3473
+ truncated: false,
3474
+ workingDirectory
3475
+ };
3476
+ }
3477
+ }
3478
+ function registerMcpResources(server, dependencies) {
3479
+ server.registerResource("registry", REGISTRY_RESOURCE_URI, {
3480
+ description: "Every Unreal Editor currently registered with this server, straight from the in-memory registry. Attach this instead of calling list_registered_projects when you only need to know which editors are live and which projectId to pass to the tools. Reads no files and never contacts an editor.",
3481
+ mimeType: RESOURCE_MIME_TYPE,
3482
+ title: "Registered Unreal Editors"
3483
+ }, (uri) => buildResourceContents(uri, readRegistryResource(dependencies)));
3484
+ server.registerResource("registry-project", new ResourceTemplate(REGISTRY_PROJECT_RESOURCE_URI_TEMPLATE, {
3485
+ complete: { projectId: (value) => completeRegisteredProjectId(value, dependencies) },
3486
+ list: () => listRegistryProjectResources(dependencies)
3487
+ }), {
3488
+ description: "The registration for a single Unreal Editor, addressed by canonical 32-character ProjectID. Reports found: false with a reason rather than failing when the projectId is unknown or malformed.",
3489
+ mimeType: RESOURCE_MIME_TYPE,
3490
+ title: "Registered Unreal Editor"
3491
+ }, (uri, variables) => buildResourceContents(uri, readRegistryProjectResource(readTemplateVariable(variables.projectId), dependencies)));
3492
+ server.registerResource("engines", ENGINES_RESOURCE_URI, {
3493
+ description: "Unreal Engine installs discovered under UE_INSTALLS_DIR, each with its version, engine root, and whether UnrealBuildTool resolved inside it. Source builds and GUID engine associations are excluded because the build tools cannot resolve them either.",
3494
+ mimeType: RESOURCE_MIME_TYPE,
3495
+ title: "Installed Unreal Engines"
3496
+ }, async (uri) => buildResourceContents(uri, await readEnginesResource(dependencies)));
3497
+ server.registerResource("workspace-project", WORKSPACE_PROJECT_RESOURCE_URI, {
3498
+ description: "The Unreal project identity of the server's working directory, resolved from Config/DefaultGame.ini. Reports resolved: false with a reason when the working directory is not an Unreal project.",
3499
+ mimeType: RESOURCE_MIME_TYPE,
3500
+ title: "Workspace Unreal Project"
3501
+ }, async (uri) => buildResourceContents(uri, await readWorkspaceProjectResource(dependencies)));
3502
+ }
3503
+ function listRegistryProjectResources(dependencies) {
3504
+ return { resources: dependencies.registry.snapshot().map((entry) => ({
3505
+ description: `Registration for ${entry.projectName} at ${entry.projectPath}`,
3506
+ mimeType: RESOURCE_MIME_TYPE,
3507
+ name: entry.projectId,
3508
+ title: entry.projectName,
3509
+ uri: buildRegistryProjectUri(entry.projectId)
3510
+ })) };
3511
+ }
3512
+ function completeRegisteredProjectId(value, dependencies) {
3513
+ const prefix = value.trim().toLowerCase();
3514
+ return dependencies.registry.snapshot().map((entry) => entry.projectId).filter((projectId) => projectId.startsWith(prefix));
3515
+ }
3516
+ function buildRegistryProjectUri(projectId) {
3517
+ return `${REGISTRY_RESOURCE_URI}/${projectId}`;
3518
+ }
3519
+ function buildResourceContents(uri, payload) {
3520
+ return { contents: [{
3521
+ mimeType: RESOURCE_MIME_TYPE,
3522
+ text: serializeResourcePayload(payload),
3523
+ uri: uri.href
3524
+ }] };
3525
+ }
3526
+ function readTemplateVariable(value) {
3527
+ if (Array.isArray(value)) return value[0] ?? "";
3528
+ return value ?? "";
3529
+ }
3530
+ function clampResourceList(items, build) {
3531
+ const fullPayload = build(items, false);
3532
+ if (measureResourcePayloadBytes(fullPayload) <= 65536) return fullPayload;
3533
+ let smallestRejected = items.length;
3534
+ let largestAccepted = 0;
3535
+ while (largestAccepted + 1 < smallestRejected) {
3536
+ const candidateCount = Math.floor((largestAccepted + smallestRejected) / 2);
3537
+ if (measureResourcePayloadBytes(build(items.slice(0, candidateCount), true)) <= 65536) {
3538
+ largestAccepted = candidateCount;
3539
+ continue;
3540
+ }
3541
+ smallestRejected = candidateCount;
3542
+ }
3543
+ return build(items.slice(0, largestAccepted), true);
3544
+ }
3545
+ function clampResourcePayload(payload, buildMinimal) {
3546
+ if (measureResourcePayloadBytes(payload) <= 65536) return payload;
3547
+ return buildMinimal();
3548
+ }
3549
+ function measureResourcePayloadBytes(payload) {
3550
+ return Buffer$1.byteLength(serializeResourcePayload(payload), "utf8");
3551
+ }
3552
+ function serializeResourcePayload(payload) {
3553
+ return JSON.stringify(payload, null, 2);
3554
+ }
3555
+ //#endregion
3556
+ //#region src/mcpPrompts.ts
3557
+ const projectIdPromptArgument = projectIdSchema.describe("Canonical 32-character Unreal ProjectID of the target editor. Read uemcp://registry to find one.");
3558
+ const diagnoseCrashPromptArgs = { projectId: projectIdPromptArgument };
3559
+ const diagnoseBridgeDisconnectPromptArgs = { projectPath: z.string().trim().min(1).optional().describe("Filesystem path of the Unreal project directory or .uproject, when it is known.") };
3560
+ const smokeTestBridgePromptArgs = { projectId: projectIdPromptArgument };
3561
+ const addGameplayFeaturePromptArgs = {
3562
+ className: bridgeCreateCppClassRequestSchema.shape.className.describe("Name of the C++ class to create, without the type prefix (for example PatrolComponent)."),
3563
+ moduleName: bridgeCreateCppClassRequestSchema.shape.moduleName.describe("Target module for the new class. Omitted means the project’s primary game module."),
3564
+ parentClass: bridgeCreateCppClassRequestSchema.shape.parentClass.describe("Parent class to derive from (for example Actor, ActorComponent, or a project class)."),
3565
+ projectId: projectIdPromptArgument
3566
+ };
3567
+ function buildDiagnoseCrashPrompt(args) {
3568
+ return buildPromptResult("Investigate the most recent Unreal Editor crash for a registered project.", [
3569
+ `Investigate the most recent crash for Unreal project ${args.projectId}.`,
3570
+ "",
3571
+ "Work in this order and stop as soon as the cause is established:",
3572
+ `1. Read ${buildRegistryProjectUri(args.projectId)}, then call ping_project. A registration that is present but unreachable is the signature of a crash: deregistration only runs on a clean shutdown, so a killed process leaves its entry behind. An absent entry proves nothing on its own — the editor may never have registered, or a prior health check may already have evicted it.`,
3573
+ "2. Call get_crash_callstack for the project. Read the newest record first. Note the crash type, the assert or exception text, and the topmost project frames — engine frames below them are usually the victim, not the cause.",
3574
+ "3. Call get_editor_state to see what the editor was doing (PIE state, loaded level, current selection). A crash during PIE and a crash while idle have different suspects.",
3575
+ "4. Call stream_output_log with maxEvents around 50 and minVerbosity of warning. The first call establishes the subscription and returns no events; call it again with the returned cursor to drain. Look for the warnings immediately preceding the crash.",
3576
+ "5. If the callstack names a Blueprint, call get_blueprint_compile_errors for it; if it names a C++ symbol, call get_compile_status to check whether the running binary matches the current source.",
3577
+ "",
3578
+ "Report the crash type, the failing frame, the evidence that supports it, and the smallest change that would address it. Say so plainly if the callstack is inconclusive rather than guessing."
3579
+ ].join("\n"));
3580
+ }
3581
+ function buildDiagnoseBridgeDisconnectPrompt(args) {
3582
+ const projectPathStep = args.projectPath ? `2. Call get_project_id with projectPath ${args.projectPath} to get the canonical ProjectID, then compare it to the registry entries from step 1. A mismatch means the editor and this server are reading different Config/DefaultGame.ini files.` : "2. Ask for the project path if you need one, then call get_project_id with it and compare the result to the registry entries from step 1. A mismatch means the editor and this server are reading different Config/DefaultGame.ini files.";
3583
+ return buildPromptResult("Diagnose why this server cannot reach a running Unreal Editor over the MCPBridge.", [
3584
+ "The MCP server cannot reach a running Unreal Editor. Find which leg of the connection is broken before changing anything.",
3585
+ "",
3586
+ "The two processes meet in two places: the editor registers itself over this server’s HTTP API, and this server opens a TCP bridge socket to the port the editor recorded in that registration.",
3587
+ "",
3588
+ `1. Read ${REGISTRY_RESOURCE_URI}. No entry at all means registration never landed or was evicted. An entry with a stale lastSeenAt means registration landed but health checks are failing.`,
3589
+ projectPathStep,
3590
+ "3. Call ping_project with the registered projectId. reachable: false with registrationRemoved: true means the editor’s TCP listener is gone — the editor died, or the plugin lost its port. Note that this call evicts a stale registration, so read the registry resource before calling it, not after.",
3591
+ "4. If the registration looks healthy but one specific tool fails, call stream_output_log and drain with the returned cursor. Plugin-side faults are logged under the LogMCPBridgeRuntime category: failures resolving the ProjectID, failures binding the bridge socket, and rejected registrations all name themselves there.",
3592
+ "",
3593
+ "Common causes, in rough order of likelihood: the editor was closed or crashed and the registration is stale; the editor is open on a different project than the one being asked about; the project directory was copied, so two directories share one ProjectID and fight over one registration; the plugin is pointed at a different server address.",
3594
+ "",
3595
+ "Report which leg is broken and the single smallest action that fixes it. Do not propose code changes until the diagnosis is complete — this failure is usually configuration, not a bug."
3596
+ ].join("\n"));
3597
+ }
3598
+ function buildSmokeTestBridgePrompt(args) {
3599
+ return buildPromptResult("Prove the MCPBridge works end to end for a registered project.", [
3600
+ `Verify the MCPBridge is healthy for Unreal project ${args.projectId}. This is green-path verification, not fault diagnosis.`,
3601
+ "",
3602
+ `1. Read ${buildRegistryProjectUri(args.projectId)}. Expect a registration whose lastSeenAt is recent and whose port is in the 7000-7100 range.`,
3603
+ "2. Call ping_project. Expect reachable: true, registrationRemoved: false, and attemptsUsed: 1.",
3604
+ "3. Call get_editor_state. Expect the loaded level and PIE state of the editor actually open in front of the user.",
3605
+ "4. Exercise log streaming, which is the one path that covers subscribe, emit, drain, and unsubscribe together:",
3606
+ " a. Call stream_output_log with categoryAllow of [\"LogTemp\"], textContains of \"smoke-bridge-output\", and maxEvents of 10. The first call returns an empty events array and a cursor. That is correct — the subscription has nothing buffered yet.",
3607
+ " b. Call exec_console_command with the command: log LogTemp Warning smoke-bridge-output ping",
3608
+ " c. Call stream_output_log again with the same arguments plus the cursor from step 4a. Expect at least one event whose message contains smoke-bridge-output, and a nextCursor that has advanced.",
3609
+ "",
3610
+ "Report each step as pass or fail with the value that proves it. If any step fails, stop and say which one — the diagnose_bridge_disconnect prompt takes it from there. Treat an empty first drain as expected, never as a failure."
3611
+ ].join("\n"));
3612
+ }
3613
+ function buildAddGameplayFeaturePrompt(args) {
3614
+ const moduleClause = args.moduleName ? ` in module ${args.moduleName}` : "";
3615
+ return buildPromptResult("Add a C++ gameplay class to a live Unreal project and take it through compile and test.", [
3616
+ `Add a C++ class ${args.className} deriving from ${args.parentClass}${moduleClause} to Unreal project ${args.projectId}, then get it compiling and tested.`,
3617
+ "",
3618
+ "Every step here is asynchronous. Each start returns a job id; do not assume success from the start call alone.",
3619
+ "",
3620
+ `1. Call create_cpp_class with className ${args.className}, parentClass ${args.parentClass}${args.moduleName ? `, moduleName ${args.moduleName}` : ""}. It returns a createClassJobId immediately.`,
3621
+ "2. Wait for it with wait_for_job using kind create_class, or poll get_create_class_status. Do not edit the generated files until it reports completion — the editor is still writing them.",
3622
+ "3. Read the generated header and source, then write the actual implementation. A generated class compiles but does nothing.",
3623
+ "4. Call trigger_live_compile, then wait_for_job with kind compile. On failure, read the diagnostics from get_compile_status, fix the source, and compile again. Do not proceed past a failing compile.",
3624
+ "5. Call list_automation_tests to find the tests covering this area, then run_automation_tests, then wait_for_job with kind test_run. Report which tests ran and which failed.",
3625
+ "6. Call save_dirty_packages once the work is done so nothing is left unsaved in the editor.",
3626
+ "",
3627
+ "If the project has no automation tests covering the new class, say so rather than reporting the step as passed."
3628
+ ].join("\n"));
3629
+ }
3630
+ function registerMcpPrompts(server) {
3631
+ server.registerPrompt("diagnose_crash", {
3632
+ argsSchema: diagnoseCrashPromptArgs,
3633
+ description: "Walk the most recent Unreal Editor crash for a project back to its cause using the crash callstack, editor state, and output log.",
3634
+ title: "Diagnose Unreal Crash"
3635
+ }, (args) => buildDiagnoseCrashPrompt(args));
3636
+ server.registerPrompt("diagnose_bridge_disconnect", {
3637
+ argsSchema: diagnoseBridgeDisconnectPromptArgs,
3638
+ description: "Find which leg of the MCPBridge is broken when this server cannot reach a running Unreal Editor — missing registration, ProjectID mismatch, dead listener, or a plugin-side failure.",
3639
+ title: "Diagnose Bridge Disconnect"
3640
+ }, (args) => buildDiagnoseBridgeDisconnectPrompt(args));
3641
+ server.registerPrompt("smoke_test_bridge", {
3642
+ argsSchema: smokeTestBridgePromptArgs,
3643
+ description: "Prove the MCPBridge round-trips end to end for a project: registration, ping, editor state, and a full log subscribe-emit-drain cycle.",
3644
+ title: "Smoke-Test the Bridge"
3645
+ }, (args) => buildSmokeTestBridgePrompt(args));
3646
+ server.registerPrompt("add_gameplay_feature", {
3647
+ argsSchema: addGameplayFeaturePromptArgs,
3648
+ description: "Add a C++ gameplay class to a live Unreal project and drive it through class creation, implementation, live compile, and automation tests.",
3649
+ title: "Add Gameplay Feature"
3650
+ }, (args) => buildAddGameplayFeaturePrompt(args));
3651
+ }
3652
+ function buildPromptResult(description, text) {
3653
+ return {
3654
+ description,
3655
+ messages: [{
3656
+ content: {
3657
+ text,
3658
+ type: "text"
3659
+ },
3660
+ role: "user"
3661
+ }]
3662
+ };
3663
+ }
3664
+ //#endregion
3665
+ //#region src/buildDiagnostics.ts
3666
+ const CLANG_DIAGNOSTIC_PATTERN = /^(?<file>.+?):(?<line>\d+):(?:(?<column>\d+):)?\s+(?<severity>fatal error|error|warning):\s+(?<message>.*)$/u;
3667
+ const MSVC_DIAGNOSTIC_PATTERN = /^(?<file>.+?)\((?<line>\d+)(?:,\d+)?\):\s+(?<severity>fatal error|error|warning)\s+(?<code>[A-Z]+\d+):\s+(?<message>.*)$/u;
3668
+ const CLANG_TIDY_PATTERN = /^(?<file>.+?):(?<line>\d+):(?<column>\d+):\s+(?<severity>error|warning):\s+(?<message>.*?)(?:\s+\[(?<check>[^\]]+)\])?$/u;
3669
+ const COOK_LINE_PREFIX_PATTERN = /^(?:\[[^\]]*\]\s*)*(?:[A-Za-z]+Helper:\s*[^:]*:\s*)?/u;
3670
+ const COOK_DIAGNOSTIC_PATTERN = /^(?<category>[A-Za-z_][A-Za-z0-9_]*):\s+(?<severity>Error|Warning)(?::)?\s+(?<message>.+)$/u;
3671
+ const UAT_DIAGNOSTIC_PATTERN = /^(?<severity>ERROR|WARNING):\s+(?<message>.+)$/u;
3672
+ function diagnosticKey(diagnostic) {
3673
+ return `${diagnostic.file ?? ""}|${diagnostic.line ?? ""}|${diagnostic.message}`;
3674
+ }
3675
+ function matchCookDiagnostic(rawLine) {
3676
+ const line = rawLine.trim().replace(COOK_LINE_PREFIX_PATTERN, "").trim();
3677
+ if (line.length === 0) return null;
3678
+ const categoryMatch = COOK_DIAGNOSTIC_PATTERN.exec(line);
3679
+ if (categoryMatch?.groups) {
3680
+ const { category, message, severity } = categoryMatch.groups;
3681
+ if (category === void 0 || message === void 0 || severity === void 0) return null;
3682
+ return {
3683
+ isError: severity === "Error",
3684
+ message: `${category}: ${message.trim()}`
3685
+ };
3686
+ }
3687
+ const uatMatch = UAT_DIAGNOSTIC_PATTERN.exec(line);
3688
+ if (!uatMatch?.groups) return null;
3689
+ const { message, severity } = uatMatch.groups;
3690
+ if (message === void 0 || severity === void 0) return null;
3691
+ return {
3692
+ isError: severity === "ERROR",
3693
+ message: message.trim()
3694
+ };
3695
+ }
3696
+ var CookDiagnosticCollector = class {
3697
+ maxEntriesPerSeverity;
3698
+ errors = [];
3699
+ warnings = [];
3700
+ seenErrors = /* @__PURE__ */ new Set();
3701
+ seenWarnings = /* @__PURE__ */ new Set();
3702
+ pending = "";
3703
+ truncatedFlag = false;
3704
+ constructor(maxEntriesPerSeverity) {
3705
+ this.maxEntriesPerSeverity = maxEntriesPerSeverity;
3706
+ }
3707
+ push(chunk) {
3708
+ const lines = (this.pending + chunk).split(/\r?\n/u);
3709
+ this.pending = lines.pop() ?? "";
3710
+ for (const line of lines) this.pushLine(line);
3711
+ }
3712
+ flush() {
3713
+ if (this.pending.length === 0) return;
3714
+ const remaining = this.pending;
3715
+ this.pending = "";
3716
+ this.pushLine(remaining);
3717
+ }
3718
+ snapshot() {
3719
+ return {
3720
+ errorCount: this.errors.length,
3721
+ errors: [...this.errors],
3722
+ truncated: this.truncatedFlag,
3723
+ warningCount: this.warnings.length,
3724
+ warnings: [...this.warnings]
3725
+ };
3726
+ }
3727
+ pushLine(rawLine) {
3728
+ const matched = matchCookDiagnostic(rawLine);
3729
+ if (!matched) return;
3730
+ const diagnostic = { message: matched.message };
3731
+ const target = matched.isError ? this.errors : this.warnings;
3732
+ const seen = matched.isError ? this.seenErrors : this.seenWarnings;
3733
+ const key = diagnosticKey(diagnostic);
3734
+ if (seen.has(key)) return;
3735
+ seen.add(key);
3736
+ if (target.length >= this.maxEntriesPerSeverity) {
3737
+ this.truncatedFlag = true;
3738
+ return;
3739
+ }
3740
+ target.push(diagnostic);
3741
+ }
3742
+ };
3743
+ function parseBuildDiagnostics(output) {
3744
+ const errors = [];
3745
+ const warnings = [];
3746
+ const seenErrors = /* @__PURE__ */ new Set();
3747
+ const seenWarnings = /* @__PURE__ */ new Set();
3748
+ for (const rawLine of output.split(/\r?\n/u)) {
3749
+ const line = rawLine.trim();
3750
+ if (line.length === 0) continue;
3751
+ const match = CLANG_DIAGNOSTIC_PATTERN.exec(line) ?? MSVC_DIAGNOSTIC_PATTERN.exec(line);
3752
+ if (!match?.groups) continue;
3753
+ const groups = match.groups;
3754
+ const { file, line: lineText, message, severity } = groups;
3755
+ if (file === void 0 || lineText === void 0 || message === void 0 || severity === void 0) continue;
3756
+ const code = typeof groups.code === "string" ? `${groups.code}: ` : "";
3757
+ const diagnostic = {
3758
+ file,
3759
+ line: Number.parseInt(lineText, 10),
3760
+ message: `${code}${message}`.trim()
3761
+ };
3762
+ const isError = severity === "error" || severity === "fatal error";
3763
+ const target = isError ? errors : warnings;
3764
+ const seen = isError ? seenErrors : seenWarnings;
3765
+ const key = diagnosticKey(diagnostic);
3766
+ if (seen.has(key)) continue;
3767
+ seen.add(key);
3768
+ target.push(diagnostic);
3769
+ }
3770
+ return {
3771
+ errorCount: errors.length,
3772
+ errors,
3773
+ warningCount: warnings.length,
3774
+ warnings
3775
+ };
3776
+ }
3777
+ function parseClangTidyFindings(output) {
3778
+ const findings = [];
3779
+ const seen = /* @__PURE__ */ new Set();
3780
+ for (const rawLine of output.split(/\r?\n/u)) {
3781
+ const match = CLANG_TIDY_PATTERN.exec(rawLine.trim());
3782
+ if (!match?.groups) continue;
3783
+ const groups = match.groups;
3784
+ const { file, line, message } = groups;
3785
+ if (file === void 0 || line === void 0 || message === void 0) continue;
3786
+ const severity = groups.severity === "error" ? "error" : "warning";
3787
+ const finding = {
3788
+ check: groups.check ?? "unknown",
3789
+ file,
3790
+ line: Number.parseInt(line, 10),
3791
+ message: message.trim(),
3792
+ severity
3793
+ };
3794
+ if (typeof groups.column === "string") finding.column = Number.parseInt(groups.column, 10);
3795
+ const key = `${finding.file}|${finding.line}|${finding.column ?? ""}|${finding.check}|${finding.message}`;
3796
+ if (seen.has(key)) continue;
3797
+ seen.add(key);
3798
+ findings.push(finding);
3799
+ }
3800
+ return findings;
3801
+ }
2802
3802
  //#endregion
2803
3803
  //#region src/moduleScaffold.ts
2804
3804
  const MODULE_DESCRIPTOR_TYPES = [
@@ -3013,11 +4013,393 @@ function isPlainObject(value) {
3013
4013
  function getErrorCode(error) {
3014
4014
  if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
3015
4015
  }
4016
+ const PACKAGE_JOB_MAX_OUTPUT_BYTES = 2097152;
4017
+ var PackageJobCapacityError = class extends Error {
4018
+ constructor(message) {
4019
+ super(message);
4020
+ this.name = "PackageJobCapacityError";
4021
+ }
4022
+ };
4023
+ var PackageJobRegistry = class {
4024
+ jobs = /* @__PURE__ */ new Map();
4025
+ start(options) {
4026
+ this.evictFinishedJobs();
4027
+ const projectKey = projectComparisonKey(options.projectPath);
4028
+ this.assertCapacity(projectKey, options.projectPath);
4029
+ const record = {
4030
+ abortController: new AbortController(),
4031
+ args: [...options.args],
4032
+ cancelled: false,
4033
+ collector: new CookDiagnosticCollector(200),
4034
+ command: options.command,
4035
+ kind: options.kind,
4036
+ outputTail: "",
4037
+ outputTruncated: false,
4038
+ packageJobId: randomUUID().replaceAll("-", ""),
4039
+ projectKey,
4040
+ projectPath: options.projectPath,
4041
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4042
+ state: "running",
4043
+ timedOut: false
4044
+ };
4045
+ if (options.platform !== void 0) record.platform = options.platform;
4046
+ this.jobs.set(record.packageJobId, record);
4047
+ this.run(record, options);
4048
+ return toPackageJobStatus(record);
4049
+ }
4050
+ get(packageJobId) {
4051
+ const record = this.jobs.get(packageJobId);
4052
+ return record ? toPackageJobStatus(record) : null;
4053
+ }
4054
+ cancel(packageJobId) {
4055
+ const record = this.jobs.get(packageJobId);
4056
+ if (!record) return {
4057
+ cancelled: false,
4058
+ state: "unknown_job"
4059
+ };
4060
+ if (record.state !== "running") return {
4061
+ cancelled: false,
4062
+ state: record.state
4063
+ };
4064
+ record.cancelled = true;
4065
+ record.abortController.abort();
4066
+ return {
4067
+ cancelled: true,
4068
+ state: record.state
4069
+ };
4070
+ }
4071
+ async run(record, options) {
4072
+ try {
4073
+ const result = await options.runProcess({
4074
+ args: options.args,
4075
+ command: options.command,
4076
+ cwd: options.cwd,
4077
+ ...options.env ? { env: options.env } : {},
4078
+ maxOutputBytes: PACKAGE_JOB_MAX_OUTPUT_BYTES,
4079
+ onOutput: (chunk) => {
4080
+ record.collector.push(chunk);
4081
+ appendOutputTail(record, chunk);
4082
+ },
4083
+ signal: record.abortController.signal,
4084
+ timeoutMs: options.timeoutMs
4085
+ });
4086
+ record.collector.flush();
4087
+ record.durationMs = result.durationMs;
4088
+ record.exitCode = result.exitCode;
4089
+ record.outputTail = result.combinedOutput.slice(-8e3);
4090
+ record.outputTruncated = record.outputTruncated || result.truncated || result.combinedOutput.length > 8e3;
4091
+ record.timedOut = result.timedOut;
4092
+ record.state = resolveTerminalState(record, result.exitCode, result.timedOut);
4093
+ } catch (error) {
4094
+ record.collector.flush();
4095
+ record.failureReason = error instanceof Error ? error.message : String(error);
4096
+ record.state = record.cancelled ? "cancelled" : "failed";
4097
+ options.logger.error({
4098
+ error,
4099
+ kind: record.kind,
4100
+ packageJobId: record.packageJobId,
4101
+ projectPath: record.projectPath
4102
+ }, "Package job process failed to run");
4103
+ } finally {
4104
+ record.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
4105
+ }
4106
+ }
4107
+ assertCapacity(projectKey, projectPath) {
4108
+ let activeCount = 0;
4109
+ let activeForProject = 0;
4110
+ for (const record of this.jobs.values()) {
4111
+ if (record.state !== "running") continue;
4112
+ activeCount += 1;
4113
+ if (record.projectKey === projectKey) activeForProject += 1;
4114
+ }
4115
+ 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.`);
4116
+ 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.`);
4117
+ }
4118
+ evictFinishedJobs() {
4119
+ if (this.jobs.size < 20) return;
4120
+ for (const [packageJobId, record] of this.jobs) {
4121
+ if (this.jobs.size < 20) return;
4122
+ if (record.state !== "running") this.jobs.delete(packageJobId);
4123
+ }
4124
+ }
4125
+ };
4126
+ function projectComparisonKey(projectPath) {
4127
+ const resolved = path.resolve(projectPath.trim());
4128
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
4129
+ }
4130
+ function appendOutputTail(record, chunk) {
4131
+ const outputTail = record.outputTail + chunk;
4132
+ if (outputTail.length > 8e3) record.outputTruncated = true;
4133
+ record.outputTail = outputTail.slice(-8e3);
4134
+ }
4135
+ function resolveTerminalState(record, exitCode, timedOut) {
4136
+ if (record.cancelled) return "cancelled";
4137
+ return exitCode === 0 && !timedOut ? "succeeded" : "failed";
4138
+ }
4139
+ function toPackageJobStatus(record) {
4140
+ const diagnostics = record.collector.snapshot();
4141
+ const status = {
4142
+ args: [...record.args],
4143
+ command: record.command,
4144
+ diagnosticsTruncated: diagnostics.truncated,
4145
+ errorCount: diagnostics.errorCount,
4146
+ errors: diagnostics.errors,
4147
+ kind: record.kind,
4148
+ outputTail: record.outputTail,
4149
+ outputTruncated: record.outputTruncated,
4150
+ packageJobId: record.packageJobId,
4151
+ projectPath: record.projectPath,
4152
+ startedAt: record.startedAt,
4153
+ state: record.state,
4154
+ timedOut: record.timedOut,
4155
+ warningCount: diagnostics.warningCount,
4156
+ warnings: diagnostics.warnings
4157
+ };
4158
+ if (record.durationMs !== void 0) status.durationMs = record.durationMs;
4159
+ if (record.exitCode !== void 0) status.exitCode = record.exitCode;
4160
+ if (record.failureReason !== void 0) status.failureReason = record.failureReason;
4161
+ if (record.finishedAt !== void 0) status.finishedAt = record.finishedAt;
4162
+ if (record.platform !== void 0) status.platform = record.platform;
4163
+ return status;
4164
+ }
4165
+ //#endregion
4166
+ //#region src/traceAnalysis.ts
4167
+ const SECONDS_TO_MS = 1e3;
4168
+ const FRAME_ROOT_TIMER_NAMES = /* @__PURE__ */ new Set(["FEngineLoop::Tick"]);
4169
+ function parseCsvRows(text) {
4170
+ const [headerLine, ...dataLines] = splitCsvLines(text);
4171
+ if (headerLine === void 0) return [];
4172
+ const headers = parseCsvLine(headerLine);
4173
+ return dataLines.map((line) => {
4174
+ const values = parseCsvLine(line);
4175
+ const row = {};
4176
+ headers.forEach((header, index) => {
4177
+ row[header] = values[index] ?? "";
4178
+ });
4179
+ return row;
4180
+ });
4181
+ }
4182
+ function splitCsvLines(text) {
4183
+ return text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
4184
+ }
4185
+ function parseCsvLine(line) {
4186
+ const values = [];
4187
+ let current = "";
4188
+ let inQuotes = false;
4189
+ for (let index = 0; index < line.length; index += 1) {
4190
+ const character = line[index];
4191
+ if (character === "\"") {
4192
+ if (inQuotes && line[index + 1] === "\"") {
4193
+ current += "\"";
4194
+ index += 1;
4195
+ continue;
4196
+ }
4197
+ inQuotes = !inQuotes;
4198
+ continue;
4199
+ }
4200
+ if (character === "," && !inQuotes) {
4201
+ values.push(current.trim());
4202
+ current = "";
4203
+ continue;
4204
+ }
4205
+ current += character ?? "";
4206
+ }
4207
+ values.push(current.trim());
4208
+ return values;
4209
+ }
4210
+ function findColumn(row, ...candidates) {
4211
+ const headers = Object.keys(row);
4212
+ for (const candidate of candidates) {
4213
+ const match = headers.find((header) => header.toLowerCase() === candidate.toLowerCase());
4214
+ if (match !== void 0) return match;
4215
+ }
4216
+ for (const candidate of candidates) {
4217
+ const match = headers.find((header) => header.toLowerCase().includes(candidate.toLowerCase()));
4218
+ if (match !== void 0) return match;
4219
+ }
4220
+ }
4221
+ function readNumber(value) {
4222
+ if (value === void 0 || value.length === 0) return;
4223
+ const parsed = Number.parseFloat(value);
4224
+ return Number.isFinite(parsed) ? parsed : void 0;
4225
+ }
4226
+ function summarizeTimerStatistics(rows, maxScopes) {
4227
+ const [firstRow] = rows;
4228
+ if (firstRow === void 0) return {
4229
+ byExclusiveTime: [],
4230
+ byInclusiveTime: [],
4231
+ totalScopeCount: 0,
4232
+ truncated: false
4233
+ };
4234
+ const nameColumn = findColumn(firstRow, "Name", "TimerName");
4235
+ const inclusiveColumn = findColumn(firstRow, "Incl", "TotalInclusiveTime", "InclusiveTime");
4236
+ const exclusiveColumn = findColumn(firstRow, "Excl", "TotalExclusiveTime", "ExclusiveTime");
4237
+ const countColumn = findColumn(firstRow, "Count", "InstanceCount");
4238
+ if (nameColumn === void 0 || inclusiveColumn === void 0 || exclusiveColumn === void 0) return {
4239
+ byExclusiveTime: [],
4240
+ byInclusiveTime: [],
4241
+ totalScopeCount: 0,
4242
+ truncated: false
4243
+ };
4244
+ const scopes = [];
4245
+ for (const row of rows) {
4246
+ const name = row[nameColumn];
4247
+ const inclusive = readNumber(row[inclusiveColumn]);
4248
+ const exclusive = readNumber(row[exclusiveColumn]);
4249
+ if (name === void 0 || name.length === 0 || inclusive === void 0 || exclusive === void 0) continue;
4250
+ const scope = {
4251
+ exclusiveTimeMs: exclusive * SECONDS_TO_MS,
4252
+ inclusiveTimeMs: inclusive * SECONDS_TO_MS,
4253
+ name
4254
+ };
4255
+ const count = countColumn === void 0 ? void 0 : readNumber(row[countColumn]);
4256
+ if (count !== void 0) scope.count = count;
4257
+ scopes.push(scope);
4258
+ }
4259
+ const byInclusiveTime = [...scopes].sort((left, right) => right.inclusiveTimeMs - left.inclusiveTimeMs);
4260
+ return {
4261
+ byExclusiveTime: [...scopes].sort((left, right) => right.exclusiveTimeMs - left.exclusiveTimeMs).slice(0, maxScopes),
4262
+ byInclusiveTime: byInclusiveTime.slice(0, maxScopes),
4263
+ totalScopeCount: scopes.length,
4264
+ truncated: scopes.length > maxScopes
4265
+ };
4266
+ }
4267
+ function isGpuThread(threadName) {
4268
+ return threadName.toUpperCase().includes("GPU");
4269
+ }
4270
+ async function summarizeTimingEventStream(lines, options) {
4271
+ let headers;
4272
+ let threadIndex = -1;
4273
+ let timerIndex = -1;
4274
+ let durationIndex = -1;
4275
+ let startIndex = -1;
4276
+ let depthIndex = -1;
4277
+ let cpuTimeMs = 0;
4278
+ let gpuTimeMs = 0;
4279
+ const frameDurationsMs = [];
4280
+ const worstHitches = [];
4281
+ let hitchCount = 0;
4282
+ for await (const rawLine of lines) {
4283
+ const line = rawLine.trim();
4284
+ if (line.length === 0) continue;
4285
+ const values = parseCsvLine(line);
4286
+ if (headers === void 0) {
4287
+ headers = values;
4288
+ const headerRow = Object.fromEntries(headers.map((header) => [header, ""]));
4289
+ threadIndex = columnIndex(headers, findColumn(headerRow, "ThreadName", "Thread", "TrackName"));
4290
+ timerIndex = columnIndex(headers, findColumn(headerRow, "TimerName"));
4291
+ durationIndex = columnIndex(headers, findColumn(headerRow, "Duration", "InclusiveTime"));
4292
+ startIndex = columnIndex(headers, findColumn(headerRow, "StartTime", "Start"));
4293
+ depthIndex = columnIndex(headers, findColumn(headerRow, "Depth"));
4294
+ if (threadIndex < 0 || durationIndex < 0) break;
4295
+ continue;
4296
+ }
4297
+ const duration = readNumber(values[durationIndex]);
4298
+ const threadName = values[threadIndex];
4299
+ if (duration === void 0 || threadName === void 0) continue;
4300
+ if ((depthIndex < 0 ? 0 : readNumber(values[depthIndex]) ?? 0) !== 0) continue;
4301
+ const durationMs = duration * SECONDS_TO_MS;
4302
+ if (isGpuThread(threadName)) {
4303
+ gpuTimeMs += durationMs;
4304
+ continue;
4305
+ }
4306
+ cpuTimeMs += durationMs;
4307
+ if (timerIndex < 0 || !FRAME_ROOT_TIMER_NAMES.has(values[timerIndex] ?? "")) continue;
4308
+ frameDurationsMs.push(durationMs);
4309
+ if (durationMs < options.hitchThresholdMs) continue;
4310
+ hitchCount += 1;
4311
+ retainWorstHitch(worstHitches, options.maxHitches, {
4312
+ durationMs,
4313
+ startTimeMs: (startIndex < 0 ? 0 : readNumber(values[startIndex]) ?? 0) * SECONDS_TO_MS
4314
+ });
4315
+ }
4316
+ return {
4317
+ frames: summarizeFrameDurations(frameDurationsMs, worstHitches, hitchCount, options),
4318
+ threads: {
4319
+ cpuTimeMs,
4320
+ gpuTimeMs
4321
+ }
4322
+ };
4323
+ }
4324
+ function retainWorstHitch(worstHitches, maxHitches, hitch) {
4325
+ if (worstHitches.length >= maxHitches) {
4326
+ const weakest = worstHitches[worstHitches.length - 1];
4327
+ if (weakest === void 0 || hitch.durationMs <= weakest.durationMs) return;
4328
+ worstHitches.pop();
4329
+ }
4330
+ const insertAt = worstHitches.findIndex((candidate) => hitch.durationMs > candidate.durationMs);
4331
+ worstHitches.splice(insertAt < 0 ? worstHitches.length : insertAt, 0, hitch);
4332
+ }
4333
+ function columnIndex(headers, header) {
4334
+ return header === void 0 ? -1 : headers.indexOf(header);
4335
+ }
4336
+ function summarizeFrameDurations(frameDurationsMs, worstHitches, hitchCount, options) {
4337
+ if (frameDurationsMs.length === 0) return {
4338
+ frameCount: 0,
4339
+ hitches: [],
4340
+ hitchesTruncated: false
4341
+ };
4342
+ const sortedDurations = [...frameDurationsMs].sort((left, right) => left - right);
4343
+ return {
4344
+ frameCount: frameDurationsMs.length,
4345
+ hitches: worstHitches,
4346
+ hitchesTruncated: hitchCount > options.maxHitches,
4347
+ percentilesMs: {
4348
+ p50: percentile(sortedDurations, .5),
4349
+ p90: percentile(sortedDurations, .9),
4350
+ p99: percentile(sortedDurations, .99)
4351
+ },
4352
+ worstFrameMs: sortedDurations[sortedDurations.length - 1]
4353
+ };
4354
+ }
4355
+ function percentile(sortedValues, fraction) {
4356
+ if (sortedValues.length === 0) return 0;
4357
+ return sortedValues[Math.min(sortedValues.length - 1, Math.max(0, Math.ceil(fraction * sortedValues.length) - 1))] ?? 0;
4358
+ }
3016
4359
  //#endregion
3017
4360
  //#region src/mcpTools.ts
3018
4361
  const EDITOR_WORK_TIMEOUT_MS = 1e4;
4362
+ const WAIT_FOR_JOB_KINDS = [
4363
+ "compile",
4364
+ "create_class",
4365
+ "pie",
4366
+ "test_run"
4367
+ ];
4368
+ const WAIT_FOR_JOB_MAX_TIMEOUT_MS = 6e5;
4369
+ const WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS = {
4370
+ compile: 12e4,
4371
+ create_class: 6e4,
4372
+ pie: 3e4,
4373
+ test_run: 3e5
4374
+ };
4375
+ const WAIT_FOR_JOB_POLL_CADENCE_MS = {
4376
+ compile: {
4377
+ capMs: 2e3,
4378
+ initialMs: 250
4379
+ },
4380
+ create_class: {
4381
+ capMs: 2e3,
4382
+ initialMs: 250
4383
+ },
4384
+ pie: {
4385
+ capMs: 2e3,
4386
+ initialMs: 250
4387
+ },
4388
+ test_run: {
4389
+ capMs: 5e3,
4390
+ initialMs: 1e3
4391
+ }
4392
+ };
4393
+ const ANALYZE_TRACE_DEFAULT_TIMEOUT_MS = 6e5;
4394
+ const ANALYZE_TRACE_DEFAULT_HITCH_THRESHOLD_MS = 33.4;
4395
+ const ANALYZE_TRACE_MAX_RESPONSE_BYTES = 32768;
4396
+ const BRIDGE_LOOPBACK_HOST = "127.0.0.1";
3019
4397
  const getProjectIdInputSchema = { projectPath: z.string().trim().min(1) };
3020
4398
  const pingProjectInputSchema = { projectId: projectIdSchema };
4399
+ const listRegisteredProjectsInputSchema = {
4400
+ 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.`),
4401
+ 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.")
4402
+ };
3021
4403
  const startPieInputSchema = {
3022
4404
  map: z.string().trim().min(1).max(512).optional().describe("Optional level/map path to load before play (e.g. /Game/Maps/MyLevel)."),
3023
4405
  mode: z.enum(BRIDGE_PIE_MODES).optional().describe("Play-in-Editor mode: 'selected_viewport', 'new_window' (default), or 'standalone'."),
@@ -3106,6 +4488,12 @@ const getTestRunStatusInputSchema = {
3106
4488
  projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the automation run to poll."),
3107
4489
  testRunId: z.string().trim().min(1).describe("The testRunId returned by run_automation_tests for the run to poll.")
3108
4490
  };
4491
+ const waitForJobInputSchema = {
4492
+ 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)."),
4493
+ 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."),
4494
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the job."),
4495
+ 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.`)
4496
+ };
3109
4497
  const getEditorStateInputSchema = { projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should report its state.") };
3110
4498
  const saveDirtyPackagesInputSchema = {
3111
4499
  projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should save its changes."),
@@ -3147,6 +4535,45 @@ const cleanBuildInputSchema = {
3147
4535
  target: targetSchema,
3148
4536
  timeoutMs: buildTimeoutSchema
3149
4537
  };
4538
+ 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)}).`);
4539
+ const cookPlatformSchema = z.enum(UNREAL_COOK_TARGET_PLATFORMS).describe("Cook/package target platform (Win64, Mac, Linux, LinuxArm64, Android, IOS, TVOS, VisionOS).");
4540
+ 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.");
4541
+ const cookContentInputSchema = {
4542
+ engineVersion: engineVersionSchema,
4543
+ force: packageForceSchema,
4544
+ 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)."),
4545
+ platform: cookPlatformSchema,
4546
+ projectPath: projectPathSchema,
4547
+ timeoutMs: packageTimeoutSchema
4548
+ };
4549
+ const packageProjectInputSchema = {
4550
+ archiveDirectory: z.string().trim().min(1).describe("Directory the staged build is archived into (-archivedirectory)."),
4551
+ configuration: configurationSchema,
4552
+ engineVersion: engineVersionSchema,
4553
+ force: packageForceSchema,
4554
+ platform: cookPlatformSchema,
4555
+ projectPath: projectPathSchema,
4556
+ timeoutMs: packageTimeoutSchema
4557
+ };
4558
+ const runEditorCommandletInputSchema = {
4559
+ 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."),
4560
+ commandletName: z.string().trim().min(1).max(128).describe("Commandlet class name, identifier characters only (e.g. ResavePackages, FixupRedirects, DataValidation)."),
4561
+ engineVersion: engineVersionSchema,
4562
+ force: packageForceSchema,
4563
+ projectPath: projectPathSchema,
4564
+ timeoutMs: packageTimeoutSchema
4565
+ };
4566
+ const analyzeTraceInputSchema = {
4567
+ engineVersion: engineVersionSchema,
4568
+ 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).`),
4569
+ maxHitches: z.number().int().min(1).max(50).optional().describe(`Maximum worst-hitch entries to return (default ${String(10)}, max ${String(50)}).`),
4570
+ maxScopes: z.number().int().min(1).max(100).optional().describe(`Maximum timing scopes to return per ranking (default ${String(20)}, max ${String(100)}).`),
4571
+ projectPath: z.string().trim().min(1).optional().describe("Project whose EngineAssociation selects the analyzer engine. Omit when engineVersion is given."),
4572
+ 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)}).`),
4573
+ tracePath: z.string().trim().min(1).describe("Path to the .utrace file produced by stop_insights_trace.")
4574
+ };
4575
+ const getPackageStatusInputSchema = { packageJobId: z.string().trim().min(1).max(512).describe("packageJobId returned by cook_content, package_project, or run_editor_commandlet.") };
4576
+ const cancelPackageJobInputSchema = { packageJobId: z.string().trim().min(1).max(512).describe("packageJobId of the in-flight job to abort.") };
3150
4577
  const runClangTidyInputSchema = {
3151
4578
  checks: z.string().trim().min(1).optional().describe("clang-tidy --checks expression (e.g. \"-*,bugprone-*\"); omit to use the .clang-tidy config."),
3152
4579
  compileCommandsDir: z.string().trim().min(1).describe("Directory containing the compile_commands.json produced by the compile-commands sync."),
@@ -3219,6 +4646,313 @@ async function buildTarget(input, dependencies, context = createEngineToolchainC
3219
4646
  warnings: diagnostics.warnings
3220
4647
  };
3221
4648
  }
4649
+ async function analyzeTrace(input, dependencies, context = createEngineToolchainContext()) {
4650
+ const tracePath = path.resolve(input.tracePath);
4651
+ if (!await context.fileExists(tracePath)) throw new EngineToolchainError(`Trace file not found or unreadable: ${tracePath}`);
4652
+ const engine = await resolveEngineForTrace(input, context);
4653
+ const analyzerPath = await resolveUnrealInsights(engine.engineRoot, context);
4654
+ const hitchThresholdMs = input.hitchThresholdMs ?? 33.4;
4655
+ if (!analyzerPath) return {
4656
+ durationMs: 0,
4657
+ engineVersion: engine.engineVersion,
4658
+ hitchThresholdMs,
4659
+ hitches: [],
4660
+ 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.`,
4661
+ state: "unsupported",
4662
+ topScopesByExclusiveTime: [],
4663
+ topScopesByInclusiveTime: [],
4664
+ tracePath,
4665
+ truncated: []
4666
+ };
4667
+ const timeoutMs = input.timeoutMs ?? 6e5;
4668
+ const exportDirectory = await context.createTempDirectory("uemcp-trace-");
4669
+ const timerStatisticsPath = path.join(exportDirectory, "timer-statistics.csv");
4670
+ const timingEventsPath = path.join(exportDirectory, "timing-events.csv");
4671
+ const commandFilePath = path.join(exportDirectory, "export.rsp");
4672
+ try {
4673
+ await context.writeFile(commandFilePath, [`TimingInsights.ExportTimerStatistics ${quoteConsoleArgument(timerStatisticsPath)}`, `TimingInsights.ExportTimingEvents ${quoteConsoleArgument(timingEventsPath)} -columns=ThreadName,TimerName,StartTime,Duration,Depth`].join("\n"));
4674
+ dependencies.logger.info({
4675
+ engineVersion: engine.engineVersion,
4676
+ tracePath
4677
+ }, "Running Unreal Insights trace analysis");
4678
+ const isWindowsHost = context.platform === "win32";
4679
+ const quoteValue = (value) => isWindowsHost ? `"${value}"` : value;
4680
+ const run = await context.runProcess({
4681
+ args: [
4682
+ `-OpenTraceFile=${quoteValue(tracePath)}`,
4683
+ `-ExecOnAnalysisCompleteCmd=${quoteValue(`@=${commandFilePath}`)}`,
4684
+ "-AutoQuit",
4685
+ "-NoUI"
4686
+ ],
4687
+ command: analyzerPath,
4688
+ timeoutMs,
4689
+ windowsVerbatimArguments: isWindowsHost
4690
+ });
4691
+ if (run.timedOut) throw new EngineToolchainError(`Unreal Insights did not finish analyzing ${tracePath} within ${String(timeoutMs)}ms; raise timeoutMs or trim the trace.`);
4692
+ if (run.exitCode !== 0) throw new EngineToolchainError(`Unreal Insights exited with code ${String(run.exitCode)} while analyzing ${tracePath}: ${run.combinedOutput.slice(-1e3).trim()}`);
4693
+ const scopeSummaries = summarizeTimerStatistics(await readCsvRows(timerStatisticsPath, context), input.maxScopes ?? 20);
4694
+ const timingEvents = await summarizeTimingEventStream(await context.fileExists(timingEventsPath) ? context.readTextLines(timingEventsPath) : emptyTextLines(), {
4695
+ hitchThresholdMs,
4696
+ maxHitches: input.maxHitches ?? 10
4697
+ });
4698
+ const frames = timingEvents.frames;
4699
+ const threadTimes = timingEvents.threads;
4700
+ const truncated = [];
4701
+ if (scopeSummaries.truncated) truncated.push("topScopes");
4702
+ if (frames.hitchesTruncated) truncated.push("hitches");
4703
+ const result = {
4704
+ analyzerPath,
4705
+ cpuTimeMs: threadTimes.cpuTimeMs,
4706
+ durationMs: run.durationMs,
4707
+ engineVersion: engine.engineVersion,
4708
+ exitCode: run.exitCode,
4709
+ frameCount: frames.frameCount,
4710
+ gpuTimeMs: threadTimes.gpuTimeMs,
4711
+ hitchThresholdMs,
4712
+ hitches: frames.hitches,
4713
+ state: "analyzed",
4714
+ topScopesByExclusiveTime: scopeSummaries.byExclusiveTime,
4715
+ topScopesByInclusiveTime: scopeSummaries.byInclusiveTime,
4716
+ totalScopeCount: scopeSummaries.totalScopeCount,
4717
+ tracePath,
4718
+ truncated
4719
+ };
4720
+ if (frames.percentilesMs) result.framePercentilesMs = frames.percentilesMs;
4721
+ if (frames.worstFrameMs !== void 0) result.worstFrameMs = frames.worstFrameMs;
4722
+ 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.";
4723
+ return boundAnalyzeTraceResult(result);
4724
+ } finally {
4725
+ try {
4726
+ await context.removeDirectory(exportDirectory);
4727
+ } catch (error) {
4728
+ dependencies.logger.warn({
4729
+ error,
4730
+ exportDirectory
4731
+ }, "Failed to remove the Unreal Insights export directory");
4732
+ }
4733
+ }
4734
+ }
4735
+ async function resolveEngineForTrace(input, context) {
4736
+ if (input.projectPath === void 0) return await resolveEngine({
4737
+ context,
4738
+ engineVersion: input.engineVersion
4739
+ });
4740
+ const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
4741
+ return await resolveEngine({
4742
+ context,
4743
+ engineVersion: input.engineVersion,
4744
+ uprojectPath
4745
+ });
4746
+ }
4747
+ function quoteConsoleArgument(value) {
4748
+ return `"${value.replace(/\\/gu, "\\\\")}"`;
4749
+ }
4750
+ async function* emptyTextLines() {}
4751
+ async function readCsvRows(filePath, context) {
4752
+ if (!await context.fileExists(filePath)) return [];
4753
+ return parseCsvRows(await context.readFile(filePath));
4754
+ }
4755
+ function boundAnalyzeTraceResult(result) {
4756
+ const bounded = {
4757
+ ...result,
4758
+ truncated: [...result.truncated]
4759
+ };
4760
+ while (JSON.stringify(bounded).length > ANALYZE_TRACE_MAX_RESPONSE_BYTES) {
4761
+ const longestListLength = Math.max(bounded.topScopesByInclusiveTime.length, bounded.topScopesByExclusiveTime.length, bounded.hitches.length);
4762
+ if (longestListLength <= 1) {
4763
+ addTruncationReason(bounded, "responseBytesExceeded");
4764
+ return bounded;
4765
+ }
4766
+ if (bounded.topScopesByInclusiveTime.length === longestListLength) {
4767
+ bounded.topScopesByInclusiveTime = bounded.topScopesByInclusiveTime.slice(0, longestListLength - 1);
4768
+ bounded.topScopesByExclusiveTime = bounded.topScopesByExclusiveTime.slice(0, longestListLength - 1);
4769
+ addTruncationReason(bounded, "topScopes");
4770
+ continue;
4771
+ }
4772
+ bounded.hitches = bounded.hitches.slice(0, bounded.hitches.length - 1);
4773
+ addTruncationReason(bounded, "hitches");
4774
+ }
4775
+ return bounded;
4776
+ }
4777
+ function addTruncationReason(result, reason) {
4778
+ if (!result.truncated.includes(reason)) result.truncated.push(reason);
4779
+ }
4780
+ const COMMANDLET_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/u;
4781
+ const ENGINE_CONTROLLED_COMMANDLET_SWITCHES = ["-run=", "-project="];
4782
+ const defaultPackageJobRegistry = new PackageJobRegistry();
4783
+ function resolvePackageJobRegistry(dependencies) {
4784
+ return dependencies.packageJobRegistry ?? defaultPackageJobRegistry;
4785
+ }
4786
+ async function cookContent(input, dependencies, context = createEngineToolchainContext()) {
4787
+ if (!input.force) await assertEditorNotRunning(input.projectPath, dependencies);
4788
+ const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
4789
+ const engine = await resolveEngine({
4790
+ context,
4791
+ engineVersion: input.engineVersion,
4792
+ uprojectPath
4793
+ });
4794
+ const editorCmd = await resolveUnrealEditorCmd(engine.engineRoot, context);
4795
+ const mapArgs = input.maps && input.maps.length > 0 ? [`-Map=${input.maps.join("+")}`] : ["-CookAll"];
4796
+ const args = [
4797
+ uprojectPath,
4798
+ "-run=Cook",
4799
+ `-TargetPlatform=${cookCommandletTargetPlatform(input.platform)}`,
4800
+ ...mapArgs,
4801
+ "-Unversioned",
4802
+ "-unattended",
4803
+ "-nopause",
4804
+ "-nosplash",
4805
+ "-stdout"
4806
+ ];
4807
+ dependencies.logger.info({
4808
+ engineVersion: engine.engineVersion,
4809
+ platform: input.platform,
4810
+ projectPath: uprojectPath
4811
+ }, "Starting Unreal cook");
4812
+ return toStartPackageJobResult(resolvePackageJobRegistry(dependencies).start({
4813
+ args,
4814
+ command: editorCmd,
4815
+ cwd: engine.engineRoot,
4816
+ kind: "cook",
4817
+ logger: dependencies.logger,
4818
+ platform: input.platform,
4819
+ projectPath: uprojectPath,
4820
+ runProcess: context.runProcess,
4821
+ timeoutMs: input.timeoutMs ?? 54e5
4822
+ }), engine.engineVersion);
4823
+ }
4824
+ async function packageProject(input, dependencies, context = createEngineToolchainContext()) {
4825
+ if (!input.force) await assertEditorNotRunning(input.projectPath, dependencies);
4826
+ const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
4827
+ const engine = await resolveEngine({
4828
+ context,
4829
+ engineVersion: input.engineVersion,
4830
+ uprojectPath
4831
+ });
4832
+ const automationTool = await resolveAutomationTool(engine.engineRoot, context);
4833
+ const args = [
4834
+ ...automationTool.args,
4835
+ "BuildCookRun",
4836
+ `-project=${uprojectPath}`,
4837
+ "-noP4",
4838
+ "-utf8output",
4839
+ "-nocompileeditor",
4840
+ "-unattended",
4841
+ `-platform=${input.platform}`,
4842
+ `-clientconfig=${input.configuration}`,
4843
+ "-build",
4844
+ "-cook",
4845
+ "-stage",
4846
+ "-pak",
4847
+ "-archive",
4848
+ `-archivedirectory=${path.resolve(input.archiveDirectory)}`
4849
+ ];
4850
+ dependencies.logger.info({
4851
+ configuration: input.configuration,
4852
+ engineVersion: engine.engineVersion,
4853
+ platform: input.platform,
4854
+ projectPath: uprojectPath
4855
+ }, "Starting AutomationTool BuildCookRun");
4856
+ return toStartPackageJobResult(resolvePackageJobRegistry(dependencies).start({
4857
+ args,
4858
+ command: automationTool.command,
4859
+ cwd: automationTool.cwd ?? engine.engineRoot,
4860
+ ...automationTool.env ? { env: automationTool.env } : {},
4861
+ kind: "package",
4862
+ logger: dependencies.logger,
4863
+ platform: input.platform,
4864
+ projectPath: uprojectPath,
4865
+ runProcess: context.runProcess,
4866
+ timeoutMs: input.timeoutMs ?? 54e5
4867
+ }), engine.engineVersion);
4868
+ }
4869
+ async function runEditorCommandlet(input, dependencies, context = createEngineToolchainContext()) {
4870
+ assertValidCommandletName(input.commandletName);
4871
+ const commandletArgs = input.args ?? [];
4872
+ for (const argument of commandletArgs) assertCommandletArgumentAllowed(argument);
4873
+ if (!input.force) await assertEditorNotRunning(input.projectPath, dependencies);
4874
+ const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
4875
+ const engine = await resolveEngine({
4876
+ context,
4877
+ engineVersion: input.engineVersion,
4878
+ uprojectPath
4879
+ });
4880
+ const editorCmd = await resolveUnrealEditorCmd(engine.engineRoot, context);
4881
+ const args = [
4882
+ uprojectPath,
4883
+ `-run=${input.commandletName}`,
4884
+ ...commandletArgs,
4885
+ "-unattended",
4886
+ "-nopause",
4887
+ "-nosplash",
4888
+ "-stdout"
4889
+ ];
4890
+ dependencies.logger.info({
4891
+ commandletName: input.commandletName,
4892
+ engineVersion: engine.engineVersion,
4893
+ projectPath: uprojectPath
4894
+ }, "Starting Unreal editor commandlet");
4895
+ return toStartPackageJobResult(resolvePackageJobRegistry(dependencies).start({
4896
+ args,
4897
+ command: editorCmd,
4898
+ cwd: engine.engineRoot,
4899
+ kind: "commandlet",
4900
+ logger: dependencies.logger,
4901
+ projectPath: uprojectPath,
4902
+ runProcess: context.runProcess,
4903
+ timeoutMs: input.timeoutMs ?? 54e5
4904
+ }), engine.engineVersion);
4905
+ }
4906
+ function getPackageStatus(input, dependencies) {
4907
+ const status = resolvePackageJobRegistry(dependencies).get(input.packageJobId);
4908
+ if (status) return status;
4909
+ return {
4910
+ args: [],
4911
+ command: "",
4912
+ diagnosticsTruncated: false,
4913
+ errorCount: 0,
4914
+ errors: [],
4915
+ outputTail: "",
4916
+ outputTruncated: false,
4917
+ packageJobId: input.packageJobId,
4918
+ projectPath: "",
4919
+ startedAt: "",
4920
+ state: "unknown_job",
4921
+ timedOut: false,
4922
+ warningCount: 0,
4923
+ warnings: []
4924
+ };
4925
+ }
4926
+ function cancelPackageJob(input, dependencies) {
4927
+ const outcome = resolvePackageJobRegistry(dependencies).cancel(input.packageJobId);
4928
+ return {
4929
+ cancelled: outcome.cancelled,
4930
+ packageJobId: input.packageJobId,
4931
+ state: outcome.state
4932
+ };
4933
+ }
4934
+ function assertValidCommandletName(commandletName) {
4935
+ if (!COMMANDLET_NAME_PATTERN.test(commandletName)) throw new EngineToolchainError(`Commandlet name '${commandletName}' is invalid; use identifier characters only (e.g. ResavePackages).`);
4936
+ }
4937
+ function assertCommandletArgumentAllowed(argument) {
4938
+ const normalized = argument.toLowerCase();
4939
+ 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.`);
4940
+ if (/[\0\r\n]/u.test(argument)) throw new EngineToolchainError("Commandlet arguments cannot contain control characters.");
4941
+ }
4942
+ function toStartPackageJobResult(status, engineVersion) {
4943
+ const result = {
4944
+ args: status.args,
4945
+ command: status.command,
4946
+ engineVersion,
4947
+ kind: status.kind,
4948
+ packageJobId: status.packageJobId,
4949
+ projectPath: status.projectPath,
4950
+ startedAt: status.startedAt,
4951
+ state: status.state
4952
+ };
4953
+ if (status.platform !== void 0) result.platform = status.platform;
4954
+ return result;
4955
+ }
3222
4956
  async function generateProjectFiles(input, dependencies, context = createEngineToolchainContext()) {
3223
4957
  const { uprojectPath } = await resolveProjectPaths(input.projectPath, context);
3224
4958
  const engine = await resolveEngine({
@@ -3420,6 +5154,30 @@ const execConsoleCommandInputSchema = {
3420
5154
  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
5155
  projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should execute the console command.")
3422
5156
  };
5157
+ const configFileInputSchema = z.enum(BRIDGE_CONFIG_FILES).describe("Which engine ini to read or write: Engine (DefaultEngine.ini), Game, Input, Editor, or EditorPerProjectUserSettings.");
5158
+ const configSectionInputSchema = bridgeGetConfigValueRequestSchema.shape.section.describe("Ini section without brackets, e.g. /Script/Engine.RendererSettings or SystemSettings.");
5159
+ const configKeyInputSchema = bridgeGetConfigValueRequestSchema.shape.key.describe("Key inside the section.");
5160
+ const getConfigValueInputSchema = {
5161
+ file: configFileInputSchema,
5162
+ key: configKeyInputSchema,
5163
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the config."),
5164
+ section: configSectionInputSchema
5165
+ };
5166
+ const setConfigValueInputSchema = {
5167
+ file: configFileInputSchema,
5168
+ key: configKeyInputSchema,
5169
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should write the config."),
5170
+ 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)."),
5171
+ section: configSectionInputSchema,
5172
+ 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.")
5173
+ };
5174
+ const listConfigSectionInputSchema = {
5175
+ cursor: bridgeListConfigSectionRequestSchema.shape.cursor.describe("Offset into the stable-ordered key list (default 0). Pass the previous response nextCursor for the next page."),
5176
+ file: configFileInputSchema,
5177
+ maxResults: bridgeListConfigSectionRequestSchema.shape.maxResults.describe(`Maximum key/value pairs per page (1-${String(200)}, default ${String(100)}).`),
5178
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the config."),
5179
+ section: configSectionInputSchema
5180
+ };
3423
5181
  const getStatSnapshotInputSchema = { projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should sample its performance counters.") };
3424
5182
  const startInsightsTraceInputSchema = {
3425
5183
  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 +5211,40 @@ const findBlueprintReferencesInputSchema = {
3453
5211
  projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should query its Asset Registry."),
3454
5212
  referenceKinds: bridgeFindBlueprintReferencesRequestSchema.shape.referenceKinds.describe("Restrict results to 'subclass' and/or 'module_dependency'; omit to return both.")
3455
5213
  };
5214
+ const getBlueprintSummaryInputSchema = {
5215
+ 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)."),
5216
+ componentCursor: bridgeGetBlueprintSummaryRequestSchema.shape.componentCursor.describe("Offset into the component list (default 0). Pass the previous response nextComponentCursor to fetch the next page."),
5217
+ functionCursor: bridgeGetBlueprintSummaryRequestSchema.shape.functionCursor.describe("Offset into the function/event/macro list (default 0). Pass the previous response nextFunctionCursor to fetch the next page."),
5218
+ graphCursor: bridgeGetBlueprintSummaryRequestSchema.shape.graphCursor.describe("Offset into the graph inventory (default 0). Pass the previous response nextGraphCursor to fetch the next page."),
5219
+ 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."),
5220
+ maxResults: bridgeGetBlueprintSummaryRequestSchema.shape.maxResults.default(25).describe("Maximum entries per list per page (1-200, default 25). The *Count fields stay authoritative."),
5221
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should read the Blueprint."),
5222
+ variableCursor: bridgeGetBlueprintSummaryRequestSchema.shape.variableCursor.describe("Offset into the variable list (default 0). Pass the previous response nextVariableCursor to fetch the next page.")
5223
+ };
5224
+ const getBlueprintGraphInputSchema = {
5225
+ blueprintPath: bridgeGetBlueprintGraphRequestSchema.shape.blueprintPath.describe("Package name or object path of the Blueprint that owns the graph."),
5226
+ graphName: bridgeGetBlueprintGraphRequestSchema.shape.graphName.describe("Name of the graph to dump, as reported by get_blueprint_summary (e.g. 'EventGraph', 'UserConstructionScript', a function name)."),
5227
+ 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."),
5228
+ maxNodes: bridgeGetBlueprintGraphRequestSchema.shape.maxNodes.default(25).describe("Maximum nodes to return per page (1-200, default 25). nodeCount still reports the whole graph."),
5229
+ nodeCursor: bridgeGetBlueprintGraphRequestSchema.shape.nodeCursor.describe("Offset into the GUID-ordered node list (default 0). Pass the previous response nextNodeCursor to fetch the next page."),
5230
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should read the Blueprint.")
5231
+ };
5232
+ const getBlueprintCompileErrorsInputSchema = {
5233
+ blueprintPaths: bridgeGetBlueprintCompileErrorsRequestSchema.shape.blueprintPaths.describe("Blueprints to report on, as package names or object paths (1-32 entries)."),
5234
+ cursor: bridgeGetBlueprintCompileErrorsRequestSchema.shape.cursor.describe("Offset into the path-ordered result list (default 0). Pass the previous response nextCursor to fetch the next page."),
5235
+ maxResults: bridgeGetBlueprintCompileErrorsRequestSchema.shape.maxResults.default(50).describe("Maximum results to return per page (1-100, default 50)."),
5236
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should report Blueprint status.")
5237
+ };
5238
+ const compileBlueprintInputSchema = {
5239
+ blueprintPaths: bridgeCompileBlueprintRequestSchema.shape.blueprintPaths.describe("Blueprints to compile, as package names or object paths (1-32 entries). Each is loaded if needed."),
5240
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should compile the Blueprints.")
5241
+ };
5242
+ const getBlueprintCompileStatusInputSchema = {
5243
+ compileJobId: bridgeGetBlueprintCompileStatusRequestSchema.shape.compileJobId.describe("Job id returned by compile_blueprint."),
5244
+ cursor: bridgeGetBlueprintCompileStatusRequestSchema.shape.cursor.describe("Offset into the result list (default 0). Pass the previous response nextCursor to fetch the next page."),
5245
+ maxResults: bridgeGetBlueprintCompileStatusRequestSchema.shape.maxResults.default(50).describe("Maximum results to return per page (1-100, default 50)."),
5246
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor owns the compile job.")
5247
+ };
3456
5248
  function registerMcpTools(server, dependencies) {
3457
5249
  server.registerTool("get_project_id", {
3458
5250
  description: "Resolve the canonical Unreal ProjectID from a project path by reading Config/DefaultGame.ini.",
@@ -3482,6 +5274,16 @@ function registerMcpTools(server, dependencies) {
3482
5274
  }, async ({ projectId }) => {
3483
5275
  return toToolSuccess(await pingProject(projectId, dependencies));
3484
5276
  });
5277
+ server.registerTool("list_registered_projects", {
5278
+ 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.",
5279
+ inputSchema: listRegisteredProjectsInputSchema,
5280
+ title: "List Registered Projects"
5281
+ }, async ({ maxResults, verify }) => {
5282
+ return toToolSuccess(await listRegisteredProjects({
5283
+ maxResults,
5284
+ verify
5285
+ }, dependencies));
5286
+ });
3485
5287
  server.registerTool("trigger_live_compile", {
3486
5288
  description: "Trigger an Unreal Live Coding recompile of patched modules for a projectId; tail stream_output_log (LogLiveCoding) for build output.",
3487
5289
  inputSchema: triggerLiveCompileInputSchema,
@@ -3538,6 +5340,48 @@ function registerMcpTools(server, dependencies) {
3538
5340
  }, async (input) => {
3539
5341
  return toToolSuccess(await cleanBuild(input, dependencies));
3540
5342
  });
5343
+ server.registerTool("cook_content", {
5344
+ 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.",
5345
+ inputSchema: cookContentInputSchema,
5346
+ title: "Cook Content"
5347
+ }, async (input) => {
5348
+ return toToolSuccess(await cookContent(input, dependencies));
5349
+ });
5350
+ server.registerTool("package_project", {
5351
+ 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.",
5352
+ inputSchema: packageProjectInputSchema,
5353
+ title: "Package Project"
5354
+ }, async (input) => {
5355
+ return toToolSuccess(await packageProject(input, dependencies));
5356
+ });
5357
+ server.registerTool("run_editor_commandlet", {
5358
+ 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.",
5359
+ inputSchema: runEditorCommandletInputSchema,
5360
+ title: "Run Editor Commandlet"
5361
+ }, async (input) => {
5362
+ return toToolSuccess(await runEditorCommandlet(input, dependencies));
5363
+ });
5364
+ server.registerTool("analyze_trace", {
5365
+ 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.",
5366
+ inputSchema: analyzeTraceInputSchema,
5367
+ title: "Analyze Trace"
5368
+ }, async (input) => {
5369
+ return toToolSuccess(await analyzeTrace(input, dependencies));
5370
+ });
5371
+ server.registerTool("get_package_status", {
5372
+ 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.",
5373
+ inputSchema: getPackageStatusInputSchema,
5374
+ title: "Get Package Status"
5375
+ }, async ({ packageJobId }) => {
5376
+ return toToolSuccess(getPackageStatus({ packageJobId }, dependencies));
5377
+ });
5378
+ server.registerTool("cancel_package_job", {
5379
+ 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.",
5380
+ inputSchema: cancelPackageJobInputSchema,
5381
+ title: "Cancel Package Job"
5382
+ }, async ({ packageJobId }) => {
5383
+ return toToolSuccess(cancelPackageJob({ packageJobId }, dependencies));
5384
+ });
3541
5385
  server.registerTool("run_clang_tidy", {
3542
5386
  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
5387
  inputSchema: runClangTidyInputSchema,
@@ -3718,6 +5562,18 @@ function registerMcpTools(server, dependencies) {
3718
5562
  testRunId
3719
5563
  }, dependencies));
3720
5564
  });
5565
+ server.registerTool("wait_for_job", {
5566
+ 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.`,
5567
+ inputSchema: waitForJobInputSchema,
5568
+ title: "Wait For Job"
5569
+ }, async ({ jobId, kind, projectId, timeoutMs }) => {
5570
+ return toToolSuccess(await waitForJob({
5571
+ jobId,
5572
+ kind,
5573
+ projectId,
5574
+ timeoutMs
5575
+ }, dependencies));
5576
+ });
3721
5577
  server.registerTool("cancel_test_run", {
3722
5578
  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
5579
  inputSchema: cancelTestRunInputSchema,
@@ -3745,6 +5601,45 @@ function registerMcpTools(server, dependencies) {
3745
5601
  projectId
3746
5602
  }, dependencies));
3747
5603
  });
5604
+ server.registerTool("get_config_value", {
5605
+ 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.",
5606
+ inputSchema: getConfigValueInputSchema,
5607
+ title: "Get Config Value"
5608
+ }, async ({ file, key, projectId, section }) => {
5609
+ return toToolSuccess(await getConfigValue({
5610
+ file,
5611
+ key,
5612
+ projectId,
5613
+ section
5614
+ }, dependencies));
5615
+ });
5616
+ server.registerTool("set_config_value", {
5617
+ 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.",
5618
+ inputSchema: setConfigValueInputSchema,
5619
+ title: "Set Config Value"
5620
+ }, async ({ file, key, projectId, scope, section, value }) => {
5621
+ return toToolSuccess(await setConfigValue({
5622
+ file,
5623
+ key,
5624
+ projectId,
5625
+ scope,
5626
+ section,
5627
+ value
5628
+ }, dependencies));
5629
+ });
5630
+ server.registerTool("list_config_section", {
5631
+ 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.",
5632
+ inputSchema: listConfigSectionInputSchema,
5633
+ title: "List Config Section"
5634
+ }, async ({ cursor, file, maxResults, projectId, section }) => {
5635
+ return toToolSuccess(await listConfigSection({
5636
+ cursor,
5637
+ file,
5638
+ maxResults,
5639
+ projectId,
5640
+ section
5641
+ }, dependencies));
5642
+ });
3748
5643
  server.registerTool("get_class_reflection", {
3749
5644
  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
5645
  inputSchema: getClassReflectionInputSchema,
@@ -3861,6 +5756,70 @@ function registerMcpTools(server, dependencies) {
3861
5756
  referenceKinds
3862
5757
  }, dependencies));
3863
5758
  });
5759
+ server.registerTool("get_blueprint_summary", {
5760
+ 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.",
5761
+ inputSchema: getBlueprintSummaryInputSchema,
5762
+ title: "Get Blueprint Summary"
5763
+ }, async ({ blueprintPath, componentCursor, functionCursor, graphCursor, loadIfNeeded, maxResults, projectId, variableCursor }) => {
5764
+ return toToolSuccess(await getBlueprintSummary({
5765
+ blueprintPath,
5766
+ componentCursor,
5767
+ functionCursor,
5768
+ graphCursor,
5769
+ loadIfNeeded,
5770
+ maxResults,
5771
+ projectId,
5772
+ variableCursor
5773
+ }, dependencies));
5774
+ });
5775
+ server.registerTool("get_blueprint_graph", {
5776
+ 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.",
5777
+ inputSchema: getBlueprintGraphInputSchema,
5778
+ title: "Get Blueprint Graph"
5779
+ }, async ({ blueprintPath, graphName, loadIfNeeded, maxNodes, nodeCursor, projectId }) => {
5780
+ return toToolSuccess(await getBlueprintGraph({
5781
+ blueprintPath,
5782
+ graphName,
5783
+ loadIfNeeded,
5784
+ maxNodes,
5785
+ nodeCursor,
5786
+ projectId
5787
+ }, dependencies));
5788
+ });
5789
+ server.registerTool("get_blueprint_compile_errors", {
5790
+ 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.",
5791
+ inputSchema: getBlueprintCompileErrorsInputSchema,
5792
+ title: "Get Blueprint Compile Errors"
5793
+ }, async ({ blueprintPaths, cursor, maxResults, projectId }) => {
5794
+ return toToolSuccess(await getBlueprintCompileErrors({
5795
+ blueprintPaths,
5796
+ cursor,
5797
+ maxResults,
5798
+ projectId
5799
+ }, dependencies));
5800
+ });
5801
+ server.registerTool("compile_blueprint", {
5802
+ 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.",
5803
+ inputSchema: compileBlueprintInputSchema,
5804
+ title: "Compile Blueprint"
5805
+ }, async ({ blueprintPaths, projectId }) => {
5806
+ return toToolSuccess(await compileBlueprint({
5807
+ blueprintPaths,
5808
+ projectId
5809
+ }, dependencies));
5810
+ });
5811
+ server.registerTool("get_blueprint_compile_status", {
5812
+ 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.",
5813
+ inputSchema: getBlueprintCompileStatusInputSchema,
5814
+ title: "Get Blueprint Compile Status"
5815
+ }, async ({ compileJobId, cursor, maxResults, projectId }) => {
5816
+ return toToolSuccess(await getBlueprintCompileStatus({
5817
+ compileJobId,
5818
+ cursor,
5819
+ maxResults,
5820
+ projectId
5821
+ }, dependencies));
5822
+ });
3864
5823
  }
3865
5824
  async function findAssets(input, dependencies) {
3866
5825
  const normalizedProjectId = projectIdSchema.parse(input.projectId);
@@ -3946,6 +5905,142 @@ async function findBlueprintReferences(input, dependencies) {
3946
5905
  truncated: response.truncated
3947
5906
  };
3948
5907
  }
5908
+ async function getBlueprintSummary(input, dependencies) {
5909
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
5910
+ const entry = dependencies.registry.getProject(normalizedProjectId);
5911
+ if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
5912
+ const response = await sendBridgeGetBlueprintSummaryRequest(createBridgeGetBlueprintSummaryRequest(input.blueprintPath, {
5913
+ componentCursor: input.componentCursor,
5914
+ functionCursor: input.functionCursor,
5915
+ graphCursor: input.graphCursor,
5916
+ loadIfNeeded: input.loadIfNeeded,
5917
+ maxResults: input.maxResults,
5918
+ variableCursor: input.variableCursor
5919
+ }), {
5920
+ connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
5921
+ host: "127.0.0.1",
5922
+ port: entry.port,
5923
+ readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
5924
+ });
5925
+ if (!response.ok) throw new Error(`Bridge get_blueprint_summary failed (${response.error.code}): ${response.error.message}`);
5926
+ return {
5927
+ blueprintName: response.blueprintName,
5928
+ blueprintPath: response.blueprintPath,
5929
+ componentCount: response.componentCount,
5930
+ components: response.components,
5931
+ functionCount: response.functionCount,
5932
+ functions: response.functions,
5933
+ generatedClassPath: response.generatedClassPath,
5934
+ graphCount: response.graphCount,
5935
+ graphs: response.graphs,
5936
+ interfaces: response.interfaces,
5937
+ kind: response.kind,
5938
+ loaded: response.loaded,
5939
+ nextComponentCursor: response.nextComponentCursor,
5940
+ nextFunctionCursor: response.nextFunctionCursor,
5941
+ nextGraphCursor: response.nextGraphCursor,
5942
+ nextVariableCursor: response.nextVariableCursor,
5943
+ parentClass: response.parentClass,
5944
+ parentClassPath: response.parentClassPath,
5945
+ projectId: entry.projectId,
5946
+ scanning: response.scanning,
5947
+ truncated: response.truncated,
5948
+ variableCount: response.variableCount,
5949
+ variables: response.variables
5950
+ };
5951
+ }
5952
+ async function getBlueprintGraph(input, dependencies) {
5953
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
5954
+ const entry = dependencies.registry.getProject(normalizedProjectId);
5955
+ if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
5956
+ const response = await sendBridgeGetBlueprintGraphRequest(createBridgeGetBlueprintGraphRequest(input.blueprintPath, input.graphName, {
5957
+ loadIfNeeded: input.loadIfNeeded,
5958
+ maxNodes: input.maxNodes,
5959
+ nodeCursor: input.nodeCursor
5960
+ }), {
5961
+ connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
5962
+ host: "127.0.0.1",
5963
+ port: entry.port,
5964
+ readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
5965
+ });
5966
+ if (!response.ok) throw new Error(`Bridge get_blueprint_graph failed (${response.error.code}): ${response.error.message}`);
5967
+ return {
5968
+ blueprintPath: response.blueprintPath,
5969
+ graphKind: response.graphKind,
5970
+ graphName: response.graphName,
5971
+ nextNodeCursor: response.nextNodeCursor,
5972
+ nodeCount: response.nodeCount,
5973
+ nodes: response.nodes,
5974
+ projectId: entry.projectId,
5975
+ truncated: response.truncated
5976
+ };
5977
+ }
5978
+ async function getBlueprintCompileErrors(input, dependencies) {
5979
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
5980
+ const entry = dependencies.registry.getProject(normalizedProjectId);
5981
+ if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
5982
+ const response = await sendBridgeGetBlueprintCompileErrorsRequest(createBridgeGetBlueprintCompileErrorsRequest(input.blueprintPaths, {
5983
+ cursor: input.cursor,
5984
+ maxResults: input.maxResults
5985
+ }), {
5986
+ connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
5987
+ host: "127.0.0.1",
5988
+ port: entry.port,
5989
+ readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
5990
+ });
5991
+ if (!response.ok) throw new Error(`Bridge get_blueprint_compile_errors failed (${response.error.code}): ${response.error.message}`);
5992
+ return {
5993
+ nextCursor: response.nextCursor,
5994
+ projectId: entry.projectId,
5995
+ results: response.results,
5996
+ totalCount: response.totalCount,
5997
+ truncated: response.truncated
5998
+ };
5999
+ }
6000
+ async function compileBlueprint(input, dependencies) {
6001
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
6002
+ const entry = dependencies.registry.getProject(normalizedProjectId);
6003
+ if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
6004
+ const response = await sendBridgeCompileBlueprintRequest(createBridgeCompileBlueprintRequest(input.blueprintPaths), {
6005
+ connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
6006
+ host: "127.0.0.1",
6007
+ port: entry.port,
6008
+ readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
6009
+ });
6010
+ if (!response.ok) throw new Error(`Bridge compile_blueprint failed (${response.error.code}): ${response.error.message}`);
6011
+ return {
6012
+ compileJobId: response.compileJobId,
6013
+ message: response.message,
6014
+ projectId: entry.projectId,
6015
+ state: response.state
6016
+ };
6017
+ }
6018
+ async function getBlueprintCompileStatus(input, dependencies) {
6019
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
6020
+ const entry = dependencies.registry.getProject(normalizedProjectId);
6021
+ if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
6022
+ const response = await sendBridgeGetBlueprintCompileStatusRequest(createBridgeGetBlueprintCompileStatusRequest(input.compileJobId, {
6023
+ cursor: input.cursor,
6024
+ maxResults: input.maxResults
6025
+ }), {
6026
+ connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
6027
+ host: "127.0.0.1",
6028
+ port: entry.port,
6029
+ readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
6030
+ });
6031
+ if (!response.ok) throw new Error(`Bridge get_blueprint_compile_status failed (${response.error.code}): ${response.error.message}`);
6032
+ return {
6033
+ compileJobId: response.compileJobId,
6034
+ durationMs: response.durationMs,
6035
+ finishedAt: response.finishedAt,
6036
+ nextCursor: response.nextCursor,
6037
+ projectId: entry.projectId,
6038
+ results: response.results,
6039
+ state: response.state,
6040
+ totalCount: response.totalCount,
6041
+ truncated: response.truncated
6042
+ };
6043
+ }
3949
6044
  async function getClassReflection(input, dependencies) {
3950
6045
  const normalizedProjectId = projectIdSchema.parse(input.projectId);
3951
6046
  const entry = dependencies.registry.getProject(normalizedProjectId);
@@ -4234,6 +6329,71 @@ async function pingProject(projectId, dependencies) {
4234
6329
  registrationRemoved: true
4235
6330
  };
4236
6331
  }
6332
+ async function listRegisteredProjects(input, dependencies) {
6333
+ const maxResults = input.maxResults ?? 50;
6334
+ const entries = dependencies.registry.snapshot().sort(compareRegistryEntries);
6335
+ const page = entries.slice(0, maxResults);
6336
+ const verified = input.verify === true;
6337
+ return {
6338
+ projects: verified ? await verifyRegisteredProjects(page, dependencies) : page.map(toRegisteredProjectSummary),
6339
+ totalCount: entries.length,
6340
+ truncated: entries.length > page.length,
6341
+ verified
6342
+ };
6343
+ }
6344
+ async function verifyRegisteredProjects(entries, dependencies) {
6345
+ const budgetMs = deriveVerifySweepBudgetMs(dependencies.pingTimeoutMs);
6346
+ let budgetTimer;
6347
+ const budgetExpiry = new Promise((resolve) => {
6348
+ budgetTimer = setTimeout(() => {
6349
+ resolve({
6350
+ alive: false,
6351
+ reason: `Verification budget of ${String(budgetMs)}ms expired before the editor answered.`
6352
+ });
6353
+ }, budgetMs);
6354
+ });
6355
+ try {
6356
+ return await Promise.all(entries.map(async (entry) => {
6357
+ const pingResult = await Promise.race([pingRegistryEntry$1(entry, {
6358
+ logger: dependencies.logger,
6359
+ timeoutMs: dependencies.pingTimeoutMs
6360
+ }), budgetExpiry]);
6361
+ const summary = toRegisteredProjectSummary(entry);
6362
+ if (pingResult.alive) return {
6363
+ ...summary,
6364
+ reachable: true
6365
+ };
6366
+ return {
6367
+ ...summary,
6368
+ reachable: false,
6369
+ reason: pingResult.reason ?? "Unknown ping failure."
6370
+ };
6371
+ }));
6372
+ } finally {
6373
+ clearTimeout(budgetTimer);
6374
+ }
6375
+ }
6376
+ function toRegisteredProjectSummary(entry) {
6377
+ return {
6378
+ host: BRIDGE_LOOPBACK_HOST,
6379
+ lastSeenAt: entry.lastSeenAt,
6380
+ pid: entry.pid,
6381
+ port: entry.port,
6382
+ projectId: entry.projectId,
6383
+ projectName: entry.projectName,
6384
+ projectPath: entry.projectPath,
6385
+ registeredAt: entry.registeredAt
6386
+ };
6387
+ }
6388
+ function compareRegistryEntries(left, right) {
6389
+ if (left.projectName !== right.projectName) return left.projectName < right.projectName ? -1 : 1;
6390
+ if (left.projectPath !== right.projectPath) return left.projectPath < right.projectPath ? -1 : 1;
6391
+ if (left.projectId === right.projectId) return 0;
6392
+ return left.projectId < right.projectId ? -1 : 1;
6393
+ }
6394
+ function deriveVerifySweepBudgetMs(pingTimeoutMs) {
6395
+ return Math.max(1, Math.round(pingTimeoutMs * 2));
6396
+ }
4237
6397
  async function startPie(input, dependencies) {
4238
6398
  const normalizedProjectId = projectIdSchema.parse(input.projectId);
4239
6399
  const entry = dependencies.registry.getProject(normalizedProjectId);
@@ -4512,6 +6672,92 @@ async function execConsoleCommand(input, dependencies) {
4512
6672
  truncated: response.truncated
4513
6673
  };
4514
6674
  }
6675
+ async function getConfigValue(input, dependencies) {
6676
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
6677
+ const entry = dependencies.registry.getProject(normalizedProjectId);
6678
+ if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
6679
+ const response = await sendBridgeGetConfigValueRequest(createBridgeGetConfigValueRequest({
6680
+ file: input.file,
6681
+ key: input.key,
6682
+ section: input.section
6683
+ }), {
6684
+ connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
6685
+ host: BRIDGE_LOOPBACK_HOST,
6686
+ port: entry.port,
6687
+ readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
6688
+ });
6689
+ if (!response.ok) throw new Error(`Bridge get_config_value failed (${response.error.code}): ${response.error.message}`);
6690
+ const result = {
6691
+ file: response.file,
6692
+ key: response.key,
6693
+ present: response.present,
6694
+ projectId: entry.projectId,
6695
+ resolvedFrom: response.resolvedFrom,
6696
+ section: response.section
6697
+ };
6698
+ if (response.filePath !== void 0) result.filePath = response.filePath;
6699
+ if (response.value !== void 0) result.value = response.value;
6700
+ if (response.valueTruncated === true) result.valueTruncated = true;
6701
+ return result;
6702
+ }
6703
+ async function setConfigValue(input, dependencies) {
6704
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
6705
+ const entry = dependencies.registry.getProject(normalizedProjectId);
6706
+ 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.`);
6707
+ const response = await sendBridgeSetConfigValueRequest(createBridgeSetConfigValueRequest({
6708
+ file: input.file,
6709
+ key: input.key,
6710
+ scope: input.scope,
6711
+ section: input.section,
6712
+ value: input.value
6713
+ }), {
6714
+ connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
6715
+ host: BRIDGE_LOOPBACK_HOST,
6716
+ port: entry.port,
6717
+ readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
6718
+ });
6719
+ if (!response.ok) throw new Error(`Bridge set_config_value failed (${response.error.code}): ${response.error.message}`);
6720
+ const result = {
6721
+ file: response.file,
6722
+ filePath: response.filePath,
6723
+ key: response.key,
6724
+ previousPresent: response.previousPresent,
6725
+ projectId: entry.projectId,
6726
+ scope: response.scope,
6727
+ section: response.section,
6728
+ value: response.value
6729
+ };
6730
+ if (response.previousValue !== void 0) result.previousValue = response.previousValue;
6731
+ if (response.previousValueTruncated === true) result.previousValueTruncated = true;
6732
+ return result;
6733
+ }
6734
+ async function listConfigSection(input, dependencies) {
6735
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
6736
+ const entry = dependencies.registry.getProject(normalizedProjectId);
6737
+ if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
6738
+ const response = await sendBridgeListConfigSectionRequest(createBridgeListConfigSectionRequest({
6739
+ cursor: input.cursor,
6740
+ file: input.file,
6741
+ maxResults: input.maxResults,
6742
+ section: input.section
6743
+ }), {
6744
+ connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
6745
+ host: BRIDGE_LOOPBACK_HOST,
6746
+ port: entry.port,
6747
+ readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
6748
+ });
6749
+ if (!response.ok) throw new Error(`Bridge list_config_section failed (${response.error.code}): ${response.error.message}`);
6750
+ const result = {
6751
+ entries: response.entries,
6752
+ file: response.file,
6753
+ projectId: entry.projectId,
6754
+ section: response.section,
6755
+ totalCount: response.totalCount,
6756
+ truncated: response.truncated
6757
+ };
6758
+ if (response.nextCursor !== void 0) result.nextCursor = response.nextCursor;
6759
+ return result;
6760
+ }
4515
6761
  async function getStatSnapshot(projectId, dependencies) {
4516
6762
  const normalizedProjectId = projectIdSchema.parse(projectId);
4517
6763
  const entry = dependencies.registry.getProject(normalizedProjectId);
@@ -4629,6 +6875,98 @@ function buildGetCreateClassStatusResult(response, projectId) {
4629
6875
  if (response.sourcePath !== void 0) result.sourcePath = response.sourcePath;
4630
6876
  return result;
4631
6877
  }
6878
+ async function waitForJob(input, dependencies) {
6879
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
6880
+ if (!dependencies.registry.getProject(normalizedProjectId)) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
6881
+ const timeoutMs = clampWaitForJobTimeoutMs(input.kind, input.timeoutMs);
6882
+ const cadence = WAIT_FOR_JOB_POLL_CADENCE_MS[input.kind];
6883
+ const startedAtMs = Date.now();
6884
+ const deadlineMs = startedAtMs + timeoutMs;
6885
+ let delayMs = cadence.initialMs;
6886
+ let pollCount = 0;
6887
+ let lastStatus;
6888
+ let lastTransientError;
6889
+ for (;;) {
6890
+ pollCount += 1;
6891
+ try {
6892
+ const status = await pollJobStatus(input.kind, input.jobId, normalizedProjectId, dependencies);
6893
+ lastStatus = status;
6894
+ lastTransientError = void 0;
6895
+ const progress = classifyJobStatus(input.kind, status);
6896
+ if (progress !== "pending") return {
6897
+ jobId: input.jobId,
6898
+ kind: input.kind,
6899
+ outcome: progress,
6900
+ pollCount,
6901
+ projectId: normalizedProjectId,
6902
+ status,
6903
+ timeoutMs,
6904
+ waitedMs: Date.now() - startedAtMs
6905
+ };
6906
+ } catch (error) {
6907
+ if (!isTransientBridgeError(error)) throw error;
6908
+ lastTransientError = error;
6909
+ dependencies.logger.warn({
6910
+ error,
6911
+ jobId: input.jobId,
6912
+ kind: input.kind,
6913
+ projectId: normalizedProjectId
6914
+ }, "Retrying wait_for_job poll after a transient bridge failure");
6915
+ }
6916
+ const remainingMs = deadlineMs - Date.now();
6917
+ if (remainingMs <= 0) break;
6918
+ await sleep(Math.min(delayMs, remainingMs));
6919
+ delayMs = Math.min(cadence.capMs, delayMs * 2);
6920
+ }
6921
+ 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.`);
6922
+ return {
6923
+ jobId: input.jobId,
6924
+ kind: input.kind,
6925
+ outcome: "timed_out",
6926
+ pollCount,
6927
+ projectId: normalizedProjectId,
6928
+ status: lastStatus,
6929
+ timeoutMs,
6930
+ waitedMs: Date.now() - startedAtMs
6931
+ };
6932
+ }
6933
+ function clampWaitForJobTimeoutMs(kind, timeoutMs) {
6934
+ return Math.min(timeoutMs ?? WAIT_FOR_JOB_DEFAULT_TIMEOUT_MS[kind], WAIT_FOR_JOB_MAX_TIMEOUT_MS);
6935
+ }
6936
+ async function pollJobStatus(kind, jobId, projectId, dependencies) {
6937
+ switch (kind) {
6938
+ case "compile": return await getCompileStatus({
6939
+ compileJobId: jobId,
6940
+ projectId
6941
+ }, dependencies);
6942
+ case "create_class": return await getCreateClassStatus({
6943
+ createClassJobId: jobId,
6944
+ projectId
6945
+ }, dependencies);
6946
+ case "pie": return await getPieStatus({
6947
+ pieSessionId: jobId,
6948
+ projectId
6949
+ }, dependencies);
6950
+ case "test_run": return await getTestRunStatus({
6951
+ projectId,
6952
+ testRunId: jobId
6953
+ }, dependencies);
6954
+ }
6955
+ }
6956
+ function classifyJobStatus(kind, status) {
6957
+ switch (kind) {
6958
+ case "compile": return status.state === "compiling" ? "pending" : toTerminalOutcome(status.state === "unknown_job");
6959
+ case "create_class": return status.state === "creating" ? "pending" : toTerminalOutcome(status.state === "unknown_job");
6960
+ case "pie": return status.state === "starting" ? "pending" : "completed";
6961
+ case "test_run": return status.state === "running" ? "pending" : toTerminalOutcome(status.state === "unknown_run");
6962
+ }
6963
+ }
6964
+ function toTerminalOutcome(isUnknownJob) {
6965
+ return isUnknownJob ? "unknown_job" : "completed";
6966
+ }
6967
+ function isTransientBridgeError(error) {
6968
+ return error instanceof BridgeSocketError && (error.code === "read_timeout" || error.code === "connection_closed");
6969
+ }
4632
6970
  async function runQueuedRegistryWrite(dependencies, operation) {
4633
6971
  return await (dependencies.registryWriteQueue ?? new RegistryWriteQueue()).run(operation);
4634
6972
  }
@@ -4876,7 +7214,7 @@ function isMissingFileError(error) {
4876
7214
  //#endregion
4877
7215
  //#region src/server.ts
4878
7216
  const SERVER_NAME = "ue-mcp-server";
4879
- const SERVER_VERSION = "0.1.0";
7217
+ const SERVER_VERSION = "0.4.0";
4880
7218
  const logger = createModuleLogger("bootstrap");
4881
7219
  const enginePingLogger = createModuleLogger("engine-ping");
4882
7220
  const httpApiLogger = createModuleLogger("http-api");
@@ -4895,8 +7233,12 @@ async function startServer(options) {
4895
7233
  readTimeoutMs: options.config.startupPingTimeoutMs
4896
7234
  });
4897
7235
  await pruneRegistryEntries(registry, store, options.config);
7236
+ let notifyRegistryChanged = () => {};
4898
7237
  const httpApi = createHttpApi({
4899
7238
  logger: httpApiLogger,
7239
+ onRegistryChanged: () => {
7240
+ notifyRegistryChanged();
7241
+ },
4900
7242
  projectIdResolver,
4901
7243
  registry,
4902
7244
  registryWriteQueue,
@@ -4909,7 +7251,7 @@ async function startServer(options) {
4909
7251
  const listeningHttpPort = getListeningHttpPort(httpApi, options.config.httpPort);
4910
7252
  let mcpServer;
4911
7253
  try {
4912
- mcpServer = createMcpServer({
7254
+ const createdMcpServer = createMcpServer({
4913
7255
  logger: mcpToolLogger,
4914
7256
  logSessionManager,
4915
7257
  pingTimeoutMs: options.config.startupPingTimeoutMs,
@@ -4918,6 +7260,10 @@ async function startServer(options) {
4918
7260
  registryWriteQueue,
4919
7261
  store
4920
7262
  });
7263
+ mcpServer = createdMcpServer;
7264
+ notifyRegistryChanged = () => {
7265
+ createdMcpServer.sendResourceListChanged();
7266
+ };
4921
7267
  if (options.connectMcpTransport !== false) await mcpServer.connect(new StdioServerTransport());
4922
7268
  } catch (error) {
4923
7269
  try {
@@ -4971,8 +7317,14 @@ function createMcpServer(dependencies) {
4971
7317
  const server = new McpServer({
4972
7318
  name: SERVER_NAME,
4973
7319
  version: SERVER_VERSION
4974
- }, { capabilities: { tools: {} } });
7320
+ }, { capabilities: {
7321
+ prompts: {},
7322
+ resources: {},
7323
+ tools: {}
7324
+ } });
4975
7325
  registerMcpTools(server, dependencies);
7326
+ registerMcpResources(server, dependencies);
7327
+ registerMcpPrompts(server);
4976
7328
  return server;
4977
7329
  }
4978
7330
  async function pruneRegistryEntries(registry, store, config) {