ultimate-unreal-engine-mcp 0.1.7 → 0.1.9

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.
@@ -177,32 +177,53 @@ export class PluginBridgeClient {
177
177
  connect() {
178
178
  return new Promise((resolve, reject) => {
179
179
  const socket = net.createConnection({ port: this.port, host: '127.0.0.1' });
180
- // JSON-newline response parser — accumulates partial chunks, splits on \n,
181
- // and resolves the correlated pending promise.
182
- // Threat T-07-09 mitigation: try/catch around JSON.parse; unknown correlationId
183
- // is silently dropped no state corruption.
180
+ // JSON response parser — handles UE's pretty-printed multi-line JSON.
181
+ // UE's FJsonSerializer outputs JSON with \r\n and tabs inside the object,
182
+ // so we can't split on \n (that would break mid-object). Instead we track
183
+ // brace depth: when depth returns to 0, we have a complete JSON object.
184
184
  socket.on('data', (chunk) => {
185
185
  this.receiveBuffer += chunk.toString('utf8');
186
- let newlineIdx;
187
- while ((newlineIdx = this.receiveBuffer.indexOf('\n')) !== -1) {
188
- const line = this.receiveBuffer.slice(0, newlineIdx).trim();
189
- this.receiveBuffer = this.receiveBuffer.slice(newlineIdx + 1);
190
- if (line.length === 0) {
191
- continue;
192
- }
193
- try {
194
- const response = JSON.parse(line);
195
- const pending = this.pendingCommands.get(response.correlationId);
196
- if (pending) {
197
- this.pendingCommands.delete(response.correlationId);
198
- pending.resolve(response);
186
+ let startIdx = -1;
187
+ let depth = 0;
188
+ for (let i = 0; i < this.receiveBuffer.length; i++) {
189
+ const ch = this.receiveBuffer[i];
190
+ if (ch === '{') {
191
+ if (depth === 0) {
192
+ startIdx = i;
199
193
  }
200
- // Unknown correlationId — silently drop (T-07-09 mitigation)
194
+ depth++;
201
195
  }
202
- catch {
203
- // Non-JSON line — ignore (T-07-09 mitigation)
196
+ else if (ch === '}') {
197
+ depth--;
198
+ if (depth === 0 && startIdx >= 0) {
199
+ const jsonStr = this.receiveBuffer.slice(startIdx, i + 1);
200
+ // Remove everything up to and including this object
201
+ this.receiveBuffer = this.receiveBuffer.slice(i + 1);
202
+ try {
203
+ const response = JSON.parse(jsonStr);
204
+ const pending = this.pendingCommands.get(response.correlationId);
205
+ if (pending) {
206
+ this.pendingCommands.delete(response.correlationId);
207
+ pending.resolve(response);
208
+ }
209
+ }
210
+ catch {
211
+ // Malformed JSON — ignore (T-07-09 mitigation)
212
+ }
213
+ // Reset loop to scan remaining buffer from the start
214
+ i = -1;
215
+ startIdx = -1;
216
+ }
204
217
  }
205
218
  }
219
+ // Keep only unprocessed data (partial object) in the buffer
220
+ if (startIdx > 0) {
221
+ this.receiveBuffer = this.receiveBuffer.slice(startIdx);
222
+ }
223
+ else if (depth === 0) {
224
+ // No open braces — discard any whitespace/newlines between objects
225
+ this.receiveBuffer = '';
226
+ }
206
227
  });
207
228
  socket.once('connect', () => {
208
229
  this.socket = socket;
@@ -93,15 +93,20 @@ export function registerEditorTools(server, bridge) {
93
93
  y: z.number().describe('Y coordinate in Unreal units (cm)'),
94
94
  z: z.number().describe('Z coordinate in Unreal units (cm)'),
95
95
  }).describe('World-space location to spawn the actor at'),
96
+ label: z.string().optional().describe('Optional editor display label for the spawned actor'),
96
97
  }),
97
98
  annotations: {
98
99
  readOnlyHint: false,
99
100
  destructiveHint: false,
100
101
  },
101
102
  }, withKnownIssues('ue_spawn_actor', async (args) => {
103
+ const payload = { class_name: args.class_name, location: args.location };
104
+ if (args.label) {
105
+ payload['label'] = args.label;
106
+ }
102
107
  return sendOrDisconnect(_bridge, {
103
108
  type: 'actor.spawn',
104
- payload: { class_name: args.class_name, location: args.location },
109
+ payload,
105
110
  });
106
111
  }));
107
112
  // --------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-unreal-engine-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "MCP server giving AI assistants full access to Unreal Engine 5.7 projects",
5
5
  "type": "module",
6
6
  "engines": {
@@ -201,6 +201,13 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
201
201
  const FVector Location(X, Y, Z);
202
202
  const FRotator Rotation(0.0, 0.0, 0.0);
203
203
 
204
+ // Optional label — sets the editor display name after spawn.
205
+ FString Label;
206
+ if (Payload->TryGetStringField(TEXT("label"), Label) && !Label.IsEmpty())
207
+ {
208
+ Params.Name = FName(*Label);
209
+ }
210
+
204
211
  AActor* Spawned = World->SpawnActor<AActor>(ActorClass, Location, Rotation, Params);
205
212
  if (!Spawned)
206
213
  {
@@ -208,6 +215,12 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
208
215
  return;
209
216
  }
210
217
 
218
+ // Apply explicit label if provided (overrides auto-generated label).
219
+ if (!Label.IsEmpty())
220
+ {
221
+ Spawned->SetActorLabel(Label);
222
+ }
223
+
211
224
  TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
212
225
  Data->SetStringField(TEXT("label"), Spawned->GetActorLabel());
213
226
  Data->SetStringField(TEXT("id"), Spawned->GetName());