@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,318 @@
1
+ const ScriptEditorService = game.GetService("ScriptEditorService");
2
+
3
+ function safeCall<T>(func: (...args: never[]) => T, ...args: never[]): T | undefined {
4
+ const [success, result] = pcall(func, ...args);
5
+ if (success) {
6
+ return result;
7
+ } else {
8
+ warn(`MCP Plugin Error: ${result}`);
9
+ return undefined;
10
+ }
11
+ }
12
+
13
+ function getInstancePath(instance: Instance): string {
14
+ if (!instance || instance === game) {
15
+ return "game";
16
+ }
17
+
18
+ const pathParts: string[] = [];
19
+ let current: Instance | undefined = instance;
20
+
21
+ while (current && current !== game) {
22
+ pathParts.unshift(current.Name);
23
+ current = current.Parent as Instance | undefined;
24
+ }
25
+
26
+ return `game.${pathParts.join(".")}`;
27
+ }
28
+
29
+ function getInstanceByPath(path: string): Instance | undefined {
30
+ if (path === "game" || path === "") {
31
+ return game;
32
+ }
33
+
34
+ const cleaned = path.gsub("^game%.", "")[0];
35
+ const parts: string[] = [];
36
+ for (const [part] of cleaned.gmatch("[^%.]+")) {
37
+ parts.push(part as string);
38
+ }
39
+
40
+ let current: Instance | undefined = game;
41
+ for (const part of parts) {
42
+ if (!current) return undefined;
43
+ current = current.FindFirstChild(part);
44
+ }
45
+
46
+ return current;
47
+ }
48
+
49
+ function splitLines(source: string): LuaTuple<[string[], boolean]> {
50
+ const normalized = ((source ?? "") as string).gsub("\r\n", "\n")[0].gsub("\r", "\n")[0];
51
+ const endsWithNewline = normalized.sub(-1) === "\n";
52
+
53
+ const lines: string[] = [];
54
+ let start = 1;
55
+
56
+ while (true) {
57
+ const [newlinePos] = string.find(normalized, "\n", start, true);
58
+ if (newlinePos !== undefined) {
59
+ lines.push(string.sub(normalized, start, newlinePos - 1));
60
+ start = newlinePos + 1;
61
+ } else {
62
+ const remainder = string.sub(normalized, start);
63
+ if (remainder !== "" || !endsWithNewline) {
64
+ lines.push(remainder);
65
+ }
66
+ break;
67
+ }
68
+ }
69
+
70
+ if (lines.size() === 0) {
71
+ lines.push("");
72
+ }
73
+
74
+ return [lines, endsWithNewline] as unknown as LuaTuple<[string[], boolean]>;
75
+ }
76
+
77
+ function joinLines(lines: string[], hadTrailingNewline: boolean): string {
78
+ let source = lines.join("\n");
79
+ if (hadTrailingNewline && source.sub(-1) !== "\n") {
80
+ source += "\n";
81
+ }
82
+ return source;
83
+ }
84
+
85
+ function readScriptSource(instance: LuaSourceContainer): string {
86
+ const [ok, result] = pcall(() => {
87
+ const doc = ScriptEditorService.FindScriptDocument(instance);
88
+ if (doc) {
89
+ return doc.GetText();
90
+ }
91
+ return undefined;
92
+ });
93
+ if (ok && result) {
94
+ return result;
95
+ }
96
+ return (instance as unknown as { Source: string }).Source;
97
+ }
98
+
99
+ function convertPropertyValue(instance: Instance, propertyName: string, propertyValue: unknown): unknown {
100
+ if (propertyValue === undefined) return undefined;
101
+
102
+ const inst = instance as unknown as Record<string, unknown>;
103
+
104
+ if (typeIs(propertyValue, "table")) {
105
+ const arr = propertyValue as unknown[];
106
+ const tbl = propertyValue as Record<string, unknown>;
107
+
108
+ if (typeIs(arr, "table") && (arr as defined[]).size() > 0) {
109
+ const len = (arr as defined[]).size();
110
+
111
+ if (len === 3) {
112
+ const prop = propertyName.lower();
113
+ if (
114
+ prop === "position" || prop === "size" || prop === "orientation" ||
115
+ prop === "velocity" || prop === "angularvelocity"
116
+ ) {
117
+ return new Vector3(
118
+ (arr[0] as number) ?? 0,
119
+ (arr[1] as number) ?? 0,
120
+ (arr[2] as number) ?? 0,
121
+ );
122
+ } else if (prop === "color" || prop === "color3") {
123
+ return new Color3(
124
+ (arr[0] as number) ?? 0,
125
+ (arr[1] as number) ?? 0,
126
+ (arr[2] as number) ?? 0,
127
+ );
128
+ } else {
129
+ const [success, currentVal] = pcall(() => inst[propertyName]);
130
+ if (success) {
131
+ if (typeOf(currentVal) === "Vector3") {
132
+ return new Vector3(
133
+ (arr[0] as number) ?? 0,
134
+ (arr[1] as number) ?? 0,
135
+ (arr[2] as number) ?? 0,
136
+ );
137
+ } else if (typeOf(currentVal) === "Color3") {
138
+ return new Color3(
139
+ (arr[0] as number) ?? 0,
140
+ (arr[1] as number) ?? 0,
141
+ (arr[2] as number) ?? 0,
142
+ );
143
+ }
144
+ }
145
+ }
146
+ } else if (len === 2) {
147
+ const [success, currentVal] = pcall(() => inst[propertyName]);
148
+ if (success && typeOf(currentVal) === "Vector2") {
149
+ return new Vector2((arr[0] as number) ?? 0, (arr[1] as number) ?? 0);
150
+ }
151
+ } else if (len === 4) {
152
+ const [success, currentVal] = pcall(() => inst[propertyName]);
153
+ if (success && typeOf(currentVal) === "UDim2") {
154
+ return new UDim2(
155
+ (arr[0] as number) ?? 0,
156
+ (arr[1] as number) ?? 0,
157
+ (arr[2] as number) ?? 0,
158
+ (arr[3] as number) ?? 0,
159
+ );
160
+ }
161
+ }
162
+ }
163
+
164
+ if (tbl.X !== undefined || tbl.Y !== undefined || tbl.Z !== undefined) {
165
+
166
+ if (typeIs(tbl.X, "table") && typeIs(tbl.Y, "table")) {
167
+ const xTbl = tbl.X as unknown as Record<string, number>;
168
+ const yTbl = tbl.Y as unknown as Record<string, number>;
169
+ return new UDim2(
170
+ xTbl.Scale ?? 0, xTbl.Offset ?? 0,
171
+ yTbl.Scale ?? 0, yTbl.Offset ?? 0,
172
+ );
173
+ }
174
+ return new Vector3(
175
+ (tbl.X as number) ?? 0,
176
+ (tbl.Y as number) ?? 0,
177
+ (tbl.Z as number) ?? 0,
178
+ );
179
+ }
180
+
181
+ if (tbl.R !== undefined || tbl.G !== undefined || tbl.B !== undefined) {
182
+ return new Color3(
183
+ (tbl.R as number) ?? 0,
184
+ (tbl.G as number) ?? 0,
185
+ (tbl.B as number) ?? 0,
186
+ );
187
+ }
188
+ }
189
+
190
+ if (typeIs(propertyValue, "string")) {
191
+ const [success, currentVal] = pcall(() => inst[propertyName]);
192
+ if (success && typeOf(currentVal) === "EnumItem") {
193
+ const enumItem = currentVal as EnumItem;
194
+ const enumTypeName = tostring(enumItem.EnumType);
195
+ const [enumSuccess, enumVal] = pcall(() => {
196
+ return (Enum as unknown as Record<string, Record<string, EnumItem>>)[enumTypeName][propertyValue];
197
+ });
198
+ if (enumSuccess && enumVal) return enumVal;
199
+ }
200
+ if (propertyName === "BrickColor") {
201
+ return new BrickColor(propertyValue as unknown as number);
202
+ }
203
+ if (propertyValue === "true") return true;
204
+ if (propertyValue === "false") return false;
205
+ }
206
+
207
+ return propertyValue;
208
+ }
209
+
210
+ function evaluateFormula(
211
+ formula: string,
212
+ variables: Record<string, unknown> | undefined,
213
+ instance: Instance | undefined,
214
+ index: number,
215
+ ): LuaTuple<[number, string | undefined]> {
216
+ let value = formula;
217
+
218
+ value = value.gsub("index", tostring(index))[0];
219
+
220
+ if (instance && instance.IsA("BasePart")) {
221
+ const pos = instance.Position;
222
+ const sz = instance.Size;
223
+ value = value.gsub("Position%.X", tostring(pos.X))[0];
224
+ value = value.gsub("Position%.Y", tostring(pos.Y))[0];
225
+ value = value.gsub("Position%.Z", tostring(pos.Z))[0];
226
+ value = value.gsub("Size%.X", tostring(sz.X))[0];
227
+ value = value.gsub("Size%.Y", tostring(sz.Y))[0];
228
+ value = value.gsub("Size%.Z", tostring(sz.Z))[0];
229
+ value = value.gsub("magnitude", tostring(pos.Magnitude))[0];
230
+ }
231
+
232
+ if (variables) {
233
+ for (const [k, v] of pairs(variables)) {
234
+ value = value.gsub(k as string, tostring(v))[0];
235
+ }
236
+ }
237
+
238
+ value = value.gsub("sin%(([%d%.%-]+)%)", (x: string) => tostring(math.sin(tonumber(x) ?? 0)))[0];
239
+ value = value.gsub("cos%(([%d%.%-]+)%)", (x: string) => tostring(math.cos(tonumber(x) ?? 0)))[0];
240
+ value = value.gsub("sqrt%(([%d%.%-]+)%)", (x: string) => tostring(math.sqrt(tonumber(x) ?? 0)))[0];
241
+ value = value.gsub("abs%(([%d%.%-]+)%)", (x: string) => tostring(math.abs(tonumber(x) ?? 0)))[0];
242
+ value = value.gsub("floor%(([%d%.%-]+)%)", (x: string) => tostring(math.floor(tonumber(x) ?? 0)))[0];
243
+ value = value.gsub("ceil%(([%d%.%-]+)%)", (x: string) => tostring(math.ceil(tonumber(x) ?? 0)))[0];
244
+
245
+ const directResult = tonumber(value);
246
+ if (directResult !== undefined) {
247
+ return [directResult, undefined] as unknown as LuaTuple<[number, string | undefined]>;
248
+ }
249
+
250
+ const [success, evalResult] = pcall(() => {
251
+ const num = tonumber(value);
252
+ if (num !== undefined) return num;
253
+
254
+ {
255
+ const [a, b] = value.match("^([%d%.%-]+)%s*%*%s*([%d%.%-]+)$") as LuaTuple<[string?, string?]>;
256
+ if (a && b) return (tonumber(a) ?? 0) * (tonumber(b) ?? 0);
257
+ }
258
+
259
+ {
260
+ const [a, b] = value.match("^([%d%.%-]+)%s*%+%s*([%d%.%-]+)$") as LuaTuple<[string?, string?]>;
261
+ if (a && b) return (tonumber(a) ?? 0) + (tonumber(b) ?? 0);
262
+ }
263
+
264
+ {
265
+ const [a, b] = value.match("^([%d%.%-]+)%s*%-%s*([%d%.%-]+)$") as LuaTuple<[string?, string?]>;
266
+ if (a && b) return (tonumber(a) ?? 0) - (tonumber(b) ?? 0);
267
+ }
268
+
269
+ {
270
+ const [a, b] = value.match("^([%d%.%-]+)%s*/%s*([%d%.%-]+)$") as LuaTuple<[string?, string?]>;
271
+ if (a && b) {
272
+ const divisor = tonumber(b) ?? 1;
273
+ if (divisor !== 0) return (tonumber(a) ?? 0) / divisor;
274
+ }
275
+ }
276
+
277
+ error(`Unsupported formula pattern: ${value}`);
278
+ });
279
+
280
+ if (success && typeIs(evalResult, "number")) {
281
+ return [evalResult, undefined] as unknown as LuaTuple<[number, string | undefined]>;
282
+ } else {
283
+ return [index, "Complex formulas not supported - using index value"] as unknown as LuaTuple<[number, string | undefined]>;
284
+ }
285
+ }
286
+
287
+ function compareVersions(v1: string, v2: string): number {
288
+ function parseVersion(v: string): number[] {
289
+ const parts: number[] = [];
290
+ for (const [num] of string.gmatch(v, "%d+")) {
291
+ parts.push(tonumber(num) ?? 0);
292
+ }
293
+ return parts;
294
+ }
295
+
296
+ const p1 = parseVersion(v1);
297
+ const p2 = parseVersion(v2);
298
+ const maxLen = math.max(p1.size(), p2.size());
299
+ for (let i = 0; i < maxLen; i++) {
300
+ const n1 = p1[i] ?? 0;
301
+ const n2 = p2[i] ?? 0;
302
+ if (n1 < n2) return -1;
303
+ if (n1 > n2) return 1;
304
+ }
305
+ return 0;
306
+ }
307
+
308
+ export = {
309
+ safeCall,
310
+ getInstancePath,
311
+ getInstanceByPath,
312
+ splitLines,
313
+ joinLines,
314
+ readScriptSource,
315
+ convertPropertyValue,
316
+ evaluateFormula,
317
+ compareVersions,
318
+ };
@@ -0,0 +1,241 @@
1
+ import Utils from "../Utils";
2
+ import Recording from "../Recording";
3
+
4
+ const AssetService = game.GetService("AssetService");
5
+ const ChangeHistoryService = game.GetService("ChangeHistoryService");
6
+ const Selection = game.GetService("Selection");
7
+
8
+ const { getInstancePath, getInstanceByPath } = Utils;
9
+ const { beginRecording, finishRecording } = Recording;
10
+
11
+ function insertAsset(requestData: Record<string, unknown>) {
12
+ const assetId = requestData.assetId as number;
13
+ const parentPath = (requestData.parentPath as string) ?? "game.Workspace";
14
+ const position = requestData.position as { x: number; y: number; z: number } | undefined;
15
+
16
+ if (!assetId) {
17
+ return { error: "assetId is required" };
18
+ }
19
+
20
+ const parentInstance = getInstanceByPath(parentPath);
21
+ if (!parentInstance) {
22
+ return { error: `Parent instance not found: ${parentPath}` };
23
+ }
24
+
25
+ const recordingId = beginRecording(`Insert asset ${assetId}`);
26
+
27
+ let wrapperModel: Instance | undefined;
28
+ const [insertSuccess, insertResult] = pcall(() => {
29
+ const loadedWrapper = (AssetService as unknown as { LoadAssetAsync(id: number): Instance }).LoadAssetAsync(assetId);
30
+ wrapperModel = loadedWrapper;
31
+
32
+ const insertedInstances: Instance[] = [];
33
+ const children = loadedWrapper.GetChildren();
34
+
35
+ for (const child of children) {
36
+ child.Parent = parentInstance;
37
+ insertedInstances.push(child);
38
+ }
39
+
40
+ if (position) {
41
+ const pos = new Vector3(position.x ?? 0, position.y ?? 0, position.z ?? 0);
42
+ for (const inst of insertedInstances) {
43
+ if (inst.IsA("BasePart")) {
44
+ inst.Position = pos;
45
+ } else if (inst.IsA("Model")) {
46
+ if (inst.PrimaryPart) {
47
+ inst.PivotTo(new CFrame(pos));
48
+ } else {
49
+ const firstPart = inst.FindFirstChildWhichIsA("BasePart", true);
50
+ if (firstPart) {
51
+ inst.PivotTo(new CFrame(pos));
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ pcall(() => {
59
+ Selection.Set(insertedInstances);
60
+ });
61
+
62
+ const resultInstances = insertedInstances.map((inst) => ({
63
+ name: inst.Name,
64
+ className: inst.ClassName,
65
+ path: getInstancePath(inst),
66
+ }));
67
+
68
+ return {
69
+ success: true,
70
+ assetId,
71
+ parentPath,
72
+ insertedCount: insertedInstances.size(),
73
+ instances: resultInstances,
74
+ };
75
+ });
76
+
77
+ if (wrapperModel) {
78
+ pcall(() => {
79
+ wrapperModel!.Destroy();
80
+ });
81
+ }
82
+
83
+ finishRecording(recordingId, insertSuccess);
84
+
85
+ if (!insertSuccess) {
86
+ return { error: `Failed to insert asset ${assetId}: ${tostring(insertResult)}` };
87
+ }
88
+
89
+ return insertResult;
90
+ }
91
+
92
+ function previewAsset(requestData: Record<string, unknown>) {
93
+ const assetId = requestData.assetId as number;
94
+ const includeProperties = (requestData.includeProperties as boolean) ?? true;
95
+ const maxDepth = (requestData.maxDepth as number) ?? 10;
96
+
97
+ if (!assetId) {
98
+ return { error: "assetId is required" };
99
+ }
100
+
101
+ const [loadSuccess, wrapperModel] = pcall(() => {
102
+ return (AssetService as unknown as { LoadAssetAsync(id: number): Instance }).LoadAssetAsync(assetId);
103
+ });
104
+
105
+ if (!loadSuccess || !wrapperModel) {
106
+ return { error: `Failed to load asset ${assetId}: ${tostring(wrapperModel)}` };
107
+ }
108
+
109
+ // Stats tracking
110
+ let totalInstances = 0;
111
+ const classCounts: Record<string, number> = {};
112
+ let hasScripts = false;
113
+ let hasAnimations = false;
114
+ let hasSounds = false;
115
+ let hasParticles = false;
116
+
117
+ function buildHierarchy(instance: Instance, depth: number): Record<string, unknown> {
118
+ totalInstances++;
119
+
120
+ const className = instance.ClassName;
121
+ classCounts[className] = (classCounts[className] ?? 0) + 1;
122
+
123
+ if (instance.IsA("LuaSourceContainer")) hasScripts = true;
124
+ if (className === "Animation" || className === "AnimationController" || className === "Animator") hasAnimations = true;
125
+ if (instance.IsA("Sound")) hasSounds = true;
126
+ if (className === "ParticleEmitter" || className === "Fire" || className === "Smoke" || className === "Sparkles") hasParticles = true;
127
+
128
+ const node: Record<string, unknown> = {
129
+ name: instance.Name,
130
+ className,
131
+ };
132
+
133
+ if (includeProperties) {
134
+ const props: Record<string, unknown> = {};
135
+
136
+ if (instance.IsA("BasePart")) {
137
+ props.size = { x: instance.Size.X, y: instance.Size.Y, z: instance.Size.Z };
138
+ props.position = { x: instance.Position.X, y: instance.Position.Y, z: instance.Position.Z };
139
+ props.material = tostring(instance.Material);
140
+ props.color = `${instance.Color.R}, ${instance.Color.G}, ${instance.Color.B}`;
141
+ props.transparency = instance.Transparency;
142
+ props.anchored = instance.Anchored;
143
+ }
144
+
145
+ if (instance.IsA("MeshPart")) {
146
+ const meshPart = instance as MeshPart;
147
+ props.meshId = meshPart.MeshId;
148
+ props.textureId = meshPart.TextureID;
149
+ }
150
+
151
+ if (instance.IsA("Model")) {
152
+ const model = instance as Model;
153
+ if (model.PrimaryPart) {
154
+ props.primaryPart = model.PrimaryPart.Name;
155
+ }
156
+ }
157
+
158
+ if (instance.IsA("LuaSourceContainer")) {
159
+ const [ok, src] = pcall(() => (instance as unknown as { Source: string }).Source);
160
+ if (ok && src) {
161
+ const preview = string.sub(src, 1, 200);
162
+ props.sourcePreview = preview;
163
+ props.sourceLength = src.size();
164
+ }
165
+ }
166
+
167
+ if (className === "Decal" || className === "Texture") {
168
+ const [ok, texId] = pcall(() => (instance as unknown as { Texture: string }).Texture);
169
+ if (ok) props.texture = texId;
170
+ }
171
+
172
+ if (instance.IsA("Sound")) {
173
+ props.soundId = (instance as Sound).SoundId;
174
+ }
175
+
176
+ // Only include props if there are any
177
+ let hasProps = false;
178
+ for (const _ of pairs(props)) {
179
+ hasProps = true;
180
+ break;
181
+ }
182
+ if (hasProps) {
183
+ node.properties = props;
184
+ }
185
+ }
186
+
187
+ if (depth < maxDepth) {
188
+ const childNodes: Record<string, unknown>[] = [];
189
+ for (const child of instance.GetChildren()) {
190
+ childNodes.push(buildHierarchy(child, depth + 1));
191
+ }
192
+ if (childNodes.size() > 0) {
193
+ node.children = childNodes;
194
+ }
195
+ } else {
196
+ const childCount = instance.GetChildren().size();
197
+ if (childCount > 0) {
198
+ node.childCount = childCount;
199
+ node.truncated = true;
200
+ }
201
+ }
202
+
203
+ return node;
204
+ }
205
+
206
+ const [previewSuccess, previewResult] = pcall(() => {
207
+ const hierarchyRoots: Record<string, unknown>[] = [];
208
+ for (const child of (wrapperModel as Instance).GetChildren()) {
209
+ hierarchyRoots.push(buildHierarchy(child, 0));
210
+ }
211
+
212
+ return {
213
+ success: true,
214
+ assetId,
215
+ hierarchy: hierarchyRoots,
216
+ summary: {
217
+ totalInstances,
218
+ classCounts,
219
+ hasScripts,
220
+ hasAnimations,
221
+ hasSounds,
222
+ hasParticles,
223
+ },
224
+ };
225
+ });
226
+
227
+ pcall(() => {
228
+ (wrapperModel as Instance).Destroy();
229
+ });
230
+
231
+ if (!previewSuccess) {
232
+ return { error: `Failed to preview asset ${assetId}: ${tostring(previewResult)}` };
233
+ }
234
+
235
+ return previewResult;
236
+ }
237
+
238
+ export = {
239
+ insertAsset,
240
+ previewAsset,
241
+ };