@chrrxs/robloxstudio-mcp-inspector 2.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 (30) hide show
  1. package/dist/index.js +4483 -0
  2. package/package.json +50 -0
  3. package/studio-plugin/INSTALLATION.md +150 -0
  4. package/studio-plugin/MCPInspectorPlugin.rbxmx +9074 -0
  5. package/studio-plugin/MCPPlugin.rbxmx +9074 -0
  6. package/studio-plugin/default.project.json +19 -0
  7. package/studio-plugin/dev.project.json +23 -0
  8. package/studio-plugin/inspector-icon.png +0 -0
  9. package/studio-plugin/package-lock.json +706 -0
  10. package/studio-plugin/package.json +19 -0
  11. package/studio-plugin/plugin.json +10 -0
  12. package/studio-plugin/src/modules/ClientBroker.ts +221 -0
  13. package/studio-plugin/src/modules/Communication.ts +399 -0
  14. package/studio-plugin/src/modules/Recording.ts +28 -0
  15. package/studio-plugin/src/modules/State.ts +94 -0
  16. package/studio-plugin/src/modules/UI.ts +725 -0
  17. package/studio-plugin/src/modules/Utils.ts +318 -0
  18. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +241 -0
  19. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +481 -0
  20. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +128 -0
  21. package/studio-plugin/src/modules/handlers/InputHandlers.ts +102 -0
  22. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +380 -0
  23. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +391 -0
  24. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +191 -0
  25. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +827 -0
  26. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +530 -0
  27. package/studio-plugin/src/modules/handlers/TestHandlers.ts +277 -0
  28. package/studio-plugin/src/server/index.server.ts +63 -0
  29. package/studio-plugin/src/types/index.d.ts +44 -0
  30. package/studio-plugin/tsconfig.json +20 -0
@@ -0,0 +1,380 @@
1
+ import Utils from "../Utils";
2
+ import Recording from "../Recording";
3
+
4
+ const { getInstancePath, getInstanceByPath, convertPropertyValue } = Utils;
5
+ const { beginRecording, finishRecording } = Recording;
6
+
7
+ type ProcessedCreateResult =
8
+ | {
9
+ instance: Instance;
10
+ className: string;
11
+ parentPath: string;
12
+ }
13
+ | {
14
+ error: string;
15
+ className?: string;
16
+ parentPath?: string;
17
+ };
18
+
19
+ type ProcessedBatchResult = {
20
+ results: Record<string, unknown>[];
21
+ successCount: number;
22
+ failureCount: number;
23
+ };
24
+
25
+ function processObjectEntries(
26
+ objects: Record<string, unknown>[],
27
+ createFn: (objData: Record<string, unknown>) => ProcessedCreateResult,
28
+ ): ProcessedBatchResult {
29
+ const results: Record<string, unknown>[] = [];
30
+ let successCount = 0;
31
+ let failureCount = 0;
32
+
33
+ const [loopSuccess, loopError] = pcall(() => {
34
+ for (const entry of objects) {
35
+ if (!typeIs(entry, "table")) {
36
+ failureCount++;
37
+ results.push({ success: false, error: "Each object entry must be a table" });
38
+ continue;
39
+ }
40
+
41
+ const objData = entry as Record<string, unknown>;
42
+ const className = objData.className as string;
43
+ const parentPath = objData.parent as string;
44
+
45
+ if (!className || !parentPath) {
46
+ failureCount++;
47
+ results.push({ success: false, error: "Class name and parent are required" });
48
+ continue;
49
+ }
50
+
51
+ const [entrySuccess, entryResult] = pcall(() => createFn(objData));
52
+ if (!entrySuccess) {
53
+ failureCount++;
54
+ results.push({ success: false, className, parent: parentPath, error: tostring(entryResult) });
55
+ continue;
56
+ }
57
+
58
+ if ("instance" in entryResult) {
59
+ successCount++;
60
+ results.push({
61
+ success: true,
62
+ className: entryResult.className,
63
+ parent: entryResult.parentPath,
64
+ instancePath: getInstancePath(entryResult.instance),
65
+ name: entryResult.instance.Name,
66
+ });
67
+ } else {
68
+ failureCount++;
69
+ results.push({
70
+ success: false,
71
+ className: entryResult.className ?? className,
72
+ parent: entryResult.parentPath ?? parentPath,
73
+ error: entryResult.error,
74
+ });
75
+ }
76
+ }
77
+ });
78
+
79
+ if (!loopSuccess) {
80
+ failureCount++;
81
+ results.push({ success: false, error: `Unexpected mass create failure: ${tostring(loopError)}` });
82
+ }
83
+
84
+ return { results, successCount, failureCount };
85
+ }
86
+
87
+ function createObject(requestData: Record<string, unknown>) {
88
+ const className = requestData.className as string;
89
+ const parentPath = requestData.parent as string;
90
+ const name = requestData.name as string | undefined;
91
+ const properties = (requestData.properties as Record<string, unknown>) ?? {};
92
+
93
+ if (!className || !parentPath) {
94
+ return { error: "Class name and parent are required" };
95
+ }
96
+
97
+ const parentInstance = getInstanceByPath(parentPath);
98
+ if (!parentInstance) return { error: `Parent instance not found: ${parentPath}` };
99
+ const recordingId = beginRecording(`Create ${className}`);
100
+
101
+ const [success, newInstance] = pcall(() => {
102
+ const instance = new Instance(className as keyof CreatableInstances);
103
+ if (name) instance.Name = name;
104
+
105
+ for (const [propertyName, propertyValue] of pairs(properties)) {
106
+ pcall(() => {
107
+ const converted = convertPropertyValue(instance, propertyName as string, propertyValue);
108
+ (instance as unknown as { [key: string]: unknown })[propertyName as string] =
109
+ converted !== undefined ? converted : propertyValue;
110
+ });
111
+ }
112
+
113
+ instance.Parent = parentInstance;
114
+ return instance;
115
+ });
116
+
117
+ if (success && newInstance) {
118
+ finishRecording(recordingId, true);
119
+ return {
120
+ success: true,
121
+ className,
122
+ parent: parentPath,
123
+ instancePath: getInstancePath(newInstance as Instance),
124
+ name: (newInstance as Instance).Name,
125
+ message: "Object created successfully",
126
+ };
127
+ } else {
128
+ finishRecording(recordingId, false);
129
+ return { error: `Failed to create object: ${newInstance}`, className, parent: parentPath };
130
+ }
131
+ }
132
+
133
+ function deleteObject(requestData: Record<string, unknown>) {
134
+ const instancePath = requestData.instancePath as string;
135
+ if (!instancePath) return { error: "Instance path is required" };
136
+
137
+ const instance = getInstanceByPath(instancePath);
138
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
139
+ if (instance === game) return { error: "Cannot delete the game instance" };
140
+ const recordingId = beginRecording(`Delete ${instance.ClassName} (${instance.Name})`);
141
+
142
+ const [success, result] = pcall(() => {
143
+ instance.Destroy();
144
+ return true;
145
+ });
146
+
147
+ if (success) {
148
+ finishRecording(recordingId, true);
149
+ return { success: true, instancePath, message: "Object deleted successfully" };
150
+ } else {
151
+ finishRecording(recordingId, false);
152
+ return { error: `Failed to delete object: ${result}`, instancePath };
153
+ }
154
+ }
155
+
156
+ function massCreateObjects(requestData: Record<string, unknown>) {
157
+ const objects = requestData.objects as Record<string, unknown>[];
158
+ if (!objects || !typeIs(objects, "table") || (objects as defined[]).size() === 0) {
159
+ return { error: "Objects array is required" };
160
+ }
161
+
162
+ const recordingId = beginRecording("Mass create objects");
163
+
164
+ const { results, successCount, failureCount } = processObjectEntries(objects, (objData) => {
165
+ const className = objData.className as string;
166
+ const parentPath = objData.parent as string;
167
+ const name = objData.name as string | undefined;
168
+ const properties = (objData.properties as Record<string, unknown>) ?? {};
169
+ const parentInstance = getInstanceByPath(parentPath);
170
+ if (!parentInstance) {
171
+ return { error: "Parent instance not found", className, parentPath };
172
+ }
173
+
174
+ const [success, newInstance] = pcall(() => {
175
+ const instance = new Instance(className as keyof CreatableInstances);
176
+ if (name) instance.Name = name;
177
+
178
+ for (const [propertyName, propertyValue] of pairs(properties)) {
179
+ pcall(() => {
180
+ const converted = convertPropertyValue(instance, propertyName as string, propertyValue);
181
+ (instance as unknown as { [key: string]: unknown })[propertyName as string] =
182
+ converted !== undefined ? converted : propertyValue;
183
+ });
184
+ }
185
+
186
+ instance.Parent = parentInstance;
187
+ return instance;
188
+ });
189
+
190
+ if (!success || !newInstance) {
191
+ return { error: tostring(newInstance), className, parentPath };
192
+ }
193
+
194
+ return { instance: newInstance as Instance, className, parentPath };
195
+ });
196
+
197
+ finishRecording(recordingId, successCount > 0);
198
+ return { results, summary: { total: (objects as defined[]).size(), succeeded: successCount, failed: failureCount } };
199
+ }
200
+
201
+
202
+
203
+ function performSmartDuplicate(requestData: Record<string, unknown>, useRecording = true) {
204
+ const instancePath = requestData.instancePath as string;
205
+ const count = requestData.count as number;
206
+ const options = (requestData.options as Record<string, unknown>) ?? {};
207
+
208
+ if (!instancePath || !count || count < 1) {
209
+ return { error: "Instance path and count > 0 are required" };
210
+ }
211
+
212
+ const instance = getInstanceByPath(instancePath);
213
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
214
+ const recordingId = useRecording ? beginRecording(`Smart duplicate ${instance.Name}`) : undefined;
215
+
216
+ const results: Record<string, unknown>[] = [];
217
+ let successCount = 0;
218
+ let failureCount = 0;
219
+
220
+ for (let i = 1; i <= count; i++) {
221
+ const [success, newInstance] = pcall(() => {
222
+ const clone = instance.Clone();
223
+
224
+ if (options.namePattern) {
225
+ clone.Name = (options.namePattern as string).gsub("{n}", tostring(i))[0];
226
+ } else {
227
+ clone.Name = instance.Name + i;
228
+ }
229
+
230
+ if (options.positionOffset && clone.IsA("BasePart")) {
231
+ const offset = options.positionOffset as number[];
232
+ const currentPos = clone.Position;
233
+ clone.Position = new Vector3(
234
+ currentPos.X + ((offset[0] ?? 0) as number) * i,
235
+ currentPos.Y + ((offset[1] ?? 0) as number) * i,
236
+ currentPos.Z + ((offset[2] ?? 0) as number) * i,
237
+ );
238
+ }
239
+
240
+ if (options.rotationOffset && clone.IsA("BasePart")) {
241
+ const offset = options.rotationOffset as number[];
242
+ clone.CFrame = clone.CFrame.mul(CFrame.Angles(
243
+ math.rad(((offset[0] ?? 0) as number) * i),
244
+ math.rad(((offset[1] ?? 0) as number) * i),
245
+ math.rad(((offset[2] ?? 0) as number) * i),
246
+ ));
247
+ }
248
+
249
+ if (options.scaleOffset && clone.IsA("BasePart")) {
250
+ const offset = options.scaleOffset as number[];
251
+ const currentSize = clone.Size;
252
+ clone.Size = new Vector3(
253
+ currentSize.X * (((offset[0] ?? 1) as number) ** i),
254
+ currentSize.Y * (((offset[1] ?? 1) as number) ** i),
255
+ currentSize.Z * (((offset[2] ?? 1) as number) ** i),
256
+ );
257
+ }
258
+
259
+ if (options.propertyVariations) {
260
+ for (const [propName, values] of pairs(options.propertyVariations as Record<string, unknown[]>)) {
261
+ if (values && (values as defined[]).size() > 0) {
262
+ const valueIndex = ((i - 1) % (values as defined[]).size());
263
+ pcall(() => {
264
+ (clone as unknown as { [key: string]: unknown })[propName as string] = (values as unknown[])[valueIndex];
265
+ });
266
+ }
267
+ }
268
+ }
269
+
270
+ const targetParents = options.targetParents as string[] | undefined;
271
+ if (targetParents && targetParents[i - 1]) {
272
+ const targetParent = getInstanceByPath(targetParents[i - 1]);
273
+ clone.Parent = targetParent ?? instance.Parent;
274
+ } else {
275
+ clone.Parent = instance.Parent;
276
+ }
277
+
278
+ return clone;
279
+ });
280
+
281
+ if (success && newInstance) {
282
+ successCount++;
283
+ results.push({
284
+ success: true,
285
+ instancePath: getInstancePath(newInstance as Instance),
286
+ name: (newInstance as Instance).Name,
287
+ index: i,
288
+ });
289
+ } else {
290
+ failureCount++;
291
+ results.push({ success: false, index: i, error: tostring(newInstance) });
292
+ }
293
+ }
294
+
295
+ finishRecording(recordingId, successCount > 0);
296
+
297
+ return {
298
+ results,
299
+ summary: { total: count, succeeded: successCount, failed: failureCount },
300
+ sourceInstance: instancePath,
301
+ };
302
+ }
303
+
304
+ function smartDuplicate(requestData: Record<string, unknown>) {
305
+ return performSmartDuplicate(requestData, true);
306
+ }
307
+
308
+ function massDuplicate(requestData: Record<string, unknown>) {
309
+ const duplications = requestData.duplications as Record<string, unknown>[];
310
+ if (!duplications || !typeIs(duplications, "table") || (duplications as defined[]).size() === 0) {
311
+ return { error: "Duplications array is required" };
312
+ }
313
+
314
+ const allResults: Record<string, unknown>[] = [];
315
+ let totalSuccess = 0;
316
+ let totalFailures = 0;
317
+ const recordingId = beginRecording("Mass duplicate operations");
318
+
319
+ for (const duplication of duplications) {
320
+ const result = performSmartDuplicate(duplication, false) as { summary?: { succeeded: number; failed: number } };
321
+ allResults.push(result as unknown as Record<string, unknown>);
322
+ if (result.summary) {
323
+ totalSuccess += result.summary.succeeded;
324
+ totalFailures += result.summary.failed;
325
+ }
326
+ }
327
+
328
+ finishRecording(recordingId, totalSuccess > 0);
329
+
330
+ return {
331
+ results: allResults,
332
+ summary: { total: totalSuccess + totalFailures, succeeded: totalSuccess, failed: totalFailures },
333
+ };
334
+ }
335
+
336
+ function cloneObject(requestData: Record<string, unknown>) {
337
+ const instancePath = requestData.instancePath as string;
338
+ const targetParentPath = requestData.targetParentPath as string;
339
+
340
+ if (!instancePath || !targetParentPath) {
341
+ return { error: "Instance path and target parent path are required" };
342
+ }
343
+
344
+ const instance = getInstanceByPath(instancePath);
345
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
346
+
347
+ const targetParent = getInstanceByPath(targetParentPath);
348
+ if (!targetParent) return { error: `Target parent not found: ${targetParentPath}` };
349
+
350
+ const recordingId = beginRecording(`Clone ${instance.Name}`);
351
+
352
+ const [success, clone] = pcall(() => {
353
+ const cloned = instance.Clone();
354
+ cloned.Parent = targetParent;
355
+ return cloned;
356
+ });
357
+
358
+ if (success && clone) {
359
+ finishRecording(recordingId, true);
360
+ return {
361
+ success: true,
362
+ instancePath: getInstancePath(clone as Instance),
363
+ name: (clone as Instance).Name,
364
+ className: (clone as Instance).ClassName,
365
+ parent: targetParentPath,
366
+ message: "Object cloned successfully",
367
+ };
368
+ }
369
+ finishRecording(recordingId, false);
370
+ return { error: `Failed to clone object: ${clone}` };
371
+ }
372
+
373
+ export = {
374
+ createObject,
375
+ deleteObject,
376
+ massCreateObjects,
377
+ smartDuplicate,
378
+ massDuplicate,
379
+ cloneObject,
380
+ };