@uemcp/server 1.7.0 → 1.8.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 +50 -0
  2. package/dist/index.mjs +1676 -10
  3. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { z } from "zod";
5
5
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { randomUUID } from "node:crypto";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  import net from "node:net";
9
9
  import Fastify from "fastify";
10
10
  import { Buffer as Buffer$1 } from "node:buffer";
@@ -362,6 +362,12 @@ const TOOL_METADATA = {
362
362
  "build.cs"
363
363
  ]),
364
364
  add_post_process_material: mutates("fx", "add_post_process_material"),
365
+ add_reflected_member: mutates("build", void 0, [
366
+ "uproperty",
367
+ "ufunction",
368
+ "header",
369
+ "member"
370
+ ]),
365
371
  add_sequence_binding: mutates("sequence", "add_sequence_binding"),
366
372
  add_sequence_keys: mutates("sequence", "add_sequence_keys"),
367
373
  add_sequence_track: mutates("sequence", "add_sequence_track"),
@@ -463,6 +469,12 @@ const TOOL_METADATA = {
463
469
  "command",
464
470
  "stat"
465
471
  ]),
472
+ export_blueprint_cpp: readOnly("blueprint", "get_blueprint_summary", [
473
+ "export",
474
+ "c++",
475
+ "skeleton",
476
+ "pseudocode"
477
+ ]),
466
478
  export_data_table: reachesOutside("asset", true, "export_data_table"),
467
479
  find_assets: readOnly("core", "find_assets", [
468
480
  "search",
@@ -470,6 +482,13 @@ const TOOL_METADATA = {
470
482
  "registry"
471
483
  ]),
472
484
  find_blueprint_references: readOnly("blueprint", "find_blueprint_references"),
485
+ find_symbol: readOnly("build", void 0, [
486
+ "symbol",
487
+ "definition",
488
+ "reference",
489
+ "c++",
490
+ "index"
491
+ ]),
473
492
  focus_actors: idempotentMutates("level", "focus_actors"),
474
493
  generate_collision: idempotentMutates("asset", "generate_collision"),
475
494
  generate_lods: idempotentMutates("asset", "generate_lods"),
@@ -498,6 +517,11 @@ const TOOL_METADATA = {
498
517
  "reflection"
499
518
  ]),
500
519
  get_compile_status: readOnly("build", "get_compile_status"),
520
+ get_live_compile_details: readOnly("build", "get_live_compile_details", [
521
+ "live coding",
522
+ "patch",
523
+ "module"
524
+ ]),
501
525
  get_config_value: readOnly("core", "get_config_value"),
502
526
  get_crash_callstack: readOnly("core", "get_crash_callstack", [
503
527
  "crash",
@@ -576,11 +600,22 @@ const TOOL_METADATA = {
576
600
  replace_actors: destroys("level", "replace_actors"),
577
601
  run_automation_tests: mutates("test", "start_automation_run"),
578
602
  run_clang_tidy: readOnly("build", void 0, ["lint", "static analysis"]),
603
+ run_editor_python: destroys("core", "run_editor_python", [
604
+ "python",
605
+ "script",
606
+ "unreal"
607
+ ]),
579
608
  run_editor_commandlet: mutates("build"),
580
609
  run_geometry_script: mutates("level", "run_geometry_script"),
581
610
  run_pcg_generate: mutates("fx", "run_pcg_generate"),
582
611
  save_asset: idempotentMutates("asset", "save_asset"),
583
612
  save_dirty_packages: idempotentMutates("core", "save_dirty_packages"),
613
+ scaffold_class_template: mutates("build", void 0, [
614
+ "scaffold",
615
+ "template",
616
+ "c++",
617
+ "class"
618
+ ]),
584
619
  sculpt_landscape: mutates("level", "sculpt_landscape"),
585
620
  search_tools: readOnly("core", void 0, ["discover", "find tool"]),
586
621
  select_actors: idempotentMutates("level", "select_actors"),
@@ -1068,6 +1103,13 @@ function resolveDefaultRegistryFilePath(options = {}) {
1068
1103
  if (platform === "win32") return joinPath(getAbsoluteDirectoryOverride(env.APPDATA, isAbsolutePath) ?? joinPath(homeDirectory, "AppData", "Roaming"), "MCPBridge", "registry.json");
1069
1104
  return joinPath(getAbsoluteDirectoryOverride(env.XDG_STATE_HOME, isAbsolutePath) ?? joinPath(homeDirectory, ".local", "state"), "MCPBridge", "registry.json");
1070
1105
  }
1106
+ function resolveSymbolIndexCacheDirectory(options = {}) {
1107
+ const env = options.env ?? process.env;
1108
+ const pathModule = (options.platform ?? process.platform) === "win32" ? path.win32 : path.posix;
1109
+ const configured = getAbsoluteDirectoryOverride(env.MCP_SERVER_SYMBOL_INDEX_DIR, pathModule.isAbsolute);
1110
+ if (configured !== void 0) return configured;
1111
+ return pathModule.join(pathModule.dirname(resolveDefaultRegistryFilePath(options)), "symbol-index");
1112
+ }
1071
1113
  function getConfiguredRegistryFilePath(env) {
1072
1114
  for (const candidate of [env.MCP_SERVER_REGISTRY_FILE, env.MCP_REGISTRY_FILE]) if (candidate && candidate.trim().length > 0) return candidate.trim();
1073
1115
  }
@@ -1674,6 +1716,7 @@ function createBridgeLaunchOnDeviceRequest(deviceId, map, requestId = randomUUID
1674
1716
  const BRIDGE_START_LIVE_COMPILE_REQUEST_TYPE = "start_live_compile";
1675
1717
  const BRIDGE_START_HOT_RELOAD_REQUEST_TYPE = "start_hot_reload";
1676
1718
  const BRIDGE_GET_COMPILE_STATUS_REQUEST_TYPE = "get_compile_status";
1719
+ const BRIDGE_GET_LIVE_COMPILE_DETAILS_REQUEST_TYPE = "get_live_compile_details";
1677
1720
  const BRIDGE_LIVE_COMPILE_STATES = [
1678
1721
  "compiling",
1679
1722
  "already_compiling",
@@ -1710,6 +1753,12 @@ const bridgeGetCompileStatusRequestSchema = z.object({
1710
1753
  type: z.literal(BRIDGE_GET_COMPILE_STATUS_REQUEST_TYPE),
1711
1754
  version: z.literal(2)
1712
1755
  });
1756
+ const bridgeGetLiveCompileDetailsRequestSchema = z.object({
1757
+ compileJobId: compileJobIdSchema,
1758
+ requestId: requestIdSchema,
1759
+ type: z.literal(BRIDGE_GET_LIVE_COMPILE_DETAILS_REQUEST_TYPE),
1760
+ version: z.literal(2)
1761
+ });
1713
1762
  const bridgeStartLiveCompileSuccessResponseSchema = z.object({
1714
1763
  compileJobId: z.string().trim().min(1),
1715
1764
  message: z.string().trim().min(1).optional(),
@@ -1761,9 +1810,30 @@ const bridgeGetCompileStatusSuccessResponseSchema = z.object({
1761
1810
  path: [field]
1762
1811
  });
1763
1812
  });
1813
+ const bridgeGetLiveCompileDetailsSuccessResponseSchema = z.object({
1814
+ compileJobId: compileJobIdSchema,
1815
+ ok: z.literal(true),
1816
+ patchedFunctions: z.array(z.string()),
1817
+ patchedModules: z.array(z.string()),
1818
+ rejections: z.array(z.string()),
1819
+ requestId: requestIdSchema,
1820
+ restartReason: z.string().trim().min(1).optional(),
1821
+ restartRequired: z.boolean().optional(),
1822
+ state: compileStatusStateSchema,
1823
+ truncated: z.boolean(),
1824
+ type: z.literal(BRIDGE_GET_LIVE_COMPILE_DETAILS_REQUEST_TYPE),
1825
+ version: z.literal(2)
1826
+ }).superRefine((value, ctx) => {
1827
+ if (value.restartReason !== void 0 && value.restartRequired !== true) ctx.addIssue({
1828
+ code: z.ZodIssueCode.custom,
1829
+ message: "restartReason must be absent unless restartRequired is true",
1830
+ path: ["restartReason"]
1831
+ });
1832
+ });
1764
1833
  const bridgeStartLiveCompileResponseSchema = z.union([bridgeStartLiveCompileSuccessResponseSchema, bridgeErrorResponseSchema]);
1765
1834
  const bridgeStartHotReloadResponseSchema = z.union([bridgeStartHotReloadSuccessResponseSchema, bridgeErrorResponseSchema]);
1766
1835
  const bridgeGetCompileStatusResponseSchema = z.union([bridgeGetCompileStatusSuccessResponseSchema, bridgeErrorResponseSchema]);
1836
+ const bridgeGetLiveCompileDetailsResponseSchema = z.union([bridgeGetLiveCompileDetailsSuccessResponseSchema, bridgeErrorResponseSchema]);
1767
1837
  function createBridgeStartLiveCompileRequest(requestId = randomUUID()) {
1768
1838
  return {
1769
1839
  requestId,
@@ -1786,6 +1856,59 @@ function createBridgeGetCompileStatusRequest(compileJobId, requestId = randomUUI
1786
1856
  version: 2
1787
1857
  });
1788
1858
  }
1859
+ function createBridgeGetLiveCompileDetailsRequest(compileJobId, requestId = randomUUID()) {
1860
+ return bridgeGetLiveCompileDetailsRequestSchema.parse({
1861
+ compileJobId,
1862
+ requestId,
1863
+ type: BRIDGE_GET_LIVE_COMPILE_DETAILS_REQUEST_TYPE,
1864
+ version: 2
1865
+ });
1866
+ }
1867
+ //#endregion
1868
+ //#region src/protocol/python.ts
1869
+ const BRIDGE_RUN_EDITOR_PYTHON_REQUEST_TYPE = "run_editor_python";
1870
+ const BRIDGE_PYTHON_EXECUTION_MODES = [
1871
+ "script",
1872
+ "statement",
1873
+ "evaluate"
1874
+ ];
1875
+ const BRIDGE_PYTHON_STATES = [
1876
+ "succeeded",
1877
+ "failed",
1878
+ "disabled",
1879
+ "unsupported"
1880
+ ];
1881
+ const bridgeRunEditorPythonRequestSchema = z.object({
1882
+ command: z.string().min(1).max(16384),
1883
+ executionMode: z.enum(BRIDGE_PYTHON_EXECUTION_MODES),
1884
+ requestId: requestIdSchema,
1885
+ type: z.literal(BRIDGE_RUN_EDITOR_PYTHON_REQUEST_TYPE),
1886
+ unattended: z.boolean(),
1887
+ version: z.literal(2)
1888
+ });
1889
+ const bridgeRunEditorPythonSuccessResponseSchema = z.object({
1890
+ errors: z.array(z.string()),
1891
+ message: z.string().trim().min(1).optional(),
1892
+ ok: z.literal(true),
1893
+ output: z.array(z.string()),
1894
+ requestId: requestIdSchema,
1895
+ result: z.string().optional(),
1896
+ state: z.enum(BRIDGE_PYTHON_STATES),
1897
+ truncated: z.boolean(),
1898
+ type: z.literal(BRIDGE_RUN_EDITOR_PYTHON_REQUEST_TYPE),
1899
+ version: z.literal(2)
1900
+ });
1901
+ const bridgeRunEditorPythonResponseSchema = z.union([bridgeRunEditorPythonSuccessResponseSchema, bridgeErrorResponseSchema]);
1902
+ function createBridgeRunEditorPythonRequest(command, options = {}, requestId = randomUUID()) {
1903
+ return bridgeRunEditorPythonRequestSchema.parse({
1904
+ command,
1905
+ executionMode: options.executionMode ?? "script",
1906
+ requestId,
1907
+ type: BRIDGE_RUN_EDITOR_PYTHON_REQUEST_TYPE,
1908
+ unattended: options.unattended ?? true,
1909
+ version: 2
1910
+ });
1911
+ }
1789
1912
  //#endregion
1790
1913
  //#region src/protocol/editor.ts
1791
1914
  const BRIDGE_GET_EDITOR_STATE_REQUEST_TYPE = "get_editor_state";
@@ -9127,7 +9250,7 @@ function createBridgeSetDefaultGamemodeRequest(scope, gameModePath, options = {}
9127
9250
  }
9128
9251
  //#endregion
9129
9252
  //#region src/version.ts
9130
- const SERVER_VERSION = "1.7.0";
9253
+ const SERVER_VERSION = "1.8.0";
9131
9254
  //#endregion
9132
9255
  //#region src/bridgeSocketClient.ts
9133
9256
  var BridgeSocketError = class extends Error {
@@ -9162,6 +9285,12 @@ async function sendBridgeStartHotReloadRequest(request, options) {
9162
9285
  async function sendBridgeGetCompileStatusRequest(request, options) {
9163
9286
  return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetCompileStatusResponseSchema, request.requestId);
9164
9287
  }
9288
+ async function sendBridgeGetLiveCompileDetailsRequest(request, options) {
9289
+ return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetLiveCompileDetailsResponseSchema, request.requestId);
9290
+ }
9291
+ async function sendBridgeRunEditorPythonRequest(request, options) {
9292
+ return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeRunEditorPythonResponseSchema, request.requestId);
9293
+ }
9165
9294
  async function sendBridgeGetCrashCallstackRequest(request, options) {
9166
9295
  return parseBridgeResponse(await exchangeBridgeMessage(encodeBridgeFrame(request), options), bridgeGetCrashCallstackResponseSchema, request.requestId);
9167
9296
  }
@@ -13113,8 +13242,323 @@ const behaviorTreeToolDomain = {
13113
13242
  "add_bt_service"
13114
13243
  ]
13115
13244
  };
13116
- //#endregion
13117
- //#region src/tools/blueprint.ts
13245
+ const EXEC_PIN_NAMES = /* @__PURE__ */ new Set(["execute", "then"]);
13246
+ const ENTRY_NODE_CLASSES = /* @__PURE__ */ new Set([
13247
+ "K2Node_ComponentBoundEvent",
13248
+ "K2Node_CustomEvent",
13249
+ "K2Node_Event",
13250
+ "K2Node_FunctionEntry",
13251
+ "K2Node_InputAction",
13252
+ "K2Node_InputKey",
13253
+ "K2Node_Timeline"
13254
+ ]);
13255
+ const HIDDEN_INPUT_PIN_NAMES = new Set(["self", "WorldContextObject"].map((name) => name.toLowerCase()));
13256
+ function firstTitleLine(node) {
13257
+ const [first] = node.title.split(/\r?\n/u);
13258
+ return (first ?? node.title).trim();
13259
+ }
13260
+ function isExecPin(pin) {
13261
+ return pin.type.trim().toLowerCase() === "exec" || EXEC_PIN_NAMES.has(pin.name.toLowerCase());
13262
+ }
13263
+ function execOutputs(node) {
13264
+ return node.pins.filter((pin) => pin.direction === "output" && isExecPin(pin));
13265
+ }
13266
+ function execInputs(node) {
13267
+ return node.pins.filter((pin) => pin.direction === "input" && isExecPin(pin));
13268
+ }
13269
+ function dataInputs(node) {
13270
+ return node.pins.filter((pin) => pin.direction === "input" && !isExecPin(pin) && !HIDDEN_INPUT_PIN_NAMES.has(pin.name.toLowerCase()));
13271
+ }
13272
+ function findPin(node, names) {
13273
+ const wanted = new Set(names.map((name) => name.toLowerCase()));
13274
+ return node.pins.find((pin) => wanted.has(pin.name.toLowerCase()));
13275
+ }
13276
+ function indent(depth) {
13277
+ return " ".repeat(depth);
13278
+ }
13279
+ var GraphWalker = class {
13280
+ nodesById;
13281
+ lines = [];
13282
+ emitted = /* @__PURE__ */ new Set();
13283
+ truncated = false;
13284
+ constructor(nodes) {
13285
+ this.nodesById = new Map(nodes.map((node) => [node.nodeId, node]));
13286
+ }
13287
+ render(nodes) {
13288
+ for (const root of this.findRoots(nodes)) {
13289
+ this.lines.push(`${this.entryHeader(root)} {`);
13290
+ this.walkFrom(root, 1);
13291
+ this.lines.push("}");
13292
+ this.lines.push("");
13293
+ }
13294
+ while (this.lines.at(-1) === "") this.lines.pop();
13295
+ return {
13296
+ lines: this.lines,
13297
+ truncated: this.truncated
13298
+ };
13299
+ }
13300
+ findRoots(nodes) {
13301
+ return nodes.filter((node) => {
13302
+ if (ENTRY_NODE_CLASSES.has(node.nodeClass)) return true;
13303
+ const inputs = execInputs(node);
13304
+ return inputs.length > 0 && inputs.every((pin) => pin.linkedTo.length === 0) && execOutputs(node).length > 0;
13305
+ }).sort((left, right) => {
13306
+ const byTitle = firstTitleLine(left).localeCompare(firstTitleLine(right));
13307
+ return byTitle === 0 ? left.nodeId.localeCompare(right.nodeId) : byTitle;
13308
+ });
13309
+ }
13310
+ entryHeader(node) {
13311
+ if (node.nodeClass === "K2Node_FunctionEntry") return `function ${firstTitleLine(node)}()`;
13312
+ return `event ${firstTitleLine(node)}()`;
13313
+ }
13314
+ walkFrom(node, depth) {
13315
+ const outputs = execOutputs(node);
13316
+ if (outputs.length <= 1) {
13317
+ const [only] = outputs;
13318
+ if (only !== void 0) this.walkPin(only, depth);
13319
+ return;
13320
+ }
13321
+ const then = findPin(node, ["then"]);
13322
+ if (then !== void 0) {
13323
+ this.walkPin(then, depth);
13324
+ return;
13325
+ }
13326
+ for (const output of outputs) {
13327
+ if (output.linkedTo.length === 0) continue;
13328
+ this.lines.push(`${indent(depth)}// ${output.name}:`);
13329
+ this.walkPin(output, depth + 1);
13330
+ }
13331
+ }
13332
+ walkPin(pin, depth) {
13333
+ const link = pin.linkedTo[0];
13334
+ if (link === void 0) return;
13335
+ const target = this.nodesById.get(link.nodeId);
13336
+ if (target === void 0) {
13337
+ this.truncated = true;
13338
+ return;
13339
+ }
13340
+ this.walkNode(target, depth);
13341
+ }
13342
+ walkNode(node, depth) {
13343
+ if (depth > 32) {
13344
+ this.lines.push(`${indent(depth)}// nesting cap reached`);
13345
+ this.truncated = true;
13346
+ return;
13347
+ }
13348
+ if (this.emitted.has(node.nodeId)) {
13349
+ this.lines.push(`${indent(depth)}goto ${firstTitleLine(node)};`);
13350
+ this.truncated = true;
13351
+ return;
13352
+ }
13353
+ this.emitted.add(node.nodeId);
13354
+ if (node.nodeClass === "K2Node_IfThenElse") {
13355
+ this.emitBranch(node, depth);
13356
+ return;
13357
+ }
13358
+ if (node.nodeClass === "K2Node_ExecutionSequence") {
13359
+ for (const output of execOutputs(node)) this.walkPin(output, depth);
13360
+ return;
13361
+ }
13362
+ const loop = this.loopHeader(node);
13363
+ if (loop !== void 0) {
13364
+ this.lines.push(`${indent(depth)}${loop} {`);
13365
+ const body = findPin(node, ["LoopBody", "Loop Body"]);
13366
+ if (body !== void 0) this.walkPin(body, depth + 1);
13367
+ this.lines.push(`${indent(depth)}}`);
13368
+ const completed = findPin(node, ["Completed"]);
13369
+ if (completed !== void 0) this.walkPin(completed, depth);
13370
+ return;
13371
+ }
13372
+ this.lines.push(`${indent(depth)}${this.statement(node)};`);
13373
+ this.walkFrom(node, depth);
13374
+ }
13375
+ emitBranch(node, depth) {
13376
+ const condition = findPin(node, ["Condition"]);
13377
+ this.lines.push(`${indent(depth)}if (${condition === void 0 ? "true" : this.expression(condition, 0)}) {`);
13378
+ const trueBranch = findPin(node, ["then", "True"]);
13379
+ if (trueBranch !== void 0) this.walkPin(trueBranch, depth + 1);
13380
+ const falseBranch = findPin(node, ["else", "False"]);
13381
+ if (falseBranch !== void 0 && falseBranch.linkedTo.length > 0) {
13382
+ this.lines.push(`${indent(depth)}} else {`);
13383
+ this.walkPin(falseBranch, depth + 1);
13384
+ }
13385
+ this.lines.push(`${indent(depth)}}`);
13386
+ }
13387
+ loopHeader(node) {
13388
+ if (node.nodeClass !== "K2Node_MacroInstance") return;
13389
+ const lowered = firstTitleLine(node).toLowerCase();
13390
+ if (lowered.startsWith("while loop")) {
13391
+ const condition = findPin(node, ["Condition"]);
13392
+ return `while (${condition === void 0 ? "true" : this.expression(condition, 0)})`;
13393
+ }
13394
+ if (lowered.startsWith("for each loop")) {
13395
+ const array = findPin(node, ["Array"]);
13396
+ return `for (Element : ${array === void 0 ? "Array" : this.expression(array, 0)})`;
13397
+ }
13398
+ if (lowered.startsWith("for loop")) {
13399
+ const first = findPin(node, ["First Index", "FirstIndex"]);
13400
+ const last = findPin(node, ["Last Index", "LastIndex"]);
13401
+ return `for (Index = ${first === void 0 ? "0" : this.expression(first, 0)}; Index <= ${last === void 0 ? "0" : this.expression(last, 0)}; Index++)`;
13402
+ }
13403
+ }
13404
+ statement(node) {
13405
+ if (node.nodeClass === "K2Node_VariableSet") {
13406
+ const [assigned] = dataInputs(node);
13407
+ return assigned === void 0 ? firstTitleLine(node) : `${assigned.name} = ${this.expression(assigned, 0)}`;
13408
+ }
13409
+ if (node.nodeClass === "K2Node_FunctionResult") {
13410
+ const values = dataInputs(node).map((pin) => this.expression(pin, 0));
13411
+ return values.length === 0 ? "return" : `return ${values.join(", ")}`;
13412
+ }
13413
+ return this.callExpression(node);
13414
+ }
13415
+ callExpression(node) {
13416
+ const args = dataInputs(node).map((pin) => this.expression(pin, 0));
13417
+ return `${firstTitleLine(node)}(${args.join(", ")})`;
13418
+ }
13419
+ expression(pin, depth) {
13420
+ const link = pin.linkedTo[0];
13421
+ if (link === void 0) {
13422
+ const literal = pin.defaultValue?.trim();
13423
+ return literal !== void 0 && literal.length > 0 ? literal : pin.name;
13424
+ }
13425
+ if (depth >= 8) {
13426
+ this.truncated = true;
13427
+ return pin.name;
13428
+ }
13429
+ const source = this.nodesById.get(link.nodeId);
13430
+ if (source === void 0) {
13431
+ this.truncated = true;
13432
+ return pin.name;
13433
+ }
13434
+ if (source.nodeClass === "K2Node_VariableGet") return source.pins.find((candidate) => candidate.direction === "output" && !isExecPin(candidate))?.name ?? firstTitleLine(source);
13435
+ const args = dataInputs(source).map((input) => this.expression(input, depth + 1));
13436
+ if (args.length === 0 && execInputs(source).length === 0 && source.pins.every((candidate) => !isExecPin(candidate))) return `${firstTitleLine(source)}()`;
13437
+ return `${firstTitleLine(source)}(${args.join(", ")})`;
13438
+ }
13439
+ };
13440
+ function renderGraphPseudoCode(graph) {
13441
+ const { lines, truncated } = new GraphWalker(graph.nodes).render(graph.nodes);
13442
+ return {
13443
+ graphKind: graph.graphKind,
13444
+ graphName: graph.graphName,
13445
+ lines,
13446
+ truncated: truncated || graph.truncated
13447
+ };
13448
+ }
13449
+ const BLUEPRINT_TYPE_TO_CPP = /* @__PURE__ */ new Map([
13450
+ ["boolean", "bool"],
13451
+ ["float (single-precision)", "float"],
13452
+ ["float (double-precision)", "double"],
13453
+ ["byte", "uint8"],
13454
+ ["integer", "int32"],
13455
+ ["integer64", "int64"],
13456
+ ["float", "double"],
13457
+ ["real", "double"],
13458
+ ["name", "FName"],
13459
+ ["string", "FString"],
13460
+ ["text", "FText"],
13461
+ ["vector", "FVector"],
13462
+ ["rotator", "FRotator"],
13463
+ ["transform", "FTransform"]
13464
+ ]);
13465
+ function blueprintTypeToCpp(type) {
13466
+ const trimmed = type.trim();
13467
+ const arrayMatch = /^array of (?<inner>.+)$/iu.exec(trimmed);
13468
+ if (arrayMatch?.groups?.inner !== void 0) return `TArray<${blueprintTypeToCpp(arrayMatch.groups.inner)}>`;
13469
+ return BLUEPRINT_TYPE_TO_CPP.get(trimmed.toLowerCase()) ?? trimmed;
13470
+ }
13471
+ const ACTOR_DERIVED_PARENT_NAMES = new Set([
13472
+ "Actor",
13473
+ "AIController",
13474
+ "Character",
13475
+ "DefaultPawn",
13476
+ "GameMode",
13477
+ "GameModeBase",
13478
+ "GameSession",
13479
+ "GameState",
13480
+ "GameStateBase",
13481
+ "HUD",
13482
+ "Pawn",
13483
+ "PlayerController",
13484
+ "PlayerState",
13485
+ "SpectatorPawn",
13486
+ "WorldSettings"
13487
+ ].map((name) => name.toLowerCase()));
13488
+ function blueprintParentToCpp(parentClass) {
13489
+ if (parentClass === void 0 || parentClass.trim().length === 0) return {
13490
+ name: "UObject",
13491
+ prefix: "U"
13492
+ };
13493
+ const sanitized = sanitizeIdentifier(parentClass);
13494
+ if (ACTOR_DERIVED_PARENT_NAMES.has(sanitized.toLowerCase())) return {
13495
+ name: `A${sanitized}`,
13496
+ prefix: "A"
13497
+ };
13498
+ if (/^[AUI][A-Z]/u.test(sanitized)) return {
13499
+ name: sanitized,
13500
+ prefix: sanitized.startsWith("A") ? "A" : "U"
13501
+ };
13502
+ return {
13503
+ name: `U${sanitized}`,
13504
+ prefix: "U"
13505
+ };
13506
+ }
13507
+ function sanitizeIdentifier(value) {
13508
+ const cleaned = value.replace(/[^A-Za-z0-9_]/gu, "");
13509
+ return cleaned.length === 0 ? "Unnamed" : cleaned;
13510
+ }
13511
+ function variableSpecifiers(variable) {
13512
+ const specifiers = ["EditAnywhere", "BlueprintReadWrite"];
13513
+ if (variable.replication === "replicated") specifiers.push("Replicated");
13514
+ if (variable.replication === "rep_notify") specifiers.push(`ReplicatedUsing = OnRep_${sanitizeIdentifier(variable.name)}`);
13515
+ if (variable.category !== void 0 && variable.category.trim().length > 0) specifiers.push(`Category = "${variable.category.trim()}"`);
13516
+ return specifiers.join(", ");
13517
+ }
13518
+ function repNotifyDeclaration(variable) {
13519
+ return [" UFUNCTION()", `\tvoid OnRep_${sanitizeIdentifier(variable.name)}();`];
13520
+ }
13521
+ function functionDeclaration(member) {
13522
+ const returnParameter = member.parameters.find((parameter) => parameter.isReturn);
13523
+ const returnType = returnParameter === void 0 ? "void" : blueprintTypeToCpp(returnParameter.type);
13524
+ const parameters = member.parameters.filter((parameter) => !parameter.isReturn).map((parameter) => {
13525
+ const type = blueprintTypeToCpp(parameter.type);
13526
+ return parameter.isOut ? `${type}& ${sanitizeIdentifier(parameter.name)}` : `${type} ${sanitizeIdentifier(parameter.name)}`;
13527
+ });
13528
+ return [`\tUFUNCTION(${member.kind === "event" ? "BlueprintCallable, Category = \"Events\"" : "BlueprintCallable"})`, `\t${returnType} ${sanitizeIdentifier(member.name)}(${parameters.join(", ")});`];
13529
+ }
13530
+ function renderCppSkeleton(source) {
13531
+ const parent = blueprintParentToCpp(source.parentClass);
13532
+ const lines = [
13533
+ "#pragma once",
13534
+ "",
13535
+ "#include \"CoreMinimal.h\"",
13536
+ "",
13537
+ "UCLASS(BlueprintType, Blueprintable)",
13538
+ `class ${`${parent.prefix}${sanitizeIdentifier(source.blueprintName)}`} : public ${parent.name}`,
13539
+ "{",
13540
+ " GENERATED_BODY()",
13541
+ "",
13542
+ "public:"
13543
+ ];
13544
+ for (const interfaceName of source.interfaces) lines.push(`\t// implements ${interfaceName}`);
13545
+ for (const variable of source.variables) {
13546
+ lines.push(`\tUPROPERTY(${variableSpecifiers(variable)})`);
13547
+ lines.push(`\t${blueprintTypeToCpp(variable.type)} ${sanitizeIdentifier(variable.name)};`);
13548
+ if (variable.replication === "rep_notify") {
13549
+ lines.push("");
13550
+ lines.push(...repNotifyDeclaration(variable));
13551
+ }
13552
+ lines.push("");
13553
+ }
13554
+ for (const member of source.functions) {
13555
+ lines.push(...functionDeclaration(member));
13556
+ lines.push("");
13557
+ }
13558
+ while (lines.at(-1) === "") lines.pop();
13559
+ lines.push("};");
13560
+ return lines.join("\n");
13561
+ }
13118
13562
  const findBlueprintReferencesInputSchema = {
13119
13563
  className: bridgeFindBlueprintReferencesRequestSchema.shape.className.describe("C++ class to look for, as a class path (/Script/MyGame.MyActor) or a class name with or without its prefix (AMyActor, MyActor)."),
13120
13564
  cursor: bridgeFindBlueprintReferencesRequestSchema.shape.cursor.describe("Offset into the stable-ordered reference list (default 0). Pass the previous response nextCursor to fetch the next page."),
@@ -13558,6 +14002,132 @@ function registerBlueprintTools(server, dependencies) {
13558
14002
  transactionTitle
13559
14003
  }, dependencies));
13560
14004
  });
14005
+ server.registerTool("export_blueprint_cpp", {
14006
+ description: "Render a Blueprint as readable pseudo-code plus a C++ class skeleton, as the starting point for porting it to C++. Drains every page of the Blueprint summary and of each selected graph, walks exec pins into statements (branches, loops, function calls, variable assignment, returns), and declares each Blueprint variable and function as a UPROPERTY/UFUNCTION. The skeleton is a scaffold, not compilable nativization; types the editor spells differently from C++ are left verbatim.",
14007
+ inputSchema: exportBlueprintCppInputSchema,
14008
+ title: "Export Blueprint as C++"
14009
+ }, async (input) => {
14010
+ return toToolSuccess(await exportBlueprintCpp(input, dependencies));
14011
+ });
14012
+ }
14013
+ const exportBlueprintCppInputSchema = {
14014
+ blueprintPath: bridgeGetBlueprintGraphRequestSchema.shape.blueprintPath.describe("Package name or object path of the Blueprint to export."),
14015
+ graphName: bridgeGetBlueprintGraphRequestSchema.shape.graphName.optional().describe("Export only this graph; defaults to every graph on the Blueprint, up to maxGraphs."),
14016
+ loadIfNeeded: bridgeGetBlueprintGraphRequestSchema.shape.loadIfNeeded.describe("Load the Blueprint if it is not already in memory (default false). Graph contents require the asset in memory."),
14017
+ maxGraphs: z.number().int().min(1).max(50).default(10).describe("Maximum graphs to render when graphName is not given (1-50, default 10)."),
14018
+ maxNodesPerGraph: bridgeGetBlueprintGraphRequestSchema.shape.maxNodes.default(25).describe("Nodes fetched per bridge page while draining a graph (1-200, default 25)."),
14019
+ projectId: projectIdSchema.describe("Canonical Unreal ProjectID whose registered editor should read the Blueprint.")
14020
+ };
14021
+ async function collectBlueprintSummary(input, dependencies) {
14022
+ const first = await getBlueprintSummary({
14023
+ blueprintPath: input.blueprintPath,
14024
+ ...input.loadIfNeeded === void 0 ? {} : { loadIfNeeded: input.loadIfNeeded },
14025
+ projectId: input.projectId
14026
+ }, dependencies);
14027
+ const summary = {
14028
+ ...first,
14029
+ functions: [...first.functions],
14030
+ graphs: [...first.graphs],
14031
+ variables: [...first.variables]
14032
+ };
14033
+ let truncated = first.truncated;
14034
+ let functionCursor = first.nextFunctionCursor;
14035
+ let graphCursor = first.nextGraphCursor;
14036
+ let variableCursor = first.nextVariableCursor;
14037
+ for (let page = 0; page < 20; page += 1) {
14038
+ if (functionCursor === void 0 && graphCursor === void 0 && variableCursor === void 0) return {
14039
+ summary,
14040
+ truncated
14041
+ };
14042
+ const next = await getBlueprintSummary({
14043
+ blueprintPath: input.blueprintPath,
14044
+ ...functionCursor === void 0 ? {} : { functionCursor },
14045
+ ...graphCursor === void 0 ? {} : { graphCursor },
14046
+ ...input.loadIfNeeded === void 0 ? {} : { loadIfNeeded: input.loadIfNeeded },
14047
+ projectId: input.projectId,
14048
+ ...variableCursor === void 0 ? {} : { variableCursor }
14049
+ }, dependencies);
14050
+ if (functionCursor !== void 0) summary.functions.push(...next.functions);
14051
+ if (graphCursor !== void 0) summary.graphs.push(...next.graphs);
14052
+ if (variableCursor !== void 0) summary.variables.push(...next.variables);
14053
+ truncated = truncated || next.truncated;
14054
+ functionCursor = functionCursor === void 0 ? void 0 : next.nextFunctionCursor;
14055
+ graphCursor = graphCursor === void 0 ? void 0 : next.nextGraphCursor;
14056
+ variableCursor = variableCursor === void 0 ? void 0 : next.nextVariableCursor;
14057
+ }
14058
+ return {
14059
+ summary,
14060
+ truncated: true
14061
+ };
14062
+ }
14063
+ async function collectGraphNodes(input, graphName, dependencies) {
14064
+ const nodes = [];
14065
+ let cursor;
14066
+ let graphKind = "unknown";
14067
+ let truncated = false;
14068
+ for (let page = 0; page < 40; page += 1) {
14069
+ const response = await getBlueprintGraph({
14070
+ blueprintPath: input.blueprintPath,
14071
+ graphName,
14072
+ ...input.loadIfNeeded === void 0 ? {} : { loadIfNeeded: input.loadIfNeeded },
14073
+ maxNodes: input.maxNodesPerGraph,
14074
+ ...cursor === void 0 ? {} : { nodeCursor: cursor },
14075
+ projectId: input.projectId
14076
+ }, dependencies);
14077
+ nodes.push(...response.nodes);
14078
+ graphKind = response.graphKind;
14079
+ truncated = truncated || response.truncated;
14080
+ if (response.nextNodeCursor === void 0) return {
14081
+ graphKind,
14082
+ nodes,
14083
+ truncated
14084
+ };
14085
+ cursor = response.nextNodeCursor;
14086
+ }
14087
+ return {
14088
+ graphKind,
14089
+ nodes,
14090
+ truncated: true
14091
+ };
14092
+ }
14093
+ async function exportBlueprintCpp(input, dependencies) {
14094
+ const { summary, truncated: summaryTruncated } = await collectBlueprintSummary(input, dependencies);
14095
+ const selected = input.graphName === void 0 ? summary.graphs.slice(0, input.maxGraphs) : summary.graphs.filter((graph) => graph.name === input.graphName);
14096
+ if (input.graphName !== void 0 && selected.length === 0) throw new Error(`Blueprint '${summary.blueprintPath}' has no graph named '${input.graphName}'.`);
14097
+ const rendered = [];
14098
+ let truncated = summaryTruncated || input.graphName === void 0 && selected.length < summary.graphs.length;
14099
+ for (const graph of selected) {
14100
+ const { graphKind, nodes, truncated: graphTruncated } = await collectGraphNodes(input, graph.name, dependencies);
14101
+ const pseudoCode = renderGraphPseudoCode({
14102
+ graphKind,
14103
+ graphName: graph.name,
14104
+ nodes,
14105
+ truncated: graphTruncated
14106
+ });
14107
+ truncated = truncated || pseudoCode.truncated;
14108
+ rendered.push(pseudoCode);
14109
+ }
14110
+ const skeleton = renderCppSkeleton({
14111
+ blueprintName: summary.blueprintName,
14112
+ functions: summary.functions,
14113
+ interfaces: summary.interfaces,
14114
+ ...summary.parentClass === void 0 ? {} : { parentClass: summary.parentClass },
14115
+ variables: summary.variables
14116
+ });
14117
+ dependencies.logger.info({
14118
+ blueprintPath: summary.blueprintPath,
14119
+ graphCount: rendered.length,
14120
+ projectId: summary.projectId
14121
+ }, "Exported Blueprint graphs as pseudo-code");
14122
+ return {
14123
+ blueprintName: summary.blueprintName,
14124
+ blueprintPath: summary.blueprintPath,
14125
+ graphs: rendered,
14126
+ projectId: summary.projectId,
14127
+ pseudoCode: rendered.map((graph) => [`// ${graph.graphName} (${graph.graphKind})`, ...graph.lines].join("\n")).join("\n\n"),
14128
+ skeleton,
14129
+ truncated
14130
+ };
13561
14131
  }
13562
14132
  const blueprintToolDomain = {
13563
14133
  domain: "blueprint",
@@ -13576,10 +14146,498 @@ const blueprintToolDomain = {
13576
14146
  "add_bp_component",
13577
14147
  "add_bp_interface",
13578
14148
  "set_bp_default",
13579
- "batch_edit"
14149
+ "batch_edit",
14150
+ "export_blueprint_cpp"
13580
14151
  ]
13581
14152
  };
13582
14153
  //#endregion
14154
+ //#region src/classScaffold.ts
14155
+ const REFLECTED_MEMBER_KINDS = ["property", "function"];
14156
+ const CLASS_TEMPLATES = [
14157
+ "interface",
14158
+ "game_instance_subsystem",
14159
+ "world_subsystem",
14160
+ "editor_subsystem"
14161
+ ];
14162
+ const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
14163
+ const INTERFACE_NAME_PATTERN = /^I[A-Za-z0-9_]+$/u;
14164
+ const UCLASS_NAME_PATTERN = /^U[A-Za-z0-9_]+$/u;
14165
+ function specifierSet(names) {
14166
+ return new Set(names.map((name) => name.toLowerCase()));
14167
+ }
14168
+ const PROPERTY_SPECIFIERS = specifierSet([
14169
+ "AdvancedDisplay",
14170
+ "AssetRegistrySearchable",
14171
+ "BlueprintAssignable",
14172
+ "BlueprintAuthorityOnly",
14173
+ "BlueprintCallable",
14174
+ "BlueprintGetter",
14175
+ "BlueprintReadOnly",
14176
+ "BlueprintReadWrite",
14177
+ "BlueprintSetter",
14178
+ "Config",
14179
+ "DuplicateTransient",
14180
+ "EditAnywhere",
14181
+ "EditDefaultsOnly",
14182
+ "EditFixedSize",
14183
+ "EditInline",
14184
+ "EditInstanceOnly",
14185
+ "ExportObject",
14186
+ "GlobalConfig",
14187
+ "Instanced",
14188
+ "Interp",
14189
+ "Meta",
14190
+ "NoClear",
14191
+ "NonPIEDuplicateTransient",
14192
+ "NonTransactional",
14193
+ "NotReplicated",
14194
+ "Replicated",
14195
+ "ReplicatedUsing",
14196
+ "SaveGame",
14197
+ "SimpleDisplay",
14198
+ "SkipSerialization",
14199
+ "TextExportTransient",
14200
+ "Transient",
14201
+ "VisibleAnywhere",
14202
+ "VisibleDefaultsOnly",
14203
+ "VisibleInstanceOnly"
14204
+ ]);
14205
+ const FUNCTION_SPECIFIERS = specifierSet([
14206
+ "BlueprintAuthorityOnly",
14207
+ "BlueprintCallable",
14208
+ "BlueprintCosmetic",
14209
+ "BlueprintImplementableEvent",
14210
+ "BlueprintInternalUseOnly",
14211
+ "BlueprintNativeEvent",
14212
+ "BlueprintPure",
14213
+ "CallInEditor",
14214
+ "Client",
14215
+ "Const",
14216
+ "Custom",
14217
+ "CustomThunk",
14218
+ "Exec",
14219
+ "Meta",
14220
+ "NetMulticast",
14221
+ "Reliable",
14222
+ "SealedEvent",
14223
+ "Server",
14224
+ "ServiceRequest",
14225
+ "ServiceResponse",
14226
+ "Unreliable",
14227
+ "WithValidation"
14228
+ ]);
14229
+ const EDIT_SPECIFIER_NAMES = [
14230
+ "EditAnywhere",
14231
+ "EditDefaultsOnly",
14232
+ "EditInstanceOnly"
14233
+ ];
14234
+ const VISIBLE_SPECIFIER_NAMES = [
14235
+ "VisibleAnywhere",
14236
+ "VisibleDefaultsOnly",
14237
+ "VisibleInstanceOnly"
14238
+ ];
14239
+ const RPC_SPECIFIER_NAMES = [
14240
+ "Server",
14241
+ "Client",
14242
+ "NetMulticast"
14243
+ ];
14244
+ const CATEGORY_REQUIRING_SPECIFIER_NAMES = [
14245
+ ...EDIT_SPECIFIER_NAMES,
14246
+ ...VISIBLE_SPECIFIER_NAMES,
14247
+ "BlueprintReadOnly",
14248
+ "BlueprintReadWrite"
14249
+ ];
14250
+ function normalizeSpecifierName(specifier) {
14251
+ const [name] = specifier.split("=");
14252
+ return (name ?? specifier).trim().toLowerCase();
14253
+ }
14254
+ function hasSpecifier(specifiers, name) {
14255
+ const wanted = name.toLowerCase();
14256
+ return specifiers.some((specifier) => normalizeSpecifierName(specifier) === wanted);
14257
+ }
14258
+ function assertKnownSpecifiers(specifiers, kind) {
14259
+ const known = kind === "property" ? PROPERTY_SPECIFIERS : FUNCTION_SPECIFIERS;
14260
+ const macro = kind === "property" ? "UPROPERTY" : "UFUNCTION";
14261
+ for (const specifier of specifiers) {
14262
+ const trimmed = specifier.trim();
14263
+ if (trimmed.length === 0) throw new ModuleScaffoldError(`${macro} specifiers cannot contain an empty entry.`);
14264
+ if (!known.has(normalizeSpecifierName(trimmed))) throw new ModuleScaffoldError(`'${trimmed}' is not a recognised ${macro} specifier.`);
14265
+ }
14266
+ }
14267
+ function validatePropertySpecifiers(specifiers) {
14268
+ assertKnownSpecifiers(specifiers, "property");
14269
+ const editSpecifiers = EDIT_SPECIFIER_NAMES.filter((name) => hasSpecifier(specifiers, name));
14270
+ const visibleSpecifiers = VISIBLE_SPECIFIER_NAMES.filter((name) => hasSpecifier(specifiers, name));
14271
+ if (editSpecifiers.length + visibleSpecifiers.length > 1) throw new ModuleScaffoldError(`UPROPERTY takes at most one of ${[...EDIT_SPECIFIER_NAMES, ...VISIBLE_SPECIFIER_NAMES].join(", ")}; got ${[...editSpecifiers, ...visibleSpecifiers].join(", ")}.`);
14272
+ if (hasSpecifier(specifiers, "BlueprintReadOnly") && hasSpecifier(specifiers, "BlueprintReadWrite")) throw new ModuleScaffoldError("UPROPERTY cannot be both BlueprintReadOnly and BlueprintReadWrite.");
14273
+ if (hasSpecifier(specifiers, "Replicated") && hasSpecifier(specifiers, "ReplicatedUsing")) throw new ModuleScaffoldError("UPROPERTY cannot be both Replicated and ReplicatedUsing.");
14274
+ const replicatedUsing = specifiers.find((specifier) => normalizeSpecifierName(specifier) === "ReplicatedUsing".toLowerCase());
14275
+ if (replicatedUsing !== void 0 && !replicatedUsing.includes("=")) throw new ModuleScaffoldError("ReplicatedUsing needs a handler function, e.g. ReplicatedUsing = OnRep_Health.");
14276
+ if (hasSpecifier(specifiers, "Config") && hasSpecifier(specifiers, "GlobalConfig")) throw new ModuleScaffoldError("UPROPERTY cannot be both Config and GlobalConfig.");
14277
+ if (hasSpecifier(specifiers, "Transient") && hasSpecifier(specifiers, "Config")) throw new ModuleScaffoldError("A Transient UPROPERTY cannot also be Config.");
14278
+ if ((hasSpecifier(specifiers, "Replicated") || hasSpecifier(specifiers, "ReplicatedUsing")) && hasSpecifier(specifiers, "NotReplicated")) throw new ModuleScaffoldError("UPROPERTY cannot be both replicated and NotReplicated.");
14279
+ }
14280
+ function validateFunctionSpecifiers(specifiers, returnType) {
14281
+ assertKnownSpecifiers(specifiers, "function");
14282
+ if (hasSpecifier(specifiers, "BlueprintPure") && hasSpecifier(specifiers, "BlueprintCallable")) throw new ModuleScaffoldError("UFUNCTION cannot be both BlueprintPure and BlueprintCallable.");
14283
+ if (hasSpecifier(specifiers, "BlueprintImplementableEvent") && hasSpecifier(specifiers, "BlueprintNativeEvent")) throw new ModuleScaffoldError("UFUNCTION cannot be both BlueprintImplementableEvent and BlueprintNativeEvent.");
14284
+ const rpcs = RPC_SPECIFIER_NAMES.filter((name) => hasSpecifier(specifiers, name));
14285
+ if (rpcs.length > 1) throw new ModuleScaffoldError(`UFUNCTION takes at most one of Server, Client, NetMulticast; got ${rpcs.join(", ")}.`);
14286
+ if (rpcs.length === 1) {
14287
+ if (!hasSpecifier(specifiers, "Reliable") && !hasSpecifier(specifiers, "Unreliable")) throw new ModuleScaffoldError("A replicated UFUNCTION must also be Reliable or Unreliable.");
14288
+ if (returnType.trim() !== "void") throw new ModuleScaffoldError(`A replicated UFUNCTION must return void, got '${returnType.trim()}'.`);
14289
+ }
14290
+ if (hasSpecifier(specifiers, "Reliable") && hasSpecifier(specifiers, "Unreliable")) throw new ModuleScaffoldError("UFUNCTION cannot be both Reliable and Unreliable.");
14291
+ if (rpcs.length === 0 && (hasSpecifier(specifiers, "Reliable") || hasSpecifier(specifiers, "Unreliable"))) throw new ModuleScaffoldError("Reliable and Unreliable only apply to a Server, Client or NetMulticast UFUNCTION.");
14292
+ }
14293
+ function renderSpecifierList(specifiers, category) {
14294
+ const rendered = specifiers.map((specifier) => specifier.trim());
14295
+ if (category !== void 0 && category.trim().length > 0 && !rendered.some((specifier) => normalizeSpecifierName(specifier) === "Category".toLowerCase())) rendered.push(`Category = "${category.trim()}"`);
14296
+ return rendered.join(", ");
14297
+ }
14298
+ function requiresCategory(specifiers) {
14299
+ return CATEGORY_REQUIRING_SPECIFIER_NAMES.some((name) => hasSpecifier(specifiers, name));
14300
+ }
14301
+ function maskCommentsAndLiterals(text) {
14302
+ const masked = text.split("");
14303
+ const charLiteralPattern = /'(?:\\.|[^'\\\n])'/y;
14304
+ const blank = (start, end) => {
14305
+ for (let index = start; index < end && index < masked.length; index += 1) if (masked[index] !== "\n") masked[index] = " ";
14306
+ };
14307
+ let index = 0;
14308
+ while (index < text.length) {
14309
+ const character = text[index];
14310
+ if (character === "/" && text[index + 1] === "/") {
14311
+ const end = text.indexOf("\n", index);
14312
+ const stop = end === -1 ? text.length : end;
14313
+ blank(index, stop);
14314
+ index = stop;
14315
+ continue;
14316
+ }
14317
+ if (character === "/" && text[index + 1] === "*") {
14318
+ const end = text.indexOf("*/", index + 2);
14319
+ const stop = end === -1 ? text.length : end + 2;
14320
+ blank(index, stop);
14321
+ index = stop;
14322
+ continue;
14323
+ }
14324
+ if (character === "R" && text[index + 1] === "\"") {
14325
+ const open = text.indexOf("(", index + 2);
14326
+ const delimiter = open === -1 ? void 0 : text.slice(index + 2, open);
14327
+ if (delimiter !== void 0 && !/[\s()\\]/u.test(delimiter)) {
14328
+ const end = text.indexOf(`)${delimiter}"`, open + 1);
14329
+ const stop = end === -1 ? text.length : end + delimiter.length + 2;
14330
+ blank(index, stop);
14331
+ index = stop;
14332
+ continue;
14333
+ }
14334
+ }
14335
+ if (character === "\"") {
14336
+ let scan = index + 1;
14337
+ while (scan < text.length && text[scan] !== "\"") scan += text[scan] === "\\" ? 2 : 1;
14338
+ const stop = Math.min(scan + 1, text.length);
14339
+ blank(index, stop);
14340
+ index = stop;
14341
+ continue;
14342
+ }
14343
+ if (character === "'") {
14344
+ charLiteralPattern.lastIndex = index;
14345
+ const match = charLiteralPattern.exec(text);
14346
+ if (match !== null) {
14347
+ blank(index, index + match[0].length);
14348
+ index += match[0].length;
14349
+ continue;
14350
+ }
14351
+ }
14352
+ index += 1;
14353
+ }
14354
+ return masked.join("");
14355
+ }
14356
+ function findDeclarationHeadEnd(masked, start) {
14357
+ let depth = 0;
14358
+ for (let index = start; index < masked.length; index += 1) {
14359
+ const character = masked[index];
14360
+ if (character === "(") {
14361
+ depth += 1;
14362
+ continue;
14363
+ }
14364
+ if (character === ")") {
14365
+ depth -= 1;
14366
+ continue;
14367
+ }
14368
+ if (depth === 0 && (character === ";" || character === "{" || character === ":")) return index;
14369
+ }
14370
+ return -1;
14371
+ }
14372
+ function findClassBody(masked, className) {
14373
+ const declarationPattern = /^[ \t]*(?:class|struct)\b/gmu;
14374
+ for (let match = declarationPattern.exec(masked); match !== null; match = declarationPattern.exec(masked)) {
14375
+ const headEnd = findDeclarationHeadEnd(masked, match.index);
14376
+ if (headEnd === -1) continue;
14377
+ if (masked.slice(match.index, headEnd).trim().split(/\s+/u).filter((token) => token !== "final").at(-1) !== className || masked[headEnd] === ";") continue;
14378
+ const bodyStart = masked.indexOf("{", headEnd);
14379
+ if (bodyStart === -1) continue;
14380
+ let depth = 0;
14381
+ for (let index = bodyStart; index < masked.length; index += 1) {
14382
+ if (masked[index] === "{") {
14383
+ depth += 1;
14384
+ continue;
14385
+ }
14386
+ if (masked[index] !== "}") continue;
14387
+ depth -= 1;
14388
+ if (depth === 0) return {
14389
+ end: index,
14390
+ start: bodyStart
14391
+ };
14392
+ }
14393
+ return {
14394
+ end: masked.length,
14395
+ start: bodyStart
14396
+ };
14397
+ }
14398
+ }
14399
+ function insertMemberIntoHeader(headerText, className, memberLines) {
14400
+ const masked = maskCommentsAndLiterals(headerText);
14401
+ const body = findClassBody(masked, className);
14402
+ if (body === void 0) throw new ModuleScaffoldError(`Header does not declare a class or struct named '${className}'.`);
14403
+ const bodyMatch = /^[ \t]*GENERATED_(?:BODY|UCLASS_BODY|USTRUCT_BODY|IINTERFACE_BODY)\(\)[ \t]*$/mu.exec(masked.slice(body.start, body.end));
14404
+ if (bodyMatch === null) throw new ModuleScaffoldError(`Class '${className}' has no GENERATED_BODY() to insert after.`);
14405
+ const insertionOffset = body.start + bodyMatch.index + bodyMatch[0].length;
14406
+ const block = [
14407
+ "",
14408
+ "",
14409
+ "public:",
14410
+ ...memberLines
14411
+ ].join("\n");
14412
+ return `${headerText.slice(0, insertionOffset)}${block}${headerText.slice(insertionOffset)}`;
14413
+ }
14414
+ const UE_CLASS_REFERENCE_PATTERN = /\b([AUI][A-Z][A-Za-z0-9_]*)\s*[*&>]/gu;
14415
+ function ensureForwardDeclarations(headerText, signature, className) {
14416
+ const candidates = /* @__PURE__ */ new Set();
14417
+ for (const match of signature.matchAll(UE_CLASS_REFERENCE_PATTERN)) {
14418
+ const typeName = match[1];
14419
+ if (typeName === void 0 || typeName === className) continue;
14420
+ if (new RegExp(String.raw`\b${typeName}\b`, "u").test(headerText)) continue;
14421
+ candidates.add(typeName);
14422
+ }
14423
+ if (candidates.size === 0) return {
14424
+ addedTypes: [],
14425
+ headerText
14426
+ };
14427
+ const macroMatch = /^[ \t]*(?:UCLASS|USTRUCT|UINTERFACE|UENUM)\s*\(/mu.exec(headerText);
14428
+ if (macroMatch === null) return {
14429
+ addedTypes: [],
14430
+ headerText
14431
+ };
14432
+ const addedTypes = [...candidates].sort();
14433
+ const block = `${addedTypes.map((typeName) => `class ${typeName};`).join("\n")}\n\n`;
14434
+ return {
14435
+ addedTypes,
14436
+ headerText: `${headerText.slice(0, macroMatch.index)}${block}${headerText.slice(macroMatch.index)}`
14437
+ };
14438
+ }
14439
+ function memberAlreadyDeclared(headerText, name) {
14440
+ return new RegExp(String.raw`\b${name}\b\s*(?:[;=(\[])`, "u").test(headerText);
14441
+ }
14442
+ function deriveSourcePath(headerPath) {
14443
+ const parsed = path.parse(headerPath);
14444
+ const segments = parsed.dir.split(path.sep);
14445
+ const publicIndex = segments.lastIndexOf("Public");
14446
+ const directory = publicIndex === -1 ? parsed.dir : [
14447
+ ...segments.slice(0, publicIndex),
14448
+ "Private",
14449
+ ...segments.slice(publicIndex + 1)
14450
+ ].join(path.sep);
14451
+ return path.join(directory, `${parsed.name}.cpp`);
14452
+ }
14453
+ async function fileExists(filePath) {
14454
+ try {
14455
+ await stat(filePath);
14456
+ return true;
14457
+ } catch {
14458
+ return false;
14459
+ }
14460
+ }
14461
+ async function addReflectedMember(input) {
14462
+ const name = input.name.trim();
14463
+ if (!IDENTIFIER_PATTERN.test(name)) throw new ModuleScaffoldError(`Member name '${name}' is not a valid C++ identifier.`);
14464
+ const className = input.className.trim();
14465
+ if (!IDENTIFIER_PATTERN.test(className)) throw new ModuleScaffoldError(`Class name '${className}' is not a valid C++ identifier.`);
14466
+ const headerPath = path.resolve(input.headerPath.trim());
14467
+ let headerText;
14468
+ try {
14469
+ headerText = await readFile(headerPath, "utf8");
14470
+ } catch (error) {
14471
+ throw new ModuleScaffoldError(`Header not found at ${headerPath}.`, { cause: error });
14472
+ }
14473
+ if (memberAlreadyDeclared(headerText, name)) throw new ModuleScaffoldError(`'${name}' is already declared in ${headerPath}.`);
14474
+ if (input.kind === "property") return await addProperty(input, {
14475
+ className,
14476
+ headerPath,
14477
+ headerText,
14478
+ name
14479
+ });
14480
+ return await addFunction(input, {
14481
+ className,
14482
+ headerPath,
14483
+ headerText,
14484
+ name
14485
+ });
14486
+ }
14487
+ async function addProperty(input, context) {
14488
+ const type = input.type?.trim();
14489
+ if (type === void 0 || type.length === 0) throw new ModuleScaffoldError("A UPROPERTY needs a type.");
14490
+ validatePropertySpecifiers(input.specifiers);
14491
+ if (requiresCategory(input.specifiers) && (input.category === void 0 || input.category.trim().length === 0)) throw new ModuleScaffoldError("A UPROPERTY exposed to the editor or Blueprint needs a category.");
14492
+ const initializer = input.defaultValue?.trim();
14493
+ const declaration = `\t${type} ${context.name}${initializer === void 0 || initializer.length === 0 ? "" : ` = ${initializer}`};`;
14494
+ const memberLines = [`\tUPROPERTY(${renderSpecifierList(input.specifiers, input.category)})`, declaration];
14495
+ const forwardDeclared = ensureForwardDeclarations(context.headerText, type, context.className);
14496
+ await writeFile(context.headerPath, insertMemberIntoHeader(forwardDeclared.headerText, context.className, memberLines), "utf8");
14497
+ return {
14498
+ className: context.className,
14499
+ declaration: memberLines.join("\n"),
14500
+ forwardDeclaredTypes: forwardDeclared.addedTypes,
14501
+ headerPath: context.headerPath,
14502
+ name: context.name
14503
+ };
14504
+ }
14505
+ async function addFunction(input, context) {
14506
+ const returnType = input.returnType?.trim() ?? "void";
14507
+ const parameters = input.parameters?.trim() ?? "";
14508
+ validateFunctionSpecifiers(input.specifiers, returnType);
14509
+ if (requiresCategory(input.specifiers) && (input.category === void 0 || input.category.trim().length === 0)) throw new ModuleScaffoldError("A UFUNCTION exposed to Blueprint needs a category.");
14510
+ const declaration = `\t${returnType} ${context.name}(${parameters});`;
14511
+ const memberLines = [`\tUFUNCTION(${renderSpecifierList(input.specifiers, input.category)})`, declaration];
14512
+ const forwardDeclared = ensureForwardDeclarations(context.headerText, `${returnType} ${parameters}`, context.className);
14513
+ await writeFile(context.headerPath, insertMemberIntoHeader(forwardDeclared.headerText, context.className, memberLines), "utf8");
14514
+ const result = {
14515
+ className: context.className,
14516
+ declaration: memberLines.join("\n"),
14517
+ forwardDeclaredTypes: forwardDeclared.addedTypes,
14518
+ headerPath: context.headerPath,
14519
+ name: context.name
14520
+ };
14521
+ if (hasSpecifier(input.specifiers, "BlueprintImplementableEvent")) return result;
14522
+ const sourcePath = path.resolve((input.sourcePath ?? deriveSourcePath(context.headerPath)).trim());
14523
+ if (!await fileExists(sourcePath)) return result;
14524
+ const body = returnType === "void" ? "" : " return {};\n";
14525
+ const definedName = hasSpecifier(input.specifiers, "BlueprintNativeEvent") ? `${context.name}_Implementation` : context.name;
14526
+ const definition = `\n${returnType} ${context.className}::${definedName}(${parameters})\n{\n${body}}\n`;
14527
+ const sourceText = await readFile(sourcePath, "utf8");
14528
+ await writeFile(sourcePath, `${sourceText.replace(/\s*$/u, "\n")}${definition}`, "utf8");
14529
+ result.definition = definition.trim();
14530
+ result.sourcePath = sourcePath;
14531
+ return result;
14532
+ }
14533
+ const SUBSYSTEM_LIFECYCLE = [{
14534
+ declaration: "virtual void Initialize(FSubsystemCollectionBase& Collection) override;",
14535
+ definition: "void %CLASS%::Initialize(FSubsystemCollectionBase& Collection)\n{\n Super::Initialize(Collection);\n}"
14536
+ }, {
14537
+ declaration: "virtual void Deinitialize() override;",
14538
+ definition: "void %CLASS%::Deinitialize()\n{\n Super::Deinitialize();\n}"
14539
+ }];
14540
+ const CLASS_TEMPLATE_SPECS = {
14541
+ editor_subsystem: {
14542
+ headerIncludes: ["EditorSubsystem.h"],
14543
+ overrides: SUBSYSTEM_LIFECYCLE,
14544
+ parentClass: "UEditorSubsystem",
14545
+ requiredModuleDependencies: ["EditorSubsystem"]
14546
+ },
14547
+ game_instance_subsystem: {
14548
+ headerIncludes: ["Subsystems/GameInstanceSubsystem.h"],
14549
+ overrides: SUBSYSTEM_LIFECYCLE,
14550
+ parentClass: "UGameInstanceSubsystem",
14551
+ requiredModuleDependencies: ["Engine"]
14552
+ },
14553
+ world_subsystem: {
14554
+ headerIncludes: ["Subsystems/WorldSubsystem.h"],
14555
+ overrides: SUBSYSTEM_LIFECYCLE,
14556
+ parentClass: "UWorldSubsystem",
14557
+ requiredModuleDependencies: ["Engine"]
14558
+ }
14559
+ };
14560
+ function renderInterfaceHeader(interfaceName, apiMacro, includePath) {
14561
+ const shimClassName = `U${interfaceName.slice(1)}`;
14562
+ return `#pragma once
14563
+
14564
+ #include "CoreMinimal.h"
14565
+ #include "UObject/Interface.h"
14566
+
14567
+ #include "${path.basename(includePath, ".h")}.generated.h"
14568
+
14569
+ UINTERFACE(MinimalAPI, BlueprintType)
14570
+ class ${shimClassName} : public UInterface
14571
+ {
14572
+ \tGENERATED_BODY()
14573
+ };
14574
+
14575
+ class ${apiMacro} ${interfaceName}
14576
+ {
14577
+ \tGENERATED_BODY()
14578
+
14579
+ public:
14580
+ };
14581
+ `;
14582
+ }
14583
+ function renderSubsystemHeader(className, apiMacro, includePath, spec) {
14584
+ const includes = spec.headerIncludes.map((include) => `#include "${include}"`).join("\n");
14585
+ const overrides = spec.overrides.map((entry) => `\t${entry.declaration}`).join("\n");
14586
+ return `#pragma once
14587
+
14588
+ #include "CoreMinimal.h"
14589
+ ${includes}
14590
+
14591
+ #include "${path.basename(includePath, ".h")}.generated.h"
14592
+
14593
+ UCLASS()
14594
+ class ${apiMacro} ${className} : public ${spec.parentClass}
14595
+ {
14596
+ \tGENERATED_BODY()
14597
+
14598
+ public:
14599
+ ${overrides}
14600
+ };
14601
+ `;
14602
+ }
14603
+ function renderTemplateSource(className, includePath, spec) {
14604
+ const header = `#include "${includePath}"\n`;
14605
+ if (spec === void 0 || spec.overrides.length === 0) return header;
14606
+ return `${header}\n${spec.overrides.map((entry) => entry.definition.replaceAll("%CLASS%", className)).join("\n\n")}\n`;
14607
+ }
14608
+ async function scaffoldClassTemplate(input) {
14609
+ const className = input.className.trim();
14610
+ const moduleName = input.moduleName.trim();
14611
+ if (!IDENTIFIER_PATTERN.test(moduleName)) throw new ModuleScaffoldError(`Module name '${moduleName}' is not a valid C++ identifier.`);
14612
+ if (input.template === "interface") {
14613
+ if (!INTERFACE_NAME_PATTERN.test(className)) throw new ModuleScaffoldError(`Interface name '${className}' must start with I, e.g. IDamageable.`);
14614
+ } else if (!UCLASS_NAME_PATTERN.test(className)) throw new ModuleScaffoldError(`Class name '${className}' must start with U, e.g. UCombatSubsystem.`);
14615
+ const subdirectory = input.subdirectory?.trim().replace(/^[\\/]+|[\\/]+$/gu, "") ?? "";
14616
+ if (subdirectory.includes("..")) throw new ModuleScaffoldError(`Subdirectory '${subdirectory}' must stay inside the module.`);
14617
+ const fileStem = className.slice(1);
14618
+ const moduleRoot = path.resolve(input.moduleRoot.trim());
14619
+ const headerPath = path.join(moduleRoot, "Public", subdirectory, `${fileStem}.h`);
14620
+ const sourcePath = path.join(moduleRoot, "Private", subdirectory, `${fileStem}.cpp`);
14621
+ if (await fileExists(headerPath)) throw new ModuleScaffoldError(`Header already exists at ${headerPath}; refusing to overwrite it.`);
14622
+ if (await fileExists(sourcePath)) throw new ModuleScaffoldError(`Source already exists at ${sourcePath}; refusing to overwrite it.`);
14623
+ const apiMacro = `${moduleName.toUpperCase()}_API`;
14624
+ const includePath = subdirectory.length === 0 ? `${fileStem}.h` : `${subdirectory.replaceAll("\\", "/")}/${fileStem}.h`;
14625
+ const spec = input.template === "interface" ? void 0 : CLASS_TEMPLATE_SPECS[input.template];
14626
+ const headerText = spec === void 0 ? renderInterfaceHeader(className, apiMacro, includePath) : renderSubsystemHeader(className, apiMacro, includePath, spec);
14627
+ await mkdir(path.dirname(headerPath), { recursive: true });
14628
+ await mkdir(path.dirname(sourcePath), { recursive: true });
14629
+ await writeFile(headerPath, headerText, "utf8");
14630
+ await writeFile(sourcePath, renderTemplateSource(className, includePath, spec), "utf8");
14631
+ return {
14632
+ className,
14633
+ createdFiles: [headerPath, sourcePath],
14634
+ headerPath,
14635
+ requiredModuleDependencies: spec?.requiredModuleDependencies ?? ["CoreUObject"],
14636
+ sourcePath,
14637
+ template: input.template
14638
+ };
14639
+ }
14640
+ //#endregion
13583
14641
  //#region src/moduleScaffold.ts
13584
14642
  const MODULE_DESCRIPTOR_TYPES = [
13585
14643
  "Runtime",
@@ -13793,8 +14851,412 @@ function isPlainObject(value) {
13793
14851
  function getErrorCode(error) {
13794
14852
  if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
13795
14853
  }
14854
+ const SKIPPED_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
14855
+ ".git",
14856
+ ".svn",
14857
+ "Binaries",
14858
+ "DerivedDataCache",
14859
+ "Intermediate",
14860
+ "Saved",
14861
+ "ThirdParty",
14862
+ "node_modules"
14863
+ ]);
14864
+ const UCLASS_MACRO_PATTERN = /^\s*UCLASS\s*\(/u;
14865
+ const UINTERFACE_MACRO_PATTERN = /^\s*UINTERFACE\s*\(/u;
14866
+ const USTRUCT_MACRO_PATTERN = /^\s*USTRUCT\s*\(/u;
14867
+ const UENUM_MACRO_PATTERN = /^\s*UENUM\s*\(/u;
14868
+ const UFUNCTION_MACRO_PATTERN = /^\s*UFUNCTION\s*\(/u;
14869
+ const UPROPERTY_MACRO_PATTERN = /^\s*UPROPERTY\s*\(/u;
14870
+ const CLASS_DECLARATION_PATTERN = /^\s*class\s+(?:[A-Za-z_][A-Za-z0-9_]*_API\s+)?(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\b(?<rest>.*)$/u;
14871
+ const STRUCT_DECLARATION_PATTERN = /^\s*struct\s+(?:[A-Za-z_][A-Za-z0-9_]*_API\s+)?(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\b(?<rest>.*)$/u;
14872
+ const ENUM_DECLARATION_PATTERN = /^\s*enum(?:\s+(?:class|struct))?\s+(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\b/u;
14873
+ const BASE_CLASS_PATTERN = /:\s*public\s+(?<parent>[A-Za-z_][A-Za-z0-9_:]*)/u;
14874
+ const FUNCTION_DECLARATION_PATTERN = /(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\s*\(/u;
14875
+ const PROPERTY_DECLARATION_PATTERN = /(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\s*(?:\[[^\]]*\])?\s*(?:=[^;]*)?;\s*$/u;
14876
+ const symbolKindSchema = z.enum([
14877
+ "class",
14878
+ "enum",
14879
+ "function",
14880
+ "interface",
14881
+ "property",
14882
+ "struct"
14883
+ ]);
14884
+ const symbolRecordSchema = z.object({
14885
+ file: z.string(),
14886
+ includePath: z.string().optional(),
14887
+ kind: symbolKindSchema,
14888
+ line: z.number().int().min(1),
14889
+ module: z.string().optional(),
14890
+ owner: z.string().optional(),
14891
+ parent: z.string().optional(),
14892
+ symbol: z.string()
14893
+ });
14894
+ const symbolIndexCacheSchema = z.object({
14895
+ files: z.record(z.string(), z.object({
14896
+ mtimeMs: z.number(),
14897
+ records: z.array(symbolRecordSchema),
14898
+ size: z.number()
14899
+ })),
14900
+ version: z.literal(1)
14901
+ });
14902
+ const nodeFileSystem = {
14903
+ listDirectory: async (dirPath) => {
14904
+ return (await readdir(dirPath, { withFileTypes: true })).map((entry) => ({
14905
+ isDirectory: entry.isDirectory(),
14906
+ name: entry.name
14907
+ }));
14908
+ },
14909
+ readCache: async (filePath) => readFile(filePath, "utf8"),
14910
+ readFile: async (filePath) => readFile(filePath, "utf8"),
14911
+ stat: async (filePath) => {
14912
+ const stats = await stat(filePath);
14913
+ return {
14914
+ mtimeMs: stats.mtimeMs,
14915
+ size: stats.size
14916
+ };
14917
+ },
14918
+ writeCache: async (filePath, contents) => {
14919
+ await mkdir(path.dirname(filePath), { recursive: true });
14920
+ await writeFile(filePath, contents, "utf8");
14921
+ }
14922
+ };
14923
+ function symbolIndexCacheFileName(roots) {
14924
+ return `symbols-${createHash("sha256").update([...roots].sort().join("\0")).digest("hex").slice(0, 16)}.json`;
14925
+ }
14926
+ function deriveIncludePath(filePath) {
14927
+ const segments = filePath.split(/[\\/]/u);
14928
+ for (let index = segments.length - 2; index >= 0; index -= 1) {
14929
+ const segment = segments[index];
14930
+ if (segment === "Public" || segment === "Classes" || segment === "Private") return segments.slice(index + 1).join("/");
14931
+ }
14932
+ }
14933
+ function stripLineComment(line) {
14934
+ const commentIndex = line.indexOf("//");
14935
+ return commentIndex === -1 ? line : line.slice(0, commentIndex);
14936
+ }
14937
+ function isDeclarationCandidate(line) {
14938
+ const trimmed = stripLineComment(line).trim();
14939
+ return trimmed.length > 0 && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*");
14940
+ }
14941
+ function parseReflectedSymbols(contents, filePath, module) {
14942
+ const includePath = deriveIncludePath(filePath);
14943
+ const lines = contents.split(/\r?\n/u);
14944
+ const records = [];
14945
+ let owner;
14946
+ function base(kind, symbol, line) {
14947
+ const record = {
14948
+ file: filePath,
14949
+ kind,
14950
+ line,
14951
+ symbol
14952
+ };
14953
+ if (includePath !== void 0) record.includePath = includePath;
14954
+ if (module !== void 0) record.module = module;
14955
+ return record;
14956
+ }
14957
+ function nextDeclaration(startIndex) {
14958
+ for (let index = startIndex; index < Math.min(lines.length, startIndex + 8); index += 1) {
14959
+ const raw = lines[index];
14960
+ if (raw === void 0) return;
14961
+ if (isDeclarationCandidate(raw)) return {
14962
+ index,
14963
+ text: stripLineComment(raw)
14964
+ };
14965
+ }
14966
+ }
14967
+ for (let index = 0; index < lines.length; index += 1) {
14968
+ const line = lines[index];
14969
+ if (line === void 0) continue;
14970
+ const isEnum = UENUM_MACRO_PATTERN.test(line);
14971
+ const isStruct = USTRUCT_MACRO_PATTERN.test(line);
14972
+ const isInterface = UINTERFACE_MACRO_PATTERN.test(line);
14973
+ if (UCLASS_MACRO_PATTERN.test(line) || isStruct || isEnum || isInterface) {
14974
+ const declaration = nextDeclaration(index + 1);
14975
+ if (!declaration) continue;
14976
+ if (isEnum) {
14977
+ const match = ENUM_DECLARATION_PATTERN.exec(declaration.text);
14978
+ if (match?.groups?.symbol !== void 0) {
14979
+ owner = void 0;
14980
+ records.push(base("enum", match.groups.symbol, declaration.index + 1));
14981
+ }
14982
+ continue;
14983
+ }
14984
+ const match = (isStruct ? STRUCT_DECLARATION_PATTERN : CLASS_DECLARATION_PATTERN).exec(declaration.text);
14985
+ const symbol = match?.groups?.symbol;
14986
+ if (symbol === void 0) continue;
14987
+ const record = base(isStruct ? "struct" : isInterface ? "interface" : "class", symbol, declaration.index + 1);
14988
+ const parent = BASE_CLASS_PATTERN.exec(match?.groups?.rest ?? "")?.groups?.parent;
14989
+ if (parent !== void 0) record.parent = parent;
14990
+ owner = symbol;
14991
+ records.push(record);
14992
+ continue;
14993
+ }
14994
+ if (UFUNCTION_MACRO_PATTERN.test(line) || UPROPERTY_MACRO_PATTERN.test(line)) {
14995
+ const isFunction = UFUNCTION_MACRO_PATTERN.test(line);
14996
+ const declaration = nextDeclaration(index + 1);
14997
+ if (!declaration) continue;
14998
+ const symbol = (isFunction ? FUNCTION_DECLARATION_PATTERN : PROPERTY_DECLARATION_PATTERN).exec(declaration.text)?.groups?.symbol;
14999
+ if (symbol === void 0) continue;
15000
+ const record = base(isFunction ? "function" : "property", symbol, declaration.index + 1);
15001
+ if (owner !== void 0) record.owner = owner;
15002
+ records.push(record);
15003
+ }
15004
+ }
15005
+ return records;
15006
+ }
15007
+ async function collectHeaderFiles(root, fileSystem, limit, accumulator) {
15008
+ const queue = [root];
15009
+ while (queue.length > 0) {
15010
+ const current = queue.pop();
15011
+ if (current === void 0) break;
15012
+ let entries;
15013
+ try {
15014
+ entries = await fileSystem.listDirectory(current);
15015
+ } catch {
15016
+ continue;
15017
+ }
15018
+ for (const entry of entries) {
15019
+ if (entry.isDirectory) {
15020
+ if (!SKIPPED_DIRECTORY_NAMES.has(entry.name)) queue.push(path.join(current, entry.name));
15021
+ continue;
15022
+ }
15023
+ if (!entry.name.endsWith(".h") || entry.name.endsWith(".generated.h")) continue;
15024
+ if (accumulator.length >= limit) return true;
15025
+ accumulator.push(path.join(current, entry.name));
15026
+ }
15027
+ }
15028
+ return false;
15029
+ }
15030
+ async function resolveModuleName(filePath, fileSystem, cache) {
15031
+ const directories = [];
15032
+ let current = path.dirname(filePath);
15033
+ for (let depth = 0; depth < 12; depth += 1) {
15034
+ const cached = cache.get(current);
15035
+ if (cached !== void 0 || cache.has(current)) {
15036
+ for (const directory of directories) cache.set(directory, cached);
15037
+ return cached;
15038
+ }
15039
+ directories.push(current);
15040
+ let moduleName;
15041
+ try {
15042
+ moduleName = (await fileSystem.listDirectory(current)).find((entry) => !entry.isDirectory && entry.name.endsWith(".Build.cs"))?.name.slice(0, -9);
15043
+ } catch {
15044
+ moduleName = void 0;
15045
+ }
15046
+ if (moduleName !== void 0) {
15047
+ for (const directory of directories) cache.set(directory, moduleName);
15048
+ return moduleName;
15049
+ }
15050
+ const parent = path.dirname(current);
15051
+ if (parent === current) break;
15052
+ current = parent;
15053
+ }
15054
+ for (const directory of directories) cache.set(directory, void 0);
15055
+ }
15056
+ async function loadCache(cacheFilePath, fileSystem) {
15057
+ if (cacheFilePath === void 0) return {
15058
+ files: {},
15059
+ version: 1
15060
+ };
15061
+ try {
15062
+ const contents = await fileSystem.readCache(cacheFilePath);
15063
+ const parsed = symbolIndexCacheSchema.safeParse(JSON.parse(contents));
15064
+ if (parsed.success) return parsed.data;
15065
+ } catch {}
15066
+ return {
15067
+ files: {},
15068
+ version: 1
15069
+ };
15070
+ }
15071
+ async function buildSymbolIndex(options) {
15072
+ const fileSystem = options.fileSystem ?? nodeFileSystem;
15073
+ const limit = options.maxFiles ?? 8e4;
15074
+ const files = [];
15075
+ let truncated = false;
15076
+ for (const root of options.roots) if (await collectHeaderFiles(root, fileSystem, limit, files)) {
15077
+ truncated = true;
15078
+ break;
15079
+ }
15080
+ const previous = options.refresh === true ? {
15081
+ files: {},
15082
+ version: 1
15083
+ } : await loadCache(options.cacheFilePath, fileSystem);
15084
+ const next = {
15085
+ files: {},
15086
+ version: 1
15087
+ };
15088
+ const moduleCache = /* @__PURE__ */ new Map();
15089
+ const records = [];
15090
+ let parsedFileCount = 0;
15091
+ for (const file of files) {
15092
+ let stats;
15093
+ try {
15094
+ stats = await fileSystem.stat(file);
15095
+ } catch {
15096
+ continue;
15097
+ }
15098
+ const cached = previous.files[file];
15099
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
15100
+ next.files[file] = cached;
15101
+ records.push(...cached.records);
15102
+ continue;
15103
+ }
15104
+ let contents;
15105
+ try {
15106
+ contents = await fileSystem.readFile(file);
15107
+ } catch {
15108
+ continue;
15109
+ }
15110
+ const module = await resolveModuleName(file, fileSystem, moduleCache);
15111
+ const parsed = parseReflectedSymbols(contents, file, module);
15112
+ parsedFileCount += 1;
15113
+ next.files[file] = {
15114
+ mtimeMs: stats.mtimeMs,
15115
+ records: parsed,
15116
+ size: stats.size
15117
+ };
15118
+ records.push(...parsed);
15119
+ }
15120
+ if (options.cacheFilePath !== void 0) try {
15121
+ await fileSystem.writeCache(options.cacheFilePath, JSON.stringify(next));
15122
+ } catch {}
15123
+ const result = {
15124
+ fileCount: files.length,
15125
+ parsedFileCount,
15126
+ records,
15127
+ truncated
15128
+ };
15129
+ if (options.cacheFilePath !== void 0) result.cacheFilePath = options.cacheFilePath;
15130
+ return result;
15131
+ }
15132
+ function searchSymbolIndex(records, options) {
15133
+ const needle = options.symbol.toLowerCase();
15134
+ const allowedKinds = options.kinds === void 0 ? void 0 : new Set(options.kinds);
15135
+ const exact = [];
15136
+ const prefix = [];
15137
+ const substring = [];
15138
+ for (const record of records) {
15139
+ if (allowedKinds && !allowedKinds.has(record.kind)) continue;
15140
+ const candidate = record.symbol.toLowerCase();
15141
+ if (candidate === needle) {
15142
+ exact.push({
15143
+ ...record,
15144
+ match: "exact"
15145
+ });
15146
+ continue;
15147
+ }
15148
+ if (candidate.startsWith(needle)) {
15149
+ prefix.push({
15150
+ ...record,
15151
+ match: "prefix"
15152
+ });
15153
+ continue;
15154
+ }
15155
+ if (candidate.includes(needle)) substring.push({
15156
+ ...record,
15157
+ match: "substring"
15158
+ });
15159
+ }
15160
+ const ordered = [
15161
+ ...exact,
15162
+ ...prefix,
15163
+ ...substring
15164
+ ];
15165
+ return {
15166
+ matches: ordered.slice(0, options.maxResults),
15167
+ totalCount: ordered.length
15168
+ };
15169
+ }
13796
15170
  //#endregion
13797
15171
  //#region src/tools/codeGen.ts
15172
+ const findSymbolInputSchema = {
15173
+ engineVersion: engineVersionSchema,
15174
+ kinds: z.array(z.enum([
15175
+ "class",
15176
+ "enum",
15177
+ "function",
15178
+ "interface",
15179
+ "property",
15180
+ "struct"
15181
+ ])).min(1).optional().describe("Restrict results to these reflected symbol kinds; defaults to every kind."),
15182
+ maxResults: z.number().int().min(1).max(200).default(25).describe("Maximum matches to return, ordered exact then prefix then substring (1-200, default 25)."),
15183
+ projectPath: z.string().trim().min(1).optional().describe("Path to the .uproject file or its directory; required unless scope is \"engine\"."),
15184
+ refresh: z.boolean().default(false).describe("Discard the cached index and rescan every header."),
15185
+ scope: z.enum([
15186
+ "all",
15187
+ "engine",
15188
+ "project"
15189
+ ]).default("all").describe("Which source trees to index: the engine install, the project, or both."),
15190
+ symbol: z.string().trim().min(1).describe("Reflected symbol name to look up, e.g. \"UCharacterMovementComponent\".")
15191
+ };
15192
+ const addReflectedMemberInputSchema = {
15193
+ category: z.string().trim().min(1).optional().describe("Editor category for the member; required when a Blueprint or editor specifier is used."),
15194
+ className: z.string().trim().min(1).describe("Class or struct in the header to add the member to."),
15195
+ defaultValue: z.string().trim().min(1).optional().describe("In-class initializer for a property, e.g. \"100.f\"; properties only."),
15196
+ headerPath: z.string().trim().min(1).describe("Absolute path to the header declaring the class."),
15197
+ kind: z.enum(REFLECTED_MEMBER_KINDS).describe("Whether to add a UPROPERTY or a UFUNCTION."),
15198
+ name: z.string().trim().min(1).describe("Member name, a valid C++ identifier."),
15199
+ parameters: z.string().trim().optional().describe("Parameter list for a function, verbatim C++ (e.g. \"float Amount, AActor* Instigator\")."),
15200
+ returnType: z.string().trim().min(1).optional().describe("Return type for a function; defaults to void."),
15201
+ sourcePath: z.string().trim().min(1).optional().describe("Where to append the function stub; defaults to the Private/*.cpp matching the header."),
15202
+ specifiers: z.array(z.string().trim().min(1)).max(32).default([]).describe("UPROPERTY/UFUNCTION specifiers, e.g. [\"EditAnywhere\", \"BlueprintReadWrite\"]. Validated against the UHT rules before anything is written."),
15203
+ type: z.string().trim().min(1).optional().describe("C++ type for a property, e.g. \"float\" or \"TObjectPtr<AActor>\".")
15204
+ };
15205
+ const scaffoldClassTemplateInputSchema = {
15206
+ className: z.string().trim().min(1).describe("Class name with its UE prefix: I-prefixed for an interface, U-prefixed for a subsystem."),
15207
+ moduleName: z.string().trim().min(1).describe("Owning module, used for the MODULENAME_API export macro."),
15208
+ moduleRoot: z.string().trim().min(1).describe("Absolute path to the module directory holding Public/ and Private/."),
15209
+ subdirectory: z.string().trim().optional().describe("Optional path under Public/ and Private/ to place the pair in, e.g. \"Combat\"."),
15210
+ template: z.enum(CLASS_TEMPLATES).describe("Which class template to generate.")
15211
+ };
15212
+ async function findSymbol(input, dependencies, context = createEngineToolchainContext(), overrides = {}) {
15213
+ if (input.scope !== "engine" && input.projectPath === void 0) throw new EngineToolchainError(`find_symbol scope "${input.scope}" needs a projectPath.`);
15214
+ const roots = [];
15215
+ let engineVersion;
15216
+ if (input.scope !== "project") {
15217
+ const uprojectPath = input.projectPath === void 0 ? void 0 : (await resolveProjectPaths(input.projectPath, context)).uprojectPath;
15218
+ const engine = await resolveEngine({
15219
+ context,
15220
+ engineVersion: input.engineVersion,
15221
+ uprojectPath
15222
+ });
15223
+ engineVersion = engine.engineVersion;
15224
+ roots.push(path.join(engine.engineRoot, "Engine", "Source"), path.join(engine.engineRoot, "Engine", "Plugins"));
15225
+ }
15226
+ if (input.scope !== "engine" && input.projectPath !== void 0) {
15227
+ const { projectDirectory } = await resolveProjectPaths(input.projectPath, context);
15228
+ roots.push(path.join(projectDirectory, "Source"), path.join(projectDirectory, "Plugins"));
15229
+ }
15230
+ const cacheDirectory = overrides.cacheDirectory ?? resolveSymbolIndexCacheDirectory({ env: context.env });
15231
+ const index = await buildSymbolIndex({
15232
+ cacheFilePath: path.join(cacheDirectory, symbolIndexCacheFileName(roots)),
15233
+ ...overrides.fileSystem === void 0 ? {} : { fileSystem: overrides.fileSystem },
15234
+ refresh: input.refresh,
15235
+ roots
15236
+ });
15237
+ const { matches, totalCount } = searchSymbolIndex(index.records, {
15238
+ ...input.kinds === void 0 ? {} : { kinds: input.kinds },
15239
+ maxResults: input.maxResults,
15240
+ symbol: input.symbol
15241
+ });
15242
+ dependencies.logger.info({
15243
+ fileCount: index.fileCount,
15244
+ parsedFileCount: index.parsedFileCount,
15245
+ symbol: input.symbol,
15246
+ totalCount
15247
+ }, "Searched the Unreal reflected symbol index");
15248
+ return {
15249
+ ...engineVersion === void 0 ? {} : { engineVersion },
15250
+ fileCount: index.fileCount,
15251
+ matches,
15252
+ parsedFileCount: index.parsedFileCount,
15253
+ roots,
15254
+ scope: input.scope,
15255
+ symbol: input.symbol,
15256
+ totalCount,
15257
+ truncated: index.truncated || matches.length < totalCount
15258
+ };
15259
+ }
13798
15260
  const createCppClassInputSchema = {
13799
15261
  className: bridgeCreateCppClassRequestSchema.shape.className.describe("Unprefixed C++ class name to generate (e.g. MyCharacter); the engine adds the A/U/F prefix that matches the parent class."),
13800
15262
  classPath: bridgeCreateCppClassRequestSchema.shape.classPath.describe("Optional path relative to the target module's source directory (e.g. Public/Characters). Defaults to the module source root."),
@@ -13911,6 +15373,27 @@ function registerCodeGenTools(server, dependencies) {
13911
15373
  projectId
13912
15374
  }, dependencies));
13913
15375
  });
15376
+ server.registerTool("find_symbol", {
15377
+ description: "Look up a reflected Unreal symbol (UCLASS/USTRUCT/UENUM/UINTERFACE type, or a UFUNCTION/UPROPERTY member) across the engine install and the project, returning the declaring header, the owning module and the #include path to use. Results are ordered exact, then prefix, then substring. The header index is cached per source-root set and refreshed incrementally by file mtime, so the first call after an engine install pays a full scan. Server-side; does not require a running editor.",
15378
+ inputSchema: findSymbolInputSchema,
15379
+ title: "Find Symbol"
15380
+ }, async (input) => {
15381
+ return toToolSuccess(await findSymbol(input, dependencies));
15382
+ });
15383
+ server.registerTool("add_reflected_member", {
15384
+ description: "Insert a UPROPERTY or UFUNCTION into an existing class or struct header, and append a matching .cpp stub for a function. Specifiers are validated against the UHT rules (conflicting edit/visibility specifiers, BlueprintReadOnly with BlueprintReadWrite, an RPC without Reliable/Unreliable or with a non-void return, a bare ReplicatedUsing) and nothing is written when they fail. The member goes in its own public: section right after GENERATED_BODY(), and any UE type it names by pointer or reference is forward-declared above the class. Server-side; does not require a running editor.",
15385
+ inputSchema: addReflectedMemberInputSchema,
15386
+ title: "Add Reflected Member"
15387
+ }, async (input) => {
15388
+ return toToolSuccess(await addReflectedMember(input));
15389
+ });
15390
+ server.registerTool("scaffold_class_template", {
15391
+ description: "Write a header/source pair for a class shape the New C++ Class wizard does not template well: a UINTERFACE/IInterface pair, or a Game Instance, World or Editor subsystem with its Initialize/Deinitialize overrides. Returns the module dependencies the template needs in Build.cs. Refuses to overwrite an existing file. Server-side; does not require a running editor.",
15392
+ inputSchema: scaffoldClassTemplateInputSchema,
15393
+ title: "Scaffold Class Template"
15394
+ }, async (input) => {
15395
+ return toToolSuccess(await scaffoldClassTemplate(input));
15396
+ });
13914
15397
  }
13915
15398
  const codeGenToolDomain = {
13916
15399
  domain: "codeGen",
@@ -13918,7 +15401,10 @@ const codeGenToolDomain = {
13918
15401
  toolNames: [
13919
15402
  "add_module",
13920
15403
  "create_cpp_class",
13921
- "get_create_class_status"
15404
+ "get_create_class_status",
15405
+ "find_symbol",
15406
+ "add_reflected_member",
15407
+ "scaffold_class_template"
13922
15408
  ]
13923
15409
  };
13924
15410
  //#endregion
@@ -13929,6 +15415,10 @@ const getCompileStatusInputSchema = {
13929
15415
  compileJobId: z.string().trim().min(1).max(512).describe("compileJobId returned by trigger_live_compile or trigger_hot_reload identifying the compile job to poll."),
13930
15416
  projectId: projectIdSchema
13931
15417
  };
15418
+ const getLiveCompileDetailsInputSchema = {
15419
+ compileJobId: z.string().trim().min(1).max(512).describe("compileJobId returned by trigger_live_compile identifying the compile job to report on."),
15420
+ projectId: projectIdSchema
15421
+ };
13932
15422
  async function triggerLiveCompile(projectId, dependencies) {
13933
15423
  const normalizedProjectId = projectIdSchema.parse(projectId);
13934
15424
  const entry = dependencies.registry.getProject(normalizedProjectId);
@@ -13996,6 +15486,29 @@ function buildGetCompileStatusResult(response, projectId) {
13996
15486
  if (response.restartReason !== void 0) result.restartReason = response.restartReason;
13997
15487
  return result;
13998
15488
  }
15489
+ async function getLiveCompileDetails(input, dependencies) {
15490
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
15491
+ const entry = dependencies.registry.getProject(normalizedProjectId);
15492
+ if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
15493
+ const response = await sendBridgeGetLiveCompileDetailsRequest(createBridgeGetLiveCompileDetailsRequest(input.compileJobId), {
15494
+ connectTimeoutMs: dependencies.pingTimeoutMs,
15495
+ host: "127.0.0.1",
15496
+ port: entry.port,
15497
+ readTimeoutMs: dependencies.pingTimeoutMs
15498
+ });
15499
+ if (!response.ok) throw new Error(`Bridge get_live_compile_details failed (${response.error.code}): ${response.error.message}`);
15500
+ return {
15501
+ compileJobId: response.compileJobId,
15502
+ patchedFunctions: response.patchedFunctions,
15503
+ patchedModules: response.patchedModules,
15504
+ projectId: normalizedProjectId,
15505
+ rejections: response.rejections,
15506
+ ...response.restartReason === void 0 ? {} : { restartReason: response.restartReason },
15507
+ ...response.restartRequired === void 0 ? {} : { restartRequired: response.restartRequired },
15508
+ state: response.state,
15509
+ truncated: response.truncated
15510
+ };
15511
+ }
13999
15512
  function registerCompileTools(server, dependencies) {
14000
15513
  server.registerTool("trigger_live_compile", {
14001
15514
  description: "Trigger an Unreal Live Coding recompile of patched modules for a projectId; tail stream_output_log (LogLiveCoding) for build output.",
@@ -14021,6 +15534,16 @@ function registerCompileTools(server, dependencies) {
14021
15534
  projectId
14022
15535
  }, dependencies));
14023
15536
  });
15537
+ server.registerTool("get_live_compile_details", {
15538
+ description: "Report what a Live Coding compile actually did for a projectId: the modules it patched, the reasons it refused a change, and whether the editor needs restarting because a reflected type's layout changed. get_compile_status answers pass/fail; this answers what was applied. Windows only in practice — Live Coding ships only where the engine supports it.",
15539
+ inputSchema: getLiveCompileDetailsInputSchema,
15540
+ title: "Get Live Compile Details"
15541
+ }, async ({ compileJobId, projectId }) => {
15542
+ return toToolSuccess(await getLiveCompileDetails({
15543
+ compileJobId,
15544
+ projectId
15545
+ }, dependencies));
15546
+ });
14024
15547
  }
14025
15548
  const compileToolDomain = {
14026
15549
  domain: "compile",
@@ -14028,7 +15551,8 @@ const compileToolDomain = {
14028
15551
  toolNames: [
14029
15552
  "trigger_live_compile",
14030
15553
  "trigger_hot_reload",
14031
- "get_compile_status"
15554
+ "get_compile_status",
15555
+ "get_live_compile_details"
14032
15556
  ]
14033
15557
  };
14034
15558
  //#endregion
@@ -16229,6 +17753,8 @@ async function sendProgressNotification(extra, logger, params) {
16229
17753
  //#region src/buildDiagnostics.ts
16230
17754
  const CLANG_DIAGNOSTIC_PATTERN = /^(?<file>.+?):(?<line>\d+):(?:(?<column>\d+):)?\s+(?<severity>fatal error|error|warning):\s+(?<message>.*)$/u;
16231
17755
  const MSVC_DIAGNOSTIC_PATTERN = /^(?<file>.+?)\((?<line>\d+)(?:,\d+)?\):\s+(?<severity>fatal error|error|warning)\s+(?<code>[A-Z]+\d+):\s+(?<message>.*)$/u;
17756
+ const UHT_LINE_PREFIX_PATTERN = /^(?:\[\d+\/\d+\]\s+)?(?:(?:LogCompile|LogInit|UnrealHeaderTool)\s*:\s*)?/u;
17757
+ const UHT_DIAGNOSTIC_PATTERN = /^(?<file>.+?)\((?<line>\d+)(?:,\d+)?\):\s+(?<severity>Error|Warning)(?:\s+(?<code>UHT\d+))?:\s+(?<message>.+)$/u;
16232
17758
  const CLANG_TIDY_PATTERN = /^(?<file>.+?):(?<line>\d+):(?<column>\d+):\s+(?<severity>error|warning):\s+(?<message>.*?)(?:\s+\[(?<check>[^\]]+)\])?$/u;
16233
17759
  const COOK_LINE_PREFIX_PATTERN = /^(?:\[[^\]]*\]\s*)*(?:[A-Za-z]+Helper:\s*[^:]*:\s*)?/u;
16234
17760
  const COOK_DIAGNOSTIC_PATTERN = /^(?<category>[A-Za-z_][A-Za-z0-9_]*):\s+(?<severity>Error|Warning)(?::)?\s+(?<message>.+)$/u;
@@ -16312,7 +17838,7 @@ function parseBuildDiagnostics(output) {
16312
17838
  for (const rawLine of output.split(/\r?\n/u)) {
16313
17839
  const line = rawLine.trim();
16314
17840
  if (line.length === 0) continue;
16315
- const match = CLANG_DIAGNOSTIC_PATTERN.exec(line) ?? MSVC_DIAGNOSTIC_PATTERN.exec(line);
17841
+ const match = CLANG_DIAGNOSTIC_PATTERN.exec(line) ?? MSVC_DIAGNOSTIC_PATTERN.exec(line) ?? UHT_DIAGNOSTIC_PATTERN.exec(line.replace(UHT_LINE_PREFIX_PATTERN, ""));
16316
17842
  if (!match?.groups) continue;
16317
17843
  const groups = match.groups;
16318
17844
  const { file, line: lineText, message, severity } = groups;
@@ -16323,7 +17849,7 @@ function parseBuildDiagnostics(output) {
16323
17849
  line: Number.parseInt(lineText, 10),
16324
17850
  message: `${code}${message}`.trim()
16325
17851
  };
16326
- const isError = severity === "error" || severity === "fatal error";
17852
+ const isError = severity === "error" || severity === "fatal error" || severity === "Error";
16327
17853
  const target = isError ? errors : warnings;
16328
17854
  const seen = isError ? seenErrors : seenWarnings;
16329
17855
  const key = diagnosticKey(diagnostic);
@@ -16363,6 +17889,79 @@ function parseClangTidyFindings(output) {
16363
17889
  }
16364
17890
  return findings;
16365
17891
  }
17892
+ function clampInteger(value, minimum, maximum) {
17893
+ return Math.min(Math.max(Math.trunc(value), minimum), maximum);
17894
+ }
17895
+ async function readSourceLines(filePath, cache, maxFileBytes, readFile) {
17896
+ const cached = cache.get(filePath);
17897
+ if (cached !== void 0) return cached;
17898
+ let lines = null;
17899
+ try {
17900
+ const contents = await readFile(filePath);
17901
+ lines = Buffer.byteLength(contents, "utf8") > maxFileBytes ? null : contents.split(/\r?\n/u);
17902
+ } catch {
17903
+ lines = null;
17904
+ }
17905
+ cache.set(filePath, lines);
17906
+ return lines;
17907
+ }
17908
+ async function attachSourceContext(diagnostics, options) {
17909
+ const contextLines = clampInteger(options.contextLines ?? 3, 0, 10);
17910
+ const maxFileBytes = Math.max(0, Math.trunc(options.maxFileBytes ?? 524288));
17911
+ const cache = /* @__PURE__ */ new Map();
17912
+ let budget = clampInteger(options.maxDiagnostics ?? 20, 0, 50);
17913
+ const readAllowance = budget;
17914
+ let reads = 0;
17915
+ let truncated = false;
17916
+ async function decorate(entries) {
17917
+ const decorated = [];
17918
+ for (const entry of entries) {
17919
+ if (entry.file === void 0 || entry.line === void 0) {
17920
+ decorated.push({ ...entry });
17921
+ continue;
17922
+ }
17923
+ if (budget <= 0) {
17924
+ truncated = true;
17925
+ decorated.push({ ...entry });
17926
+ continue;
17927
+ }
17928
+ if (!cache.has(entry.file)) {
17929
+ if (reads >= readAllowance) {
17930
+ truncated = true;
17931
+ decorated.push({ ...entry });
17932
+ continue;
17933
+ }
17934
+ reads += 1;
17935
+ }
17936
+ const lines = await readSourceLines(entry.file, cache, maxFileBytes, options.readFile);
17937
+ if (!lines || entry.line > lines.length) {
17938
+ decorated.push({ ...entry });
17939
+ continue;
17940
+ }
17941
+ const startLine = Math.max(1, entry.line - contextLines);
17942
+ const endLine = Math.min(lines.length, entry.line + contextLines);
17943
+ budget -= 1;
17944
+ decorated.push({
17945
+ ...entry,
17946
+ sourceContext: {
17947
+ endLine,
17948
+ lines: lines.slice(startLine - 1, endLine),
17949
+ startLine
17950
+ }
17951
+ });
17952
+ }
17953
+ return decorated;
17954
+ }
17955
+ const errors = await decorate(diagnostics.errors);
17956
+ const warnings = await decorate(diagnostics.warnings);
17957
+ return {
17958
+ errorCount: diagnostics.errorCount,
17959
+ errors,
17960
+ sourceContextTruncated: truncated,
17961
+ warningCount: diagnostics.warningCount,
17962
+ warnings
17963
+ };
17964
+ }
16366
17965
  const PACKAGE_JOB_MAX_OUTPUT_BYTES = 2097152;
16367
17966
  var PackageJobCapacityError = class extends Error {
16368
17967
  constructor(message) {
@@ -18971,6 +20570,62 @@ const projectSettingsToolDomain = {
18971
20570
  toolNames: ["set_project_settings", "set_default_gamemode"]
18972
20571
  };
18973
20572
  //#endregion
20573
+ //#region src/tools/python.ts
20574
+ const runEditorPythonInputSchema = {
20575
+ command: bridgeRunEditorPythonRequestSchema.shape.command.describe("Python to run in the editor: a multi-statement block, a single statement, an expression to evaluate, or a .py file path with optional arguments."),
20576
+ executionMode: bridgeRunEditorPythonRequestSchema.shape.executionMode.default("script").describe("\"script\" (default) runs a whole block of Python, or a .py file path with arguments; \"statement\" runs exactly one statement; \"evaluate\" evaluates one expression and returns its value."),
20577
+ projectId: projectIdSchema,
20578
+ unattended: bridgeRunEditorPythonRequestSchema.shape.unattended.default(true).describe("Suppress modal dialogs the script would otherwise block the editor on (default true).")
20579
+ };
20580
+ async function runEditorPython(input, dependencies) {
20581
+ const normalizedProjectId = projectIdSchema.parse(input.projectId);
20582
+ const entry = dependencies.registry.getProject(normalizedProjectId);
20583
+ if (!entry) throw new Error(`No registered Unreal engine was found for projectId '${normalizedProjectId}'.`);
20584
+ const response = await sendBridgeRunEditorPythonRequest(createBridgeRunEditorPythonRequest(input.command, {
20585
+ executionMode: input.executionMode,
20586
+ unattended: input.unattended
20587
+ }), {
20588
+ connectTimeoutMs: deriveBridgeConnectTimeoutMs$1(dependencies.pingTimeoutMs),
20589
+ host: "127.0.0.1",
20590
+ port: entry.port,
20591
+ readTimeoutMs: deriveEditorWorkTimeoutMs(dependencies.pingTimeoutMs)
20592
+ });
20593
+ if (!response.ok) throw new Error(`Bridge run_editor_python failed (${response.error.code}): ${response.error.message}`);
20594
+ dependencies.logger.info({
20595
+ executionMode: input.executionMode,
20596
+ projectId: normalizedProjectId,
20597
+ state: response.state
20598
+ }, "Ran editor Python through the bridge");
20599
+ return {
20600
+ errors: response.errors,
20601
+ ...response.message === void 0 ? {} : { message: response.message },
20602
+ output: response.output,
20603
+ projectId: normalizedProjectId,
20604
+ ...response.result === void 0 ? {} : { result: response.result },
20605
+ state: response.state,
20606
+ truncated: response.truncated
20607
+ };
20608
+ }
20609
+ function registerPythonTools(server, dependencies) {
20610
+ server.registerTool("run_editor_python", {
20611
+ description: "Execute Python inside the running editor for a projectId and return its log output, error lines and evaluated result. Runs inside one transaction, so whatever the script changes through Unreal's transaction system undoes as a single step; work the engine does not record in a transaction, such as an asset import or a file write, is not undoable. A failed run is rolled back. Disabled by default: the tool answers state 'disabled' until a human enables Allow run_editor_python in the project's MCP Bridge settings, and 'unsupported' when the Python Script Plugin is not available.",
20612
+ inputSchema: runEditorPythonInputSchema,
20613
+ title: "Run Editor Python"
20614
+ }, async ({ command, executionMode, projectId, unattended }) => {
20615
+ return toToolSuccess(await runEditorPython({
20616
+ command,
20617
+ executionMode,
20618
+ projectId,
20619
+ unattended
20620
+ }, dependencies));
20621
+ });
20622
+ }
20623
+ const pythonToolDomain = {
20624
+ domain: "python",
20625
+ register: registerPythonTools,
20626
+ toolNames: ["run_editor_python"]
20627
+ };
20628
+ //#endregion
18974
20629
  //#region src/tools/reflection.ts
18975
20630
  const getClassReflectionInputSchema = {
18976
20631
  className: bridgeGetClassReflectionRequestSchema.shape.className.describe("Reflected UCLASS or USTRUCT name. Both the UHT name and the C++ spelling work (Actor or AActor); a full path such as /Script/Engine.Actor also resolves."),
@@ -19218,8 +20873,10 @@ const buildTargetInputSchema = {
19218
20873
  configuration: configurationSchema,
19219
20874
  engineVersion: engineVersionSchema,
19220
20875
  force: forceSchema,
20876
+ includeSourceContext: z.boolean().default(false).describe("Attach the source lines around each located diagnostic. Off by default; snippets are capped at 20 diagnostics per build."),
19221
20877
  platform: platformSchema,
19222
20878
  projectPath: projectPathSchema,
20879
+ sourceContextLines: z.number().int().min(0).max(10).default(3).describe("Source lines to include on each side of a diagnostic when includeSourceContext is true (0-10)."),
19223
20880
  target: targetSchema,
19224
20881
  timeoutMs: buildTimeoutSchema
19225
20882
  };
@@ -19271,7 +20928,14 @@ async function buildTarget(input, dependencies, context = createEngineToolchainC
19271
20928
  cwd: engine.engineRoot,
19272
20929
  timeoutMs: input.timeoutMs
19273
20930
  });
19274
- const diagnostics = parseBuildDiagnostics(result.combinedOutput);
20931
+ const parsed = parseBuildDiagnostics(result.combinedOutput);
20932
+ const diagnostics = input.includeSourceContext ? await attachSourceContext(parsed, {
20933
+ contextLines: input.sourceContextLines,
20934
+ readFile: context.readFile
20935
+ }) : {
20936
+ ...parsed,
20937
+ sourceContextTruncated: void 0
20938
+ };
19275
20939
  const success = result.exitCode === 0 && !result.timedOut;
19276
20940
  if (!success) dependencies.logger.warn({
19277
20941
  errorCount: diagnostics.errorCount,
@@ -19288,6 +20952,7 @@ async function buildTarget(input, dependencies, context = createEngineToolchainC
19288
20952
  exitCode: result.exitCode,
19289
20953
  outputTruncated: result.truncated,
19290
20954
  platform,
20955
+ sourceContextTruncated: diagnostics.sourceContextTruncated,
19291
20956
  success,
19292
20957
  target: input.target,
19293
20958
  timedOut: result.timedOut,
@@ -19930,6 +21595,7 @@ function registerWorldPartitionTools(server, dependencies) {
19930
21595
  const TOOL_CATALOG = [
19931
21596
  projectToolDomain,
19932
21597
  doctorToolDomain,
21598
+ pythonToolDomain,
19933
21599
  codeGenToolDomain,
19934
21600
  compileToolDomain,
19935
21601
  diagnosticsToolDomain,