ue-mcp 1.2.0 → 1.2.2

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.
@@ -10,20 +10,29 @@
10
10
  * - `afterWrite` run after a write-classified call
11
11
  *
12
12
  * No new plugin-activation concept is needed: the loader already registers every
13
- * `manifest.tasks` entry by name, so a `guard.*.*` task is picked up here. Each
14
- * matched task becomes one `BridgeGuard`. The core knows nothing about what a
15
- * guard does - source control, access policy, audit, and rate limiting are all
16
- * just guards.
13
+ * `manifest.tasks` entry by name, so a `guard.*.*` task is picked up here. The
14
+ * core knows nothing about what a guard does - source control, access policy,
15
+ * audit, and rate limiting are all just guards.
17
16
  *
18
- * A guard task is invoked with `{ method, params, paths }` (paths = the existing
19
- * on-disk files the call will touch, empty for non-writes) plus, for `after`
20
- * guards, `result`. A `before` guard denies the call by returning `success:false`
21
- * or throwing.
17
+ * The naming convention and the discovery walk are flowkit's; this module binds
18
+ * them to the bridge: the `write` scope comes from `write-methods.ts`, the guard
19
+ * task is invoked with `{ method, params, paths }` (plus `result` for `after`
20
+ * guards), and a denial surfaces as an `McpError` the tool layer already knows
21
+ * how to render.
22
22
  */
23
23
  import * as fs from "node:fs";
24
+ import { discoverTaskGuards as discoverFlowkitGuards } from "@db-lyon/flowkit/guard";
25
+ import { writeScope } from "./guard.js";
24
26
  import { McpError, ErrorCode } from "../errors.js";
25
- import { debug } from "../log.js";
26
- const GUARD_TASK_RE = /^guard\.(.+)\.(before|beforeWrite|after|afterWrite)$/;
27
+ import { debug, info } from "../log.js";
28
+ /** Adapts flowkit's logger to this server's `guard`-component logging. */
29
+ const GUARD_LOGGER = {
30
+ debug: (...args) => debug("guard", args.map(String).join(" ")),
31
+ info: (...args) => info("guard", args.map(String).join(" ")),
32
+ warn: (...args) => info("guard", args.map(String).join(" ")),
33
+ error: (...args) => info("guard", args.map(String).join(" ")),
34
+ child: () => GUARD_LOGGER,
35
+ };
27
36
  /** Resolve a UE content path to an absolute file, or null if it does not exist. */
28
37
  export function makeResolveExistingFile(project) {
29
38
  return (contentPath) => {
@@ -42,63 +51,30 @@ export function makeResolveExistingFile(project) {
42
51
  * guard that itself calls the bridge cannot recurse through the pipeline.
43
52
  */
44
53
  export function discoverTaskGuards(registry, ctx, rawBridge) {
45
- const guards = [];
46
- for (const taskName of registry.listRegistered()) {
47
- const m = GUARD_TASK_RE.exec(taskName);
48
- if (!m)
49
- continue;
50
- const [, name, phase] = m;
51
- const writeScoped = phase.endsWith("Write");
52
- const isBefore = phase.startsWith("before");
53
- const runTask = async (cc, result) => {
54
- // Bind the guard to the editor whose call it is guarding, not to
55
- // whichever bridge happened to be built first. cc.bridge is the RAW
56
- // bridge of the session serving this call, so a guard can neither
57
- // recurse through the pipeline nor act on another project's editor.
58
- const guardCtx = cc.session
59
- ? { ...ctx, bridge: cc.bridge, project: cc.session.project, session: cc.session }
60
- : { ...ctx, bridge: rawBridge };
61
- const options = {
62
- method: cc.method,
63
- params: cc.params,
64
- paths: cc.writeFiles(),
65
- };
66
- if (result !== undefined)
67
- options.result = result;
68
- try {
69
- const task = await registry.create(taskName, guardCtx, options);
70
- return await task.run();
71
- }
72
- catch (e) {
73
- throw new McpError(ErrorCode.WRITE_BLOCKED, `guard '${name}' errored on ${cc.method}: ${e.message}`);
74
- }
75
- };
76
- const guard = {
77
- name: `${name}.${phase}`,
78
- appliesTo: writeScoped ? (cc) => cc.writeFiles().length > 0 : undefined,
79
- };
80
- if (isBefore) {
81
- guard.before = async (cc) => {
82
- const r = await runTask(cc);
83
- if (!r.success) {
84
- const reason = r.error?.message ?? `denied by guard '${name}'`;
85
- const scope = cc.writeFiles().length ? ` on ${cc.writeFiles().join(", ")}` : "";
86
- throw new McpError(ErrorCode.WRITE_BLOCKED, `blocked (${cc.method})${scope}: ${reason}`);
87
- }
88
- debug("guard", `guard '${name}' allowed ${cc.method}`);
89
- };
90
- }
91
- else {
92
- // `after` guards observe the result for side effects (audit); a failing
93
- // after-guard is logged but does not fail the already-completed call.
94
- guard.after = async (cc, result) => {
95
- const r = await runTask(cc, result);
96
- if (!r.success)
97
- debug("guard", `after-guard '${name}' reported failure on ${cc.method}: ${r.error?.message}`);
98
- };
99
- }
100
- guards.push(guard);
101
- }
102
- return guards;
54
+ return discoverFlowkitGuards(registry, {
55
+ scopes: { write: writeScope },
56
+ // Bind the guard to the editor whose call it is guarding, not to whichever
57
+ // bridge happened to be built first. cc.bridge is the RAW bridge of the
58
+ // session serving this call, so a guard can neither recurse through the
59
+ // pipeline nor act on another project's editor.
60
+ contextFor: (cc) => cc.session
61
+ ? { ...ctx, bridge: cc.bridge, project: cc.session.project, session: cc.session }
62
+ : { ...ctx, bridge: rawBridge },
63
+ optionsFor: (cc, result) => ({
64
+ method: cc.method,
65
+ params: cc.params,
66
+ paths: cc.writeFiles(),
67
+ ...(result !== undefined ? { result } : {}),
68
+ }),
69
+ onDeny: (info) => {
70
+ const files = info.ctx.writeFiles();
71
+ const scope = files.length ? ` on ${files.join(", ")}` : "";
72
+ return new McpError(ErrorCode.WRITE_BLOCKED, `blocked (${info.ctx.method})${scope}: ${info.reason}`);
73
+ },
74
+ onError: (info) => new McpError(ErrorCode.WRITE_BLOCKED, `guard '${info.guard}' errored on ${info.ctx.method}: ${info.reason}`),
75
+ // A failing after-guard is logged but does not fail the already-completed call.
76
+ onAfterFailure: (info) => debug("guard", `after-guard '${info.guard}' reported failure on ${info.ctx.method}: ${info.reason}`),
77
+ logger: GUARD_LOGGER,
78
+ });
103
79
  }
104
80
  //# sourceMappingURL=task-guards.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-guards.js","sourceRoot":"","sources":["../../src/flow/task-guards.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"task-guards.js","sourceRoot":"","sources":["../../src/flow/task-guards.ts"],"names":[],"mappings":"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"}
@@ -31,6 +31,6 @@
31
31
  },
32
32
  "nativeToolsets": 52,
33
33
  "nativeToolActions": 830,
34
- "generatedAt": "2026-08-06T02:39:13.220Z",
35
- "version": "1.2.0"
34
+ "generatedAt": "2026-08-07T17:54:44.285Z",
35
+ "version": "1.2.2"
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ue-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Unreal Engine MCP server - 24 tools, 783+ actions for AI-driven editor control",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -61,7 +61,8 @@
61
61
  "audit:docs": "node scripts/audit-docs.mjs && node scripts/audit-params.mjs",
62
62
  "audit:handlers": "node scripts/audit-handlers.mjs",
63
63
  "audit:em-dash": "node scripts/check-em-dashes.mjs",
64
- "audit": "npm run audit:docs && npm run audit:handlers && npm run audit:em-dash",
64
+ "audit:unity": "node scripts/audit-unity-collisions.mjs",
65
+ "audit": "npm run audit:docs && npm run audit:handlers && npm run audit:em-dash && npm run audit:unity",
65
66
  "test:watch": "vitest",
66
67
  "test:level": "vitest run tests/smoke/level.test.ts",
67
68
  "test:asset": "vitest run tests/smoke/asset.test.ts",
@@ -84,10 +85,11 @@
84
85
  "generate": "tsx scripts/generate-default-config.ts",
85
86
  "generate:metadata": "tsx scripts/generate-tool-metadata.ts",
86
87
  "prepublishOnly": "npx tsc && tsx scripts/generate-default-config.ts && tsx scripts/generate-tool-metadata.ts",
87
- "gen:epic-docs": "npx tsc && node scripts/gen-epic-docs.mjs"
88
+ "gen:epic-docs": "npx tsc && node scripts/gen-epic-docs.mjs",
89
+ "prepare": "husky"
88
90
  },
89
91
  "dependencies": {
90
- "@db-lyon/flowkit": "~0.5.3",
92
+ "@db-lyon/flowkit": "^0.14.0",
91
93
  "@modelcontextprotocol/sdk": "^1.12.1",
92
94
  "js-yaml": "^4.1.0",
93
95
  "ws": "^8.18.0",
@@ -97,6 +99,7 @@
97
99
  "@types/js-yaml": "^4.0.9",
98
100
  "@types/node": "^22.0.0",
99
101
  "@types/ws": "^8.5.0",
102
+ "husky": "^9.1.7",
100
103
  "tsx": "^4.19.0",
101
104
  "typescript": "^5.7.0",
102
105
  "vitest": "^3.2.4"
@@ -75,32 +75,10 @@
75
75
  #include "AI/Navigation/NavCollisionBase.h"
76
76
 
77
77
  // ─── Protected mount guardrail ──────────────────────────────────────────
78
- // Engine-shipped content (/Engine/, /Script/, /Memory/, /Temp/) and Verse
79
- // runtime classes must never be mutated through the bridge. UE's
80
- // UEditorAssetLibrary::DeleteAsset will happily destroy files under
81
- // <engineRoot>/Engine/Content/ if not stopped - verified the hard way.
82
- // Apply this check to every handler that deletes, moves, or renames an
83
- // asset. Plugin content roots (mounted under /<PluginName>/) are NOT
84
- // protected here; per-project plugin content is expected to be writable.
78
+ // The rule itself is MCPIsProtectedAssetPath in HandlerUtils.h, shared by
79
+ // every asset translation unit so the write paths cannot drift apart.
85
80
  namespace
86
81
  {
87
- bool IsProtectedAssetPath(const FString& Path)
88
- {
89
- FString P = Path;
90
- P.TrimStartAndEndInline();
91
- if (P.IsEmpty()) return false;
92
- // Tolerate leading whitespace and the surface form (no leading slash).
93
- if (!P.StartsWith(TEXT("/"))) P = TEXT("/") + P;
94
- const FString L = P.ToLower();
95
- if (L.StartsWith(TEXT("/engine/"))) return true;
96
- if (L.StartsWith(TEXT("/script/"))) return true;
97
- if (L.StartsWith(TEXT("/memory/"))) return true;
98
- if (L.StartsWith(TEXT("/temp/"))) return true;
99
- // Verse runtime objects surface as /Script/CoreUObject.* etc.
100
- if (L.Contains(TEXT("/script/"))) return true;
101
- return false;
102
- }
103
-
104
82
  TSharedPtr<FJsonValue> MakeProtectedPathError(const FString& Path)
105
83
  {
106
84
  return MCPError(FString::Printf(
@@ -1488,8 +1466,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::RenameAsset(const TSharedPtr<FJsonObject>
1488
1466
  return MCPError(TEXT("Missing 'sourcePath'+'destinationPath' or 'assetPath'+'newName'"));
1489
1467
  }
1490
1468
 
1491
- if (IsProtectedAssetPath(SourcePath)) return MakeProtectedPathError(SourcePath);
1492
- if (IsProtectedAssetPath(DestPath)) return MakeProtectedPathError(DestPath);
1469
+ if (MCPIsProtectedAssetPath(SourcePath)) return MakeProtectedPathError(SourcePath);
1470
+ if (MCPIsProtectedAssetPath(DestPath)) return MakeProtectedPathError(DestPath);
1493
1471
 
1494
1472
  // Idempotency: if already at destination, no-op.
1495
1473
  if (SourcePath == DestPath)
@@ -1697,7 +1675,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAsset(const TSharedPtr<FJsonObject>
1697
1675
  FString AssetPath;
1698
1676
  if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
1699
1677
 
1700
- if (IsProtectedAssetPath(AssetPath)) return MakeProtectedPathError(AssetPath);
1678
+ if (MCPIsProtectedAssetPath(AssetPath)) return MakeProtectedPathError(AssetPath);
1701
1679
 
1702
1680
  const bool bForce = OptionalBool(Params, TEXT("force"), false);
1703
1681
 
@@ -1763,7 +1741,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAssetBatch(const TSharedPtr<FJsonOb
1763
1741
  TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
1764
1742
  Entry->SetStringField(TEXT("path"), Path);
1765
1743
 
1766
- if (IsProtectedAssetPath(Path))
1744
+ if (MCPIsProtectedAssetPath(Path))
1767
1745
  {
1768
1746
  Entry->SetStringField(TEXT("status"), TEXT("protected"));
1769
1747
  Entry->SetStringField(TEXT("reason"), TEXT("Engine/Script/Memory/Temp mounts are read-only via the bridge"));
@@ -1892,7 +1870,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkRename(const TSharedPtr<FJsonObject>&
1892
1870
  continue;
1893
1871
  }
1894
1872
 
1895
- if (IsProtectedAssetPath(SourcePath) || IsProtectedAssetPath(NewPackagePath))
1873
+ if (MCPIsProtectedAssetPath(SourcePath) || MCPIsProtectedAssetPath(NewPackagePath))
1896
1874
  {
1897
1875
  Record->SetStringField(TEXT("status"), TEXT("protected"));
1898
1876
  Record->SetStringField(TEXT("reason"), TEXT("Engine/Script/Memory/Temp mounts are read-only via the bridge"));
@@ -2555,8 +2533,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::MoveFolder(const TSharedPtr<FJsonObject>&
2555
2533
  SourcePath.RemoveFromEnd(TEXT("/"));
2556
2534
  DestinationPath.RemoveFromEnd(TEXT("/"));
2557
2535
 
2558
- if (IsProtectedAssetPath(SourcePath)) return MakeProtectedPathError(SourcePath);
2559
- if (IsProtectedAssetPath(DestinationPath)) return MakeProtectedPathError(DestinationPath);
2536
+ if (MCPIsProtectedAssetPath(SourcePath)) return MakeProtectedPathError(SourcePath);
2537
+ if (MCPIsProtectedAssetPath(DestinationPath)) return MakeProtectedPathError(DestinationPath);
2560
2538
 
2561
2539
  // Scan source path to discover all assets
2562
2540
  IAssetRegistry& AR = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
@@ -2738,7 +2716,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteFolder(const TSharedPtr<FJsonObject
2738
2716
  continue;
2739
2717
  }
2740
2718
 
2741
- if (IsProtectedAssetPath(Norm))
2719
+ if (MCPIsProtectedAssetPath(Norm))
2742
2720
  {
2743
2721
  Entry->SetStringField(TEXT("status"), TEXT("failed"));
2744
2722
  Entry->SetStringField(TEXT("reason"), TEXT("protected_path"));
@@ -64,26 +64,6 @@ namespace
64
64
  bool PassedPreflight() const { return Error.IsEmpty(); }
65
65
  };
66
66
 
67
- // Mirrors IsProtectedAssetPath in AssetHandlers.cpp, which is file-local to
68
- // that translation unit. Keep the two rule sets identical: a write path that
69
- // enforces a weaker rule than its neighbours is how engine content gets
70
- // mutated through the bridge.
71
- bool IsProtectedBulkAssetPath(const FString& Path)
72
- {
73
- FString Normalized = Path;
74
- Normalized.TrimStartAndEndInline();
75
- if (Normalized.IsEmpty()) return false;
76
- if (!Normalized.StartsWith(TEXT("/"))) Normalized = TEXT("/") + Normalized;
77
- const FString Lower = Normalized.ToLower();
78
- if (Lower.StartsWith(TEXT("/engine/"))) return true;
79
- if (Lower.StartsWith(TEXT("/script/"))) return true;
80
- if (Lower.StartsWith(TEXT("/memory/"))) return true;
81
- if (Lower.StartsWith(TEXT("/temp/"))) return true;
82
- // Verse runtime objects surface as /Script/CoreUObject.* etc.
83
- if (Lower.Contains(TEXT("/script/"))) return true;
84
- return false;
85
- }
86
-
87
67
  TSharedPtr<FJsonObject> MakePropertyReadback(
88
68
  const FPreparedPropertyWrite& Prepared,
89
69
  const TSharedPtr<FJsonValue>& ActualValue,
@@ -198,7 +178,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkSetAssetProperties(const TSharedPtr<F
198
178
  }
199
179
  PreparedAsset.AssetPath = AssetPath;
200
180
 
201
- if (IsProtectedBulkAssetPath(AssetPath))
181
+ if (MCPIsProtectedAssetPath(AssetPath))
202
182
  {
203
183
  RejectItem(BulkStatusProtected, FString::Printf(
204
184
  TEXT("Refusing to mutate protected mount: %s. Engine, /Script/, /Memory/, /Temp/ are read-only via the bridge."),
@@ -50,21 +50,6 @@ struct FPreparedUpsertItem
50
50
  TArray<FPreparedProperty> Properties;
51
51
  };
52
52
 
53
- bool IsProtectedAssetPath(const FString& Path)
54
- {
55
- FString Normalized = Path;
56
- Normalized.TrimStartAndEndInline();
57
- if (!Normalized.StartsWith(TEXT("/")))
58
- {
59
- Normalized = TEXT("/") + Normalized;
60
- }
61
- const FString Lower = Normalized.ToLower();
62
- return Lower.StartsWith(TEXT("/engine/"))
63
- || Lower.StartsWith(TEXT("/script/"))
64
- || Lower.StartsWith(TEXT("/memory/"))
65
- || Lower.StartsWith(TEXT("/temp/"));
66
- }
67
-
68
53
  UClass* ResolveDataAssetClass(const FString& ClassName)
69
54
  {
70
55
  UClass* DataClass = nullptr;
@@ -144,7 +129,7 @@ bool ParseAssetIdentity(
144
129
  }
145
130
 
146
131
  const FString LongPackageName = OutItem.PackagePath + TEXT("/") + OutItem.Name;
147
- if (IsProtectedAssetPath(LongPackageName))
132
+ if (MCPIsProtectedAssetPath(LongPackageName))
148
133
  {
149
134
  OutError = FString::Printf(TEXT("Refusing to mutate protected package '%s'"), *LongPackageName);
150
135
  return false;
@@ -357,7 +342,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkRestoreDataAssets(const TSharedPtr<FJ
357
342
  FString AssetPath;
358
343
  const TSharedPtr<FJsonObject>* Properties = nullptr;
359
344
  if (!(*Item)->TryGetStringField(TEXT("assetPath"), AssetPath)
360
- || IsProtectedAssetPath(AssetPath)
345
+ || MCPIsProtectedAssetPath(AssetPath)
361
346
  || !(*Item)->TryGetObjectField(TEXT("properties"), Properties)
362
347
  || !Properties
363
348
  || !Properties->IsValid())
@@ -414,7 +399,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkRestoreDataAssets(const TSharedPtr<FJ
414
399
  FString AssetPath;
415
400
  if (!(*CreatedAssetPaths)[PathIndex].IsValid()
416
401
  || !(*CreatedAssetPaths)[PathIndex]->TryGetString(AssetPath)
417
- || IsProtectedAssetPath(AssetPath))
402
+ || MCPIsProtectedAssetPath(AssetPath))
418
403
  {
419
404
  Errors.Add(MakeShared<FJsonValueString>(FString::Printf(TEXT("createdAssetPaths[%d] is invalid"), PathIndex)));
420
405
  continue;
@@ -120,24 +120,6 @@ namespace
120
120
  const TCHAR* const MeshMatStatusFailed = TEXT("failed");
121
121
  const TCHAR* const MeshMatStatusSkipped = TEXT("skipped");
122
122
 
123
- // Mirrors IsProtectedAssetPath in AssetHandlers.cpp, which is file-local to
124
- // that translation unit. Keep the two rule sets identical: a write path that
125
- // enforces a weaker rule than its neighbours is how engine content gets
126
- // mutated through the bridge.
127
- bool IsProtectedMeshMaterialPath(const FString& Path)
128
- {
129
- FString Normalized = Path;
130
- Normalized.TrimStartAndEndInline();
131
- if (Normalized.IsEmpty()) return false;
132
- if (!Normalized.StartsWith(TEXT("/"))) Normalized = TEXT("/") + Normalized;
133
- const FString Lower = Normalized.ToLower();
134
- if (Lower.StartsWith(TEXT("/engine/"))) return true;
135
- if (Lower.StartsWith(TEXT("/memory/"))) return true;
136
- if (Lower.StartsWith(TEXT("/temp/"))) return true;
137
- if (Lower.Contains(TEXT("/script/"))) return true;
138
- return false;
139
- }
140
-
141
123
  struct FPreparedMeshMaterialAssignment
142
124
  {
143
125
  int32 ItemIndex = 0;
@@ -297,7 +279,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetMeshMaterialsBatch(const TSharedPtr<FJ
297
279
  }
298
280
  Item.AssetPath = AssetPath;
299
281
 
300
- if (IsProtectedMeshMaterialPath(AssetPath))
282
+ if (MCPIsProtectedAssetPath(AssetPath))
301
283
  {
302
284
  Reject(MeshMatStatusProtected, FString::Printf(
303
285
  TEXT("Refusing to mutate protected mount: %s. Engine, /Script/, /Memory/, /Temp/ are read-only via the bridge."),
@@ -135,6 +135,36 @@ inline TSharedPtr<FJsonValue> MCPCheckAssetExists(
135
135
  return TSharedPtr<FJsonValue>();
136
136
  }
137
137
 
138
+ /** Protected mount guardrail. Engine-shipped content (/Engine/, /Script/,
139
+ * /Memory/, /Temp/) and Verse runtime classes must never be mutated through
140
+ * the bridge: UEditorAssetLibrary::DeleteAsset will happily destroy files
141
+ * under <engineRoot>/Engine/Content/ if not stopped. Every handler that
142
+ * deletes, moves, renames or writes an asset calls this. Plugin content roots
143
+ * (mounted under /<PluginName>/) are NOT protected; per-project plugin content
144
+ * is expected to be writable.
145
+ *
146
+ * This lives here rather than as a file-local copy per translation unit
147
+ * because the asset handlers are split across several files that share one
148
+ * unity blob: duplicate definitions collide at compile time, and independent
149
+ * copies drift, which is how a write path ends up enforcing a weaker rule
150
+ * than its neighbours. */
151
+ inline bool MCPIsProtectedAssetPath(const FString& Path)
152
+ {
153
+ FString Normalized = Path;
154
+ Normalized.TrimStartAndEndInline();
155
+ if (Normalized.IsEmpty()) return false;
156
+ // Tolerate the surface form, which may arrive without a leading slash.
157
+ if (!Normalized.StartsWith(TEXT("/"))) Normalized = TEXT("/") + Normalized;
158
+ const FString Lower = Normalized.ToLower();
159
+ if (Lower.StartsWith(TEXT("/engine/"))) return true;
160
+ if (Lower.StartsWith(TEXT("/memory/"))) return true;
161
+ if (Lower.StartsWith(TEXT("/temp/"))) return true;
162
+ // Verse runtime objects surface as /Script/CoreUObject.* etc, so /Script/
163
+ // is rejected wherever it appears, not just as a prefix.
164
+ if (Lower.Contains(TEXT("/script/"))) return true;
165
+ return false;
166
+ }
167
+
138
168
  /** Emit the standard delete_asset rollback record on a create result. */
139
169
  inline void MCPSetDeleteAssetRollback(TSharedPtr<FJsonObject> Result, const FString& AssetPath)
140
170
  {