@chrrxs/robloxstudio-mcp 2.23.1 → 3.0.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.
@@ -123,47 +123,6 @@ function getPlaceInfo(_requestData: Record<string, unknown>) {
123
123
  };
124
124
  }
125
125
 
126
- function getServices(requestData: Record<string, unknown>) {
127
- const serviceName = requestData.serviceName as string | undefined;
128
-
129
- if (serviceName) {
130
- const [ok, service] = pcall(() => game.GetService(serviceName as keyof Services));
131
- if (ok && service) {
132
- return {
133
- service: {
134
- name: service.Name,
135
- className: service.ClassName,
136
- path: getInstancePath(service as Instance),
137
- childCount: (service as Instance).GetChildren().size(),
138
- },
139
- };
140
- } else {
141
- return { error: `Service not found: ${serviceName}` };
142
- }
143
- } else {
144
- const services: { name: string; className: string; path: string; childCount: number }[] = [];
145
- const commonServices = [
146
- "Workspace", "Players", "StarterGui", "StarterPack", "StarterPlayer",
147
- "ReplicatedStorage", "ServerStorage", "ServerScriptService",
148
- "HttpService", "TeleportService", "DataStoreService",
149
- ];
150
-
151
- for (const svcName of commonServices) {
152
- const [ok, service] = pcall(() => game.GetService(svcName as keyof Services));
153
- if (ok && service) {
154
- services.push({
155
- name: service.Name,
156
- className: service.ClassName,
157
- path: getInstancePath(service as Instance),
158
- childCount: (service as Instance).GetChildren().size(),
159
- });
160
- }
161
- }
162
-
163
- return { services };
164
- }
165
- }
166
-
167
126
  function searchObjects(requestData: Record<string, unknown>) {
168
127
  const query = requestData.query as string;
169
128
  const searchType = (requestData.searchType as string) ?? "name";
@@ -316,31 +275,6 @@ function getInstanceProperties(requestData: Record<string, unknown>) {
316
275
  }
317
276
  }
318
277
 
319
- function getInstanceChildren(requestData: Record<string, unknown>) {
320
- const instancePath = requestData.instancePath as string;
321
- if (!instancePath) return { error: "Instance path is required" };
322
-
323
- const instance = getInstanceByPath(instancePath);
324
- if (!instance) return { error: `Instance not found: ${instancePath}` };
325
-
326
- const children: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean }[] = [];
327
- for (const child of instance.GetChildren()) {
328
- const entry: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean } = {
329
- name: child.Name,
330
- className: child.ClassName,
331
- path: getInstancePath(child),
332
- hasChildren: child.GetChildren().size() > 0,
333
- hasSource: child.IsA("LuaSourceContainer"),
334
- };
335
- if (child.IsA("BaseScript")) {
336
- entry.enabled = child.Enabled;
337
- }
338
- children.push(entry);
339
- }
340
-
341
- return { instancePath, children, count: children.size() };
342
- }
343
-
344
278
  function searchByProperty(requestData: Record<string, unknown>) {
345
279
  const propertyName = requestData.propertyName as string;
346
280
  const propertyValue = requestData.propertyValue as string;
@@ -769,106 +703,14 @@ function grepScripts(requestData: Record<string, unknown>) {
769
703
  };
770
704
  }
771
705
 
772
- function getDescendants(requestData: Record<string, unknown>) {
773
- const instancePath = requestData.instancePath as string;
774
- if (!instancePath) return { error: "Instance path is required" };
775
-
776
- const maxDepth = (requestData.maxDepth as number) ?? 10;
777
- const classFilter = requestData.classFilter as string | undefined;
778
-
779
- const instance = getInstanceByPath(instancePath);
780
- if (!instance) return { error: `Instance not found: ${instancePath}` };
781
-
782
- const descendants: { name: string; className: string; path: string; depth: number }[] = [];
783
-
784
- function collect(inst: Instance, depth: number) {
785
- if (depth > maxDepth) return;
786
- for (const child of inst.GetChildren()) {
787
- if (classFilter && !child.IsA(classFilter as keyof Instances)) continue;
788
- descendants.push({
789
- name: child.Name,
790
- className: child.ClassName,
791
- path: getInstancePath(child),
792
- depth,
793
- });
794
- collect(child, depth + 1);
795
- }
796
- }
797
-
798
- collect(instance, 1);
799
-
800
- return { instancePath, descendants, count: descendants.size(), maxDepth };
801
- }
802
-
803
- function compareInstances(requestData: Record<string, unknown>) {
804
- const instancePathA = requestData.instancePathA as string;
805
- const instancePathB = requestData.instancePathB as string;
806
-
807
- if (!instancePathA || !instancePathB) {
808
- return { error: "Both instancePathA and instancePathB are required" };
809
- }
810
-
811
- const instA = getInstanceByPath(instancePathA);
812
- if (!instA) return { error: `Instance not found: ${instancePathA}` };
813
-
814
- const instB = getInstanceByPath(instancePathB);
815
- if (!instB) return { error: `Instance not found: ${instancePathB}` };
816
-
817
- const commonProps = [
818
- "Name", "ClassName",
819
- "Size", "Position", "Rotation", "CFrame", "Anchored", "CanCollide",
820
- "Transparency", "BrickColor", "Material", "Color", "Text", "TextColor3",
821
- "BackgroundColor3", "Image", "ImageColor3", "Visible", "Active", "ZIndex",
822
- "BorderSizePixel", "BackgroundTransparency", "ImageTransparency",
823
- "TextTransparency", "Value", "Enabled", "Brightness", "Range", "Shadows",
824
- ];
825
-
826
- const matching: Record<string, string> = {};
827
- const differing: Record<string, { a: string; b: string }> = {};
828
- const onlyA: string[] = [];
829
- const onlyB: string[] = [];
830
-
831
- for (const prop of commonProps) {
832
- const [okA, valA] = pcall(() => tostring((instA as unknown as Record<string, unknown>)[prop]));
833
- const [okB, valB] = pcall(() => tostring((instB as unknown as Record<string, unknown>)[prop]));
834
-
835
- if (okA && okB) {
836
- if (valA === valB) {
837
- matching[prop] = valA as string;
838
- } else {
839
- differing[prop] = { a: valA as string, b: valB as string };
840
- }
841
- } else if (okA) {
842
- onlyA.push(prop);
843
- } else if (okB) {
844
- onlyB.push(prop);
845
- }
846
- }
847
-
848
- return {
849
- instancePathA,
850
- instancePathB,
851
- classNameA: instA.ClassName,
852
- classNameB: instB.ClassName,
853
- matching,
854
- differing,
855
- onlyA,
856
- onlyB,
857
- };
858
- }
859
-
860
706
  export = {
861
- getFileTree,
862
- searchFiles,
863
- getPlaceInfo,
864
- getServices,
865
- searchObjects,
866
- getInstanceProperties,
867
- getInstanceChildren,
868
- searchByProperty,
869
- getClassInfo,
870
- getProjectStructure,
871
- grepScripts,
872
- getDescendants,
873
- compareInstances,
707
+ getFileTree,
708
+ searchFiles,
709
+ getPlaceInfo,
710
+ searchObjects,
711
+ getInstanceProperties,
712
+ searchByProperty,
713
+ getClassInfo,
714
+ getProjectStructure,
715
+ grepScripts,
874
716
  };
@@ -1,9 +1,7 @@
1
1
  import Utils from "../Utils";
2
2
  import Recording from "../Recording";
3
3
 
4
- const ScriptEditorService = game.GetService("ScriptEditorService");
5
-
6
- const { getInstancePath, getInstanceByPath, readScriptSource, splitLines, joinLines } = Utils;
4
+ const { getInstancePath, getInstanceByPath, readScriptSource, applyScriptSource, splitLines, joinLines } = Utils;
7
5
  const { beginRecording, finishRecording } = Recording;
8
6
 
9
7
  const SOURCE_TRUNCATE_CHAR_BUDGET = 25000;
@@ -125,42 +123,22 @@ function setScriptSource(requestData: Record<string, unknown>) {
125
123
  const sourceToSet = normalizeEscapes(newSource);
126
124
  const recordingId = beginRecording(`Set script source: ${instance.Name}`);
127
125
 
128
- const [updateSuccess, updateResult] = pcall(() => {
129
- const oldSourceLength = readScriptSource(instance).size();
130
-
131
- ScriptEditorService.UpdateSourceAsync(instance, () => sourceToSet);
132
- if (readScriptSource(instance) !== sourceToSet) {
133
- error("UpdateSourceAsync completed without updating the script source");
134
- }
135
-
136
- return {
137
- success: true, instancePath,
138
- oldSourceLength, newSourceLength: sourceToSet.size(),
139
- method: "UpdateSourceAsync",
140
- message: "Script source updated successfully (editor-safe)",
141
- };
142
- });
143
-
144
- if (updateSuccess) {
145
- finishRecording(recordingId, true);
146
- return updateResult;
126
+ const [readSuccess, readResult] = pcall(() => readScriptSource(instance).size());
127
+ if (!readSuccess) {
128
+ finishRecording(recordingId, false);
129
+ return { error: `Failed to read script source before updating: ${readResult}` };
147
130
  }
131
+ const oldSourceLength = readResult as number;
132
+ const applyResult = applyScriptSource(instance, sourceToSet);
148
133
 
149
- const [directSuccess, directResult] = pcall(() => {
150
- const oldSource = (instance as unknown as { Source: string }).Source;
151
- (instance as unknown as { Source: string }).Source = sourceToSet;
152
-
134
+ if (applyResult.success) {
135
+ finishRecording(recordingId, true);
153
136
  return {
154
137
  success: true, instancePath,
155
- oldSourceLength: oldSource.size(), newSourceLength: sourceToSet.size(),
156
- method: "direct",
157
- message: "Script source updated successfully (direct assignment)",
138
+ oldSourceLength, newSourceLength: sourceToSet.size(),
139
+ method: applyResult.method,
140
+ message: `Script source updated successfully (${applyResult.method === "UpdateSourceAsync" ? "editor-safe" : "direct assignment"})`,
158
141
  };
159
- });
160
-
161
- if (directSuccess) {
162
- finishRecording(recordingId, true);
163
- return directResult;
164
142
  }
165
143
 
166
144
  const [replaceSuccess, replaceResult] = pcall(() => {
@@ -172,9 +150,15 @@ function setScriptSource(requestData: Record<string, unknown>) {
172
150
 
173
151
  const newScript = new Instance(className as keyof CreatableInstances) as LuaSourceContainer;
174
152
  newScript.Name = name;
175
- (newScript as unknown as { Source: string }).Source = sourceToSet;
153
+ // @rbxts/types does not expose PluginSecurity Source writes.
154
+ const writableNewScript = newScript as unknown as { Source: string };
155
+ writableNewScript.Source = sourceToSet;
156
+ if (readScriptSource(newScript) !== sourceToSet) {
157
+ error("Replacement script source did not match the requested source");
158
+ }
176
159
  if (wasBaseScript && enabled !== undefined) {
177
- (newScript as BaseScript).Enabled = enabled;
160
+ const newBaseScript = newScript as BaseScript;
161
+ newBaseScript.Enabled = enabled;
178
162
  }
179
163
 
180
164
  newScript.Parent = parent;
@@ -195,7 +179,7 @@ function setScriptSource(requestData: Record<string, unknown>) {
195
179
 
196
180
  finishRecording(recordingId, false);
197
181
  return {
198
- error: `Failed to set script source. UpdateSourceAsync failed: ${updateResult}. Direct assignment failed: ${directResult}. Replace method failed: ${replaceResult}`,
182
+ error: `Failed to set script source. ${applyResult.error} Replace method failed: ${replaceResult}`,
199
183
  };
200
184
  }
201
185
 
@@ -264,11 +248,13 @@ function editScriptLines(requestData: Record<string, unknown>) {
264
248
  // Byte-slice replacement avoids Lua pattern escaping (safe for multi-byte chars like em dashes).
265
249
  const newSource = string.sub(source, 1, matchStart - 1) + newString + string.sub(source, matchStart + searchLen);
266
250
 
267
- ScriptEditorService.UpdateSourceAsync(instance, () => newSource);
251
+ const applyResult = applyScriptSource(instance, newSource, source);
252
+ if (!applyResult.success) error(applyResult.error);
268
253
 
269
254
  return {
270
255
  success: true,
271
256
  instancePath,
257
+ method: applyResult.method,
272
258
  message: "Script edited successfully",
273
259
  };
274
260
  });
@@ -299,7 +285,8 @@ function insertScriptLines(requestData: Record<string, unknown>) {
299
285
  const recordingId = beginRecording(`Insert script lines after line ${afterLine}: ${instance.Name}`);
300
286
 
301
287
  const [success, result] = pcall(() => {
302
- const [lines, hadTrailingNewline] = splitLines(readScriptSource(instance));
288
+ const source = readScriptSource(instance);
289
+ const [lines, hadTrailingNewline] = splitLines(source);
303
290
  const totalLines = lines.size();
304
291
 
305
292
  if (afterLine < 0 || afterLine > totalLines) error(`afterLine out of range (0-${totalLines})`);
@@ -312,13 +299,15 @@ function insertScriptLines(requestData: Record<string, unknown>) {
312
299
  for (let i = afterLine; i < totalLines; i++) resultLines.push(lines[i]);
313
300
 
314
301
  const newSource = joinLines(resultLines, hadTrailingNewline);
315
- ScriptEditorService.UpdateSourceAsync(instance, () => newSource);
302
+ const applyResult = applyScriptSource(instance, newSource, source);
303
+ if (!applyResult.success) error(applyResult.error);
316
304
 
317
305
  return {
318
306
  success: true, instancePath,
319
307
  insertedAfterLine: afterLine,
320
308
  linesInserted: newLines.size(),
321
309
  newLineCount: resultLines.size(),
310
+ method: applyResult.method,
322
311
  message: "Script lines inserted successfully",
323
312
  };
324
313
  });
@@ -349,7 +338,8 @@ function deleteScriptLines(requestData: Record<string, unknown>) {
349
338
  const recordingId = beginRecording(`Delete script lines ${startLine}-${endLine}: ${instance.Name}`);
350
339
 
351
340
  const [success, result] = pcall(() => {
352
- const [lines, hadTrailingNewline] = splitLines(readScriptSource(instance));
341
+ const source = readScriptSource(instance);
342
+ const [lines, hadTrailingNewline] = splitLines(source);
353
343
  const totalLines = lines.size();
354
344
 
355
345
  if (startLine < 1 || startLine > totalLines) error(`startLine out of range (1-${totalLines})`);
@@ -360,13 +350,15 @@ function deleteScriptLines(requestData: Record<string, unknown>) {
360
350
  for (let i = endLine; i < totalLines; i++) resultLines.push(lines[i]);
361
351
 
362
352
  const newSource = joinLines(resultLines, hadTrailingNewline);
363
- ScriptEditorService.UpdateSourceAsync(instance, () => newSource);
353
+ const applyResult = applyScriptSource(instance, newSource, source);
354
+ if (!applyResult.success) error(applyResult.error);
364
355
 
365
356
  return {
366
357
  success: true, instancePath,
367
358
  deletedLines: { startLine, endLine },
368
359
  linesDeleted: endLine - startLine + 1,
369
360
  newLineCount: resultLines.size(),
361
+ method: applyResult.method,
370
362
  message: "Script lines deleted successfully",
371
363
  };
372
364
  });
@@ -435,6 +427,7 @@ function findAndReplaceInScripts(requestData: Record<string, unknown>) {
435
427
  name: string;
436
428
  className: string;
437
429
  replacements: number;
430
+ error?: string;
438
431
  }
439
432
 
440
433
  const changes: ScriptChange[] = [];
@@ -447,9 +440,9 @@ function findAndReplaceInScripts(requestData: Record<string, unknown>) {
447
440
  function processInstance(instance: Instance) {
448
441
  if (hitLimit) return;
449
442
 
450
- if (instance.IsA("LuaSourceContainer")) {
451
- if (classFilter && !instance.ClassName.lower().find(classFilter.lower())[0]) return;
452
-
443
+ const matchesClass = classFilter === undefined
444
+ || instance.ClassName.lower().find(classFilter.lower())[0] !== undefined;
445
+ if (instance.IsA("LuaSourceContainer") && matchesClass) {
453
446
  scriptsSearched++;
454
447
  const source = readScriptSource(instance);
455
448
 
@@ -475,23 +468,27 @@ function findAndReplaceInScripts(requestData: Record<string, unknown>) {
475
468
  hitLimit = true;
476
469
  return;
477
470
  }
478
- totalReplacements += replCount;
479
471
 
480
- if (!dryRun) {
481
- const [ok] = pcall(() => {
482
- ScriptEditorService.UpdateSourceAsync(instance, () => newSource);
472
+ const applyResult = dryRun
473
+ ? undefined
474
+ : applyScriptSource(instance, newSource, source);
475
+ if (applyResult !== undefined && !applyResult.success) {
476
+ changes.push({
477
+ instancePath: getInstancePath(instance),
478
+ name: instance.Name,
479
+ className: instance.ClassName,
480
+ replacements: 0,
481
+ error: applyResult.error ?? "Script write failed verification",
482
+ });
483
+ } else {
484
+ totalReplacements += replCount;
485
+ changes.push({
486
+ instancePath: getInstancePath(instance),
487
+ name: instance.Name,
488
+ className: instance.ClassName,
489
+ replacements: replCount,
483
490
  });
484
- if (!ok) {
485
- (instance as unknown as { Source: string }).Source = newSource;
486
- }
487
491
  }
488
-
489
- changes.push({
490
- instancePath: getInstancePath(instance),
491
- name: instance.Name,
492
- className: instance.ClassName,
493
- replacements: replCount,
494
- });
495
492
  }
496
493
  }
497
494
 
@@ -501,20 +498,24 @@ function findAndReplaceInScripts(requestData: Record<string, unknown>) {
501
498
  }
502
499
  }
503
500
 
504
- processInstance(startInstance);
501
+ const [traversalSuccess, traversalResult] = pcall(() => processInstance(startInstance));
505
502
 
503
+ const failedScripts = changes.filter((change) => change.error !== undefined).size();
504
+ const scriptsModified = changes.size() - failedScripts;
506
505
  if (recordingId !== undefined) {
507
- finishRecording(recordingId, changes.size() > 0);
506
+ finishRecording(recordingId, scriptsModified > 0);
508
507
  }
509
508
 
510
509
  return {
511
- success: true,
510
+ success: traversalSuccess && failedScripts === 0,
511
+ error: traversalSuccess ? undefined : `Script traversal failed: ${traversalResult}`,
512
512
  dryRun,
513
513
  pattern: searchPattern,
514
514
  replacement,
515
515
  totalReplacements,
516
516
  scriptsSearched,
517
- scriptsModified: changes.size(),
517
+ scriptsModified,
518
+ scriptsFailed: failedScripts,
518
519
  changes,
519
520
  truncated: hitLimit,
520
521
  };