@adiholick/nvstudio-mcp 2.2.2 → 2.3.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/README.md +43 -47
- package/agent-plugin/mcp_config.json +8 -0
- package/agent-plugin/plugin.json +6 -0
- package/agent-plugin/rules/AGENTS.md +87 -0
- package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
- package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
- package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +69 -0
- package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
- package/dist/build-plugin.js +55 -0
- package/dist/index.js +988 -146
- package/dist/luau-validator.js +99 -34
- package/dist/roblox-bridge.js +390 -72
- package/dist/roblox-docs.js +264 -0
- package/dist/task-queue.js +57 -6
- package/dist/ws-gateway.js +579 -0
- package/package.json +7 -3
- package/public/index.html +634 -109
- package/studio-plugin/Main.server.lua +484 -53
- package/studio-plugin/batch_create_instances.lua +128 -0
- package/studio-plugin/batch_set_properties.lua +69 -0
- package/studio-plugin/get_properties.lua +25 -2
- package/studio-plugin/insert_asset.lua +85 -12
- package/studio-plugin/nvstudio_mcp.rbxmx +1393 -68
- package/studio-plugin/preview_asset.lua +99 -0
- package/studio-plugin/scan_security.lua +81 -0
- package/studio-plugin/security_scanner.lua +346 -0
- package/studio-plugin/set_properties.lua +10 -0
- package/studio-plugin/ui_manager.lua +1 -1
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const task_queue_1 = require("./task-queue");
|
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const luau_validator_1 = require("./luau-validator");
|
|
15
|
+
const roblox_docs_1 = require("./roblox-docs");
|
|
15
16
|
const HISTORY_DIR = path_1.default.join(__dirname, '..', '.history');
|
|
16
17
|
if (!fs_1.default.existsSync(HISTORY_DIR)) {
|
|
17
18
|
fs_1.default.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
@@ -35,166 +36,1009 @@ const server = new index_js_1.Server({
|
|
|
35
36
|
tools: {},
|
|
36
37
|
},
|
|
37
38
|
});
|
|
38
|
-
// 3
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
// 3. Definisi Semantic Tools MCP untuk Roblox Studio
|
|
40
|
+
const TOOLS = [
|
|
41
|
+
{
|
|
42
|
+
name: "execute_luau",
|
|
43
|
+
description: "Eksekusi kode Luau sembarang secara langsung di Roblox Studio dan kembalikan output/hasilnya (mendukung return table, primitif, dan console log).",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
code: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Kode Luau yang akan dieksekusi di Roblox Studio.",
|
|
50
|
+
},
|
|
51
|
+
target: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "Path opsional objek target (contoh: 'Workspace.Map').",
|
|
54
|
+
},
|
|
55
|
+
placeId: {
|
|
56
|
+
type: "number",
|
|
57
|
+
description: "Place ID Roblox tujuan jika membuka beberapa Studio (multi-Studio routing).",
|
|
58
|
+
},
|
|
59
|
+
studioId: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "Session ID Studio spesifik jika diketahui.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ["code"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "get_children",
|
|
69
|
+
description: "Ambil daftar child langsung (nama dan ClassName) dari suatu Instance di Roblox Studio.",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
target: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Path absolut dari objek target di Roblox (contoh: 'Workspace', 'ReplicatedStorage.Modules').",
|
|
76
|
+
},
|
|
77
|
+
placeId: {
|
|
78
|
+
type: "number",
|
|
79
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
80
|
+
},
|
|
81
|
+
studioId: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "Target Studio session ID.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ["target"],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "get_full_tree",
|
|
91
|
+
description: "Ambil struktur hierarki pohon instance secara rekursif hingga kedalaman (depth) tertentu.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
target: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Path root objek untuk hierarki (contoh: 'Workspace', 'StarterGui').",
|
|
98
|
+
},
|
|
99
|
+
max_depth: {
|
|
100
|
+
type: "number",
|
|
101
|
+
description: "Kedalaman maksimal penelusuran hierarki (default: 2).",
|
|
102
|
+
},
|
|
103
|
+
placeId: {
|
|
104
|
+
type: "number",
|
|
105
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
106
|
+
},
|
|
107
|
+
studioId: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "Target Studio session ID.",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ["target"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "get_script_source",
|
|
117
|
+
description: "Baca seluruh isi source code Luau dari sebuah Script, LocalScript, atau ModuleScript.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
target: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Path absolut ke Script target (contoh: 'ServerScriptService.GameManager').",
|
|
124
|
+
},
|
|
125
|
+
placeId: {
|
|
126
|
+
type: "number",
|
|
127
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
128
|
+
},
|
|
129
|
+
studioId: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Target Studio session ID.",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
required: ["target"],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "update_script_source",
|
|
139
|
+
description: "Ganti seluruh isi source code script dengan kode baru. Dilengkapi validasi sintaks otomatis (AST check) dan riwayat rollback.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
target: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "Path absolut ke Script target (contoh: 'ServerScriptService.MainHandler').",
|
|
146
|
+
},
|
|
147
|
+
code: {
|
|
148
|
+
type: "string",
|
|
149
|
+
description: "Source code Luau baru yang akan ditulis.",
|
|
150
|
+
},
|
|
151
|
+
placeId: {
|
|
152
|
+
type: "number",
|
|
153
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
154
|
+
},
|
|
155
|
+
studioId: {
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "Target Studio session ID.",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ["target", "code"],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "patch_script_source",
|
|
165
|
+
description: "Ganti bagian tertentu dari source code script (search and replace chunk) tanpa menimpa seluruh file.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
target: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description: "Path absolut ke Script target.",
|
|
172
|
+
},
|
|
173
|
+
target_content: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "Potongan kode lama yang ingin diganti (persis karakter per karakter).",
|
|
176
|
+
},
|
|
177
|
+
replacement_content: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "Potongan kode baru sebagai pengganti.",
|
|
180
|
+
},
|
|
181
|
+
allow_multiple: {
|
|
182
|
+
type: "boolean",
|
|
183
|
+
description: "Jika true, ganti semua kemunculan alih-alih hanya kemunculan pertama (default: false).",
|
|
184
|
+
},
|
|
185
|
+
placeId: {
|
|
186
|
+
type: "number",
|
|
187
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
188
|
+
},
|
|
189
|
+
studioId: {
|
|
190
|
+
type: "string",
|
|
191
|
+
description: "Target Studio session ID.",
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
required: ["target", "target_content", "replacement_content"],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "rollback_script",
|
|
199
|
+
description: "Kembalikan source code script ke versi sebelumnya dari riwayat backup sesi saat ini.",
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
target: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Path absolut ke Script yang ingin di-rollback.",
|
|
206
|
+
},
|
|
207
|
+
placeId: {
|
|
208
|
+
type: "number",
|
|
209
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
210
|
+
},
|
|
211
|
+
studioId: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "Target Studio session ID.",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
required: ["target"],
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "get_properties",
|
|
221
|
+
description: "Ambil properti umum, attributes, dan tags dari suatu Instance di Roblox Studio.",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
target: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "Path absolut ke Instance (contoh: 'Workspace.Baseplate', 'Lighting').",
|
|
228
|
+
},
|
|
229
|
+
placeId: {
|
|
230
|
+
type: "number",
|
|
231
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
232
|
+
},
|
|
233
|
+
studioId: {
|
|
234
|
+
type: "string",
|
|
235
|
+
description: "Target Studio session ID.",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
required: ["target"],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "set_properties",
|
|
243
|
+
description: "Ubah properti dari suatu Instance di Roblox Studio menggunakan key-value JSON.",
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: "object",
|
|
246
|
+
properties: {
|
|
247
|
+
target: {
|
|
248
|
+
type: "string",
|
|
249
|
+
description: "Path absolut ke Instance target (contoh: 'Workspace.Part').",
|
|
250
|
+
},
|
|
251
|
+
properties: {
|
|
46
252
|
type: "object",
|
|
47
|
-
|
|
48
|
-
command: {
|
|
49
|
-
type: "string",
|
|
50
|
-
description: "Perintah yang dieksekusi (contoh: 'set_properties', 'get_full_tree', 'patch_script_source').",
|
|
51
|
-
},
|
|
52
|
-
target: {
|
|
53
|
-
type: "string",
|
|
54
|
-
description: "Path absolut dari objek target di Roblox Studio (contoh: 'Workspace.Map.Script').",
|
|
55
|
-
},
|
|
56
|
-
data: {
|
|
57
|
-
type: "string",
|
|
58
|
-
description: "Data tambahan. Untuk set_properties: JSON string dari properti. Untuk delete_instance: kirim 'force' jika descendants > 10. Untuk patch/update: source code.",
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
required: ["command"],
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
};
|
|
66
|
-
});
|
|
67
|
-
// 5. Handler untuk menangani pemanggilan call_mcp_tool
|
|
68
|
-
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
69
|
-
await roblox_bridge_1.bridgeReadyPromise;
|
|
70
|
-
if (request.params.name !== "call_mcp_tool") {
|
|
71
|
-
throw new Error(`Tool tidak dikenali: ${request.params.name}`);
|
|
72
|
-
}
|
|
73
|
-
const args = request.params.arguments;
|
|
74
|
-
const { command, target, data } = args;
|
|
75
|
-
if (!command) {
|
|
76
|
-
return {
|
|
77
|
-
content: [
|
|
78
|
-
{
|
|
79
|
-
type: "text",
|
|
80
|
-
text: JSON.stringify({
|
|
81
|
-
status: "error",
|
|
82
|
-
error: "Parameter 'command' wajib diisi.",
|
|
83
|
-
}),
|
|
253
|
+
description: "Objek key-value properti (contoh: { \"Anchored\": true, \"Size\": [10, 2, 10], \"Color\": [255, 0, 0] }).",
|
|
84
254
|
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
255
|
+
placeId: {
|
|
256
|
+
type: "number",
|
|
257
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
258
|
+
},
|
|
259
|
+
studioId: {
|
|
260
|
+
type: "string",
|
|
261
|
+
description: "Target Studio session ID.",
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
required: ["target", "properties"],
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "create_instance",
|
|
269
|
+
description: "Buat Instance baru di Roblox Studio di bawah parent target dengan ClassName dan properti awal opsional.",
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: "object",
|
|
272
|
+
properties: {
|
|
273
|
+
class_name: {
|
|
274
|
+
type: "string",
|
|
275
|
+
description: "ClassName Roblox (contoh: 'Part', 'Folder', 'RemoteEvent', 'Script').",
|
|
276
|
+
},
|
|
277
|
+
target: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description: "Path parent tempat Instance akan dibuat (contoh: 'Workspace', 'ReplicatedStorage').",
|
|
280
|
+
},
|
|
281
|
+
properties: {
|
|
282
|
+
type: "object",
|
|
283
|
+
description: "Properti awal opsional yang disetel saat instance dibuat.",
|
|
284
|
+
},
|
|
285
|
+
placeId: {
|
|
286
|
+
type: "number",
|
|
287
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
288
|
+
},
|
|
289
|
+
studioId: {
|
|
290
|
+
type: "string",
|
|
291
|
+
description: "Target Studio session ID.",
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
required: ["class_name", "target"],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: "delete_instance",
|
|
299
|
+
description: "Hapus (Destroy) Instance dari game. Service utama dilindungi guardrail dan objek dengan >10 descendant memerlukan force: true.",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: {
|
|
303
|
+
target: {
|
|
304
|
+
type: "string",
|
|
305
|
+
description: "Path absolut ke Instance yang ingin dihapus.",
|
|
306
|
+
},
|
|
307
|
+
force: {
|
|
308
|
+
type: "boolean",
|
|
309
|
+
description: "Wajib disetel ke true jika instance memiliki lebih dari 10 descendants.",
|
|
310
|
+
},
|
|
311
|
+
placeId: {
|
|
312
|
+
type: "number",
|
|
313
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
314
|
+
},
|
|
315
|
+
studioId: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "Target Studio session ID.",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
required: ["target"],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "search_instance",
|
|
325
|
+
description: "Cari Instance berdasarkan nama atau substring di dalam hierarki Roblox Studio.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
query: {
|
|
330
|
+
type: "string",
|
|
331
|
+
description: "Nama atau substring instance yang dicari.",
|
|
332
|
+
},
|
|
333
|
+
target: {
|
|
334
|
+
type: "string",
|
|
335
|
+
description: "Scope root opsional tempat mencari (default: seluruh service utama).",
|
|
336
|
+
},
|
|
337
|
+
placeId: {
|
|
338
|
+
type: "number",
|
|
339
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
340
|
+
},
|
|
341
|
+
studioId: {
|
|
342
|
+
type: "string",
|
|
343
|
+
description: "Target Studio session ID.",
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
required: ["query"],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "generate_terrain",
|
|
351
|
+
description: "Generate blok voxel terrain di Workspace.Terrain dengan material, posisi, dan ukuran tertentu.",
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
material: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: "Nama material terrain (contoh: 'Grass', 'Water', 'Sand', 'Rock', 'Snow').",
|
|
358
|
+
},
|
|
359
|
+
position: {
|
|
360
|
+
type: "array",
|
|
361
|
+
items: { type: "number" },
|
|
362
|
+
description: "Posisi voxel region [X, Y, Z].",
|
|
363
|
+
},
|
|
364
|
+
size: {
|
|
365
|
+
type: "array",
|
|
366
|
+
items: { type: "number" },
|
|
367
|
+
description: "Ukuran dimensi voxel region [X, Y, Z].",
|
|
368
|
+
},
|
|
369
|
+
placeId: {
|
|
370
|
+
type: "number",
|
|
371
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
372
|
+
},
|
|
373
|
+
studioId: {
|
|
374
|
+
type: "string",
|
|
375
|
+
description: "Target Studio session ID.",
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
required: ["material", "position", "size"],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "preview_asset",
|
|
383
|
+
description: "Pratinjau hierarki dan pemindaian keamanan aset Creator Store secara terisolasi sebelum dimasukkan ke dalam game. Mendeteksi skrip backdoor, require eksternal numerik, obfuscation, webhook discord, dan virus script tanpa risiko mengeksekusi kode berbahaya.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
asset_id: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "Roblox Asset ID angka (atau rbxassetid://) dari Creator Store.",
|
|
390
|
+
},
|
|
391
|
+
deep_scan: {
|
|
392
|
+
type: "boolean",
|
|
393
|
+
description: "Jika true (default), memindai dan menganalisis source code seluruh skrip di dalam model.",
|
|
394
|
+
},
|
|
395
|
+
placeId: {
|
|
396
|
+
type: "number",
|
|
397
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
398
|
+
},
|
|
399
|
+
studioId: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Target Studio session ID.",
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
required: ["asset_id"],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "scan_security",
|
|
409
|
+
description: "Audit keamanan instance atau model yang sudah ada di dalam game (Workspace, ServerScriptService, dll.) untuk mendeteksi skrip backdoor, virus, webhook discord, dan kode berbahaya.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: {
|
|
413
|
+
target: {
|
|
414
|
+
type: "string",
|
|
415
|
+
description: "Path instance atau folder yang ingin diaudit (default: 'Workspace').",
|
|
416
|
+
},
|
|
417
|
+
deep_scan: {
|
|
418
|
+
type: "boolean",
|
|
419
|
+
description: "Jika true (default), memindai isi source code skrip.",
|
|
420
|
+
},
|
|
421
|
+
clean_threats: {
|
|
422
|
+
type: "boolean",
|
|
423
|
+
description: "Jika true, secara otomatis menghapus skrip berbahaya yang terdeteksi (default: false).",
|
|
424
|
+
},
|
|
425
|
+
placeId: {
|
|
426
|
+
type: "number",
|
|
427
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
428
|
+
},
|
|
429
|
+
studioId: {
|
|
430
|
+
type: "string",
|
|
431
|
+
description: "Target Studio session ID.",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "insert_asset",
|
|
438
|
+
description: "Muat dan masukkan aset Roblox (Model/Package) berdasarkan Asset ID ke dalam Workspace atau target tertentu dengan proteksi sanitasi skrip otomatis.",
|
|
439
|
+
inputSchema: {
|
|
440
|
+
type: "object",
|
|
441
|
+
properties: {
|
|
442
|
+
asset_id: {
|
|
443
|
+
type: "string",
|
|
444
|
+
description: "Roblox Asset ID angka (atau rbxassetid://).",
|
|
445
|
+
},
|
|
446
|
+
target: {
|
|
447
|
+
type: "string",
|
|
448
|
+
description: "Parent tujuan tempat menempatkan model (default: 'Workspace').",
|
|
449
|
+
},
|
|
450
|
+
strip_scripts: {
|
|
451
|
+
type: "boolean",
|
|
452
|
+
description: "Jika true (default: true), secara otomatis menghapus semua skrip dari model sebelum dimasukkan ke game demi keamanan mutlak dari backdoor.",
|
|
453
|
+
},
|
|
454
|
+
scan_security: {
|
|
455
|
+
type: "boolean",
|
|
456
|
+
description: "Jika true (default: true), memindai potensi ancaman keamanan sebelum insert.",
|
|
457
|
+
},
|
|
458
|
+
placeId: {
|
|
459
|
+
type: "number",
|
|
460
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
461
|
+
},
|
|
462
|
+
studioId: {
|
|
463
|
+
type: "string",
|
|
464
|
+
description: "Target Studio session ID.",
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
required: ["asset_id"],
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: "search_asset",
|
|
472
|
+
description: "Cari aset publik di Roblox Creator Store / Marketplace (Model, Audio, Decal, Image).",
|
|
473
|
+
inputSchema: {
|
|
474
|
+
type: "object",
|
|
475
|
+
properties: {
|
|
476
|
+
keyword: {
|
|
477
|
+
type: "string",
|
|
478
|
+
description: "Kata kunci pencarian aset.",
|
|
479
|
+
},
|
|
480
|
+
category: {
|
|
481
|
+
type: "string",
|
|
482
|
+
enum: ["Model", "Audio", "Decal", "Image"],
|
|
483
|
+
description: "Kategori aset yang dicari (default: 'Model').",
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
required: ["keyword"],
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: "script_grep",
|
|
491
|
+
description: "Cari teks atau pola (pattern) di dalam seluruh script di dalam game (Workspace, ServerScriptService, dll.).",
|
|
492
|
+
inputSchema: {
|
|
493
|
+
type: "object",
|
|
494
|
+
properties: {
|
|
495
|
+
query: {
|
|
496
|
+
type: "string",
|
|
497
|
+
description: "String atau pola teks yang ingin dicari di baris kode.",
|
|
498
|
+
},
|
|
499
|
+
target: {
|
|
500
|
+
type: "string",
|
|
501
|
+
description: "Scope root opsional tempat mencari script.",
|
|
502
|
+
},
|
|
503
|
+
placeId: {
|
|
504
|
+
type: "number",
|
|
505
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
506
|
+
},
|
|
507
|
+
studioId: {
|
|
508
|
+
type: "string",
|
|
509
|
+
description: "Target Studio session ID.",
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
required: ["query"],
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "get_logs",
|
|
517
|
+
description: "Ambil log konsol / output terbaru Roblox Studio yang ditangkap oleh plugin.",
|
|
518
|
+
inputSchema: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
placeId: {
|
|
522
|
+
type: "number",
|
|
523
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
524
|
+
},
|
|
525
|
+
studioId: {
|
|
526
|
+
type: "string",
|
|
527
|
+
description: "Target Studio session ID.",
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: "list_connected_studios",
|
|
534
|
+
description: "Lihat daftar seluruh sesi Roblox Studio yang sedang terhubung ke Bridge Server, Place ID masing-masing, status koneksi, dan transport (WebSocket/Long-Polling).",
|
|
535
|
+
inputSchema: {
|
|
536
|
+
type: "object",
|
|
537
|
+
properties: {},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "call_mcp_tool",
|
|
542
|
+
description: "Legacy tool dispatcher untuk kompatibilitas mundur. Mendukung semua command: execute_luau, get_children, get_full_tree, get_script_source, update_script_source, patch_script_source, rollback_script, get_properties, set_properties, create_instance, delete_instance, search_instance, generate_terrain, insert_asset, preview_asset, scan_security, batch_create_instances, batch_set_properties, search_asset, script_grep, get_roblox_docs, get_logs.",
|
|
543
|
+
inputSchema: {
|
|
544
|
+
type: "object",
|
|
545
|
+
properties: {
|
|
546
|
+
command: {
|
|
547
|
+
type: "string",
|
|
548
|
+
description: "Nama perintah yang dieksekusi.",
|
|
549
|
+
},
|
|
550
|
+
target: {
|
|
551
|
+
type: "string",
|
|
552
|
+
description: "Path objek target di Roblox Studio.",
|
|
553
|
+
},
|
|
554
|
+
data: {
|
|
555
|
+
type: "string",
|
|
556
|
+
description: "Data tambahan sesuai command.",
|
|
557
|
+
},
|
|
558
|
+
placeId: {
|
|
559
|
+
type: "number",
|
|
560
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
561
|
+
},
|
|
562
|
+
studioId: {
|
|
563
|
+
type: "string",
|
|
564
|
+
description: "Target Studio session ID.",
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
required: ["command"],
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: "get_roblox_docs",
|
|
572
|
+
description: "Ambil dokumentasi resmi Roblox Engine API langsung dari Creator Docs (Classes, Services, Data types, Enums, dan Libraries). Mengembalikan format Markdown lengkap beserta signature fungsi, deskripsi parameter, properti, event, dan contoh kode untuk mencegah halusinasi kode Luau.",
|
|
573
|
+
inputSchema: {
|
|
574
|
+
type: "object",
|
|
575
|
+
properties: {
|
|
576
|
+
query: {
|
|
577
|
+
type: "string",
|
|
578
|
+
description: "Nama Class, Service, Data type, Enum, atau Library Roblox (contoh: 'TweenService', 'CanvasGroup', 'Vector3', 'EasingStyle', 'task').",
|
|
579
|
+
},
|
|
580
|
+
member: {
|
|
581
|
+
type: "string",
|
|
582
|
+
description: "Opsional: Nama method, property, event, atau enum item spesifik untuk menghemat token (contoh: 'Create', 'GroupTransparency', 'Completed').",
|
|
583
|
+
},
|
|
584
|
+
category: {
|
|
585
|
+
type: "string",
|
|
586
|
+
enum: ["class", "datatype", "enum", "library", "auto"],
|
|
587
|
+
description: "Opsional: Kategori dokumentasi (default: 'auto').",
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
required: ["query"],
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: "batch_create_instances",
|
|
595
|
+
description: "Pembangun instance massal dan deklaratif dalam satu kali pemanggilan. Mendukung mode hierarki bersarang (tree) atau daftar array datar (instances) lengkap dengan penyetelan properti langsung (UDim, Vector2, UDim2, Color3, Enum, dll.) untuk menghemat token dan latensi jaringan.",
|
|
596
|
+
inputSchema: {
|
|
597
|
+
type: "object",
|
|
598
|
+
properties: {
|
|
599
|
+
target: {
|
|
600
|
+
type: "string",
|
|
601
|
+
description: "Path root tujuan tempat menaruh instance (contoh: 'StarterGui.MyScreen', 'Workspace.Map').",
|
|
602
|
+
},
|
|
603
|
+
tree: {
|
|
604
|
+
type: "object",
|
|
605
|
+
description: "Objek hierarki bersarang { className, name, properties, children: [...] } untuk membangun struktur pohon secara rekursif.",
|
|
606
|
+
},
|
|
607
|
+
instances: {
|
|
608
|
+
type: "array",
|
|
609
|
+
description: "Daftar array datar [{ className, name, parent, properties }] jika tidak menggunakan mode tree.",
|
|
610
|
+
},
|
|
611
|
+
placeId: {
|
|
612
|
+
type: "number",
|
|
613
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
614
|
+
},
|
|
615
|
+
studioId: {
|
|
616
|
+
type: "string",
|
|
617
|
+
description: "Target Studio session ID.",
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
name: "batch_set_properties",
|
|
624
|
+
description: "Ubah properti pada banyak instance sekaligus dalam satu kali request atomik.",
|
|
625
|
+
inputSchema: {
|
|
626
|
+
type: "object",
|
|
627
|
+
properties: {
|
|
628
|
+
updates: {
|
|
629
|
+
type: "array",
|
|
630
|
+
description: "Array objek pembaruan: [{ target: 'Path.To.Instance', properties: { PropName: value, ... } }].",
|
|
631
|
+
},
|
|
632
|
+
placeId: {
|
|
633
|
+
type: "number",
|
|
634
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
635
|
+
},
|
|
636
|
+
studioId: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "Target Studio session ID.",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
required: ["updates"],
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
];
|
|
645
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
646
|
+
return { tools: TOOLS };
|
|
647
|
+
});
|
|
648
|
+
/**
|
|
649
|
+
* Dispatcher pusat untuk meneruskan task ke Roblox Studio (Bridge Hosting lokal atau eksternal).
|
|
650
|
+
*/
|
|
651
|
+
async function dispatchTaskToStudio(command, target, data, options) {
|
|
652
|
+
const placeId = options?.placeId;
|
|
653
|
+
const studioId = options?.studioId;
|
|
654
|
+
if (roblox_bridge_1.isBridgeHosting) {
|
|
655
|
+
if (!(0, roblox_bridge_1.isStudioConnected)({ placeId, studioId })) {
|
|
656
|
+
for (let i = 0; i < 10; i++) {
|
|
657
|
+
await new Promise(r => setTimeout(r, 50));
|
|
658
|
+
if ((0, roblox_bridge_1.isStudioConnected)({ placeId, studioId }))
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
if (!(0, roblox_bridge_1.isStudioConnected)({ placeId, studioId })) {
|
|
662
|
+
const errorMsg = placeId !== undefined
|
|
663
|
+
? `Roblox Studio dengan Place ID ${placeId} tidak terdeteksi aktif. Studio yang saat ini terhubung: ${JSON.stringify((0, roblox_bridge_1.getConnectedStudios)())}`
|
|
664
|
+
: "Roblox Studio tidak terdeteksi aktif. Pastikan Roblox Studio dibuka, place telah dimuat, dan plugin nvstudio-mcp dalam status terhubung.";
|
|
665
|
+
throw new Error(errorMsg);
|
|
666
|
+
}
|
|
103
667
|
}
|
|
668
|
+
return await (0, task_queue_1.addTaskToQueue)(command, target, data, { placeId, studioId });
|
|
104
669
|
}
|
|
105
|
-
|
|
106
|
-
|
|
670
|
+
else {
|
|
671
|
+
let forwardedToBridge = false;
|
|
107
672
|
try {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
]
|
|
130
|
-
};
|
|
673
|
+
const controller = new AbortController();
|
|
674
|
+
const timeoutId = setTimeout(() => controller.abort(), 35000);
|
|
675
|
+
const bridgeRes = await fetch("http://localhost:3055/api/tasks/enqueue", {
|
|
676
|
+
method: "POST",
|
|
677
|
+
headers: { "Content-Type": "application/json" },
|
|
678
|
+
body: JSON.stringify({ command, target, data, placeId, studioId }),
|
|
679
|
+
signal: controller.signal,
|
|
680
|
+
});
|
|
681
|
+
clearTimeout(timeoutId);
|
|
682
|
+
if (bridgeRes.ok) {
|
|
683
|
+
forwardedToBridge = true;
|
|
684
|
+
const bridgeJson = (await bridgeRes.json());
|
|
685
|
+
if (bridgeJson.status === "error") {
|
|
686
|
+
throw new Error(bridgeJson.error);
|
|
687
|
+
}
|
|
688
|
+
return bridgeJson.result;
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
throw new Error(`Bridge server returned HTTP ${bridgeRes.status}`);
|
|
692
|
+
}
|
|
131
693
|
}
|
|
132
694
|
catch (err) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
};
|
|
695
|
+
if (forwardedToBridge)
|
|
696
|
+
throw err;
|
|
697
|
+
throw new Error("Gagal menghubungi server Bridge. Pastikan server daemon berjalan di port 3055.");
|
|
137
698
|
}
|
|
138
699
|
}
|
|
139
|
-
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Helper untuk mengeksekusi pencarian aset di katalog web Roblox.
|
|
703
|
+
*/
|
|
704
|
+
async function handleSearchAsset(keyword, category = "Model") {
|
|
705
|
+
const encodedKeyword = encodeURIComponent(keyword || "");
|
|
706
|
+
let assetType = "10"; // Default: Model
|
|
707
|
+
if (category === "Audio") {
|
|
708
|
+
assetType = "9";
|
|
709
|
+
}
|
|
710
|
+
else if (category === "Decal" || category === "Image") {
|
|
711
|
+
assetType = "13";
|
|
712
|
+
}
|
|
713
|
+
const url = `https://catalog.roblox.com/v1/search/items?category=All&keyword=${encodedKeyword}&limit=10&itemTypes=Asset&assetTypes=${assetType}`;
|
|
714
|
+
const res = await fetch(url);
|
|
715
|
+
const jsonRes = await res.json();
|
|
716
|
+
return jsonRes.data || jsonRes;
|
|
717
|
+
}
|
|
718
|
+
// 4. Handler untuk memproses panggilan tools
|
|
719
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
720
|
+
await roblox_bridge_1.bridgeReadyPromise;
|
|
721
|
+
const toolName = request.params.name;
|
|
722
|
+
const args = (request.params.arguments || {});
|
|
140
723
|
try {
|
|
141
724
|
let result;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
725
|
+
switch (toolName) {
|
|
726
|
+
case "execute_luau": {
|
|
727
|
+
const code = String(args.code ?? "");
|
|
728
|
+
if (!code)
|
|
729
|
+
throw new Error("Parameter 'code' wajib diisi.");
|
|
730
|
+
result = await dispatchTaskToStudio("execute_luau", args.target || "", code, {
|
|
731
|
+
placeId: args.placeId,
|
|
732
|
+
studioId: args.studioId,
|
|
733
|
+
});
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case "get_children": {
|
|
737
|
+
const target = String(args.target ?? "");
|
|
738
|
+
if (!target)
|
|
739
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
740
|
+
result = await dispatchTaskToStudio("get_children", target, "", {
|
|
741
|
+
placeId: args.placeId,
|
|
742
|
+
studioId: args.studioId,
|
|
743
|
+
});
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
case "get_full_tree": {
|
|
747
|
+
const target = String(args.target ?? "");
|
|
748
|
+
if (!target)
|
|
749
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
750
|
+
const depth = String(args.max_depth ?? 2);
|
|
751
|
+
result = await dispatchTaskToStudio("get_full_tree", target, depth, {
|
|
752
|
+
placeId: args.placeId,
|
|
753
|
+
studioId: args.studioId,
|
|
754
|
+
});
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "get_script_source": {
|
|
758
|
+
const target = String(args.target ?? "");
|
|
759
|
+
if (!target)
|
|
760
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
761
|
+
result = await dispatchTaskToStudio("get_script_source", target, "", {
|
|
762
|
+
placeId: args.placeId,
|
|
763
|
+
studioId: args.studioId,
|
|
764
|
+
});
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case "update_script_source": {
|
|
768
|
+
const target = String(args.target ?? "");
|
|
769
|
+
const code = String(args.code ?? "");
|
|
770
|
+
if (!target)
|
|
771
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
772
|
+
// Pre-commit AST syntax check di Node.js
|
|
773
|
+
const validation = (0, luau_validator_1.validateLuauSyntax)(code);
|
|
774
|
+
if (!validation.valid) {
|
|
775
|
+
throw new Error(`Syntax error terdeteksi (Pre-commit check Node.js): ${validation.error} pada baris ${validation.line || 'unknown'}`);
|
|
163
776
|
}
|
|
777
|
+
backupScript(target, code);
|
|
778
|
+
result = await dispatchTaskToStudio("update_script_source", target, code, {
|
|
779
|
+
placeId: args.placeId,
|
|
780
|
+
studioId: args.studioId,
|
|
781
|
+
});
|
|
782
|
+
break;
|
|
164
783
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
784
|
+
case "patch_script_source": {
|
|
785
|
+
const target = String(args.target ?? "");
|
|
786
|
+
if (!target)
|
|
787
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
788
|
+
if (!args.target_content)
|
|
789
|
+
throw new Error("Parameter 'target_content' wajib diisi.");
|
|
790
|
+
if (args.replacement_content === undefined)
|
|
791
|
+
throw new Error("Parameter 'replacement_content' wajib diisi.");
|
|
792
|
+
const payload = JSON.stringify({
|
|
793
|
+
target_content: args.target_content,
|
|
794
|
+
replacement_content: args.replacement_content,
|
|
795
|
+
allow_multiple: !!args.allow_multiple,
|
|
796
|
+
});
|
|
797
|
+
result = await dispatchTaskToStudio("patch_script_source", target, payload, {
|
|
798
|
+
placeId: args.placeId,
|
|
799
|
+
studioId: args.studioId,
|
|
800
|
+
});
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
case "rollback_script": {
|
|
804
|
+
const target = String(args.target ?? "");
|
|
805
|
+
if (!target)
|
|
806
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
807
|
+
result = await dispatchTaskToStudio("rollback_script", target, "", {
|
|
808
|
+
placeId: args.placeId,
|
|
809
|
+
studioId: args.studioId,
|
|
810
|
+
});
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
case "get_properties": {
|
|
814
|
+
const target = String(args.target ?? "");
|
|
815
|
+
if (!target)
|
|
816
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
817
|
+
result = await dispatchTaskToStudio("get_properties", target, "", {
|
|
818
|
+
placeId: args.placeId,
|
|
819
|
+
studioId: args.studioId,
|
|
820
|
+
});
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
case "set_properties": {
|
|
824
|
+
const target = String(args.target ?? "");
|
|
825
|
+
if (!target)
|
|
826
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
827
|
+
if (!args.properties)
|
|
828
|
+
throw new Error("Parameter 'properties' wajib diisi.");
|
|
829
|
+
const propsData = typeof args.properties === "string" ? args.properties : JSON.stringify(args.properties);
|
|
830
|
+
result = await dispatchTaskToStudio("set_properties", target, propsData, {
|
|
831
|
+
placeId: args.placeId,
|
|
832
|
+
studioId: args.studioId,
|
|
833
|
+
});
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
case "create_instance": {
|
|
837
|
+
const className = String(args.class_name ?? "");
|
|
838
|
+
const target = String(args.target ?? "");
|
|
839
|
+
if (!className)
|
|
840
|
+
throw new Error("Parameter 'class_name' wajib diisi.");
|
|
841
|
+
if (!target)
|
|
842
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
843
|
+
const payload = JSON.stringify({
|
|
844
|
+
ClassName: className,
|
|
845
|
+
Properties: args.properties || {},
|
|
846
|
+
});
|
|
847
|
+
result = await dispatchTaskToStudio("create_instance", target, payload, {
|
|
848
|
+
placeId: args.placeId,
|
|
849
|
+
studioId: args.studioId,
|
|
850
|
+
});
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
case "delete_instance": {
|
|
854
|
+
const target = String(args.target ?? "");
|
|
855
|
+
if (!target)
|
|
856
|
+
throw new Error("Parameter 'target' wajib diisi.");
|
|
857
|
+
const data = args.force ? "force" : "";
|
|
858
|
+
result = await dispatchTaskToStudio("delete_instance", target, data, {
|
|
859
|
+
placeId: args.placeId,
|
|
860
|
+
studioId: args.studioId,
|
|
861
|
+
});
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
case "search_instance": {
|
|
865
|
+
const query = String(args.query ?? "");
|
|
866
|
+
if (!query)
|
|
867
|
+
throw new Error("Parameter 'query' wajib diisi.");
|
|
868
|
+
result = await dispatchTaskToStudio("search_instance", args.target || "", query, {
|
|
869
|
+
placeId: args.placeId,
|
|
870
|
+
studioId: args.studioId,
|
|
871
|
+
});
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
case "generate_terrain": {
|
|
875
|
+
const material = String(args.material ?? "");
|
|
876
|
+
if (!material)
|
|
877
|
+
throw new Error("Parameter 'material' wajib diisi.");
|
|
878
|
+
if (!args.position || !Array.isArray(args.position))
|
|
879
|
+
throw new Error("Parameter 'position' [X, Y, Z] wajib diisi.");
|
|
880
|
+
if (!args.size || !Array.isArray(args.size))
|
|
881
|
+
throw new Error("Parameter 'size' [X, Y, Z] wajib diisi.");
|
|
882
|
+
const payload = JSON.stringify({
|
|
883
|
+
Material: material,
|
|
884
|
+
Position: args.position,
|
|
885
|
+
Size: args.size,
|
|
886
|
+
});
|
|
887
|
+
result = await dispatchTaskToStudio("generate_terrain", "", payload, {
|
|
888
|
+
placeId: args.placeId,
|
|
889
|
+
studioId: args.studioId,
|
|
890
|
+
});
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
case "preview_asset": {
|
|
894
|
+
const assetId = String(args.asset_id ?? "");
|
|
895
|
+
if (!assetId)
|
|
896
|
+
throw new Error("Parameter 'asset_id' wajib diisi.");
|
|
897
|
+
const payload = JSON.stringify({
|
|
898
|
+
asset_id: assetId,
|
|
899
|
+
deep_scan: args.deep_scan !== false,
|
|
900
|
+
});
|
|
901
|
+
result = await dispatchTaskToStudio("preview_asset", "", payload, {
|
|
902
|
+
placeId: args.placeId,
|
|
903
|
+
studioId: args.studioId,
|
|
904
|
+
});
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
case "scan_security": {
|
|
908
|
+
const payload = JSON.stringify({
|
|
909
|
+
deep_scan: args.deep_scan !== false,
|
|
910
|
+
clean_threats: args.clean_threats === true,
|
|
911
|
+
});
|
|
912
|
+
result = await dispatchTaskToStudio("scan_security", args.target || "Workspace", payload, {
|
|
913
|
+
placeId: args.placeId,
|
|
914
|
+
studioId: args.studioId,
|
|
915
|
+
});
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
case "insert_asset": {
|
|
919
|
+
const assetId = String(args.asset_id ?? "");
|
|
920
|
+
if (!assetId)
|
|
921
|
+
throw new Error("Parameter 'asset_id' wajib diisi.");
|
|
922
|
+
const payload = JSON.stringify({
|
|
923
|
+
asset_id: assetId,
|
|
924
|
+
strip_scripts: args.strip_scripts !== false,
|
|
925
|
+
scan_security: args.scan_security !== false,
|
|
926
|
+
});
|
|
927
|
+
result = await dispatchTaskToStudio("insert_asset", args.target || "Workspace", payload, {
|
|
928
|
+
placeId: args.placeId,
|
|
929
|
+
studioId: args.studioId,
|
|
930
|
+
});
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
case "search_asset": {
|
|
934
|
+
const keyword = String(args.keyword ?? "");
|
|
935
|
+
if (!keyword)
|
|
936
|
+
throw new Error("Parameter 'keyword' wajib diisi.");
|
|
937
|
+
result = await handleSearchAsset(keyword, args.category);
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
940
|
+
case "script_grep": {
|
|
941
|
+
const query = String(args.query ?? "");
|
|
942
|
+
if (!query)
|
|
943
|
+
throw new Error("Parameter 'query' wajib diisi.");
|
|
944
|
+
result = await dispatchTaskToStudio("script_grep", args.target || "", query, {
|
|
945
|
+
placeId: args.placeId,
|
|
946
|
+
studioId: args.studioId,
|
|
947
|
+
});
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
case "get_logs": {
|
|
951
|
+
result = await dispatchTaskToStudio("get_logs", "", "", {
|
|
952
|
+
placeId: args.placeId,
|
|
953
|
+
studioId: args.studioId,
|
|
954
|
+
});
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
case "list_connected_studios": {
|
|
958
|
+
if (roblox_bridge_1.isBridgeHosting) {
|
|
959
|
+
result = (0, roblox_bridge_1.getConnectedStudios)();
|
|
187
960
|
}
|
|
188
961
|
else {
|
|
189
|
-
|
|
962
|
+
try {
|
|
963
|
+
const statsRes = await fetch("http://localhost:3055/api/stats");
|
|
964
|
+
const statsJson = (await statsRes.json());
|
|
965
|
+
result = statsJson.connectedStudios || [];
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
result = [];
|
|
969
|
+
}
|
|
190
970
|
}
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
case "get_roblox_docs": {
|
|
974
|
+
const query = String(args.query ?? "").trim();
|
|
975
|
+
if (!query)
|
|
976
|
+
throw new Error("Parameter 'query' wajib diisi.");
|
|
977
|
+
const member = args.member ? String(args.member).trim() : undefined;
|
|
978
|
+
const category = args.category ? String(args.category).trim() : "auto";
|
|
979
|
+
result = await (0, roblox_docs_1.getRobloxDocs)(query, member, category);
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
case "batch_create_instances": {
|
|
983
|
+
const payload = JSON.stringify({
|
|
984
|
+
target: args.target,
|
|
985
|
+
tree: args.tree,
|
|
986
|
+
instances: args.instances,
|
|
987
|
+
});
|
|
988
|
+
result = await dispatchTaskToStudio("batch_create_instances", args.target || "Workspace", payload, {
|
|
989
|
+
placeId: args.placeId,
|
|
990
|
+
studioId: args.studioId,
|
|
991
|
+
});
|
|
992
|
+
break;
|
|
191
993
|
}
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
throw
|
|
994
|
+
case "batch_set_properties": {
|
|
995
|
+
if (!args.updates || !Array.isArray(args.updates))
|
|
996
|
+
throw new Error("Parameter 'updates' wajib berupa array.");
|
|
997
|
+
const payload = JSON.stringify({
|
|
998
|
+
updates: args.updates,
|
|
999
|
+
});
|
|
1000
|
+
result = await dispatchTaskToStudio("batch_set_properties", "", payload, {
|
|
1001
|
+
placeId: args.placeId,
|
|
1002
|
+
studioId: args.studioId,
|
|
1003
|
+
});
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
case "call_mcp_tool": {
|
|
1007
|
+
const { command, target, data, placeId, studioId } = args;
|
|
1008
|
+
if (!command)
|
|
1009
|
+
throw new Error("Parameter 'command' wajib diisi.");
|
|
1010
|
+
// Pre-commit validation for full source replacement
|
|
1011
|
+
if (command === "update_script_source" && data) {
|
|
1012
|
+
const validation = (0, luau_validator_1.validateLuauSyntax)(String(data));
|
|
1013
|
+
if (!validation.valid) {
|
|
1014
|
+
throw new Error(`Syntax error terdeteksi (Pre-commit check Node.js): ${validation.error} pada baris ${validation.line || 'unknown'}`);
|
|
1015
|
+
}
|
|
1016
|
+
if (target)
|
|
1017
|
+
backupScript(target, String(data));
|
|
1018
|
+
}
|
|
1019
|
+
// Intercept get_roblox_docs di Node.js
|
|
1020
|
+
if (command === "get_roblox_docs") {
|
|
1021
|
+
const queryParams = typeof data === "string" ? (function () { try {
|
|
1022
|
+
return JSON.parse(data);
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
return { query: data };
|
|
1026
|
+
} })() : (data || {});
|
|
1027
|
+
const q = String(queryParams.query || target || "").trim();
|
|
1028
|
+
result = await (0, roblox_docs_1.getRobloxDocs)(q, queryParams.member, queryParams.category);
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
// Intercept search_asset di Node.js
|
|
1032
|
+
if (command === "search_asset" && data) {
|
|
1033
|
+
const queryParams = typeof data === "string" ? JSON.parse(data) : data;
|
|
1034
|
+
result = await handleSearchAsset(queryParams.keyword || "", queryParams.category);
|
|
1035
|
+
break;
|
|
195
1036
|
}
|
|
196
|
-
|
|
1037
|
+
result = await dispatchTaskToStudio(command, target || "", data, { placeId, studioId });
|
|
1038
|
+
break;
|
|
197
1039
|
}
|
|
1040
|
+
default:
|
|
1041
|
+
throw new Error(`Tool tidak dikenali: ${toolName}`);
|
|
198
1042
|
}
|
|
199
1043
|
return {
|
|
200
1044
|
content: [
|
|
@@ -220,27 +1064,25 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
220
1064
|
};
|
|
221
1065
|
}
|
|
222
1066
|
});
|
|
223
|
-
//
|
|
1067
|
+
// 5. Jalankan koneksi MCP menggunakan StdioServerTransport (hanya jika bukan mode Daemon)
|
|
224
1068
|
async function main() {
|
|
225
1069
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
226
1070
|
await server.connect(transport);
|
|
227
1071
|
console.error("[nvstudio-mcp] MCP Server aktif melalui StdioServerTransport.");
|
|
228
|
-
// Beritahu SSE plugin bahwa MCP agent sekarang aktif
|
|
1072
|
+
// Beritahu SSE/WS plugin bahwa MCP agent sekarang aktif
|
|
229
1073
|
(0, roblox_bridge_1.setMcpConnected)(true);
|
|
230
1074
|
let isShuttingDown = false;
|
|
231
1075
|
const shutdown = () => {
|
|
232
1076
|
if (isShuttingDown)
|
|
233
1077
|
return;
|
|
234
1078
|
isShuttingDown = true;
|
|
235
|
-
(0, roblox_bridge_1.setMcpConnected)(false);
|
|
1079
|
+
(0, roblox_bridge_1.setMcpConnected)(false);
|
|
236
1080
|
console.error("[nvstudio-mcp] Koneksi ditutup oleh IDE. Mematikan server...");
|
|
237
1081
|
process.exit(0);
|
|
238
1082
|
};
|
|
239
|
-
// Tangani penutupan koneksi stdin (ketika IDE menutup pipe)
|
|
240
1083
|
process.stdin.on('close', shutdown);
|
|
241
1084
|
process.stdin.on('end', shutdown);
|
|
242
1085
|
process.stdin.on('error', shutdown);
|
|
243
|
-
// Fallback dihapus karena menyebabkan disconnect palsu di beberapa environment AGY
|
|
244
1086
|
}
|
|
245
1087
|
if (isDaemon) {
|
|
246
1088
|
console.error("[nvstudio-mcp] Berjalan dalam mode DAEMON (Background Bridge Server di port 3055).");
|