robloxstudio-mcp 2.3.0 → 2.4.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 (49) hide show
  1. package/dist/index.js +3507 -1061
  2. package/package.json +6 -29
  3. package/studio-plugin/MCPPlugin.rbxmx +1 -1
  4. package/studio-plugin/src/modules/Communication.ts +13 -0
  5. package/studio-plugin/src/modules/Recording.ts +28 -0
  6. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +230 -0
  7. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +476 -0
  8. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +22 -16
  9. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +54 -8
  10. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +11 -12
  11. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +144 -1
  12. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +38 -13
  13. package/LICENSE +0 -21
  14. package/README.md +0 -71
  15. package/dist/__tests__/bridge-service.test.d.ts +0 -2
  16. package/dist/__tests__/bridge-service.test.d.ts.map +0 -1
  17. package/dist/__tests__/bridge-service.test.js +0 -95
  18. package/dist/__tests__/bridge-service.test.js.map +0 -1
  19. package/dist/__tests__/http-server.test.d.ts +0 -2
  20. package/dist/__tests__/http-server.test.d.ts.map +0 -1
  21. package/dist/__tests__/http-server.test.js +0 -176
  22. package/dist/__tests__/http-server.test.js.map +0 -1
  23. package/dist/__tests__/integration.test.d.ts +0 -2
  24. package/dist/__tests__/integration.test.d.ts.map +0 -1
  25. package/dist/__tests__/integration.test.js +0 -156
  26. package/dist/__tests__/integration.test.js.map +0 -1
  27. package/dist/__tests__/smoke.test.d.ts +0 -2
  28. package/dist/__tests__/smoke.test.d.ts.map +0 -1
  29. package/dist/__tests__/smoke.test.js +0 -53
  30. package/dist/__tests__/smoke.test.js.map +0 -1
  31. package/dist/bridge-service.d.ts +0 -17
  32. package/dist/bridge-service.d.ts.map +0 -1
  33. package/dist/bridge-service.js +0 -78
  34. package/dist/bridge-service.js.map +0 -1
  35. package/dist/http-server.d.ts +0 -4
  36. package/dist/http-server.d.ts.map +0 -1
  37. package/dist/http-server.js +0 -478
  38. package/dist/http-server.js.map +0 -1
  39. package/dist/index.d.ts +0 -3
  40. package/dist/index.d.ts.map +0 -1
  41. package/dist/index.js.map +0 -1
  42. package/dist/tools/index.d.ts +0 -273
  43. package/dist/tools/index.d.ts.map +0 -1
  44. package/dist/tools/index.js +0 -587
  45. package/dist/tools/index.js.map +0 -1
  46. package/dist/tools/studio-client.d.ts +0 -7
  47. package/dist/tools/studio-client.d.ts.map +0 -1
  48. package/dist/tools/studio-client.js +0 -19
  49. package/dist/tools/studio-client.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robloxstudio-mcp",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "MCP Server for Roblox Studio Integration - Access Studio data, scripts, and objects through AI tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -9,21 +9,12 @@
9
9
  },
10
10
  "files": [
11
11
  "dist/**/*",
12
- "studio-plugin/**/*",
13
- "README.md"
12
+ "studio-plugin/**/*"
14
13
  ],
15
14
  "scripts": {
16
- "build": "tsc",
17
- "build:plugin": "node scripts/build-plugin.mjs",
18
- "build:all": "npm run build && npm run build:plugin",
19
- "dev": "tsx src/index.ts",
20
- "start": "node dist/index.js",
21
- "lint": "eslint src/**/*.ts",
22
- "typecheck": "tsc --noEmit",
23
- "test": "jest",
24
- "test:watch": "jest --watch",
25
- "test:coverage": "jest --coverage",
26
- "prepublishOnly": "npm run build:all"
15
+ "build": "tsup",
16
+ "prepack": "node ../../scripts/prepack.mjs",
17
+ "postpack": "node ../../scripts/postpack.mjs"
27
18
  },
28
19
  "keywords": [
29
20
  "mcp",
@@ -52,20 +43,6 @@
52
43
  "ws": "^8.14.2"
53
44
  },
54
45
  "devDependencies": {
55
- "@types/cors": "^2.8.17",
56
- "@types/express": "^4.17.21",
57
- "@types/jest": "^29.5.11",
58
- "@types/node": "^20.10.0",
59
- "@types/supertest": "^6.0.2",
60
- "@types/uuid": "^9.0.7",
61
- "@types/ws": "^8.5.10",
62
- "@typescript-eslint/eslint-plugin": "^7.0.0",
63
- "@typescript-eslint/parser": "^7.0.0",
64
- "eslint": "^8.57.0",
65
- "jest": "^29.7.0",
66
- "supertest": "^6.3.3",
67
- "ts-jest": "^29.1.1",
68
- "tsx": "^4.6.0",
69
- "typescript": "^5.3.2"
46
+ "@robloxstudio-mcp/core": "*"
70
47
  }
71
48
  }
@@ -3173,7 +3173,7 @@ return {
3173
3173
  <Properties>
3174
3174
  <string name="Name">State</string>
3175
3175
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
3176
- local CURRENT_VERSION = "2.3.0"
3176
+ local CURRENT_VERSION = "2.4.0"
3177
3177
  local MAX_CONNECTIONS = 5
3178
3178
  local BASE_PORT = 58741
3179
3179
  local activeTabIndex = 0
@@ -8,6 +8,8 @@ import InstanceHandlers from "./handlers/InstanceHandlers";
8
8
  import ScriptHandlers from "./handlers/ScriptHandlers";
9
9
  import MetadataHandlers from "./handlers/MetadataHandlers";
10
10
  import TestHandlers from "./handlers/TestHandlers";
11
+ import BuildHandlers from "./handlers/BuildHandlers";
12
+ import AssetHandlers from "./handlers/AssetHandlers";
11
13
  import { Connection, RequestPayload, PollResponse } from "../types";
12
14
 
13
15
  type Handler = (data: Record<string, unknown>) => unknown;
@@ -24,6 +26,7 @@ const routeMap: Record<string, Handler> = {
24
26
  "/api/search-by-property": QueryHandlers.searchByProperty,
25
27
  "/api/class-info": QueryHandlers.getClassInfo,
26
28
  "/api/project-structure": QueryHandlers.getProjectStructure,
29
+ "/api/grep-scripts": QueryHandlers.grepScripts,
27
30
 
28
31
  "/api/set-property": PropertyHandlers.setProperty,
29
32
  "/api/mass-set-property": PropertyHandlers.massSetProperty,
@@ -54,10 +57,20 @@ const routeMap: Record<string, Handler> = {
54
57
  "/api/get-tagged": MetadataHandlers.getTagged,
55
58
  "/api/get-selection": MetadataHandlers.getSelection,
56
59
  "/api/execute-luau": MetadataHandlers.executeLuau,
60
+ "/api/undo": MetadataHandlers.undo,
61
+ "/api/redo": MetadataHandlers.redo,
57
62
 
58
63
  "/api/start-playtest": TestHandlers.startPlaytest,
59
64
  "/api/stop-playtest": TestHandlers.stopPlaytest,
60
65
  "/api/get-playtest-output": TestHandlers.getPlaytestOutput,
66
+
67
+ "/api/export-build": BuildHandlers.exportBuild,
68
+ "/api/import-build": BuildHandlers.importBuild,
69
+ "/api/import-scene": BuildHandlers.importScene,
70
+ "/api/search-materials": BuildHandlers.searchMaterials,
71
+
72
+ "/api/insert-asset": AssetHandlers.insertAsset,
73
+ "/api/preview-asset": AssetHandlers.previewAsset,
61
74
  };
62
75
 
63
76
  function processRequest(request: RequestPayload): unknown {
@@ -0,0 +1,28 @@
1
+ const ChangeHistoryService = game.GetService("ChangeHistoryService");
2
+
3
+ type RecordingId = string | undefined;
4
+
5
+ function beginRecording(actionName: string): RecordingId {
6
+ const [success, result] = pcall(() => ChangeHistoryService.TryBeginRecording(`MCP: ${actionName}`));
7
+ if (success) {
8
+ return result as RecordingId;
9
+ }
10
+ return undefined;
11
+ }
12
+
13
+ function finishRecording(recordingId: RecordingId, shouldCommit: boolean) {
14
+ if (recordingId === undefined) return;
15
+
16
+ const operation = shouldCommit
17
+ ? Enum.FinishRecordingOperation.Commit
18
+ : Enum.FinishRecordingOperation.Cancel;
19
+
20
+ pcall(() => {
21
+ ChangeHistoryService.FinishRecording(recordingId, operation);
22
+ });
23
+ }
24
+
25
+ export = {
26
+ beginRecording,
27
+ finishRecording,
28
+ };
@@ -0,0 +1,230 @@
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
+ const [loadSuccess, wrapperModel] = pcall(() => {
28
+ return (AssetService as unknown as { LoadAssetAsync(id: number): Instance }).LoadAssetAsync(assetId);
29
+ });
30
+
31
+ if (!loadSuccess || !wrapperModel) {
32
+ finishRecording(recordingId, false);
33
+ return { error: `Failed to load asset ${assetId}: ${tostring(wrapperModel)}` };
34
+ }
35
+
36
+ const insertedInstances: Instance[] = [];
37
+ const children = (wrapperModel as Instance).GetChildren();
38
+
39
+ for (const child of children) {
40
+ child.Parent = parentInstance;
41
+ insertedInstances.push(child);
42
+ }
43
+
44
+ // Apply position if provided
45
+ if (position) {
46
+ const pos = new Vector3(position.x ?? 0, position.y ?? 0, position.z ?? 0);
47
+ for (const inst of insertedInstances) {
48
+ if (inst.IsA("BasePart")) {
49
+ inst.Position = pos;
50
+ } else if (inst.IsA("Model")) {
51
+ if (inst.PrimaryPart) {
52
+ inst.PivotTo(new CFrame(pos));
53
+ } else {
54
+ // Find first BasePart descendant to pivot around
55
+ const firstPart = inst.FindFirstChildWhichIsA("BasePart", true);
56
+ if (firstPart) {
57
+ inst.PivotTo(new CFrame(pos));
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ // Destroy the wrapper model
65
+ (wrapperModel as Instance).Destroy();
66
+
67
+ // Set undo waypoint
68
+ finishRecording(recordingId, true);
69
+
70
+ // Select inserted instances
71
+ pcall(() => {
72
+ Selection.Set(insertedInstances);
73
+ });
74
+
75
+ const resultInstances = insertedInstances.map((inst) => ({
76
+ name: inst.Name,
77
+ className: inst.ClassName,
78
+ path: getInstancePath(inst),
79
+ }));
80
+
81
+ return {
82
+ success: true,
83
+ assetId,
84
+ parentPath,
85
+ insertedCount: insertedInstances.size(),
86
+ instances: resultInstances,
87
+ };
88
+ }
89
+
90
+ function previewAsset(requestData: Record<string, unknown>) {
91
+ const assetId = requestData.assetId as number;
92
+ const includeProperties = (requestData.includeProperties as boolean) ?? true;
93
+ const maxDepth = (requestData.maxDepth as number) ?? 10;
94
+
95
+ if (!assetId) {
96
+ return { error: "assetId is required" };
97
+ }
98
+
99
+ const [loadSuccess, wrapperModel] = pcall(() => {
100
+ return (AssetService as unknown as { LoadAssetAsync(id: number): Instance }).LoadAssetAsync(assetId);
101
+ });
102
+
103
+ if (!loadSuccess || !wrapperModel) {
104
+ return { error: `Failed to load asset ${assetId}: ${tostring(wrapperModel)}` };
105
+ }
106
+
107
+ // Stats tracking
108
+ let totalInstances = 0;
109
+ const classCounts: Record<string, number> = {};
110
+ let hasScripts = false;
111
+ let hasAnimations = false;
112
+ let hasSounds = false;
113
+ let hasParticles = false;
114
+
115
+ function buildHierarchy(instance: Instance, depth: number): Record<string, unknown> {
116
+ totalInstances++;
117
+
118
+ const className = instance.ClassName;
119
+ classCounts[className] = (classCounts[className] ?? 0) + 1;
120
+
121
+ if (instance.IsA("LuaSourceContainer")) hasScripts = true;
122
+ if (className === "Animation" || className === "AnimationController" || className === "Animator") hasAnimations = true;
123
+ if (instance.IsA("Sound")) hasSounds = true;
124
+ if (className === "ParticleEmitter" || className === "Fire" || className === "Smoke" || className === "Sparkles") hasParticles = true;
125
+
126
+ const node: Record<string, unknown> = {
127
+ name: instance.Name,
128
+ className,
129
+ };
130
+
131
+ if (includeProperties) {
132
+ const props: Record<string, unknown> = {};
133
+
134
+ if (instance.IsA("BasePart")) {
135
+ props.size = { x: instance.Size.X, y: instance.Size.Y, z: instance.Size.Z };
136
+ props.position = { x: instance.Position.X, y: instance.Position.Y, z: instance.Position.Z };
137
+ props.material = tostring(instance.Material);
138
+ props.color = `${instance.Color.R}, ${instance.Color.G}, ${instance.Color.B}`;
139
+ props.transparency = instance.Transparency;
140
+ props.anchored = instance.Anchored;
141
+ }
142
+
143
+ if (instance.IsA("MeshPart")) {
144
+ const meshPart = instance as MeshPart;
145
+ props.meshId = meshPart.MeshId;
146
+ props.textureId = meshPart.TextureID;
147
+ }
148
+
149
+ if (instance.IsA("Model")) {
150
+ const model = instance as Model;
151
+ if (model.PrimaryPart) {
152
+ props.primaryPart = model.PrimaryPart.Name;
153
+ }
154
+ }
155
+
156
+ if (instance.IsA("LuaSourceContainer")) {
157
+ const [ok, src] = pcall(() => (instance as unknown as { Source: string }).Source);
158
+ if (ok && src) {
159
+ const preview = string.sub(src, 1, 200);
160
+ props.sourcePreview = preview;
161
+ props.sourceLength = src.size();
162
+ }
163
+ }
164
+
165
+ if (className === "Decal" || className === "Texture") {
166
+ const [ok, texId] = pcall(() => (instance as unknown as { Texture: string }).Texture);
167
+ if (ok) props.texture = texId;
168
+ }
169
+
170
+ if (instance.IsA("Sound")) {
171
+ props.soundId = (instance as Sound).SoundId;
172
+ }
173
+
174
+ // Only include props if there are any
175
+ let hasProps = false;
176
+ for (const _ of pairs(props)) {
177
+ hasProps = true;
178
+ break;
179
+ }
180
+ if (hasProps) {
181
+ node.properties = props;
182
+ }
183
+ }
184
+
185
+ if (depth < maxDepth) {
186
+ const childNodes: Record<string, unknown>[] = [];
187
+ for (const child of instance.GetChildren()) {
188
+ childNodes.push(buildHierarchy(child, depth + 1));
189
+ }
190
+ if (childNodes.size() > 0) {
191
+ node.children = childNodes;
192
+ }
193
+ } else {
194
+ const childCount = instance.GetChildren().size();
195
+ if (childCount > 0) {
196
+ node.childCount = childCount;
197
+ node.truncated = true;
198
+ }
199
+ }
200
+
201
+ return node;
202
+ }
203
+
204
+ const hierarchyRoots: Record<string, unknown>[] = [];
205
+ for (const child of (wrapperModel as Instance).GetChildren()) {
206
+ hierarchyRoots.push(buildHierarchy(child, 0));
207
+ }
208
+
209
+ // CRITICAL: Destroy wrapper to prevent memory leak
210
+ (wrapperModel as Instance).Destroy();
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
+ export = {
228
+ insertAsset,
229
+ previewAsset,
230
+ };