ultimate-unreal-engine-mcp 0.1.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 (124) hide show
  1. package/README.md +729 -0
  2. package/dist/build/error-parser.js +51 -0
  3. package/dist/build/fix-suggester.js +84 -0
  4. package/dist/build/ubt-runner.js +146 -0
  5. package/dist/cli.js +13 -0
  6. package/dist/config.js +8 -0
  7. package/dist/docs/data/ue57-api.js +228 -0
  8. package/dist/docs/doc-index.js +110 -0
  9. package/dist/docs/types.js +4 -0
  10. package/dist/generators/class-generator.js +363 -0
  11. package/dist/generators/file-modifier.js +276 -0
  12. package/dist/generators/uht-validator.js +177 -0
  13. package/dist/index.js +89 -0
  14. package/dist/parsers/cpp-class-index.js +230 -0
  15. package/dist/parsers/cpp-parser.js +369 -0
  16. package/dist/parsers/ini-parser.js +216 -0
  17. package/dist/parsers/uproject-parser.js +130 -0
  18. package/dist/plugin-bridge/client.js +217 -0
  19. package/dist/plugin-bridge/protocol.js +6 -0
  20. package/dist/plugin-bridge/retry.js +23 -0
  21. package/dist/setup.js +209 -0
  22. package/dist/tools/ai-systems/index.js +247 -0
  23. package/dist/tools/ai-systems/types.js +4 -0
  24. package/dist/tools/animation/index.js +241 -0
  25. package/dist/tools/animation/types.js +4 -0
  26. package/dist/tools/audio/index.js +204 -0
  27. package/dist/tools/audio/types.js +4 -0
  28. package/dist/tools/blueprint/index.js +495 -0
  29. package/dist/tools/blueprint/types.js +4 -0
  30. package/dist/tools/build/index.js +163 -0
  31. package/dist/tools/chaos/index.js +230 -0
  32. package/dist/tools/chaos/types.js +4 -0
  33. package/dist/tools/collision-physics/index.js +211 -0
  34. package/dist/tools/config/index.js +288 -0
  35. package/dist/tools/cpp/index.js +305 -0
  36. package/dist/tools/docs/index.js +251 -0
  37. package/dist/tools/editor/index.js +242 -0
  38. package/dist/tools/gas/index.js +222 -0
  39. package/dist/tools/gas/types.js +5 -0
  40. package/dist/tools/import-export/index.js +218 -0
  41. package/dist/tools/input/index.js +146 -0
  42. package/dist/tools/known-issues/index.js +88 -0
  43. package/dist/tools/known-issues/middleware.js +55 -0
  44. package/dist/tools/known-issues/store.js +125 -0
  45. package/dist/tools/livelink/index.js +203 -0
  46. package/dist/tools/livelink/types.js +4 -0
  47. package/dist/tools/material/index.js +190 -0
  48. package/dist/tools/motion-design/index.js +251 -0
  49. package/dist/tools/motion-design/types.js +6 -0
  50. package/dist/tools/movie-render/index.js +220 -0
  51. package/dist/tools/networking/index.js +149 -0
  52. package/dist/tools/pcg/index.js +164 -0
  53. package/dist/tools/selection/index.js +180 -0
  54. package/dist/tools/sequencer/index.js +218 -0
  55. package/dist/tools/validation/index.js +183 -0
  56. package/dist/tools/validation/types.js +4 -0
  57. package/dist/tools/viewport/index.js +310 -0
  58. package/dist/tools/worldpartition/index.js +226 -0
  59. package/dist/tools/worldpartition/types.js +4 -0
  60. package/dist/utils/execFileNoThrow.js +40 -0
  61. package/dist/utils/logger.js +27 -0
  62. package/dist/utils/path-guard.js +26 -0
  63. package/package.json +40 -0
  64. package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
  65. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
  66. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
  67. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
  68. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
  69. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
  70. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
  71. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
  72. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
  73. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
  74. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
  75. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
  76. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
  77. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
  78. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
  79. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
  80. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
  81. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
  82. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
  83. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
  84. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
  85. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
  86. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
  87. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
  88. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
  89. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
  90. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
  91. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
  92. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
  93. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
  94. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
  95. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
  96. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
  97. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
  98. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
  99. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
  100. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
  101. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
  102. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
  103. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
  104. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
  105. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
  106. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
  107. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
  108. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
  109. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
  110. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
  111. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
  112. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
  113. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
  114. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
  115. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
  116. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
  117. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
  118. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
  119. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
  120. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
  121. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
  122. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
  123. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
  124. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPTcpServer.h +59 -0
@@ -0,0 +1,216 @@
1
+ // UE-aware INI parser — standalone pure string-processing module.
2
+ // No fs imports — all I/O is the caller's responsibility.
3
+ // No console.log — pure transform, no side effects.
4
+ //
5
+ // Handles all five UE INI operators:
6
+ // plain = scalar assignment; last occurrence wins
7
+ // + append if not already present (no duplicates)
8
+ // . append always (duplicates allowed)
9
+ // - remove exact match
10
+ // ! clear all values for key (value after = is ignored)
11
+ //
12
+ // Values are always stored as string[] (scalars as single-element arrays).
13
+ // Section names and key names are case-sensitive per UE requirements.
14
+ // Indexed-array syntax (MyArray[0]=val) is stored with the bracket index
15
+ // as part of the literal key name — no special flattening.
16
+ // Struct/compound values (parens) are treated as opaque strings.
17
+ // Only lines where ; is the first non-whitespace char are treated as comments;
18
+ // mid-line semicolons are preserved verbatim in the value.
19
+ // ---------------------------------------------------------------------------
20
+ // Internal helpers
21
+ // ---------------------------------------------------------------------------
22
+ const OPERATORS = new Set(['+', '-', '.', '!']);
23
+ /**
24
+ * Strips the UE operator prefix (+ - . !) from a raw key token and
25
+ * returns { operator, key }.
26
+ * If the first character is not an operator, operator is '' (plain =).
27
+ */
28
+ function splitOperator(rawKey) {
29
+ const first = rawKey[0] ?? '';
30
+ if (OPERATORS.has(first)) {
31
+ return { operator: first, key: rawKey.slice(1) };
32
+ }
33
+ return { operator: '', key: rawKey };
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // parseIni
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * Parse a full INI file content string into a structured map.
40
+ * Handles all five UE operators: plain =, +, -, ., !
41
+ * Section names are case-sensitive (UE requirement).
42
+ */
43
+ export function parseIni(content) {
44
+ const result = {};
45
+ let currentSection = '';
46
+ for (const rawLine of content.split(/\r?\n/)) {
47
+ const line = rawLine.trim();
48
+ // Skip blank lines
49
+ if (!line)
50
+ continue;
51
+ // Skip comment lines (first non-whitespace char is ;)
52
+ if (line.startsWith(';'))
53
+ continue;
54
+ // Section header: [SectionName]
55
+ if (line.startsWith('[') && line.endsWith(']')) {
56
+ currentSection = line.slice(1, -1);
57
+ if (!result[currentSection]) {
58
+ result[currentSection] = {};
59
+ }
60
+ continue;
61
+ }
62
+ // Key=Value line — must have an = sign
63
+ const eqIdx = line.indexOf('=');
64
+ if (eqIdx === -1)
65
+ continue; // malformed line — skip silently
66
+ const rawKey = line.slice(0, eqIdx).trim();
67
+ // Preserve value verbatim after the = (no trimming — spaces and semicolons kept)
68
+ const value = line.slice(eqIdx + 1);
69
+ const { operator, key } = splitOperator(rawKey);
70
+ // Ensure section exists (handles key lines before any section header)
71
+ if (!result[currentSection]) {
72
+ result[currentSection] = {};
73
+ }
74
+ const section = result[currentSection];
75
+ switch (operator) {
76
+ case '!':
77
+ // Clear all accumulated values for this key
78
+ section[key] = [];
79
+ break;
80
+ case '-':
81
+ // Remove exact match from array
82
+ section[key] = (section[key] ?? []).filter((v) => v !== value);
83
+ break;
84
+ case '+':
85
+ // Append only if value not already present
86
+ if (!(section[key] ?? []).includes(value)) {
87
+ section[key] = [...(section[key] ?? []), value];
88
+ }
89
+ break;
90
+ case '.':
91
+ // Append unconditionally (duplicates allowed)
92
+ section[key] = [...(section[key] ?? []), value];
93
+ break;
94
+ default:
95
+ // Plain = : last occurrence wins — store as single-element array
96
+ section[key] = [value];
97
+ break;
98
+ }
99
+ }
100
+ return result;
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // setIniValue
104
+ // ---------------------------------------------------------------------------
105
+ /**
106
+ * Write/update a single key in a section (plain = operator).
107
+ * Returns the new file content string — does not write to disk.
108
+ *
109
+ * Behaviour:
110
+ * - If the key exists in the target section (with any operator prefix),
111
+ * replaces the FIRST matching line with "key=value" and drops all
112
+ * subsequent lines for that key.
113
+ * - If the key is not found but the section exists, appends "key=value"
114
+ * at the end of the section block (before the next [Section] header).
115
+ * - If the section does not exist at all, appends an empty line, the
116
+ * section header, and "key=value" at the end of the file.
117
+ *
118
+ * Output uses \n line endings regardless of input style.
119
+ */
120
+ export function setIniValue(content, section, key, value) {
121
+ const lines = content.split(/\r?\n/);
122
+ let inTargetSection = false;
123
+ let sectionFound = false;
124
+ let keyReplaced = false;
125
+ const output = [];
126
+ for (const line of lines) {
127
+ const trimmed = line.trim();
128
+ // Detect section headers
129
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
130
+ // Leaving a section — if we were in target section and haven't placed
131
+ // the key yet, inject it before moving to the new section
132
+ if (inTargetSection && !keyReplaced) {
133
+ output.push(`${key}=${value}`);
134
+ keyReplaced = true;
135
+ }
136
+ inTargetSection = trimmed.slice(1, -1) === section;
137
+ if (inTargetSection)
138
+ sectionFound = true;
139
+ output.push(line);
140
+ continue;
141
+ }
142
+ // Within target section: look for lines whose bare key matches
143
+ if (inTargetSection && !keyReplaced) {
144
+ const eqIdx = trimmed.indexOf('=');
145
+ if (eqIdx !== -1) {
146
+ const rawKey = trimmed.slice(0, eqIdx).trim();
147
+ const { key: bareKey } = splitOperator(rawKey);
148
+ if (bareKey === key) {
149
+ // Replace this line with the new key=value
150
+ output.push(`${key}=${value}`);
151
+ keyReplaced = true;
152
+ continue; // drop the original line
153
+ }
154
+ }
155
+ }
156
+ else if (inTargetSection && keyReplaced) {
157
+ // Key already replaced — drop any further lines for the same key
158
+ const eqIdx = trimmed.indexOf('=');
159
+ if (eqIdx !== -1) {
160
+ const rawKey = trimmed.slice(0, eqIdx).trim();
161
+ const { key: bareKey } = splitOperator(rawKey);
162
+ if (bareKey === key) {
163
+ continue; // skip duplicate key lines
164
+ }
165
+ }
166
+ }
167
+ output.push(line);
168
+ }
169
+ // After loop: section was found but key never appeared — append at end
170
+ if (inTargetSection && !keyReplaced) {
171
+ output.push(`${key}=${value}`);
172
+ }
173
+ // Section was not in the file at all — append section + key at end
174
+ if (!sectionFound) {
175
+ output.push('', `[${section}]`, `${key}=${value}`);
176
+ }
177
+ return output.join('\n');
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // writeIniRemoveKey
181
+ // ---------------------------------------------------------------------------
182
+ /**
183
+ * Remove all lines for a key from a specified section (all operator prefixes).
184
+ * Returns the new file content string — does not write to disk.
185
+ *
186
+ * - Removes lines whose bare key (operator prefix stripped) matches target key.
187
+ * - No-op if section or key does not exist (returns content unchanged).
188
+ * - Output uses \n line endings regardless of input style.
189
+ */
190
+ export function writeIniRemoveKey(content, section, key) {
191
+ const lines = content.split(/\r?\n/);
192
+ let inTargetSection = false;
193
+ const output = [];
194
+ for (const line of lines) {
195
+ const trimmed = line.trim();
196
+ // Detect section headers
197
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
198
+ inTargetSection = trimmed.slice(1, -1) === section;
199
+ output.push(line);
200
+ continue;
201
+ }
202
+ // Within target section: skip lines whose bare key matches
203
+ if (inTargetSection) {
204
+ const eqIdx = trimmed.indexOf('=');
205
+ if (eqIdx !== -1) {
206
+ const rawKey = trimmed.slice(0, eqIdx).trim();
207
+ const { key: bareKey } = splitOperator(rawKey);
208
+ if (bareKey === key) {
209
+ continue; // remove this line
210
+ }
211
+ }
212
+ }
213
+ output.push(line);
214
+ }
215
+ return output.join('\n');
216
+ }
@@ -0,0 +1,130 @@
1
+ // src/parsers/uproject-parser.ts
2
+ // Zod-validated parsers for .uproject and .uplugin JSON descriptor files.
3
+ //
4
+ // These files are standard JSON — JSON.parse with Zod schema validation is sufficient.
5
+ // Both parsers return a discriminated union (ParseResult) and never throw.
6
+ //
7
+ // References:
8
+ // https://github.com/starkat99/unreal-schema/blob/main/uproject.schema.json
9
+ // https://github.com/starkat99/unreal-schema/blob/main/uplugin.schema.json
10
+ import { z } from 'zod';
11
+ // ---------------------------------------------------------------------------
12
+ // .uproject schemas
13
+ // ---------------------------------------------------------------------------
14
+ export const UProjectModuleSchema = z.object({
15
+ Name: z.string(),
16
+ Type: z.string(),
17
+ LoadingPhase: z.string().optional(),
18
+ PlatformAllowList: z.array(z.string()).optional(),
19
+ PlatformDenyList: z.array(z.string()).optional(),
20
+ TargetAllowList: z.array(z.string()).optional(),
21
+ TargetDenyList: z.array(z.string()).optional(),
22
+ TargetConfigurationAllowList: z.array(z.string()).optional(),
23
+ TargetConfigurationDenyList: z.array(z.string()).optional(),
24
+ });
25
+ export const UProjectPluginEntrySchema = z.object({
26
+ Name: z.string(),
27
+ Enabled: z.boolean(),
28
+ Optional: z.boolean().optional(),
29
+ Description: z.string().optional(),
30
+ MarketplaceURL: z.string().optional(),
31
+ PlatformAllowList: z.array(z.string()).optional(),
32
+ SupportedTargetPlatforms: z.array(z.string()).optional(),
33
+ });
34
+ export const UProjectSchema = z.object({
35
+ FileVersion: z.number().int().min(1).max(3),
36
+ EngineAssociation: z.string().optional(),
37
+ Category: z.string().optional(),
38
+ Description: z.string().optional(),
39
+ Enterprise: z.boolean().optional(),
40
+ DisableEnginePluginsByDefault: z.boolean().optional(),
41
+ Modules: z.array(UProjectModuleSchema).optional(),
42
+ Plugins: z.array(UProjectPluginEntrySchema).optional(),
43
+ AdditionalPluginDirectories: z.array(z.string()).optional(),
44
+ AdditionalRootDirectories: z.array(z.string()).optional(),
45
+ TargetPlatforms: z.array(z.string()).optional(),
46
+ });
47
+ // ---------------------------------------------------------------------------
48
+ // .uplugin schemas
49
+ // ---------------------------------------------------------------------------
50
+ export const UPluginModuleSchema = z.object({
51
+ Name: z.string(),
52
+ Type: z.string(),
53
+ LoadingPhase: z.string().optional(),
54
+ PlatformAllowList: z.array(z.string()).optional(),
55
+ PlatformDenyList: z.array(z.string()).optional(),
56
+ });
57
+ export const UPluginDependencySchema = z.object({
58
+ Name: z.string(),
59
+ Enabled: z.boolean(),
60
+ Optional: z.boolean().optional(),
61
+ PlatformAllowList: z.array(z.string()).optional(),
62
+ });
63
+ export const UPluginSchema = z.object({
64
+ FileVersion: z.number().int().min(1).max(3),
65
+ Version: z.number().optional(),
66
+ VersionName: z.string().optional(),
67
+ FriendlyName: z.string().optional(),
68
+ Description: z.string().optional(),
69
+ Category: z.string().optional(),
70
+ CreatedBy: z.string().optional(),
71
+ EngineVersion: z.string().optional(),
72
+ SupportedTargetPlatforms: z.array(z.string()).optional(),
73
+ Modules: z.array(UPluginModuleSchema).optional(),
74
+ Plugins: z.array(UPluginDependencySchema).optional(),
75
+ EnabledByDefault: z.boolean().optional(),
76
+ CanContainContent: z.boolean().optional(),
77
+ IsBetaVersion: z.boolean().optional(),
78
+ IsExperimentalVersion: z.boolean().optional(),
79
+ });
80
+ // ---------------------------------------------------------------------------
81
+ // Parse functions
82
+ // ---------------------------------------------------------------------------
83
+ /**
84
+ * Parse a raw JSON string from a .uproject file.
85
+ * Returns ParseResult<UProjectDescriptor> — never throws.
86
+ *
87
+ * Threat T-02-03: JSON.parse is wrapped in try/catch; SyntaxError is never propagated.
88
+ */
89
+ export function parseUproject(jsonContent) {
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(jsonContent);
93
+ }
94
+ catch (err) {
95
+ const msg = err instanceof Error ? err.message : String(err);
96
+ return { success: false, error: `Invalid JSON: ${msg}` };
97
+ }
98
+ const result = UProjectSchema.safeParse(parsed);
99
+ if (result.success) {
100
+ return { success: true, data: result.data };
101
+ }
102
+ return {
103
+ success: false,
104
+ error: result.error.issues.map((i) => i.message).join('; '),
105
+ };
106
+ }
107
+ /**
108
+ * Parse a raw JSON string from a .uplugin file.
109
+ * Returns ParseResult<UPluginDescriptor> — never throws.
110
+ *
111
+ * Threat T-02-03: JSON.parse is wrapped in try/catch; SyntaxError is never propagated.
112
+ */
113
+ export function parseUplugin(jsonContent) {
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(jsonContent);
117
+ }
118
+ catch (err) {
119
+ const msg = err instanceof Error ? err.message : String(err);
120
+ return { success: false, error: `Invalid JSON: ${msg}` };
121
+ }
122
+ const result = UPluginSchema.safeParse(parsed);
123
+ if (result.success) {
124
+ return { success: true, data: result.data };
125
+ }
126
+ return {
127
+ success: false,
128
+ error: result.error.issues.map((i) => i.message).join('; '),
129
+ };
130
+ }
@@ -0,0 +1,217 @@
1
+ // TCP client connecting to the UE Editor plugin on PLUGIN_PORT.
2
+ // Returns structured plugin_not_connected errors when the editor is not running.
3
+ // Reconnects automatically with ExponentialBackoff — never floods logs (Pitfall 3).
4
+ // JSON-newline framing with correlationId-based request/response matching (Phase 7).
5
+ import * as net from 'net';
6
+ import { ExponentialBackoff } from './retry.js';
7
+ import { PLUGIN_PORT } from '../config.js';
8
+ /**
9
+ * Thrown by sendCommand() when no connection to the UE Editor plugin exists.
10
+ * Catch this to surface a structured error to the MCP client instead of crashing.
11
+ */
12
+ export class PluginNotConnectedError extends Error {
13
+ bridgeError;
14
+ constructor(bridgeError) {
15
+ super(bridgeError.message);
16
+ this.name = 'PluginNotConnectedError';
17
+ this.bridgeError = bridgeError;
18
+ }
19
+ }
20
+ // ---------------------------------------------------------------------------
21
+ // PluginBridgeClient
22
+ // ---------------------------------------------------------------------------
23
+ /**
24
+ * TCP client that connects to the UE Editor MCPBridge plugin (port 55557 by default).
25
+ *
26
+ * Connection lifecycle:
27
+ * - Constructor starts scheduleReconnect() immediately (first attempt after 1s).
28
+ * - On connect: socket is set, backoff is reset.
29
+ * - On error/close: socket is cleared, next reconnect is scheduled.
30
+ * - Only logs on first failure and on state changes (never on every retry).
31
+ *
32
+ * Graceful degradation:
33
+ * - isConnected() returns false when no socket is active — never throws.
34
+ * - getDisconnectedError() returns the exact MCPBridgeError shape.
35
+ * - sendCommand() throws PluginNotConnectedError when disconnected.
36
+ *
37
+ * Protocol (JSON-newline framing with correlationId):
38
+ * - sendCommand() generates a UUID correlationId, writes JSON+\n to the socket.
39
+ * - The socket 'data' listener accumulates receiveBuffer, splits on \n, and
40
+ * resolves the matching pending promise by correlationId.
41
+ * - 10-second timeout rejects the promise if no response arrives.
42
+ * - destroy() rejects all pending commands to prevent leaks.
43
+ */
44
+ export class PluginBridgeClient {
45
+ socket = null;
46
+ port;
47
+ backoff;
48
+ reconnectTimer = null;
49
+ /** Log first failure only; suppress subsequent retries until state changes. */
50
+ hasLoggedFirstFailure = false;
51
+ /** Accumulates partial data received from the TCP socket between newlines. */
52
+ receiveBuffer = '';
53
+ /**
54
+ * Maps correlationId to resolve/reject callbacks for in-flight sendCommand() calls.
55
+ * Entries are added before socket.write() and removed on response or timeout.
56
+ */
57
+ pendingCommands = new Map();
58
+ constructor(port = PLUGIN_PORT) {
59
+ this.port = port;
60
+ this.backoff = new ExponentialBackoff();
61
+ this.scheduleReconnect();
62
+ }
63
+ /** Returns true only when a live (non-destroyed) socket exists. */
64
+ isConnected() {
65
+ return this.socket !== null && !this.socket.destroyed;
66
+ }
67
+ /**
68
+ * Returns the exact error object shape required by CONTEXT.md locked decision.
69
+ * Callers should include this object in their MCP tool response (isError: true).
70
+ */
71
+ getDisconnectedError() {
72
+ return {
73
+ error: 'plugin_not_connected',
74
+ message: 'This tool requires the UE Editor plugin. Start the editor with the MCPBridge plugin enabled.',
75
+ required_plugin: true,
76
+ };
77
+ }
78
+ /**
79
+ * Sends a command to the UE Editor plugin and waits for a correlated response.
80
+ *
81
+ * - Throws PluginNotConnectedError immediately if not connected.
82
+ * - Generates a crypto.randomUUID() correlationId (overwrites any caller-supplied value).
83
+ * - Writes JSON-newline framed message: JSON.stringify(cmd) + '\n'.
84
+ * - Resolves when the matching correlationId response arrives from the plugin.
85
+ * - Rejects with a timeout Error after 10 seconds if no response arrives.
86
+ * - Rejects immediately on socket write error.
87
+ *
88
+ * Threat T-07-11 mitigation: 10-second timeout clears Map entry on expiry;
89
+ * destroy() clears all remaining entries.
90
+ */
91
+ async sendCommand(cmd) {
92
+ if (!this.isConnected()) {
93
+ throw new PluginNotConnectedError(this.getDisconnectedError());
94
+ }
95
+ // Generate a unique correlationId for this request — overwrites any caller value.
96
+ // crypto.randomUUID() is available globally in Node 19+ (Node 22 used here).
97
+ const correlationId = crypto.randomUUID();
98
+ const cmdWithId = { ...cmd, correlationId };
99
+ return new Promise((resolve, reject) => {
100
+ this.pendingCommands.set(correlationId, { resolve, reject });
101
+ // 10-second timeout — T-07-11 mitigation
102
+ const timeoutHandle = setTimeout(() => {
103
+ if (this.pendingCommands.has(correlationId)) {
104
+ this.pendingCommands.delete(correlationId);
105
+ reject(new Error(`MCP command '${cmd.type}' timed out after 10 seconds`));
106
+ }
107
+ }, 10_000);
108
+ // Write JSON-newline framed message to the socket.
109
+ // The write callback fires on flush; reject immediately on error.
110
+ this.socket.write(JSON.stringify(cmdWithId) + '\n', 'utf8', (err) => {
111
+ if (err) {
112
+ clearTimeout(timeoutHandle);
113
+ this.pendingCommands.delete(correlationId);
114
+ reject(err);
115
+ }
116
+ });
117
+ });
118
+ }
119
+ /**
120
+ * Cancel the pending reconnect timer, reject all in-flight commands, and close the socket.
121
+ * Call this in tests or during graceful shutdown.
122
+ */
123
+ destroy() {
124
+ if (this.reconnectTimer !== null) {
125
+ clearTimeout(this.reconnectTimer);
126
+ this.reconnectTimer = null;
127
+ }
128
+ if (this.socket !== null) {
129
+ this.socket.destroy();
130
+ this.socket = null;
131
+ }
132
+ // Reject all pending commands to prevent leaks (T-07-11 mitigation).
133
+ for (const [, pending] of this.pendingCommands) {
134
+ pending.reject(new Error('PluginBridgeClient destroyed'));
135
+ }
136
+ this.pendingCommands.clear();
137
+ this.receiveBuffer = '';
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Private: reconnect loop
141
+ // ---------------------------------------------------------------------------
142
+ scheduleReconnect() {
143
+ const delay = this.backoff.nextDelay();
144
+ this.reconnectTimer = setTimeout(() => {
145
+ this.reconnectTimer = null;
146
+ if (this.isConnected()) {
147
+ // Already connected — reset backoff and continue monitoring
148
+ this.backoff.reset();
149
+ this.scheduleReconnect();
150
+ return;
151
+ }
152
+ this.connect().then(() => {
153
+ // Connection succeeded
154
+ this.backoff.reset();
155
+ this.hasLoggedFirstFailure = false;
156
+ console.error('[UE MCP] Connected to UE Editor plugin on port', this.port);
157
+ this.scheduleReconnect();
158
+ }).catch(() => {
159
+ // Connection failed — log only on first failure or after reconnection
160
+ if (!this.hasLoggedFirstFailure) {
161
+ console.error(`[UE MCP] Cannot reach UE Editor plugin on port ${this.port}. Retrying with backoff...`);
162
+ this.hasLoggedFirstFailure = true;
163
+ }
164
+ this.scheduleReconnect();
165
+ });
166
+ }, delay);
167
+ }
168
+ connect() {
169
+ return new Promise((resolve, reject) => {
170
+ const socket = net.createConnection({ port: this.port, host: '127.0.0.1' });
171
+ // JSON-newline response parser — accumulates partial chunks, splits on \n,
172
+ // and resolves the correlated pending promise.
173
+ // Threat T-07-09 mitigation: try/catch around JSON.parse; unknown correlationId
174
+ // is silently dropped — no state corruption.
175
+ socket.on('data', (chunk) => {
176
+ this.receiveBuffer += chunk.toString('utf8');
177
+ let newlineIdx;
178
+ while ((newlineIdx = this.receiveBuffer.indexOf('\n')) !== -1) {
179
+ const line = this.receiveBuffer.slice(0, newlineIdx).trim();
180
+ this.receiveBuffer = this.receiveBuffer.slice(newlineIdx + 1);
181
+ if (line.length === 0) {
182
+ continue;
183
+ }
184
+ try {
185
+ const response = JSON.parse(line);
186
+ const pending = this.pendingCommands.get(response.correlationId);
187
+ if (pending) {
188
+ this.pendingCommands.delete(response.correlationId);
189
+ pending.resolve(response);
190
+ }
191
+ // Unknown correlationId — silently drop (T-07-09 mitigation)
192
+ }
193
+ catch {
194
+ // Non-JSON line — ignore (T-07-09 mitigation)
195
+ }
196
+ }
197
+ });
198
+ socket.once('connect', () => {
199
+ this.socket = socket;
200
+ resolve();
201
+ });
202
+ socket.once('error', (err) => {
203
+ socket.destroy();
204
+ reject(err);
205
+ });
206
+ socket.once('close', () => {
207
+ if (this.socket === socket) {
208
+ this.socket = null;
209
+ // Log disconnection only if we were previously connected
210
+ console.error('[UE MCP] Lost connection to UE Editor plugin. Reconnecting...');
211
+ this.hasLoggedFirstFailure = false;
212
+ this.scheduleReconnect();
213
+ }
214
+ });
215
+ });
216
+ }
217
+ }
@@ -0,0 +1,6 @@
1
+ // JSON-newline framing types for communication with the UE C++ plugin.
2
+ // The UE plugin (Phase 7) runs a TCP server on PLUGIN_PORT that speaks this protocol:
3
+ // - Each message is a JSON object followed by a newline character (\n)
4
+ // - Client sends MCPCommand, server responds with MCPResponse
5
+ // - correlationId is generated by the client, echoed by the server for matching
6
+ export {};
@@ -0,0 +1,23 @@
1
+ // Exponential backoff for TCP reconnection to the UE Editor plugin.
2
+ // Delay doubles from 1s initial, capped at 30s to avoid reconnect storms.
3
+ // See: RESEARCH.md Pitfall 3 — TCP Reconnect Storm on Plugin Not Running
4
+ export class ExponentialBackoff {
5
+ delay = 1000;
6
+ maxDelay = 30000;
7
+ /**
8
+ * Returns the current delay in milliseconds, then doubles it for next call.
9
+ * Delay is capped at maxDelay (30000ms).
10
+ */
11
+ nextDelay() {
12
+ const current = this.delay;
13
+ this.delay = Math.min(this.delay * 2, this.maxDelay);
14
+ return current;
15
+ }
16
+ /**
17
+ * Resets delay back to the initial value (1000ms).
18
+ * Call this when a connection succeeds.
19
+ */
20
+ reset() {
21
+ this.delay = 1000;
22
+ }
23
+ }