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.
- package/dist/index.js +3507 -1061
- package/package.json +6 -29
- package/studio-plugin/MCPPlugin.rbxmx +1 -1
- package/studio-plugin/src/modules/Communication.ts +13 -0
- package/studio-plugin/src/modules/Recording.ts +28 -0
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +230 -0
- package/studio-plugin/src/modules/handlers/BuildHandlers.ts +476 -0
- package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +22 -16
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +54 -8
- package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +11 -12
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +144 -1
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +38 -13
- package/LICENSE +0 -21
- package/README.md +0 -71
- package/dist/__tests__/bridge-service.test.d.ts +0 -2
- package/dist/__tests__/bridge-service.test.d.ts.map +0 -1
- package/dist/__tests__/bridge-service.test.js +0 -95
- package/dist/__tests__/bridge-service.test.js.map +0 -1
- package/dist/__tests__/http-server.test.d.ts +0 -2
- package/dist/__tests__/http-server.test.d.ts.map +0 -1
- package/dist/__tests__/http-server.test.js +0 -176
- package/dist/__tests__/http-server.test.js.map +0 -1
- package/dist/__tests__/integration.test.d.ts +0 -2
- package/dist/__tests__/integration.test.d.ts.map +0 -1
- package/dist/__tests__/integration.test.js +0 -156
- package/dist/__tests__/integration.test.js.map +0 -1
- package/dist/__tests__/smoke.test.d.ts +0 -2
- package/dist/__tests__/smoke.test.d.ts.map +0 -1
- package/dist/__tests__/smoke.test.js +0 -53
- package/dist/__tests__/smoke.test.js.map +0 -1
- package/dist/bridge-service.d.ts +0 -17
- package/dist/bridge-service.d.ts.map +0 -1
- package/dist/bridge-service.js +0 -78
- package/dist/bridge-service.js.map +0 -1
- package/dist/http-server.d.ts +0 -4
- package/dist/http-server.d.ts.map +0 -1
- package/dist/http-server.js +0 -478
- package/dist/http-server.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tools/index.d.ts +0 -273
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -587
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/studio-client.d.ts +0 -7
- package/dist/tools/studio-client.d.ts.map +0 -1
- package/dist/tools/studio-client.js +0 -19
- 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
|
+
"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": "
|
|
17
|
-
"
|
|
18
|
-
"
|
|
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
|
-
"@
|
|
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.
|
|
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
|
+
};
|