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/dist/index.js CHANGED
@@ -1,1071 +1,3517 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
- import { createRequire } from 'module';
6
- import { createHttpServer } from './http-server.js';
7
- import { RobloxStudioTools } from './tools/index.js';
8
- import { BridgeService } from './bridge-service.js';
9
- const require = createRequire(import.meta.url);
10
- const { version: VERSION } = require('../package.json');
11
- class RobloxStudioMCPServer {
12
- server;
13
- tools;
14
- bridge;
15
- constructor() {
16
- this.server = new Server({
17
- name: 'robloxstudio-mcp',
18
- version: VERSION,
19
- }, {
20
- capabilities: {
21
- tools: {},
22
- },
23
- });
24
- this.bridge = new BridgeService();
25
- this.tools = new RobloxStudioTools(this.bridge);
26
- this.setupToolHandlers();
27
- }
28
- setupToolHandlers() {
29
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
30
- return {
31
- tools: [
32
- {
33
- name: 'get_file_tree',
34
- description: 'Get the Roblox instance hierarchy tree from Roblox Studio. Returns game instances (Parts, Scripts, Models, Folders, etc.) as a tree structure. NOTE: This operates on Roblox Studio instances, NOT local filesystem files.',
35
- inputSchema: {
36
- type: 'object',
37
- properties: {
38
- path: {
39
- type: 'string',
40
- description: 'Roblox instance path to start from using dot notation (e.g., "game.Workspace", "game.ServerScriptService"). Defaults to game root if empty.',
41
- default: ''
42
- }
43
- }
44
- }
45
- },
46
- {
47
- name: 'search_files',
48
- description: 'Search for Roblox instances by name, class type, or script content. NOTE: This searches Roblox Studio instances, NOT local filesystem files.',
49
- inputSchema: {
50
- type: 'object',
51
- properties: {
52
- query: {
53
- type: 'string',
54
- description: 'Search query - instance name, class type (e.g., "Script", "Part"), or Lua code pattern'
55
- },
56
- searchType: {
57
- type: 'string',
58
- enum: ['name', 'type', 'content'],
59
- description: 'Type of search: "name" for instance names, "type" for class names, "content" for script source code',
60
- default: 'name'
61
- }
62
- },
63
- required: ['query']
64
- }
65
- },
66
- {
67
- name: 'get_place_info',
68
- description: 'Get place ID, name, and game settings',
69
- inputSchema: {
70
- type: 'object',
71
- properties: {}
72
- }
73
- },
74
- {
75
- name: 'get_services',
76
- description: 'Get available Roblox services and their children',
77
- inputSchema: {
78
- type: 'object',
79
- properties: {
80
- serviceName: {
81
- type: 'string',
82
- description: 'Optional specific service name to query'
83
- }
84
- }
85
- }
86
- },
87
- {
88
- name: 'search_objects',
89
- description: 'Find instances by name, class, or properties',
90
- inputSchema: {
91
- type: 'object',
92
- properties: {
93
- query: {
94
- type: 'string',
95
- description: 'Search query'
96
- },
97
- searchType: {
98
- type: 'string',
99
- enum: ['name', 'class', 'property'],
100
- description: 'Type of search to perform',
101
- default: 'name'
102
- },
103
- propertyName: {
104
- type: 'string',
105
- description: 'Property name when searchType is "property"'
106
- }
107
- },
108
- required: ['query']
109
- }
110
- },
111
- {
112
- name: 'get_instance_properties',
113
- description: 'Get all properties of a specific Roblox instance in Studio',
114
- inputSchema: {
115
- type: 'object',
116
- properties: {
117
- instancePath: {
118
- type: 'string',
119
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part", "game.ServerScriptService.MainScript", "game.ReplicatedStorage.ModuleScript")'
120
- }
121
- },
122
- required: ['instancePath']
123
- }
124
- },
125
- {
126
- name: 'get_instance_children',
127
- description: 'Get child instances and their class types from a Roblox parent instance',
128
- inputSchema: {
129
- type: 'object',
130
- properties: {
131
- instancePath: {
132
- type: 'string',
133
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace", "game.ServerScriptService")'
134
- }
135
- },
136
- required: ['instancePath']
137
- }
138
- },
139
- {
140
- name: 'search_by_property',
141
- description: 'Find objects with specific property values',
142
- inputSchema: {
143
- type: 'object',
144
- properties: {
145
- propertyName: {
146
- type: 'string',
147
- description: 'Name of the property to search'
148
- },
149
- propertyValue: {
150
- type: 'string',
151
- description: 'Value to search for'
152
- }
153
- },
154
- required: ['propertyName', 'propertyValue']
155
- }
156
- },
157
- {
158
- name: 'get_class_info',
159
- description: 'Get available properties/methods for Roblox classes',
160
- inputSchema: {
161
- type: 'object',
162
- properties: {
163
- className: {
164
- type: 'string',
165
- description: 'Roblox class name'
166
- }
167
- },
168
- required: ['className']
169
- }
170
- },
171
- {
172
- name: 'get_project_structure',
173
- description: 'Get complete game hierarchy. IMPORTANT: Use maxDepth parameter (default: 3) to explore deeper levels of the hierarchy. Set higher values like 5-10 for comprehensive exploration',
174
- inputSchema: {
175
- type: 'object',
176
- properties: {
177
- path: {
178
- type: 'string',
179
- description: 'Optional path to start from (defaults to workspace root)',
180
- default: ''
181
- },
182
- maxDepth: {
183
- type: 'number',
184
- description: 'Maximum depth to traverse (default: 3). RECOMMENDED: Use 5-10 for thorough exploration. Higher values provide more complete structure',
185
- default: 3
186
- },
187
- scriptsOnly: {
188
- type: 'boolean',
189
- description: 'Show only scripts and script containers',
190
- default: false
191
- }
192
- }
193
- }
194
- },
195
- {
196
- name: 'set_property',
197
- description: 'Set a property on any Roblox instance',
198
- inputSchema: {
199
- type: 'object',
200
- properties: {
201
- instancePath: {
202
- type: 'string',
203
- description: 'Path to the instance (e.g., "game.Workspace.Part")'
204
- },
205
- propertyName: {
206
- type: 'string',
207
- description: 'Name of the property to set'
208
- },
209
- propertyValue: {
210
- description: 'Value to set the property to (any type)'
211
- }
212
- },
213
- required: ['instancePath', 'propertyName', 'propertyValue']
214
- }
215
- },
216
- {
217
- name: 'mass_set_property',
218
- description: 'Set the same property on multiple instances at once',
219
- inputSchema: {
220
- type: 'object',
221
- properties: {
222
- paths: {
223
- type: 'array',
224
- items: { type: 'string' },
225
- description: 'Array of instance paths to modify'
226
- },
227
- propertyName: {
228
- type: 'string',
229
- description: 'Name of the property to set'
230
- },
231
- propertyValue: {
232
- description: 'Value to set the property to (any type)'
233
- }
234
- },
235
- required: ['paths', 'propertyName', 'propertyValue']
236
- }
237
- },
238
- {
239
- name: 'mass_get_property',
240
- description: 'Get the same property from multiple instances at once',
241
- inputSchema: {
242
- type: 'object',
243
- properties: {
244
- paths: {
245
- type: 'array',
246
- items: { type: 'string' },
247
- description: 'Array of instance paths to read from'
248
- },
249
- propertyName: {
250
- type: 'string',
251
- description: 'Name of the property to get'
252
- }
253
- },
254
- required: ['paths', 'propertyName']
255
- }
256
- },
257
- {
258
- name: 'create_object',
259
- description: 'Create a new Roblox object instance (basic, without properties)',
260
- inputSchema: {
261
- type: 'object',
262
- properties: {
263
- className: {
264
- type: 'string',
265
- description: 'Roblox class name (e.g., "Part", "Script", "Folder")'
266
- },
267
- parent: {
268
- type: 'string',
269
- description: 'Path to the parent instance (e.g., "game.Workspace")'
270
- },
271
- name: {
272
- type: 'string',
273
- description: 'Optional name for the new object'
274
- }
275
- },
276
- required: ['className', 'parent']
277
- }
278
- },
279
- {
280
- name: 'create_object_with_properties',
281
- description: 'Create a new Roblox object instance with initial properties',
282
- inputSchema: {
283
- type: 'object',
284
- properties: {
285
- className: {
286
- type: 'string',
287
- description: 'Roblox class name (e.g., "Part", "Script", "Folder")'
288
- },
289
- parent: {
290
- type: 'string',
291
- description: 'Path to the parent instance (e.g., "game.Workspace")'
292
- },
293
- name: {
294
- type: 'string',
295
- description: 'Optional name for the new object'
296
- },
297
- properties: {
298
- type: 'object',
299
- description: 'Properties to set on creation'
300
- }
301
- },
302
- required: ['className', 'parent']
303
- }
304
- },
305
- {
306
- name: 'mass_create_objects',
307
- description: 'Create multiple objects at once (basic, without properties)',
308
- inputSchema: {
309
- type: 'object',
310
- properties: {
311
- objects: {
312
- type: 'array',
313
- items: {
314
- type: 'object',
315
- properties: {
316
- className: {
317
- type: 'string',
318
- description: 'Roblox class name'
319
- },
320
- parent: {
321
- type: 'string',
322
- description: 'Path to the parent instance'
323
- },
324
- name: {
325
- type: 'string',
326
- description: 'Optional name for the object'
327
- }
328
- },
329
- required: ['className', 'parent']
330
- },
331
- description: 'Array of objects to create'
332
- }
333
- },
334
- required: ['objects']
335
- }
336
- },
337
- {
338
- name: 'mass_create_objects_with_properties',
339
- description: 'Create multiple objects at once with initial properties',
340
- inputSchema: {
341
- type: 'object',
342
- properties: {
343
- objects: {
344
- type: 'array',
345
- items: {
346
- type: 'object',
347
- properties: {
348
- className: {
349
- type: 'string',
350
- description: 'Roblox class name'
351
- },
352
- parent: {
353
- type: 'string',
354
- description: 'Path to the parent instance'
355
- },
356
- name: {
357
- type: 'string',
358
- description: 'Optional name for the object'
359
- },
360
- properties: {
361
- type: 'object',
362
- description: 'Properties to set on creation'
363
- }
364
- },
365
- required: ['className', 'parent']
366
- },
367
- description: 'Array of objects to create with properties'
368
- }
369
- },
370
- required: ['objects']
371
- }
372
- },
373
- {
374
- name: 'delete_object',
375
- description: 'Delete a Roblox object instance',
376
- inputSchema: {
377
- type: 'object',
378
- properties: {
379
- instancePath: {
380
- type: 'string',
381
- description: 'Path to the instance to delete'
382
- }
383
- },
384
- required: ['instancePath']
385
- }
386
- },
387
- {
388
- name: 'smart_duplicate',
389
- description: 'Smart duplication with automatic naming, positioning, and property variations',
390
- inputSchema: {
391
- type: 'object',
392
- properties: {
393
- instancePath: {
394
- type: 'string',
395
- description: 'Path to the instance to duplicate'
396
- },
397
- count: {
398
- type: 'number',
399
- description: 'Number of duplicates to create'
400
- },
401
- options: {
402
- type: 'object',
403
- properties: {
404
- namePattern: {
405
- type: 'string',
406
- description: 'Name pattern with {n} placeholder (e.g., "Button{n}")'
407
- },
408
- positionOffset: {
409
- type: 'array',
410
- items: { type: 'number' },
411
- minItems: 3,
412
- maxItems: 3,
413
- description: 'X, Y, Z offset per duplicate'
414
- },
415
- rotationOffset: {
416
- type: 'array',
417
- items: { type: 'number' },
418
- minItems: 3,
419
- maxItems: 3,
420
- description: 'X, Y, Z rotation offset per duplicate'
421
- },
422
- scaleOffset: {
423
- type: 'array',
424
- items: { type: 'number' },
425
- minItems: 3,
426
- maxItems: 3,
427
- description: 'X, Y, Z scale multiplier per duplicate'
428
- },
429
- propertyVariations: {
430
- type: 'object',
431
- description: 'Property name to array of values'
432
- },
433
- targetParents: {
434
- type: 'array',
435
- items: { type: 'string' },
436
- description: 'Different parent for each duplicate'
437
- }
438
- }
439
- }
440
- },
441
- required: ['instancePath', 'count']
442
- }
443
- },
444
- {
445
- name: 'mass_duplicate',
446
- description: 'Perform multiple smart duplications at once',
447
- inputSchema: {
448
- type: 'object',
449
- properties: {
450
- duplications: {
451
- type: 'array',
452
- items: {
453
- type: 'object',
454
- properties: {
455
- instancePath: {
456
- type: 'string',
457
- description: 'Path to the instance to duplicate'
458
- },
459
- count: {
460
- type: 'number',
461
- description: 'Number of duplicates to create'
462
- },
463
- options: {
464
- type: 'object',
465
- properties: {
466
- namePattern: {
467
- type: 'string',
468
- description: 'Name pattern with {n} placeholder'
469
- },
470
- positionOffset: {
471
- type: 'array',
472
- items: { type: 'number' },
473
- minItems: 3,
474
- maxItems: 3,
475
- description: 'X, Y, Z offset per duplicate'
476
- },
477
- rotationOffset: {
478
- type: 'array',
479
- items: { type: 'number' },
480
- minItems: 3,
481
- maxItems: 3,
482
- description: 'X, Y, Z rotation offset per duplicate'
483
- },
484
- scaleOffset: {
485
- type: 'array',
486
- items: { type: 'number' },
487
- minItems: 3,
488
- maxItems: 3,
489
- description: 'X, Y, Z scale multiplier per duplicate'
490
- },
491
- propertyVariations: {
492
- type: 'object',
493
- description: 'Property name to array of values'
494
- },
495
- targetParents: {
496
- type: 'array',
497
- items: { type: 'string' },
498
- description: 'Different parent for each duplicate'
499
- }
500
- }
501
- }
502
- },
503
- required: ['instancePath', 'count']
504
- },
505
- description: 'Array of duplication operations'
506
- }
507
- },
508
- required: ['duplications']
509
- }
510
- },
511
- {
512
- name: 'set_calculated_property',
513
- description: 'Set properties using mathematical formulas and variables',
514
- inputSchema: {
515
- type: 'object',
516
- properties: {
517
- paths: {
518
- type: 'array',
519
- items: { type: 'string' },
520
- description: 'Array of instance paths to modify'
521
- },
522
- propertyName: {
523
- type: 'string',
524
- description: 'Name of the property to set'
525
- },
526
- formula: {
527
- type: 'string',
528
- description: 'Mathematical formula (e.g., "Position.magnitude * 2", "index * 50")'
529
- },
530
- variables: {
531
- type: 'object',
532
- description: 'Additional variables for the formula'
533
- }
534
- },
535
- required: ['paths', 'propertyName', 'formula']
536
- }
537
- },
538
- {
539
- name: 'set_relative_property',
540
- description: 'Modify properties relative to their current values',
541
- inputSchema: {
542
- type: 'object',
543
- properties: {
544
- paths: {
545
- type: 'array',
546
- items: { type: 'string' },
547
- description: 'Array of instance paths to modify'
548
- },
549
- propertyName: {
550
- type: 'string',
551
- description: 'Name of the property to modify'
552
- },
553
- operation: {
554
- type: 'string',
555
- enum: ['add', 'multiply', 'divide', 'subtract', 'power'],
556
- description: 'Mathematical operation to perform'
557
- },
558
- value: {
559
- description: 'Value to use in the operation'
560
- },
561
- component: {
562
- type: 'string',
563
- enum: ['X', 'Y', 'Z', 'XScale', 'XOffset', 'YScale', 'YOffset'],
564
- description: 'For Vector3: X, Y, Z. For UDim2: XScale, XOffset, YScale, YOffset (value must be a number)'
565
- }
566
- },
567
- required: ['paths', 'propertyName', 'operation', 'value']
568
- }
569
- },
570
- {
571
- name: 'get_script_source',
572
- description: 'Get the source code of a Roblox script (LocalScript, Script, or ModuleScript). Returns both "source" (raw code) and "numberedSource" (with line numbers prefixed like "1: code"). Use numberedSource to accurately identify line numbers for editing. For large scripts (>1500 lines), use startLine/endLine to read specific sections.',
573
- inputSchema: {
574
- type: 'object',
575
- properties: {
576
- instancePath: {
577
- type: 'string',
578
- description: 'Roblox instance path to the script using dot notation (e.g., "game.ServerScriptService.MainScript", "game.StarterPlayer.StarterPlayerScripts.LocalScript")'
579
- },
580
- startLine: {
581
- type: 'number',
582
- description: 'Optional: Start line number (1-indexed). Use for reading specific sections of large scripts.'
583
- },
584
- endLine: {
585
- type: 'number',
586
- description: 'Optional: End line number (inclusive). Use for reading specific sections of large scripts.'
587
- }
588
- },
589
- required: ['instancePath']
590
- }
591
- },
592
- {
593
- name: 'set_script_source',
594
- description: 'Replace the entire source code of a Roblox script. Uses ScriptEditorService:UpdateSourceAsync (works with open editors). For partial edits, prefer edit_script_lines, insert_script_lines, or delete_script_lines.',
595
- inputSchema: {
596
- type: 'object',
597
- properties: {
598
- instancePath: {
599
- type: 'string',
600
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
601
- },
602
- source: {
603
- type: 'string',
604
- description: 'New source code for the script'
605
- }
606
- },
607
- required: ['instancePath', 'source']
608
- }
609
- },
610
- {
611
- name: 'edit_script_lines',
612
- description: 'Replace specific lines in a Roblox script without rewriting the entire source. IMPORTANT: Use the "numberedSource" field from get_script_source to identify the correct line numbers. Lines are 1-indexed and ranges are inclusive.',
613
- inputSchema: {
614
- type: 'object',
615
- properties: {
616
- instancePath: {
617
- type: 'string',
618
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
619
- },
620
- startLine: {
621
- type: 'number',
622
- description: 'First line to replace (1-indexed). Get this from the "numberedSource" field.'
623
- },
624
- endLine: {
625
- type: 'number',
626
- description: 'Last line to replace (inclusive). Get this from the "numberedSource" field.'
627
- },
628
- newContent: {
629
- type: 'string',
630
- description: 'New content to replace the specified lines (can be multiple lines separated by newlines)'
631
- }
632
- },
633
- required: ['instancePath', 'startLine', 'endLine', 'newContent']
634
- }
635
- },
636
- {
637
- name: 'insert_script_lines',
638
- description: 'Insert new lines into a Roblox script at a specific position. IMPORTANT: Use the "numberedSource" field from get_script_source to identify the correct line numbers.',
639
- inputSchema: {
640
- type: 'object',
641
- properties: {
642
- instancePath: {
643
- type: 'string',
644
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
645
- },
646
- afterLine: {
647
- type: 'number',
648
- description: 'Insert after this line number (0 = insert at very beginning, 1 = after first line). Get line numbers from "numberedSource".',
649
- default: 0
650
- },
651
- newContent: {
652
- type: 'string',
653
- description: 'Content to insert (can be multiple lines separated by newlines)'
654
- }
655
- },
656
- required: ['instancePath', 'newContent']
657
- }
658
- },
659
- {
660
- name: 'delete_script_lines',
661
- description: 'Delete specific lines from a Roblox script. IMPORTANT: Use the "numberedSource" field from get_script_source to identify the correct line numbers.',
662
- inputSchema: {
663
- type: 'object',
664
- properties: {
665
- instancePath: {
666
- type: 'string',
667
- description: 'Roblox instance path to the script (e.g., "game.ServerScriptService.MainScript")'
668
- },
669
- startLine: {
670
- type: 'number',
671
- description: 'First line to delete (1-indexed). Get this from the "numberedSource" field.'
672
- },
673
- endLine: {
674
- type: 'number',
675
- description: 'Last line to delete (inclusive). Get this from the "numberedSource" field.'
676
- }
677
- },
678
- required: ['instancePath', 'startLine', 'endLine']
679
- }
680
- },
681
- {
682
- name: 'get_attribute',
683
- description: 'Get a single attribute value from a Roblox instance',
684
- inputSchema: {
685
- type: 'object',
686
- properties: {
687
- instancePath: {
688
- type: 'string',
689
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part", "game.ServerStorage.DataStore")'
690
- },
691
- attributeName: {
692
- type: 'string',
693
- description: 'Name of the attribute to get'
694
- }
695
- },
696
- required: ['instancePath', 'attributeName']
697
- }
698
- },
699
- {
700
- name: 'set_attribute',
701
- description: 'Set an attribute value on a Roblox instance. Supports string, number, boolean, Vector3, Color3, UDim2, and BrickColor.',
702
- inputSchema: {
703
- type: 'object',
704
- properties: {
705
- instancePath: {
706
- type: 'string',
707
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
708
- },
709
- attributeName: {
710
- type: 'string',
711
- description: 'Name of the attribute to set'
712
- },
713
- attributeValue: {
714
- description: 'Value to set. For Vector3: {X, Y, Z}, Color3: {R, G, B}, UDim2: {X: {Scale, Offset}, Y: {Scale, Offset}}'
715
- },
716
- valueType: {
717
- type: 'string',
718
- description: 'Optional type hint: "Vector3", "Color3", "UDim2", "BrickColor"'
719
- }
720
- },
721
- required: ['instancePath', 'attributeName', 'attributeValue']
722
- }
723
- },
724
- {
725
- name: 'get_attributes',
726
- description: 'Get all attributes on a Roblox instance',
727
- inputSchema: {
728
- type: 'object',
729
- properties: {
730
- instancePath: {
731
- type: 'string',
732
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
733
- }
734
- },
735
- required: ['instancePath']
736
- }
737
- },
738
- {
739
- name: 'delete_attribute',
740
- description: 'Delete an attribute from a Roblox instance',
741
- inputSchema: {
742
- type: 'object',
743
- properties: {
744
- instancePath: {
745
- type: 'string',
746
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
747
- },
748
- attributeName: {
749
- type: 'string',
750
- description: 'Name of the attribute to delete'
751
- }
752
- },
753
- required: ['instancePath', 'attributeName']
754
- }
755
- },
756
- {
757
- name: 'get_tags',
758
- description: 'Get all CollectionService tags on a Roblox instance',
759
- inputSchema: {
760
- type: 'object',
761
- properties: {
762
- instancePath: {
763
- type: 'string',
764
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
765
- }
766
- },
767
- required: ['instancePath']
768
- }
769
- },
770
- {
771
- name: 'add_tag',
772
- description: 'Add a CollectionService tag to a Roblox instance',
773
- inputSchema: {
774
- type: 'object',
775
- properties: {
776
- instancePath: {
777
- type: 'string',
778
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
779
- },
780
- tagName: {
781
- type: 'string',
782
- description: 'Name of the tag to add'
783
- }
784
- },
785
- required: ['instancePath', 'tagName']
786
- }
787
- },
788
- {
789
- name: 'remove_tag',
790
- description: 'Remove a CollectionService tag from a Roblox instance',
791
- inputSchema: {
792
- type: 'object',
793
- properties: {
794
- instancePath: {
795
- type: 'string',
796
- description: 'Roblox instance path using dot notation (e.g., "game.Workspace.Part")'
797
- },
798
- tagName: {
799
- type: 'string',
800
- description: 'Name of the tag to remove'
801
- }
802
- },
803
- required: ['instancePath', 'tagName']
804
- }
805
- },
806
- {
807
- name: 'get_tagged',
808
- description: 'Get all instances with a specific tag',
809
- inputSchema: {
810
- type: 'object',
811
- properties: {
812
- tagName: {
813
- type: 'string',
814
- description: 'Name of the tag to search for'
815
- }
816
- },
817
- required: ['tagName']
818
- }
819
- },
820
- {
821
- name: 'get_selection',
822
- description: 'Get all currently selected objects',
823
- inputSchema: {
824
- type: 'object',
825
- properties: {}
826
- }
827
- },
828
- {
829
- name: 'execute_luau',
830
- description: 'Execute arbitrary Luau code in Roblox Studio and return the result. The code runs in the plugin context with access to game, workspace, and all services. Use print() or warn() to produce output. The return value of the code (if any) is captured. Useful for querying game state, running one-off operations, or testing logic.',
831
- inputSchema: {
832
- type: 'object',
833
- properties: {
834
- code: {
835
- type: 'string',
836
- description: 'Luau code to execute. Can use print() for output. The return value is captured.'
837
- }
838
- },
839
- required: ['code']
840
- }
841
- },
842
- {
843
- name: 'start_playtest',
844
- description: 'Start a play test session in Roblox Studio. Captures all output (print/warn/error) from LogService. Use get_playtest_output to poll for logs while running, then stop_playtest to end. Typical workflow: add print/warn statements to code, start playtest, poll output to observe behavior, stop, analyze logs, fix issues with set_script_source, and repeat until correct.',
845
- inputSchema: {
846
- type: 'object',
847
- properties: {
848
- mode: {
849
- type: 'string',
850
- enum: ['play', 'run'],
851
- description: '"play" for Play Solo mode, "run" for Run mode'
852
- }
853
- },
854
- required: ['mode']
855
- }
856
- },
857
- {
858
- name: 'stop_playtest',
859
- description: 'Stop the running play test session and return all captured output. Call this after observing enough output via get_playtest_output, or when you need to make code changes before the next run.',
860
- inputSchema: {
861
- type: 'object',
862
- properties: {}
863
- }
864
- },
865
- {
866
- name: 'get_playtest_output',
867
- description: 'Poll the output buffer without stopping the test. Returns isRunning, captured print/warn/error messages, and any test result. Call repeatedly to monitor a running session — useful for waiting on specific log output or checking if errors have occurred.',
868
- inputSchema: {
869
- type: 'object',
870
- properties: {}
871
- }
872
- }
873
- ]
874
- };
875
- });
876
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
877
- const { name, arguments: args } = request.params;
878
- try {
879
- switch (name) {
880
- case 'get_file_tree':
881
- return await this.tools.getFileTree(args?.path || '');
882
- case 'search_files':
883
- return await this.tools.searchFiles(args?.query, args?.searchType || 'name');
884
- case 'get_place_info':
885
- return await this.tools.getPlaceInfo();
886
- case 'get_services':
887
- return await this.tools.getServices(args?.serviceName);
888
- case 'search_objects':
889
- return await this.tools.searchObjects(args?.query, args?.searchType || 'name', args?.propertyName);
890
- case 'get_instance_properties':
891
- return await this.tools.getInstanceProperties(args?.instancePath);
892
- case 'get_instance_children':
893
- return await this.tools.getInstanceChildren(args?.instancePath);
894
- case 'search_by_property':
895
- return await this.tools.searchByProperty(args?.propertyName, args?.propertyValue);
896
- case 'get_class_info':
897
- return await this.tools.getClassInfo(args?.className);
898
- case 'get_project_structure':
899
- return await this.tools.getProjectStructure(args?.path, args?.maxDepth, args?.scriptsOnly);
900
- case 'set_property':
901
- return await this.tools.setProperty(args?.instancePath, args?.propertyName, args?.propertyValue);
902
- case 'mass_set_property':
903
- return await this.tools.massSetProperty(args?.paths, args?.propertyName, args?.propertyValue);
904
- case 'mass_get_property':
905
- return await this.tools.massGetProperty(args?.paths, args?.propertyName);
906
- case 'create_object':
907
- return await this.tools.createObject(args?.className, args?.parent, args?.name);
908
- case 'create_object_with_properties':
909
- return await this.tools.createObjectWithProperties(args?.className, args?.parent, args?.name, args?.properties);
910
- case 'mass_create_objects':
911
- return await this.tools.massCreateObjects(args?.objects);
912
- case 'mass_create_objects_with_properties':
913
- return await this.tools.massCreateObjectsWithProperties(args?.objects);
914
- case 'delete_object':
915
- return await this.tools.deleteObject(args?.instancePath);
916
- case 'smart_duplicate':
917
- return await this.tools.smartDuplicate(args?.instancePath, args?.count, args?.options);
918
- case 'mass_duplicate':
919
- return await this.tools.massDuplicate(args?.duplications);
920
- case 'set_calculated_property':
921
- return await this.tools.setCalculatedProperty(args?.paths, args?.propertyName, args?.formula, args?.variables);
922
- case 'set_relative_property':
923
- return await this.tools.setRelativeProperty(args?.paths, args?.propertyName, args?.operation, args?.value, args?.component);
924
- case 'get_script_source':
925
- return await this.tools.getScriptSource(args?.instancePath, args?.startLine, args?.endLine);
926
- case 'set_script_source':
927
- return await this.tools.setScriptSource(args?.instancePath, args?.source);
928
- case 'edit_script_lines':
929
- return await this.tools.editScriptLines(args?.instancePath, args?.startLine, args?.endLine, args?.newContent);
930
- case 'insert_script_lines':
931
- return await this.tools.insertScriptLines(args?.instancePath, args?.afterLine, args?.newContent);
932
- case 'delete_script_lines':
933
- return await this.tools.deleteScriptLines(args?.instancePath, args?.startLine, args?.endLine);
934
- case 'get_attribute':
935
- return await this.tools.getAttribute(args?.instancePath, args?.attributeName);
936
- case 'set_attribute':
937
- return await this.tools.setAttribute(args?.instancePath, args?.attributeName, args?.attributeValue, args?.valueType);
938
- case 'get_attributes':
939
- return await this.tools.getAttributes(args?.instancePath);
940
- case 'delete_attribute':
941
- return await this.tools.deleteAttribute(args?.instancePath, args?.attributeName);
942
- case 'get_tags':
943
- return await this.tools.getTags(args?.instancePath);
944
- case 'add_tag':
945
- return await this.tools.addTag(args?.instancePath, args?.tagName);
946
- case 'remove_tag':
947
- return await this.tools.removeTag(args?.instancePath, args?.tagName);
948
- case 'get_tagged':
949
- return await this.tools.getTagged(args?.tagName);
950
- case 'get_selection':
951
- return await this.tools.getSelection();
952
- case 'execute_luau':
953
- return await this.tools.executeLuau(args?.code);
954
- case 'start_playtest':
955
- return await this.tools.startPlaytest(args?.mode);
956
- case 'stop_playtest':
957
- return await this.tools.stopPlaytest();
958
- case 'get_playtest_output':
959
- return await this.tools.getPlaytestOutput();
960
- default:
961
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
962
- }
963
- }
964
- catch (error) {
965
- throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
2
+
3
+ // ../../node_modules/@robloxstudio-mcp/core/dist/server.js
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
7
+
8
+ // ../../node_modules/@robloxstudio-mcp/core/dist/http-server.js
9
+ import express from "express";
10
+ import cors from "cors";
11
+ import http from "http";
12
+ var TOOL_HANDLERS = {
13
+ get_file_tree: (tools, body) => tools.getFileTree(body.path),
14
+ search_files: (tools, body) => tools.searchFiles(body.query, body.searchType),
15
+ get_place_info: (tools) => tools.getPlaceInfo(),
16
+ get_services: (tools, body) => tools.getServices(body.serviceName),
17
+ search_objects: (tools, body) => tools.searchObjects(body.query, body.searchType, body.propertyName),
18
+ get_instance_properties: (tools, body) => tools.getInstanceProperties(body.instancePath, body.excludeSource),
19
+ get_instance_children: (tools, body) => tools.getInstanceChildren(body.instancePath),
20
+ search_by_property: (tools, body) => tools.searchByProperty(body.propertyName, body.propertyValue),
21
+ get_class_info: (tools, body) => tools.getClassInfo(body.className),
22
+ get_project_structure: (tools, body) => tools.getProjectStructure(body.path, body.maxDepth, body.scriptsOnly),
23
+ set_property: (tools, body) => tools.setProperty(body.instancePath, body.propertyName, body.propertyValue),
24
+ mass_set_property: (tools, body) => tools.massSetProperty(body.paths, body.propertyName, body.propertyValue),
25
+ mass_get_property: (tools, body) => tools.massGetProperty(body.paths, body.propertyName),
26
+ create_object: (tools, body) => tools.createObject(body.className, body.parent, body.name, body.properties),
27
+ create_object_with_properties: (tools, body) => tools.createObject(body.className, body.parent, body.name, body.properties),
28
+ mass_create_objects: (tools, body) => tools.massCreateObjects(body.objects),
29
+ mass_create_objects_with_properties: (tools, body) => tools.massCreateObjects(body.objects),
30
+ delete_object: (tools, body) => tools.deleteObject(body.instancePath),
31
+ smart_duplicate: (tools, body) => tools.smartDuplicate(body.instancePath, body.count, body.options),
32
+ mass_duplicate: (tools, body) => tools.massDuplicate(body.duplications),
33
+ set_calculated_property: (tools, body) => tools.setCalculatedProperty(body.paths, body.propertyName, body.formula, body.variables),
34
+ set_relative_property: (tools, body) => tools.setRelativeProperty(body.paths, body.propertyName, body.operation, body.value, body.component),
35
+ grep_scripts: (tools, body) => tools.grepScripts(body.pattern, {
36
+ caseSensitive: body.caseSensitive,
37
+ usePattern: body.usePattern,
38
+ contextLines: body.contextLines,
39
+ maxResults: body.maxResults,
40
+ maxResultsPerScript: body.maxResultsPerScript,
41
+ filesOnly: body.filesOnly,
42
+ path: body.path,
43
+ classFilter: body.classFilter
44
+ }),
45
+ get_script_source: (tools, body) => tools.getScriptSource(body.instancePath, body.startLine, body.endLine),
46
+ set_script_source: (tools, body) => tools.setScriptSource(body.instancePath, body.source),
47
+ edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.startLine, body.endLine, body.newContent),
48
+ insert_script_lines: (tools, body) => tools.insertScriptLines(body.instancePath, body.afterLine, body.newContent),
49
+ delete_script_lines: (tools, body) => tools.deleteScriptLines(body.instancePath, body.startLine, body.endLine),
50
+ get_attribute: (tools, body) => tools.getAttribute(body.instancePath, body.attributeName),
51
+ set_attribute: (tools, body) => tools.setAttribute(body.instancePath, body.attributeName, body.attributeValue, body.valueType),
52
+ get_attributes: (tools, body) => tools.getAttributes(body.instancePath),
53
+ delete_attribute: (tools, body) => tools.deleteAttribute(body.instancePath, body.attributeName),
54
+ get_tags: (tools, body) => tools.getTags(body.instancePath),
55
+ add_tag: (tools, body) => tools.addTag(body.instancePath, body.tagName),
56
+ remove_tag: (tools, body) => tools.removeTag(body.instancePath, body.tagName),
57
+ get_tagged: (tools, body) => tools.getTagged(body.tagName),
58
+ get_selection: (tools) => tools.getSelection(),
59
+ execute_luau: (tools, body) => tools.executeLuau(body.code),
60
+ start_playtest: (tools, body) => tools.startPlaytest(body.mode),
61
+ stop_playtest: (tools) => tools.stopPlaytest(),
62
+ get_playtest_output: (tools) => tools.getPlaytestOutput(),
63
+ export_build: (tools, body) => tools.exportBuild(body.instancePath, body.outputId, body.style),
64
+ create_build: (tools, body) => tools.createBuild(body.id, body.style, body.palette, body.parts, body.bounds),
65
+ generate_build: (tools, body) => tools.generateBuild(body.id, body.style, body.palette, body.code, body.seed),
66
+ import_build: (tools, body) => tools.importBuild(body.buildData, body.targetPath, body.position),
67
+ list_library: (tools, body) => tools.listLibrary(body.style),
68
+ search_materials: (tools, body) => tools.searchMaterials(body.query, body.maxResults),
69
+ get_build: (tools, body) => tools.getBuild(body.id),
70
+ import_scene: (tools, body) => tools.importScene(body.sceneData, body.targetPath),
71
+ undo: (tools) => tools.undo(),
72
+ redo: (tools) => tools.redo(),
73
+ search_assets: (tools, body) => tools.searchAssets(body.assetType, body.query, body.maxResults, body.sortBy, body.verifiedCreatorsOnly),
74
+ get_asset_details: (tools, body) => tools.getAssetDetails(body.assetId),
75
+ get_asset_thumbnail: (tools, body) => tools.getAssetThumbnail(body.assetId, body.size),
76
+ insert_asset: (tools, body) => tools.insertAsset(body.assetId, body.parentPath, body.position),
77
+ preview_asset: (tools, body) => tools.previewAsset(body.assetId, body.includeProperties, body.maxDepth)
78
+ };
79
+ function createHttpServer(tools, bridge, allowedTools) {
80
+ const app = express();
81
+ let pluginConnected = false;
82
+ let mcpServerActive = false;
83
+ let lastMCPActivity = 0;
84
+ let mcpServerStartTime = 0;
85
+ let lastPluginActivity = 0;
86
+ const proxyInstances = /* @__PURE__ */ new Set();
87
+ const setMCPServerActive = (active) => {
88
+ mcpServerActive = active;
89
+ if (active) {
90
+ mcpServerStartTime = Date.now();
91
+ lastMCPActivity = Date.now();
92
+ } else {
93
+ mcpServerStartTime = 0;
94
+ lastMCPActivity = 0;
95
+ }
96
+ };
97
+ const trackMCPActivity = () => {
98
+ if (mcpServerActive) {
99
+ lastMCPActivity = Date.now();
100
+ }
101
+ };
102
+ const isMCPServerActive = () => {
103
+ if (!mcpServerActive)
104
+ return false;
105
+ return Date.now() - lastMCPActivity < 3e4;
106
+ };
107
+ const isPluginConnected = () => {
108
+ return pluginConnected && Date.now() - lastPluginActivity < 3e4;
109
+ };
110
+ app.use(cors());
111
+ app.use(express.json({ limit: "50mb" }));
112
+ app.use(express.urlencoded({ limit: "50mb", extended: true }));
113
+ app.get("/health", (req, res) => {
114
+ res.json({
115
+ status: "ok",
116
+ service: "robloxstudio-mcp",
117
+ pluginConnected,
118
+ mcpServerActive: isMCPServerActive(),
119
+ uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0,
120
+ proxyInstanceCount: proxyInstances.size
121
+ });
122
+ });
123
+ app.post("/ready", (req, res) => {
124
+ pluginConnected = true;
125
+ lastPluginActivity = Date.now();
126
+ res.json({ success: true });
127
+ });
128
+ app.post("/disconnect", (req, res) => {
129
+ pluginConnected = false;
130
+ bridge.clearAllPendingRequests();
131
+ res.json({ success: true });
132
+ });
133
+ app.get("/status", (req, res) => {
134
+ res.json({
135
+ pluginConnected: isPluginConnected(),
136
+ mcpServerActive: isMCPServerActive(),
137
+ lastMCPActivity,
138
+ uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0
139
+ });
140
+ });
141
+ app.get("/poll", (req, res) => {
142
+ if (!pluginConnected) {
143
+ pluginConnected = true;
144
+ }
145
+ lastPluginActivity = Date.now();
146
+ if (!isMCPServerActive()) {
147
+ res.status(503).json({
148
+ error: "MCP server not connected",
149
+ pluginConnected: true,
150
+ mcpConnected: false,
151
+ request: null
152
+ });
153
+ return;
154
+ }
155
+ const pendingRequest = bridge.getPendingRequest();
156
+ if (pendingRequest) {
157
+ res.json({
158
+ request: pendingRequest.request,
159
+ requestId: pendingRequest.requestId,
160
+ mcpConnected: true,
161
+ pluginConnected: true,
162
+ proxyInstanceCount: proxyInstances.size
163
+ });
164
+ } else {
165
+ res.json({
166
+ request: null,
167
+ mcpConnected: true,
168
+ pluginConnected: true,
169
+ proxyInstanceCount: proxyInstances.size
170
+ });
171
+ }
172
+ });
173
+ app.post("/response", (req, res) => {
174
+ const { requestId, response, error } = req.body;
175
+ if (error) {
176
+ bridge.rejectRequest(requestId, error);
177
+ } else {
178
+ bridge.resolveRequest(requestId, response);
179
+ }
180
+ res.json({ success: true });
181
+ });
182
+ app.post("/proxy", async (req, res) => {
183
+ const { endpoint, data, proxyInstanceId } = req.body;
184
+ if (!endpoint) {
185
+ res.status(400).json({ error: "endpoint is required" });
186
+ return;
187
+ }
188
+ if (proxyInstanceId) {
189
+ proxyInstances.add(proxyInstanceId);
190
+ }
191
+ try {
192
+ const response = await bridge.sendRequest(endpoint, data);
193
+ res.json({ response });
194
+ } catch (err) {
195
+ res.status(500).json({ error: err.message || "Proxy request failed" });
196
+ }
197
+ });
198
+ app.use("/mcp/*", (req, res, next) => {
199
+ trackMCPActivity();
200
+ next();
201
+ });
202
+ for (const [toolName, handler] of Object.entries(TOOL_HANDLERS)) {
203
+ if (allowedTools && !allowedTools.has(toolName))
204
+ continue;
205
+ app.post(`/mcp/${toolName}`, async (req, res) => {
206
+ try {
207
+ const result = await handler(tools, req.body);
208
+ res.json(result);
209
+ } catch (error) {
210
+ res.status(500).json({ error: error instanceof Error ? error.message : "Unknown error" });
211
+ }
212
+ });
213
+ }
214
+ app.isPluginConnected = isPluginConnected;
215
+ app.setMCPServerActive = setMCPServerActive;
216
+ app.isMCPServerActive = isMCPServerActive;
217
+ app.trackMCPActivity = trackMCPActivity;
218
+ return app;
219
+ }
220
+ function listenWithRetry(app, host, startPort, maxAttempts = 5) {
221
+ return new Promise(async (resolve, reject) => {
222
+ for (let i = 0; i < maxAttempts; i++) {
223
+ const port = startPort + i;
224
+ try {
225
+ const server2 = await bindPort(app, host, port);
226
+ resolve({ server: server2, port });
227
+ return;
228
+ } catch (err) {
229
+ if (err.code === "EADDRINUSE") {
230
+ console.error(`Port ${port} in use, trying next...`);
231
+ continue;
232
+ }
233
+ reject(err);
234
+ return;
235
+ }
236
+ }
237
+ reject(new Error(`All ports ${startPort}-${startPort + maxAttempts - 1} are in use. Stop some MCP server instances and retry.`));
238
+ });
239
+ }
240
+ function bindPort(app, host, port) {
241
+ return new Promise((resolve, reject) => {
242
+ const server2 = http.createServer(app);
243
+ const onError = (err) => {
244
+ server2.removeListener("error", onError);
245
+ reject(err);
246
+ };
247
+ server2.once("error", onError);
248
+ server2.listen(port, host, () => {
249
+ server2.removeListener("error", onError);
250
+ resolve(server2);
251
+ });
252
+ });
253
+ }
254
+
255
+ // ../../node_modules/@robloxstudio-mcp/core/dist/tools/studio-client.js
256
+ var StudioHttpClient = class {
257
+ bridge;
258
+ constructor(bridge) {
259
+ this.bridge = bridge;
260
+ }
261
+ async request(endpoint, data) {
262
+ try {
263
+ const response = await this.bridge.sendRequest(endpoint, data);
264
+ return response;
265
+ } catch (error) {
266
+ if (error instanceof Error && error.message === "Request timeout") {
267
+ throw new Error("Studio plugin connection timeout. Make sure the Roblox Studio plugin is running and activated.");
268
+ }
269
+ throw error;
270
+ }
271
+ }
272
+ };
273
+
274
+ // ../../node_modules/@robloxstudio-mcp/core/dist/tools/build-executor.js
275
+ import * as vm from "vm";
276
+ var DEFAULT_TIMEOUT = 1e4;
277
+ var DEFAULT_MAX_PARTS = 1e4;
278
+ var VALID_SHAPES = /* @__PURE__ */ new Set(["Block", "Wedge", "Cylinder", "Ball", "CornerWedge"]);
279
+ function createSeededRng(seed) {
280
+ let s = seed;
281
+ return () => {
282
+ s = s * 1664525 + 1013904223 & 4294967295;
283
+ return (s >>> 0) / 4294967296;
284
+ };
285
+ }
286
+ function computeBoundsFromParts(parts) {
287
+ let maxX = 0, maxY = 0, maxZ = 0;
288
+ for (const p of parts) {
289
+ const px = Math.abs(p[0]) + p[3] / 2;
290
+ const py = Math.abs(p[1]) + p[4] / 2;
291
+ const pz = Math.abs(p[2]) + p[5] / 2;
292
+ maxX = Math.max(maxX, px);
293
+ maxY = Math.max(maxY, py);
294
+ maxZ = Math.max(maxZ, pz);
295
+ }
296
+ return [
297
+ Math.round(maxX * 2 * 10) / 10,
298
+ Math.round(maxY * 2 * 10) / 10,
299
+ Math.round(maxZ * 2 * 10) / 10
300
+ ];
301
+ }
302
+ function runBuildExecutor(code, palette, seed, options) {
303
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
304
+ const maxParts = options?.maxParts ?? DEFAULT_MAX_PARTS;
305
+ const paletteKeys = new Set(Object.keys(palette));
306
+ const parts = [];
307
+ function checkLimit() {
308
+ if (parts.length >= maxParts) {
309
+ throw new Error(`Part limit exceeded: max ${maxParts} parts allowed`);
310
+ }
311
+ }
312
+ function validateKey(key, fnName) {
313
+ if (!paletteKeys.has(key)) {
314
+ throw new Error(`${fnName}: palette key "${key}" not found. Available keys: ${[...paletteKeys].join(", ")}`);
315
+ }
316
+ }
317
+ function validateNumber(val, name) {
318
+ if (typeof val !== "number" || !isFinite(val)) {
319
+ throw new Error(`${name} must be a finite number, got ${val}`);
320
+ }
321
+ }
322
+ function partFn(x, y, z, sx, sy, sz, key, shape, transparency) {
323
+ validateNumber(x, "part x");
324
+ validateNumber(y, "part y");
325
+ validateNumber(z, "part z");
326
+ validateNumber(sx, "part sx");
327
+ validateNumber(sy, "part sy");
328
+ validateNumber(sz, "part sz");
329
+ validateKey(key, "part");
330
+ if (shape !== void 0 && !VALID_SHAPES.has(shape)) {
331
+ throw new Error(`part: invalid shape "${shape}". Valid: ${[...VALID_SHAPES].join(", ")}`);
332
+ }
333
+ checkLimit();
334
+ const entry = [x, y, z, sx, sy, sz, 0, 0, 0, key];
335
+ if (shape !== void 0)
336
+ entry.push(shape);
337
+ if (transparency !== void 0)
338
+ entry.push(transparency);
339
+ parts.push(entry);
340
+ }
341
+ function rpartFn(x, y, z, sx, sy, sz, rx, ry, rz, key, shape, transparency) {
342
+ validateNumber(x, "rpart x");
343
+ validateNumber(y, "rpart y");
344
+ validateNumber(z, "rpart z");
345
+ validateNumber(sx, "rpart sx");
346
+ validateNumber(sy, "rpart sy");
347
+ validateNumber(sz, "rpart sz");
348
+ validateNumber(rx, "rpart rx");
349
+ validateNumber(ry, "rpart ry");
350
+ validateNumber(rz, "rpart rz");
351
+ validateKey(key, "rpart");
352
+ if (shape !== void 0 && !VALID_SHAPES.has(shape)) {
353
+ throw new Error(`rpart: invalid shape "${shape}". Valid: ${[...VALID_SHAPES].join(", ")}`);
354
+ }
355
+ checkLimit();
356
+ const entry = [x, y, z, sx, sy, sz, rx, ry, rz, key];
357
+ if (shape !== void 0)
358
+ entry.push(shape);
359
+ if (transparency !== void 0)
360
+ entry.push(transparency);
361
+ parts.push(entry);
362
+ }
363
+ function fillFn(x1, y1, z1, x2, y2, z2, key, unitSize) {
364
+ validateKey(key, "fill");
365
+ [x1, y1, z1, x2, y2, z2].forEach((v, i) => validateNumber(v, `fill arg${i}`));
366
+ if (!unitSize) {
367
+ const cx = (x1 + x2) / 2;
368
+ const cy = (y1 + y2) / 2;
369
+ const cz = (z1 + z2) / 2;
370
+ const sx = Math.abs(x2 - x1);
371
+ const sy = Math.abs(y2 - y1);
372
+ const sz = Math.abs(z2 - z1);
373
+ checkLimit();
374
+ parts.push([cx, cy, cz, sx, sy, sz, 0, 0, 0, key]);
375
+ } else {
376
+ const [ux, uy, uz] = unitSize;
377
+ validateNumber(ux, "fill unitSize[0]");
378
+ validateNumber(uy, "fill unitSize[1]");
379
+ validateNumber(uz, "fill unitSize[2]");
380
+ const minX = Math.min(x1, x2);
381
+ const minY = Math.min(y1, y2);
382
+ const minZ = Math.min(z1, z2);
383
+ const maxX = Math.max(x1, x2);
384
+ const maxY = Math.max(y1, y2);
385
+ const maxZ = Math.max(z1, z2);
386
+ for (let x = minX + ux / 2; x < maxX; x += ux) {
387
+ for (let y = minY + uy / 2; y < maxY; y += uy) {
388
+ for (let z = minZ + uz / 2; z < maxZ; z += uz) {
389
+ checkLimit();
390
+ parts.push([
391
+ Math.round(x * 1e3) / 1e3,
392
+ Math.round(y * 1e3) / 1e3,
393
+ Math.round(z * 1e3) / 1e3,
394
+ ux,
395
+ uy,
396
+ uz,
397
+ 0,
398
+ 0,
399
+ 0,
400
+ key
401
+ ]);
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
407
+ function beamFn(x1, y1, z1, x2, y2, z2, thickness, key) {
408
+ validateKey(key, "beam");
409
+ [x1, y1, z1, x2, y2, z2, thickness].forEach((v, i) => validateNumber(v, `beam arg${i}`));
410
+ const cx = (x1 + x2) / 2;
411
+ const cy = (y1 + y2) / 2;
412
+ const cz = (z1 + z2) / 2;
413
+ const dx = x2 - x1;
414
+ const dy = y2 - y1;
415
+ const dz = z2 - z1;
416
+ const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
417
+ const ry = Math.atan2(dx, dz) * (180 / Math.PI);
418
+ const horizontalDist = Math.sqrt(dx * dx + dz * dz);
419
+ const rx = -Math.atan2(dy, horizontalDist) * (180 / Math.PI);
420
+ checkLimit();
421
+ parts.push([
422
+ Math.round(cx * 1e3) / 1e3,
423
+ Math.round(cy * 1e3) / 1e3,
424
+ Math.round(cz * 1e3) / 1e3,
425
+ thickness,
426
+ thickness,
427
+ Math.round(length * 1e3) / 1e3,
428
+ Math.round(rx * 100) / 100,
429
+ Math.round(ry * 100) / 100,
430
+ 0,
431
+ key
432
+ ]);
433
+ }
434
+ function wallFn(x1, z1, x2, z2, height, thickness, key) {
435
+ validateKey(key, "wall");
436
+ [x1, z1, x2, z2, height, thickness].forEach((v, i) => validateNumber(v, `wall arg${i}`));
437
+ const cx = (x1 + x2) / 2;
438
+ const cz = (z1 + z2) / 2;
439
+ const cy = height / 2;
440
+ const dx = x2 - x1;
441
+ const dz = z2 - z1;
442
+ const wallLength = Math.sqrt(dx * dx + dz * dz);
443
+ const ry = Math.atan2(dx, dz) * (180 / Math.PI);
444
+ checkLimit();
445
+ parts.push([
446
+ Math.round(cx * 1e3) / 1e3,
447
+ Math.round(cy * 1e3) / 1e3,
448
+ Math.round(cz * 1e3) / 1e3,
449
+ thickness,
450
+ height,
451
+ Math.round(wallLength * 1e3) / 1e3,
452
+ 0,
453
+ Math.round(ry * 100) / 100,
454
+ 0,
455
+ key
456
+ ]);
457
+ }
458
+ function floorFn(x1, z1, x2, z2, y, thickness, key) {
459
+ validateKey(key, "floor");
460
+ [x1, z1, x2, z2, y, thickness].forEach((v, i) => validateNumber(v, `floor arg${i}`));
461
+ const cx = (x1 + x2) / 2;
462
+ const cz = (z1 + z2) / 2;
463
+ const sx = Math.abs(x2 - x1);
464
+ const sz = Math.abs(z2 - z1);
465
+ checkLimit();
466
+ parts.push([
467
+ Math.round(cx * 1e3) / 1e3,
468
+ y,
469
+ Math.round(cz * 1e3) / 1e3,
470
+ sx,
471
+ thickness,
472
+ sz,
473
+ 0,
474
+ 0,
475
+ 0,
476
+ key
477
+ ]);
478
+ }
479
+ function rowFn(x, y, z, count, spacingX, spacingZ, partFnCb) {
480
+ validateNumber(x, "row x");
481
+ validateNumber(y, "row y");
482
+ validateNumber(z, "row z");
483
+ validateNumber(count, "row count");
484
+ validateNumber(spacingX, "row spacingX");
485
+ validateNumber(spacingZ, "row spacingZ");
486
+ if (typeof partFnCb !== "function") {
487
+ throw new Error("row: partFn must be a function");
488
+ }
489
+ for (let i = 0; i < count; i++) {
490
+ partFnCb(i, x + i * spacingX, y, z + i * spacingZ);
491
+ }
492
+ }
493
+ function gridFn(x, y, z, countX, countZ, spacingX, spacingZ, partFnCb) {
494
+ validateNumber(x, "grid x");
495
+ validateNumber(y, "grid y");
496
+ validateNumber(z, "grid z");
497
+ validateNumber(countX, "grid countX");
498
+ validateNumber(countZ, "grid countZ");
499
+ validateNumber(spacingX, "grid spacingX");
500
+ validateNumber(spacingZ, "grid spacingZ");
501
+ if (typeof partFnCb !== "function") {
502
+ throw new Error("grid: partFn must be a function");
503
+ }
504
+ for (let ix = 0; ix < countX; ix++) {
505
+ for (let iz = 0; iz < countZ; iz++) {
506
+ partFnCb(ix, iz, x + ix * spacingX, y, z + iz * spacingZ);
507
+ }
508
+ }
509
+ }
510
+ function roomFn(x, y, z, w, h, d, wallKey, floorKey, ceilKey, wallThickness) {
511
+ const t = wallThickness ?? 1;
512
+ const fk = floorKey ?? wallKey;
513
+ const ck = ceilKey ?? wallKey;
514
+ floorFn(x - w / 2, z - d / 2, x + w / 2, z + d / 2, y, t, fk);
515
+ floorFn(x - w / 2, z - d / 2, x + w / 2, z + d / 2, y + h, t, ck);
516
+ wallFn(x - w / 2, z - d / 2, x - w / 2, z + d / 2, h, t, wallKey);
517
+ wallFn(x + w / 2, z - d / 2, x + w / 2, z + d / 2, h, t, wallKey);
518
+ wallFn(x - w / 2, z - d / 2, x + w / 2, z - d / 2, h, t, wallKey);
519
+ wallFn(x - w / 2, z + d / 2, x + w / 2, z + d / 2, h, t, wallKey);
520
+ }
521
+ function roofFn(x, y, z, w, d, style, key, overhang) {
522
+ const oh = overhang ?? 1;
523
+ validateKey(key, "roof");
524
+ if (style === "flat") {
525
+ floorFn(x - w / 2 - oh, z - d / 2 - oh, x + w / 2 + oh, z + d / 2 + oh, y, 1, key);
526
+ } else if (style === "gable") {
527
+ const peakH = w / 2 * 0.6;
528
+ const slopeW = Math.sqrt((w / 2 + oh) * (w / 2 + oh) + peakH * peakH);
529
+ const angle = Math.atan2(peakH, w / 2 + oh) * (180 / Math.PI);
530
+ rpartFn(x - (w / 4 + oh / 2) * 0.5, y + peakH / 2, z, slopeW, 0.5, d + oh * 2, -angle, 0, 0, key);
531
+ rpartFn(x + (w / 4 + oh / 2) * 0.5, y + peakH / 2, z, slopeW, 0.5, d + oh * 2, angle, 0, 0, key);
532
+ } else if (style === "hip") {
533
+ const peakH = w / 3;
534
+ floorFn(x - w / 4, z - d / 4, x + w / 4, z + d / 4, y + peakH, 0.5, key);
535
+ const slopeW = Math.sqrt((w / 2 + oh) * (w / 2 + oh) + peakH * peakH);
536
+ const angle = Math.atan2(peakH, w / 2 + oh) * (180 / Math.PI);
537
+ rpartFn(x - w / 4, y + peakH / 2, z, slopeW * 0.6, 0.5, d + oh, -angle, 0, 0, key);
538
+ rpartFn(x + w / 4, y + peakH / 2, z, slopeW * 0.6, 0.5, d + oh, angle, 0, 0, key);
539
+ }
540
+ }
541
+ function stairsFn(x1, y1, z1, x2, y2, z2, w, key) {
542
+ validateKey(key, "stairs");
543
+ const dx = x2 - x1;
544
+ const dy = y2 - y1;
545
+ const dz = z2 - z1;
546
+ const dist = Math.sqrt(dx * dx + dz * dz);
547
+ const stepCount = Math.max(2, Math.round(Math.abs(dy) / 0.5));
548
+ const stepH = dy / stepCount;
549
+ const stepDx = dx / stepCount;
550
+ const stepDz = dz / stepCount;
551
+ for (let i = 0; i < stepCount; i++) {
552
+ checkLimit();
553
+ const sx = x1 + stepDx * (i + 0.5);
554
+ const sy = y1 + stepH * (i + 0.5);
555
+ const sz = z1 + stepDz * (i + 0.5);
556
+ const stepDepth = dist / stepCount;
557
+ partFn(Math.round(sx * 100) / 100, Math.round(sy * 100) / 100, Math.round(sz * 100) / 100, w, Math.abs(stepH), stepDepth, key);
558
+ }
559
+ }
560
+ function archFn(x, y, z, w, h, thickness, key, segments) {
561
+ validateKey(key, "arch");
562
+ const segs = segments ?? 8;
563
+ const radius = w / 2;
564
+ const archH = h - radius;
565
+ if (archH > 0) {
566
+ partFn(x - w / 2, y + archH / 2, z, thickness, archH, thickness, key);
567
+ partFn(x + w / 2, y + archH / 2, z, thickness, archH, thickness, key);
568
+ }
569
+ for (let i = 0; i < segs; i++) {
570
+ const a1 = Math.PI / segs * i;
571
+ const a2 = Math.PI / segs * (i + 1);
572
+ const mx = x + radius * Math.cos((a1 + a2) / 2 + Math.PI / 2);
573
+ const my = y + archH + radius * Math.sin((a1 + a2) / 2 + Math.PI / 2) * (radius / (radius || 1));
574
+ checkLimit();
575
+ const segLen = 2 * radius * Math.sin((a2 - a1) / 2);
576
+ const angle = (a1 + a2) / 2 * (180 / Math.PI);
577
+ rpartFn(Math.round(mx * 100) / 100, Math.round(my * 100) / 100, z, segLen, thickness, thickness, 0, 0, Math.round(angle * 100) / 100, key);
578
+ }
579
+ }
580
+ function columnFn(x, y, z, height, radius, key, capKey) {
581
+ validateKey(key, "column");
582
+ rpartFn(x, y + height / 2, z, height, radius * 2, radius * 2, 0, 0, 90, key, "Cylinder");
583
+ const ck = capKey ?? key;
584
+ validateKey(ck, "column cap");
585
+ partFn(x, y + 0.25, z, radius * 2.5, 0.5, radius * 2.5, ck);
586
+ partFn(x, y + height - 0.25, z, radius * 2.5, 0.5, radius * 2.5, ck);
587
+ }
588
+ function pewFn(x, y, z, w, d, seatKey, legKey) {
589
+ validateKey(seatKey, "pew");
590
+ const lk = legKey ?? seatKey;
591
+ validateKey(lk, "pew legs");
592
+ partFn(x, y + 1.5, z, w, 0.3, d, seatKey);
593
+ partFn(x, y + 2.5, z - d / 2 + 0.15, w, 2, 0.3, seatKey);
594
+ partFn(x - w / 2 + 0.25, y + 0.75, z, 0.5, 1.5, d, lk);
595
+ partFn(x + w / 2 - 0.25, y + 0.75, z, 0.5, 1.5, d, lk);
596
+ }
597
+ function fenceFn(x1, z1, x2, z2, y, key, postSpacing) {
598
+ validateKey(key, "fence");
599
+ const spacing = postSpacing ?? 4;
600
+ const dx = x2 - x1;
601
+ const dz = z2 - z1;
602
+ const length = Math.sqrt(dx * dx + dz * dz);
603
+ const count = Math.max(2, Math.round(length / spacing) + 1);
604
+ for (let i = 0; i < count; i++) {
605
+ const t = i / (count - 1);
606
+ checkLimit();
607
+ partFn(x1 + dx * t, y + 1.5, z1 + dz * t, 0.5, 3, 0.5, key);
608
+ }
609
+ wallFn(x1, z1, x2, z2, 1, 0.3, key);
610
+ const cx = (x1 + x2) / 2;
611
+ const cz = (z1 + z2) / 2;
612
+ const ry = Math.atan2(dx, dz) * (180 / Math.PI);
613
+ checkLimit();
614
+ parts.push([
615
+ Math.round(cx * 1e3) / 1e3,
616
+ y + 2.5,
617
+ Math.round(cz * 1e3) / 1e3,
618
+ 0.3,
619
+ 0.3,
620
+ Math.round(length * 1e3) / 1e3,
621
+ 0,
622
+ Math.round(ry * 100) / 100,
623
+ 0,
624
+ key
625
+ ]);
626
+ }
627
+ const rng = createSeededRng(seed ?? 42);
628
+ const sandbox = {
629
+ part: partFn,
630
+ rpart: rpartFn,
631
+ fill: fillFn,
632
+ beam: beamFn,
633
+ wall: wallFn,
634
+ floor: floorFn,
635
+ row: rowFn,
636
+ grid: gridFn,
637
+ room: roomFn,
638
+ roof: roofFn,
639
+ stairs: stairsFn,
640
+ arch: archFn,
641
+ column: columnFn,
642
+ pew: pewFn,
643
+ fence: fenceFn,
644
+ Math,
645
+ GRID_SIZE: 1,
646
+ rng,
647
+ console: { log: () => {
648
+ }, warn: () => {
649
+ }, error: () => {
650
+ } }
651
+ };
652
+ const context = vm.createContext(sandbox, {
653
+ codeGeneration: { strings: false, wasm: false }
654
+ });
655
+ const script = new vm.Script(code, { filename: "build-generator.js" });
656
+ try {
657
+ script.runInContext(context, { timeout });
658
+ } catch (err) {
659
+ if (err.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
660
+ throw new Error(`Build code execution timed out after ${timeout}ms`);
661
+ }
662
+ throw new Error(`Build code execution error: ${err.message}`);
663
+ }
664
+ if (parts.length === 0) {
665
+ throw new Error("Build code produced no parts. Make sure to call part(), wall(), floor(), etc.");
666
+ }
667
+ const bounds = computeBoundsFromParts(parts);
668
+ return { parts, bounds, partCount: parts.length };
669
+ }
670
+
671
+ // ../../node_modules/@robloxstudio-mcp/core/dist/opencloud-client.js
672
+ var OpenCloudClient = class {
673
+ apiKey;
674
+ baseUrl;
675
+ timeout;
676
+ constructor(config = {}) {
677
+ this.apiKey = config.apiKey || process.env.ROBLOX_OPEN_CLOUD_API_KEY || "";
678
+ this.baseUrl = config.baseUrl || "https://apis.roblox.com";
679
+ this.timeout = config.timeout || 3e4;
680
+ }
681
+ hasApiKey() {
682
+ return !!this.apiKey;
683
+ }
684
+ async request(endpoint, options = {}) {
685
+ if (!this.apiKey) {
686
+ throw new Error("Open Cloud API key not configured. Set ROBLOX_OPEN_CLOUD_API_KEY environment variable.");
687
+ }
688
+ const { method = "GET", params, body } = options;
689
+ const url = new URL(`${this.baseUrl}${endpoint}`);
690
+ if (params) {
691
+ for (const [key, value] of Object.entries(params)) {
692
+ if (value !== void 0) {
693
+ url.searchParams.set(key, String(value));
694
+ }
695
+ }
696
+ }
697
+ const controller = new AbortController();
698
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
699
+ try {
700
+ const response = await fetch(url.toString(), {
701
+ method,
702
+ headers: {
703
+ "x-api-key": this.apiKey,
704
+ "Content-Type": "application/json"
705
+ },
706
+ body: body ? JSON.stringify(body) : void 0,
707
+ signal: controller.signal
708
+ });
709
+ clearTimeout(timeoutId);
710
+ if (!response.ok) {
711
+ const errorBody = await response.text();
712
+ let errorMessage;
713
+ try {
714
+ const errorJson = JSON.parse(errorBody);
715
+ errorMessage = errorJson.detail || errorJson.message || errorBody;
716
+ } catch {
717
+ errorMessage = errorBody;
718
+ }
719
+ if (response.status === 401) {
720
+ throw new Error("Invalid or expired API key");
721
+ } else if (response.status === 403) {
722
+ throw new Error(`API key lacks required permissions: ${errorMessage}`);
723
+ } else if (response.status === 429) {
724
+ throw new Error("Rate limit exceeded. Please try again later.");
725
+ } else {
726
+ throw new Error(`Open Cloud API error (${response.status}): ${errorMessage}`);
727
+ }
728
+ }
729
+ return await response.json();
730
+ } catch (error) {
731
+ clearTimeout(timeoutId);
732
+ if (error instanceof Error) {
733
+ if (error.name === "AbortError") {
734
+ throw new Error("Request timed out");
735
+ }
736
+ throw error;
737
+ }
738
+ throw new Error(`Unknown error: ${String(error)}`);
739
+ }
740
+ }
741
+ async searchAssets(params) {
742
+ return this.request("/toolbox-service/v2/assets:search", {
743
+ params: {
744
+ searchCategoryType: params.searchCategoryType,
745
+ query: params.query,
746
+ pageToken: params.pageToken,
747
+ pageNumber: params.pageNumber,
748
+ maxPageSize: params.maxPageSize || 25,
749
+ sortDirection: params.sortDirection,
750
+ sortCategory: params.sortCategory,
751
+ includeOnlyVerifiedCreators: params.includeOnlyVerifiedCreators,
752
+ userId: params.userId,
753
+ groupId: params.groupId
754
+ }
755
+ });
756
+ }
757
+ async getAssetDetails(assetId) {
758
+ return this.request(`/toolbox-service/v2/assets/${assetId}`);
759
+ }
760
+ async getAssetThumbnail(assetId, size = "420x420") {
761
+ const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${assetId}&size=${size}&format=Png`;
762
+ try {
763
+ const response = await fetch(url);
764
+ if (!response.ok)
765
+ return null;
766
+ const data = await response.json();
767
+ const thumbnail = data.data[0];
768
+ if (!thumbnail || thumbnail.state !== "Completed" || !thumbnail.imageUrl) {
769
+ return null;
770
+ }
771
+ const imageResponse = await fetch(thumbnail.imageUrl);
772
+ if (!imageResponse.ok)
773
+ return null;
774
+ const arrayBuffer = await imageResponse.arrayBuffer();
775
+ const base64 = Buffer.from(arrayBuffer).toString("base64");
776
+ return { base64, mimeType: "image/png" };
777
+ } catch {
778
+ return null;
779
+ }
780
+ }
781
+ async getAssetThumbnails(assetIds, size = "420x420") {
782
+ const result = /* @__PURE__ */ new Map();
783
+ if (assetIds.length === 0)
784
+ return result;
785
+ const batches = [];
786
+ for (let i = 0; i < assetIds.length; i += 100) {
787
+ batches.push(assetIds.slice(i, i + 100));
788
+ }
789
+ for (const batch of batches) {
790
+ const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${batch.join(",")}&size=${size}&format=Png`;
791
+ try {
792
+ const response = await fetch(url);
793
+ if (response.ok) {
794
+ const data = await response.json();
795
+ for (const thumbnail of data.data) {
796
+ if (thumbnail.state === "Completed" && thumbnail.imageUrl) {
797
+ result.set(thumbnail.targetId, thumbnail.imageUrl);
966
798
  }
967
- });
968
- }
969
- async run() {
970
- const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
971
- const maxPort = basePort + 4;
972
- const host = process.env.ROBLOX_STUDIO_HOST || '0.0.0.0';
973
- const httpServer = createHttpServer(this.tools, this.bridge);
974
- let boundPort = 0;
975
- for (let port = basePort; port <= maxPort; port++) {
976
- try {
977
- await new Promise((resolve, reject) => {
978
- const onError = (err) => {
979
- if (err.code === 'EADDRINUSE') {
980
- httpServer.removeListener('error', onError);
981
- reject(err);
982
- }
983
- else {
984
- reject(err);
985
- }
986
- };
987
- httpServer.once('error', onError);
988
- httpServer.listen(port, host, () => {
989
- httpServer.removeListener('error', onError);
990
- boundPort = port;
991
- console.error(`HTTP server listening on ${host}:${port} for Studio plugin`);
992
- resolve();
993
- });
994
- });
995
- break;
799
+ }
800
+ }
801
+ } catch {
802
+ }
803
+ }
804
+ return result;
805
+ }
806
+ };
807
+
808
+ // ../../node_modules/@robloxstudio-mcp/core/dist/tools/index.js
809
+ import * as fs from "fs";
810
+ import * as path from "path";
811
+ var RobloxStudioTools = class _RobloxStudioTools {
812
+ client;
813
+ openCloudClient;
814
+ constructor(bridge) {
815
+ this.client = new StudioHttpClient(bridge);
816
+ this.openCloudClient = new OpenCloudClient();
817
+ }
818
+ async getFileTree(path2 = "") {
819
+ const response = await this.client.request("/api/file-tree", { path: path2 });
820
+ return {
821
+ content: [
822
+ {
823
+ type: "text",
824
+ text: JSON.stringify(response)
825
+ }
826
+ ]
827
+ };
828
+ }
829
+ async searchFiles(query, searchType = "name") {
830
+ const response = await this.client.request("/api/search-files", { query, searchType });
831
+ return {
832
+ content: [
833
+ {
834
+ type: "text",
835
+ text: JSON.stringify(response)
836
+ }
837
+ ]
838
+ };
839
+ }
840
+ async getPlaceInfo() {
841
+ const response = await this.client.request("/api/place-info", {});
842
+ return {
843
+ content: [
844
+ {
845
+ type: "text",
846
+ text: JSON.stringify(response)
847
+ }
848
+ ]
849
+ };
850
+ }
851
+ async getServices(serviceName) {
852
+ const response = await this.client.request("/api/services", { serviceName });
853
+ return {
854
+ content: [
855
+ {
856
+ type: "text",
857
+ text: JSON.stringify(response)
858
+ }
859
+ ]
860
+ };
861
+ }
862
+ async searchObjects(query, searchType = "name", propertyName) {
863
+ const response = await this.client.request("/api/search-objects", {
864
+ query,
865
+ searchType,
866
+ propertyName
867
+ });
868
+ return {
869
+ content: [
870
+ {
871
+ type: "text",
872
+ text: JSON.stringify(response)
873
+ }
874
+ ]
875
+ };
876
+ }
877
+ async getInstanceProperties(instancePath, excludeSource) {
878
+ if (!instancePath) {
879
+ throw new Error("Instance path is required for get_instance_properties");
880
+ }
881
+ const response = await this.client.request("/api/instance-properties", { instancePath, excludeSource });
882
+ return {
883
+ content: [
884
+ {
885
+ type: "text",
886
+ text: JSON.stringify(response)
887
+ }
888
+ ]
889
+ };
890
+ }
891
+ async getInstanceChildren(instancePath) {
892
+ if (!instancePath) {
893
+ throw new Error("Instance path is required for get_instance_children");
894
+ }
895
+ const response = await this.client.request("/api/instance-children", { instancePath });
896
+ return {
897
+ content: [
898
+ {
899
+ type: "text",
900
+ text: JSON.stringify(response)
901
+ }
902
+ ]
903
+ };
904
+ }
905
+ async searchByProperty(propertyName, propertyValue) {
906
+ if (!propertyName || !propertyValue) {
907
+ throw new Error("Property name and value are required for search_by_property");
908
+ }
909
+ const response = await this.client.request("/api/search-by-property", {
910
+ propertyName,
911
+ propertyValue
912
+ });
913
+ return {
914
+ content: [
915
+ {
916
+ type: "text",
917
+ text: JSON.stringify(response)
918
+ }
919
+ ]
920
+ };
921
+ }
922
+ async getClassInfo(className) {
923
+ if (!className) {
924
+ throw new Error("Class name is required for get_class_info");
925
+ }
926
+ const response = await this.client.request("/api/class-info", { className });
927
+ return {
928
+ content: [
929
+ {
930
+ type: "text",
931
+ text: JSON.stringify(response)
932
+ }
933
+ ]
934
+ };
935
+ }
936
+ async getProjectStructure(path2, maxDepth, scriptsOnly) {
937
+ const response = await this.client.request("/api/project-structure", {
938
+ path: path2,
939
+ maxDepth,
940
+ scriptsOnly
941
+ });
942
+ return {
943
+ content: [
944
+ {
945
+ type: "text",
946
+ text: JSON.stringify(response)
947
+ }
948
+ ]
949
+ };
950
+ }
951
+ async setProperty(instancePath, propertyName, propertyValue) {
952
+ if (!instancePath || !propertyName) {
953
+ throw new Error("Instance path and property name are required for set_property");
954
+ }
955
+ const response = await this.client.request("/api/set-property", {
956
+ instancePath,
957
+ propertyName,
958
+ propertyValue
959
+ });
960
+ return {
961
+ content: [
962
+ {
963
+ type: "text",
964
+ text: JSON.stringify(response)
965
+ }
966
+ ]
967
+ };
968
+ }
969
+ async massSetProperty(paths, propertyName, propertyValue) {
970
+ if (!paths || paths.length === 0 || !propertyName) {
971
+ throw new Error("Paths array and property name are required for mass_set_property");
972
+ }
973
+ const response = await this.client.request("/api/mass-set-property", {
974
+ paths,
975
+ propertyName,
976
+ propertyValue
977
+ });
978
+ return {
979
+ content: [
980
+ {
981
+ type: "text",
982
+ text: JSON.stringify(response)
983
+ }
984
+ ]
985
+ };
986
+ }
987
+ async massGetProperty(paths, propertyName) {
988
+ if (!paths || paths.length === 0 || !propertyName) {
989
+ throw new Error("Paths array and property name are required for mass_get_property");
990
+ }
991
+ const response = await this.client.request("/api/mass-get-property", {
992
+ paths,
993
+ propertyName
994
+ });
995
+ return {
996
+ content: [
997
+ {
998
+ type: "text",
999
+ text: JSON.stringify(response)
1000
+ }
1001
+ ]
1002
+ };
1003
+ }
1004
+ async createObject(className, parent, name, properties) {
1005
+ if (!className || !parent) {
1006
+ throw new Error("Class name and parent are required for create_object");
1007
+ }
1008
+ const response = await this.client.request("/api/create-object", {
1009
+ className,
1010
+ parent,
1011
+ name,
1012
+ properties
1013
+ });
1014
+ return {
1015
+ content: [
1016
+ {
1017
+ type: "text",
1018
+ text: JSON.stringify(response)
1019
+ }
1020
+ ]
1021
+ };
1022
+ }
1023
+ async massCreateObjects(objects) {
1024
+ if (!objects || objects.length === 0) {
1025
+ throw new Error("Objects array is required for mass_create_objects");
1026
+ }
1027
+ const hasProperties = objects.some((o) => o.properties && Object.keys(o.properties).length > 0);
1028
+ const endpoint = hasProperties ? "/api/mass-create-objects-with-properties" : "/api/mass-create-objects";
1029
+ const response = await this.client.request(endpoint, { objects });
1030
+ return {
1031
+ content: [
1032
+ {
1033
+ type: "text",
1034
+ text: JSON.stringify(response)
1035
+ }
1036
+ ]
1037
+ };
1038
+ }
1039
+ async deleteObject(instancePath) {
1040
+ if (!instancePath) {
1041
+ throw new Error("Instance path is required for delete_object");
1042
+ }
1043
+ const response = await this.client.request("/api/delete-object", { instancePath });
1044
+ return {
1045
+ content: [
1046
+ {
1047
+ type: "text",
1048
+ text: JSON.stringify(response)
1049
+ }
1050
+ ]
1051
+ };
1052
+ }
1053
+ async smartDuplicate(instancePath, count, options) {
1054
+ if (!instancePath || count < 1) {
1055
+ throw new Error("Instance path and count > 0 are required for smart_duplicate");
1056
+ }
1057
+ const response = await this.client.request("/api/smart-duplicate", {
1058
+ instancePath,
1059
+ count,
1060
+ options
1061
+ });
1062
+ return {
1063
+ content: [
1064
+ {
1065
+ type: "text",
1066
+ text: JSON.stringify(response)
1067
+ }
1068
+ ]
1069
+ };
1070
+ }
1071
+ async massDuplicate(duplications) {
1072
+ if (!duplications || duplications.length === 0) {
1073
+ throw new Error("Duplications array is required for mass_duplicate");
1074
+ }
1075
+ const response = await this.client.request("/api/mass-duplicate", { duplications });
1076
+ return {
1077
+ content: [
1078
+ {
1079
+ type: "text",
1080
+ text: JSON.stringify(response)
1081
+ }
1082
+ ]
1083
+ };
1084
+ }
1085
+ async setCalculatedProperty(paths, propertyName, formula, variables) {
1086
+ if (!paths || paths.length === 0 || !propertyName || !formula) {
1087
+ throw new Error("Paths, property name, and formula are required for set_calculated_property");
1088
+ }
1089
+ const response = await this.client.request("/api/set-calculated-property", {
1090
+ paths,
1091
+ propertyName,
1092
+ formula,
1093
+ variables
1094
+ });
1095
+ return {
1096
+ content: [
1097
+ {
1098
+ type: "text",
1099
+ text: JSON.stringify(response)
1100
+ }
1101
+ ]
1102
+ };
1103
+ }
1104
+ async setRelativeProperty(paths, propertyName, operation, value, component) {
1105
+ if (!paths || paths.length === 0 || !propertyName || !operation || value === void 0) {
1106
+ throw new Error("Paths, property name, operation, and value are required for set_relative_property");
1107
+ }
1108
+ const response = await this.client.request("/api/set-relative-property", {
1109
+ paths,
1110
+ propertyName,
1111
+ operation,
1112
+ value,
1113
+ component
1114
+ });
1115
+ return {
1116
+ content: [
1117
+ {
1118
+ type: "text",
1119
+ text: JSON.stringify(response)
1120
+ }
1121
+ ]
1122
+ };
1123
+ }
1124
+ async getScriptSource(instancePath, startLine, endLine) {
1125
+ if (!instancePath) {
1126
+ throw new Error("Instance path is required for get_script_source");
1127
+ }
1128
+ const response = await this.client.request("/api/get-script-source", { instancePath, startLine, endLine });
1129
+ return {
1130
+ content: [
1131
+ {
1132
+ type: "text",
1133
+ text: JSON.stringify(response)
1134
+ }
1135
+ ]
1136
+ };
1137
+ }
1138
+ async setScriptSource(instancePath, source) {
1139
+ if (!instancePath || typeof source !== "string") {
1140
+ throw new Error("Instance path and source code string are required for set_script_source");
1141
+ }
1142
+ const response = await this.client.request("/api/set-script-source", { instancePath, source });
1143
+ return {
1144
+ content: [
1145
+ {
1146
+ type: "text",
1147
+ text: JSON.stringify(response)
1148
+ }
1149
+ ]
1150
+ };
1151
+ }
1152
+ async editScriptLines(instancePath, startLine, endLine, newContent) {
1153
+ if (!instancePath || !startLine || !endLine || typeof newContent !== "string") {
1154
+ throw new Error("Instance path, startLine, endLine, and newContent are required for edit_script_lines");
1155
+ }
1156
+ const response = await this.client.request("/api/edit-script-lines", { instancePath, startLine, endLine, newContent });
1157
+ return {
1158
+ content: [
1159
+ {
1160
+ type: "text",
1161
+ text: JSON.stringify(response)
1162
+ }
1163
+ ]
1164
+ };
1165
+ }
1166
+ async insertScriptLines(instancePath, afterLine, newContent) {
1167
+ if (!instancePath || typeof newContent !== "string") {
1168
+ throw new Error("Instance path and newContent are required for insert_script_lines");
1169
+ }
1170
+ const response = await this.client.request("/api/insert-script-lines", { instancePath, afterLine: afterLine || 0, newContent });
1171
+ return {
1172
+ content: [
1173
+ {
1174
+ type: "text",
1175
+ text: JSON.stringify(response)
1176
+ }
1177
+ ]
1178
+ };
1179
+ }
1180
+ async deleteScriptLines(instancePath, startLine, endLine) {
1181
+ if (!instancePath || !startLine || !endLine) {
1182
+ throw new Error("Instance path, startLine, and endLine are required for delete_script_lines");
1183
+ }
1184
+ const response = await this.client.request("/api/delete-script-lines", { instancePath, startLine, endLine });
1185
+ return {
1186
+ content: [
1187
+ {
1188
+ type: "text",
1189
+ text: JSON.stringify(response)
1190
+ }
1191
+ ]
1192
+ };
1193
+ }
1194
+ async grepScripts(pattern, options) {
1195
+ if (!pattern) {
1196
+ throw new Error("Pattern is required for grep_scripts");
1197
+ }
1198
+ const response = await this.client.request("/api/grep-scripts", {
1199
+ pattern,
1200
+ ...options
1201
+ });
1202
+ return {
1203
+ content: [
1204
+ {
1205
+ type: "text",
1206
+ text: JSON.stringify(response)
1207
+ }
1208
+ ]
1209
+ };
1210
+ }
1211
+ async getAttribute(instancePath, attributeName) {
1212
+ if (!instancePath || !attributeName) {
1213
+ throw new Error("Instance path and attribute name are required for get_attribute");
1214
+ }
1215
+ const response = await this.client.request("/api/get-attribute", { instancePath, attributeName });
1216
+ return {
1217
+ content: [
1218
+ {
1219
+ type: "text",
1220
+ text: JSON.stringify(response)
1221
+ }
1222
+ ]
1223
+ };
1224
+ }
1225
+ async setAttribute(instancePath, attributeName, attributeValue, valueType) {
1226
+ if (!instancePath || !attributeName) {
1227
+ throw new Error("Instance path and attribute name are required for set_attribute");
1228
+ }
1229
+ const response = await this.client.request("/api/set-attribute", { instancePath, attributeName, attributeValue, valueType });
1230
+ return {
1231
+ content: [
1232
+ {
1233
+ type: "text",
1234
+ text: JSON.stringify(response)
1235
+ }
1236
+ ]
1237
+ };
1238
+ }
1239
+ async getAttributes(instancePath) {
1240
+ if (!instancePath) {
1241
+ throw new Error("Instance path is required for get_attributes");
1242
+ }
1243
+ const response = await this.client.request("/api/get-attributes", { instancePath });
1244
+ return {
1245
+ content: [
1246
+ {
1247
+ type: "text",
1248
+ text: JSON.stringify(response)
1249
+ }
1250
+ ]
1251
+ };
1252
+ }
1253
+ async deleteAttribute(instancePath, attributeName) {
1254
+ if (!instancePath || !attributeName) {
1255
+ throw new Error("Instance path and attribute name are required for delete_attribute");
1256
+ }
1257
+ const response = await this.client.request("/api/delete-attribute", { instancePath, attributeName });
1258
+ return {
1259
+ content: [
1260
+ {
1261
+ type: "text",
1262
+ text: JSON.stringify(response)
1263
+ }
1264
+ ]
1265
+ };
1266
+ }
1267
+ async getTags(instancePath) {
1268
+ if (!instancePath) {
1269
+ throw new Error("Instance path is required for get_tags");
1270
+ }
1271
+ const response = await this.client.request("/api/get-tags", { instancePath });
1272
+ return {
1273
+ content: [
1274
+ {
1275
+ type: "text",
1276
+ text: JSON.stringify(response)
1277
+ }
1278
+ ]
1279
+ };
1280
+ }
1281
+ async addTag(instancePath, tagName) {
1282
+ if (!instancePath || !tagName) {
1283
+ throw new Error("Instance path and tag name are required for add_tag");
1284
+ }
1285
+ const response = await this.client.request("/api/add-tag", { instancePath, tagName });
1286
+ return {
1287
+ content: [
1288
+ {
1289
+ type: "text",
1290
+ text: JSON.stringify(response)
1291
+ }
1292
+ ]
1293
+ };
1294
+ }
1295
+ async removeTag(instancePath, tagName) {
1296
+ if (!instancePath || !tagName) {
1297
+ throw new Error("Instance path and tag name are required for remove_tag");
1298
+ }
1299
+ const response = await this.client.request("/api/remove-tag", { instancePath, tagName });
1300
+ return {
1301
+ content: [
1302
+ {
1303
+ type: "text",
1304
+ text: JSON.stringify(response)
1305
+ }
1306
+ ]
1307
+ };
1308
+ }
1309
+ async getTagged(tagName) {
1310
+ if (!tagName) {
1311
+ throw new Error("Tag name is required for get_tagged");
1312
+ }
1313
+ const response = await this.client.request("/api/get-tagged", { tagName });
1314
+ return {
1315
+ content: [
1316
+ {
1317
+ type: "text",
1318
+ text: JSON.stringify(response)
1319
+ }
1320
+ ]
1321
+ };
1322
+ }
1323
+ async getSelection() {
1324
+ const response = await this.client.request("/api/get-selection", {});
1325
+ return {
1326
+ content: [
1327
+ {
1328
+ type: "text",
1329
+ text: JSON.stringify(response)
1330
+ }
1331
+ ]
1332
+ };
1333
+ }
1334
+ async executeLuau(code) {
1335
+ if (!code) {
1336
+ throw new Error("Code is required for execute_luau");
1337
+ }
1338
+ const response = await this.client.request("/api/execute-luau", { code });
1339
+ return {
1340
+ content: [
1341
+ {
1342
+ type: "text",
1343
+ text: JSON.stringify(response)
1344
+ }
1345
+ ]
1346
+ };
1347
+ }
1348
+ async startPlaytest(mode) {
1349
+ if (mode !== "play" && mode !== "run") {
1350
+ throw new Error('mode must be "play" or "run"');
1351
+ }
1352
+ const response = await this.client.request("/api/start-playtest", { mode });
1353
+ return {
1354
+ content: [
1355
+ {
1356
+ type: "text",
1357
+ text: JSON.stringify(response)
1358
+ }
1359
+ ]
1360
+ };
1361
+ }
1362
+ async stopPlaytest() {
1363
+ const response = await this.client.request("/api/stop-playtest", {});
1364
+ return {
1365
+ content: [
1366
+ {
1367
+ type: "text",
1368
+ text: JSON.stringify(response)
1369
+ }
1370
+ ]
1371
+ };
1372
+ }
1373
+ async getPlaytestOutput() {
1374
+ const response = await this.client.request("/api/get-playtest-output", {});
1375
+ return {
1376
+ content: [
1377
+ {
1378
+ type: "text",
1379
+ text: JSON.stringify(response)
1380
+ }
1381
+ ]
1382
+ };
1383
+ }
1384
+ async undo() {
1385
+ const response = await this.client.request("/api/undo", {});
1386
+ return {
1387
+ content: [
1388
+ {
1389
+ type: "text",
1390
+ text: JSON.stringify(response)
1391
+ }
1392
+ ]
1393
+ };
1394
+ }
1395
+ async redo() {
1396
+ const response = await this.client.request("/api/redo", {});
1397
+ return {
1398
+ content: [
1399
+ {
1400
+ type: "text",
1401
+ text: JSON.stringify(response)
1402
+ }
1403
+ ]
1404
+ };
1405
+ }
1406
+ static findLibraryPath() {
1407
+ let dir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
1408
+ for (let i = 0; i < 6; i++) {
1409
+ const candidate = path.join(dir, "build-library");
1410
+ if (fs.existsSync(candidate))
1411
+ return candidate;
1412
+ dir = path.dirname(dir);
1413
+ }
1414
+ const fallback = path.join(dir, "build-library");
1415
+ fs.mkdirSync(fallback, { recursive: true });
1416
+ return fallback;
1417
+ }
1418
+ static LIBRARY_PATH = _RobloxStudioTools.findLibraryPath();
1419
+ async exportBuild(instancePath, outputId, style = "misc") {
1420
+ if (!instancePath) {
1421
+ throw new Error("Instance path is required for export_build");
1422
+ }
1423
+ const response = await this.client.request("/api/export-build", {
1424
+ instancePath,
1425
+ outputId,
1426
+ style
1427
+ });
1428
+ if (response && response.success && response.buildData) {
1429
+ const buildData = response.buildData;
1430
+ const buildId = buildData.id || `${style}/exported`;
1431
+ const filePath = path.join(_RobloxStudioTools.LIBRARY_PATH, `${buildId}.json`);
1432
+ const dirPath = path.dirname(filePath);
1433
+ if (!fs.existsSync(dirPath)) {
1434
+ fs.mkdirSync(dirPath, { recursive: true });
1435
+ }
1436
+ fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
1437
+ response.savedTo = filePath;
1438
+ }
1439
+ return {
1440
+ content: [
1441
+ {
1442
+ type: "text",
1443
+ text: JSON.stringify(response)
1444
+ }
1445
+ ]
1446
+ };
1447
+ }
1448
+ async createBuild(id, style, palette, parts, bounds) {
1449
+ if (!id || !palette || !parts || parts.length === 0) {
1450
+ throw new Error("id, palette, and parts are required for create_build");
1451
+ }
1452
+ for (let i = 0; i < parts.length; i++) {
1453
+ if (!Array.isArray(parts[i]) || parts[i].length < 10) {
1454
+ throw new Error(`Part ${i} must have at least 10 elements: [posX, posY, posZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey]`);
1455
+ }
1456
+ }
1457
+ const computedBounds = bounds || this.computeBounds(parts);
1458
+ const buildData = { id, style, bounds: computedBounds, palette, parts };
1459
+ const filePath = path.join(_RobloxStudioTools.LIBRARY_PATH, `${id}.json`);
1460
+ const dirPath = path.dirname(filePath);
1461
+ if (!fs.existsSync(dirPath)) {
1462
+ fs.mkdirSync(dirPath, { recursive: true });
1463
+ }
1464
+ fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
1465
+ return {
1466
+ content: [
1467
+ {
1468
+ type: "text",
1469
+ text: JSON.stringify({
1470
+ success: true,
1471
+ id,
1472
+ style,
1473
+ bounds: computedBounds,
1474
+ partCount: parts.length,
1475
+ paletteKeys: Object.keys(palette),
1476
+ savedTo: filePath
1477
+ })
1478
+ }
1479
+ ]
1480
+ };
1481
+ }
1482
+ computeBounds(parts) {
1483
+ let maxX = 0, maxY = 0, maxZ = 0;
1484
+ for (const p of parts) {
1485
+ const px = Math.abs(p[0]) + p[3] / 2;
1486
+ const py = Math.abs(p[1]) + p[4] / 2;
1487
+ const pz = Math.abs(p[2]) + p[5] / 2;
1488
+ maxX = Math.max(maxX, px);
1489
+ maxY = Math.max(maxY, py);
1490
+ maxZ = Math.max(maxZ, pz);
1491
+ }
1492
+ return [
1493
+ Math.round(maxX * 2 * 10) / 10,
1494
+ Math.round(maxY * 2 * 10) / 10,
1495
+ Math.round(maxZ * 2 * 10) / 10
1496
+ ];
1497
+ }
1498
+ async generateBuild(id, style, palette, code, seed) {
1499
+ if (!id || !palette || !code) {
1500
+ throw new Error("id, palette, and code are required for generate_build");
1501
+ }
1502
+ for (const [key, value] of Object.entries(palette)) {
1503
+ if (!Array.isArray(value) || value.length < 2 || value.length > 3) {
1504
+ throw new Error(`Palette key "${key}" must map to [BrickColor, Material] or [BrickColor, Material, MaterialVariant]`);
1505
+ }
1506
+ }
1507
+ const result = runBuildExecutor(code, palette, seed);
1508
+ const buildData = {
1509
+ id,
1510
+ style,
1511
+ bounds: result.bounds,
1512
+ palette,
1513
+ parts: result.parts,
1514
+ generatorCode: code
1515
+ };
1516
+ if (seed !== void 0)
1517
+ buildData.generatorSeed = seed;
1518
+ const filePath = path.join(_RobloxStudioTools.LIBRARY_PATH, `${id}.json`);
1519
+ const dirPath = path.dirname(filePath);
1520
+ if (!fs.existsSync(dirPath)) {
1521
+ fs.mkdirSync(dirPath, { recursive: true });
1522
+ }
1523
+ fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
1524
+ return {
1525
+ content: [
1526
+ {
1527
+ type: "text",
1528
+ text: JSON.stringify({
1529
+ success: true,
1530
+ id,
1531
+ style,
1532
+ bounds: result.bounds,
1533
+ partCount: result.partCount,
1534
+ paletteKeys: Object.keys(palette),
1535
+ savedTo: filePath
1536
+ })
1537
+ }
1538
+ ]
1539
+ };
1540
+ }
1541
+ async importBuild(buildData, targetPath, position) {
1542
+ if (!buildData || !targetPath) {
1543
+ throw new Error("buildData (or library ID string) and targetPath are required for import_build");
1544
+ }
1545
+ let resolved;
1546
+ if (typeof buildData === "string") {
1547
+ const filePath = path.join(_RobloxStudioTools.LIBRARY_PATH, `${buildData}.json`);
1548
+ if (!fs.existsSync(filePath)) {
1549
+ throw new Error(`Build not found in library: ${buildData}`);
1550
+ }
1551
+ resolved = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1552
+ } else if (buildData.id && !buildData.parts) {
1553
+ const filePath = path.join(_RobloxStudioTools.LIBRARY_PATH, `${buildData.id}.json`);
1554
+ if (!fs.existsSync(filePath)) {
1555
+ throw new Error(`Build not found in library: ${buildData.id}`);
1556
+ }
1557
+ resolved = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1558
+ } else {
1559
+ resolved = buildData;
1560
+ }
1561
+ const response = await this.client.request("/api/import-build", {
1562
+ buildData: resolved,
1563
+ targetPath,
1564
+ position
1565
+ });
1566
+ return {
1567
+ content: [
1568
+ {
1569
+ type: "text",
1570
+ text: JSON.stringify(response)
1571
+ }
1572
+ ]
1573
+ };
1574
+ }
1575
+ async listLibrary(style) {
1576
+ const libraryPath = _RobloxStudioTools.LIBRARY_PATH;
1577
+ const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
1578
+ const builds = [];
1579
+ for (const s of styles) {
1580
+ const dirPath = path.join(libraryPath, s);
1581
+ if (!fs.existsSync(dirPath))
1582
+ continue;
1583
+ const files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
1584
+ for (const file of files) {
1585
+ try {
1586
+ const content = fs.readFileSync(path.join(dirPath, file), "utf-8");
1587
+ const data = JSON.parse(content);
1588
+ builds.push({
1589
+ id: data.id || `${s}/${file.replace(".json", "")}`,
1590
+ style: data.style || s,
1591
+ bounds: data.bounds || [0, 0, 0],
1592
+ partCount: Array.isArray(data.parts) ? data.parts.length : 0
1593
+ });
1594
+ } catch {
1595
+ }
1596
+ }
1597
+ }
1598
+ return {
1599
+ content: [
1600
+ {
1601
+ type: "text",
1602
+ text: JSON.stringify({ builds, total: builds.length })
1603
+ }
1604
+ ]
1605
+ };
1606
+ }
1607
+ async searchMaterials(query, maxResults) {
1608
+ const response = await this.client.request("/api/search-materials", {
1609
+ query: query ?? "",
1610
+ maxResults: maxResults ?? 50
1611
+ });
1612
+ return {
1613
+ content: [
1614
+ {
1615
+ type: "text",
1616
+ text: JSON.stringify(response)
1617
+ }
1618
+ ]
1619
+ };
1620
+ }
1621
+ async getBuild(id) {
1622
+ if (!id) {
1623
+ throw new Error("Build ID is required for get_build");
1624
+ }
1625
+ const filePath = path.join(_RobloxStudioTools.LIBRARY_PATH, `${id}.json`);
1626
+ if (!fs.existsSync(filePath)) {
1627
+ throw new Error(`Build not found in library: ${id}`);
1628
+ }
1629
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1630
+ const result = {
1631
+ id: data.id,
1632
+ style: data.style,
1633
+ bounds: data.bounds,
1634
+ partCount: Array.isArray(data.parts) ? data.parts.length : 0,
1635
+ paletteKeys: data.palette ? Object.keys(data.palette) : [],
1636
+ palette: data.palette
1637
+ };
1638
+ if (data.generatorCode) {
1639
+ result.generatorCode = data.generatorCode;
1640
+ result.generatorSeed = data.generatorSeed;
1641
+ }
1642
+ return {
1643
+ content: [
1644
+ {
1645
+ type: "text",
1646
+ text: JSON.stringify(result)
1647
+ }
1648
+ ]
1649
+ };
1650
+ }
1651
+ async importScene(sceneData, targetPath = "game.Workspace") {
1652
+ if (!sceneData) {
1653
+ throw new Error("sceneData is required for import_scene");
1654
+ }
1655
+ const libraryPath = _RobloxStudioTools.LIBRARY_PATH;
1656
+ const expandedBuilds = [];
1657
+ const modelMap = sceneData.models || {};
1658
+ const placements = sceneData.place || [];
1659
+ for (const placement of placements) {
1660
+ const [modelKey, position, rotation] = placement;
1661
+ const buildId = modelMap[modelKey];
1662
+ if (!buildId)
1663
+ continue;
1664
+ const filePath = path.join(libraryPath, `${buildId}.json`);
1665
+ if (!fs.existsSync(filePath)) {
1666
+ throw new Error(`Build not found in library: ${buildId}`);
1667
+ }
1668
+ const content = fs.readFileSync(filePath, "utf-8");
1669
+ const buildData = JSON.parse(content);
1670
+ const buildName = buildId.split("/").pop() || buildId;
1671
+ expandedBuilds.push({
1672
+ buildData,
1673
+ position: position || [0, 0, 0],
1674
+ rotation: rotation || [0, 0, 0],
1675
+ name: buildName
1676
+ });
1677
+ }
1678
+ const customs = sceneData.custom || [];
1679
+ for (const custom of customs) {
1680
+ expandedBuilds.push({
1681
+ buildData: {
1682
+ palette: custom.palette,
1683
+ parts: custom.parts
1684
+ },
1685
+ position: custom.o || [0, 0, 0],
1686
+ rotation: [0, 0, 0],
1687
+ name: custom.n || "Custom"
1688
+ });
1689
+ }
1690
+ if (expandedBuilds.length === 0) {
1691
+ throw new Error("No builds to import \u2014 check model references and library");
1692
+ }
1693
+ const response = await this.client.request("/api/import-scene", {
1694
+ expandedBuilds,
1695
+ targetPath
1696
+ });
1697
+ return {
1698
+ content: [
1699
+ {
1700
+ type: "text",
1701
+ text: JSON.stringify(response)
1702
+ }
1703
+ ]
1704
+ };
1705
+ }
1706
+ // === Asset Tools ===
1707
+ async searchAssets(assetType, query, maxResults, sortBy, verifiedCreatorsOnly) {
1708
+ if (!this.openCloudClient.hasApiKey()) {
1709
+ return {
1710
+ content: [{
1711
+ type: "text",
1712
+ text: JSON.stringify({ error: "ROBLOX_OPEN_CLOUD_API_KEY environment variable is not set. Set it to use Creator Store asset tools." })
1713
+ }]
1714
+ };
1715
+ }
1716
+ const response = await this.openCloudClient.searchAssets({
1717
+ searchCategoryType: assetType,
1718
+ query,
1719
+ maxPageSize: maxResults,
1720
+ sortCategory: sortBy,
1721
+ includeOnlyVerifiedCreators: verifiedCreatorsOnly
1722
+ });
1723
+ return {
1724
+ content: [{
1725
+ type: "text",
1726
+ text: JSON.stringify(response)
1727
+ }]
1728
+ };
1729
+ }
1730
+ async getAssetDetails(assetId) {
1731
+ if (!assetId) {
1732
+ throw new Error("Asset ID is required for get_asset_details");
1733
+ }
1734
+ if (!this.openCloudClient.hasApiKey()) {
1735
+ return {
1736
+ content: [{
1737
+ type: "text",
1738
+ text: JSON.stringify({ error: "ROBLOX_OPEN_CLOUD_API_KEY environment variable is not set. Set it to use Creator Store asset tools." })
1739
+ }]
1740
+ };
1741
+ }
1742
+ const response = await this.openCloudClient.getAssetDetails(assetId);
1743
+ return {
1744
+ content: [{
1745
+ type: "text",
1746
+ text: JSON.stringify(response)
1747
+ }]
1748
+ };
1749
+ }
1750
+ async getAssetThumbnail(assetId, size) {
1751
+ if (!assetId) {
1752
+ throw new Error("Asset ID is required for get_asset_thumbnail");
1753
+ }
1754
+ if (!this.openCloudClient.hasApiKey()) {
1755
+ return {
1756
+ content: [{
1757
+ type: "text",
1758
+ text: JSON.stringify({ error: "ROBLOX_OPEN_CLOUD_API_KEY environment variable is not set. Set it to use Creator Store asset tools." })
1759
+ }]
1760
+ };
1761
+ }
1762
+ const result = await this.openCloudClient.getAssetThumbnail(assetId, size);
1763
+ if (!result) {
1764
+ return {
1765
+ content: [{
1766
+ type: "text",
1767
+ text: JSON.stringify({ error: "Thumbnail not available for this asset" })
1768
+ }]
1769
+ };
1770
+ }
1771
+ return {
1772
+ content: [{
1773
+ type: "image",
1774
+ data: result.base64,
1775
+ mimeType: result.mimeType
1776
+ }]
1777
+ };
1778
+ }
1779
+ async insertAsset(assetId, parentPath, position) {
1780
+ if (!assetId) {
1781
+ throw new Error("Asset ID is required for insert_asset");
1782
+ }
1783
+ const response = await this.client.request("/api/insert-asset", {
1784
+ assetId,
1785
+ parentPath: parentPath || "game.Workspace",
1786
+ position
1787
+ });
1788
+ return {
1789
+ content: [{
1790
+ type: "text",
1791
+ text: JSON.stringify(response)
1792
+ }]
1793
+ };
1794
+ }
1795
+ async previewAsset(assetId, includeProperties, maxDepth) {
1796
+ if (!assetId) {
1797
+ throw new Error("Asset ID is required for preview_asset");
1798
+ }
1799
+ const response = await this.client.request("/api/preview-asset", {
1800
+ assetId,
1801
+ includeProperties: includeProperties ?? true,
1802
+ maxDepth: maxDepth ?? 10
1803
+ });
1804
+ return {
1805
+ content: [{
1806
+ type: "text",
1807
+ text: JSON.stringify(response)
1808
+ }]
1809
+ };
1810
+ }
1811
+ };
1812
+
1813
+ // ../../node_modules/@robloxstudio-mcp/core/dist/bridge-service.js
1814
+ import { v4 as uuidv4 } from "uuid";
1815
+ var BridgeService = class {
1816
+ pendingRequests = /* @__PURE__ */ new Map();
1817
+ requestTimeout = 3e4;
1818
+ async sendRequest(endpoint, data) {
1819
+ const requestId = uuidv4();
1820
+ return new Promise((resolve, reject) => {
1821
+ const timeoutId = setTimeout(() => {
1822
+ if (this.pendingRequests.has(requestId)) {
1823
+ this.pendingRequests.delete(requestId);
1824
+ reject(new Error("Request timeout"));
1825
+ }
1826
+ }, this.requestTimeout);
1827
+ const request = {
1828
+ id: requestId,
1829
+ endpoint,
1830
+ data,
1831
+ timestamp: Date.now(),
1832
+ resolve,
1833
+ reject,
1834
+ timeoutId
1835
+ };
1836
+ this.pendingRequests.set(requestId, request);
1837
+ });
1838
+ }
1839
+ getPendingRequest() {
1840
+ let oldestRequest = null;
1841
+ for (const request of this.pendingRequests.values()) {
1842
+ if (!oldestRequest || request.timestamp < oldestRequest.timestamp) {
1843
+ oldestRequest = request;
1844
+ }
1845
+ }
1846
+ if (oldestRequest) {
1847
+ return {
1848
+ requestId: oldestRequest.id,
1849
+ request: {
1850
+ endpoint: oldestRequest.endpoint,
1851
+ data: oldestRequest.data
1852
+ }
1853
+ };
1854
+ }
1855
+ return null;
1856
+ }
1857
+ resolveRequest(requestId, response) {
1858
+ const request = this.pendingRequests.get(requestId);
1859
+ if (request) {
1860
+ clearTimeout(request.timeoutId);
1861
+ this.pendingRequests.delete(requestId);
1862
+ request.resolve(response);
1863
+ }
1864
+ }
1865
+ rejectRequest(requestId, error) {
1866
+ const request = this.pendingRequests.get(requestId);
1867
+ if (request) {
1868
+ clearTimeout(request.timeoutId);
1869
+ this.pendingRequests.delete(requestId);
1870
+ request.reject(error);
1871
+ }
1872
+ }
1873
+ cleanupOldRequests() {
1874
+ const now = Date.now();
1875
+ for (const [id, request] of this.pendingRequests.entries()) {
1876
+ if (now - request.timestamp > this.requestTimeout) {
1877
+ clearTimeout(request.timeoutId);
1878
+ this.pendingRequests.delete(id);
1879
+ request.reject(new Error("Request timeout"));
1880
+ }
1881
+ }
1882
+ }
1883
+ clearAllPendingRequests() {
1884
+ for (const [, request] of this.pendingRequests.entries()) {
1885
+ clearTimeout(request.timeoutId);
1886
+ request.reject(new Error("Connection closed"));
1887
+ }
1888
+ this.pendingRequests.clear();
1889
+ }
1890
+ };
1891
+
1892
+ // ../../node_modules/@robloxstudio-mcp/core/dist/proxy-bridge-service.js
1893
+ import { v4 as uuidv42 } from "uuid";
1894
+ var ProxyBridgeService = class extends BridgeService {
1895
+ primaryBaseUrl;
1896
+ proxyInstanceId;
1897
+ proxyRequestTimeout = 3e4;
1898
+ constructor(primaryBaseUrl) {
1899
+ super();
1900
+ this.primaryBaseUrl = primaryBaseUrl;
1901
+ this.proxyInstanceId = uuidv42();
1902
+ }
1903
+ async sendRequest(endpoint, data) {
1904
+ const controller = new AbortController();
1905
+ const timeoutId = setTimeout(() => controller.abort(), this.proxyRequestTimeout);
1906
+ try {
1907
+ const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
1908
+ method: "POST",
1909
+ headers: { "Content-Type": "application/json" },
1910
+ body: JSON.stringify({ endpoint, data, proxyInstanceId: this.proxyInstanceId }),
1911
+ signal: controller.signal
1912
+ });
1913
+ clearTimeout(timeoutId);
1914
+ if (!response.ok) {
1915
+ const body = await response.text();
1916
+ throw new Error(`Proxy request failed (${response.status}): ${body}`);
1917
+ }
1918
+ const result = await response.json();
1919
+ if (result.error) {
1920
+ throw new Error(result.error);
1921
+ }
1922
+ return result.response;
1923
+ } catch (err) {
1924
+ clearTimeout(timeoutId);
1925
+ if (err.name === "AbortError") {
1926
+ throw new Error("Proxy request timeout");
1927
+ }
1928
+ throw err;
1929
+ }
1930
+ }
1931
+ cleanupOldRequests() {
1932
+ }
1933
+ clearAllPendingRequests() {
1934
+ }
1935
+ };
1936
+
1937
+ // ../../node_modules/@robloxstudio-mcp/core/dist/server.js
1938
+ var RobloxStudioMCPServer = class {
1939
+ server;
1940
+ tools;
1941
+ bridge;
1942
+ allowedToolNames;
1943
+ config;
1944
+ constructor(config) {
1945
+ this.config = config;
1946
+ this.allowedToolNames = new Set(config.tools.map((t) => t.name));
1947
+ this.server = new Server({
1948
+ name: config.name,
1949
+ version: config.version
1950
+ }, {
1951
+ capabilities: {
1952
+ tools: {}
1953
+ }
1954
+ });
1955
+ this.bridge = new BridgeService();
1956
+ this.tools = new RobloxStudioTools(this.bridge);
1957
+ this.setupToolHandlers();
1958
+ }
1959
+ setupToolHandlers() {
1960
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
1961
+ return {
1962
+ tools: this.config.tools.map((t) => ({
1963
+ name: t.name,
1964
+ description: t.description,
1965
+ inputSchema: t.inputSchema
1966
+ }))
1967
+ };
1968
+ });
1969
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
1970
+ const { name, arguments: args } = request.params;
1971
+ if (!this.allowedToolNames.has(name)) {
1972
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1973
+ }
1974
+ try {
1975
+ switch (name) {
1976
+ case "get_file_tree":
1977
+ return await this.tools.getFileTree(args?.path || "");
1978
+ case "search_files":
1979
+ return await this.tools.searchFiles(args?.query, args?.searchType || "name");
1980
+ case "get_place_info":
1981
+ return await this.tools.getPlaceInfo();
1982
+ case "get_services":
1983
+ return await this.tools.getServices(args?.serviceName);
1984
+ case "search_objects":
1985
+ return await this.tools.searchObjects(args?.query, args?.searchType || "name", args?.propertyName);
1986
+ case "get_instance_properties":
1987
+ return await this.tools.getInstanceProperties(args?.instancePath, args?.excludeSource);
1988
+ case "get_instance_children":
1989
+ return await this.tools.getInstanceChildren(args?.instancePath);
1990
+ case "search_by_property":
1991
+ return await this.tools.searchByProperty(args?.propertyName, args?.propertyValue);
1992
+ case "get_class_info":
1993
+ return await this.tools.getClassInfo(args?.className);
1994
+ case "get_project_structure":
1995
+ return await this.tools.getProjectStructure(args?.path, args?.maxDepth, args?.scriptsOnly);
1996
+ case "set_property":
1997
+ return await this.tools.setProperty(args?.instancePath, args?.propertyName, args?.propertyValue);
1998
+ case "mass_set_property":
1999
+ return await this.tools.massSetProperty(args?.paths, args?.propertyName, args?.propertyValue);
2000
+ case "mass_get_property":
2001
+ return await this.tools.massGetProperty(args?.paths, args?.propertyName);
2002
+ case "create_object":
2003
+ case "create_object_with_properties":
2004
+ return await this.tools.createObject(args?.className, args?.parent, args?.name, args?.properties);
2005
+ case "mass_create_objects":
2006
+ case "mass_create_objects_with_properties":
2007
+ return await this.tools.massCreateObjects(args?.objects);
2008
+ case "delete_object":
2009
+ return await this.tools.deleteObject(args?.instancePath);
2010
+ case "smart_duplicate":
2011
+ return await this.tools.smartDuplicate(args?.instancePath, args?.count, args?.options);
2012
+ case "mass_duplicate":
2013
+ return await this.tools.massDuplicate(args?.duplications);
2014
+ case "set_calculated_property":
2015
+ return await this.tools.setCalculatedProperty(args?.paths, args?.propertyName, args?.formula, args?.variables);
2016
+ case "set_relative_property":
2017
+ return await this.tools.setRelativeProperty(args?.paths, args?.propertyName, args?.operation, args?.value, args?.component);
2018
+ case "grep_scripts":
2019
+ return await this.tools.grepScripts(args?.pattern, {
2020
+ caseSensitive: args?.caseSensitive,
2021
+ usePattern: args?.usePattern,
2022
+ contextLines: args?.contextLines,
2023
+ maxResults: args?.maxResults,
2024
+ maxResultsPerScript: args?.maxResultsPerScript,
2025
+ filesOnly: args?.filesOnly,
2026
+ path: args?.path,
2027
+ classFilter: args?.classFilter
2028
+ });
2029
+ case "get_script_source":
2030
+ return await this.tools.getScriptSource(args?.instancePath, args?.startLine, args?.endLine);
2031
+ case "set_script_source":
2032
+ return await this.tools.setScriptSource(args?.instancePath, args?.source);
2033
+ case "edit_script_lines":
2034
+ return await this.tools.editScriptLines(args?.instancePath, args?.startLine, args?.endLine, args?.newContent);
2035
+ case "insert_script_lines":
2036
+ return await this.tools.insertScriptLines(args?.instancePath, args?.afterLine, args?.newContent);
2037
+ case "delete_script_lines":
2038
+ return await this.tools.deleteScriptLines(args?.instancePath, args?.startLine, args?.endLine);
2039
+ case "get_attribute":
2040
+ return await this.tools.getAttribute(args?.instancePath, args?.attributeName);
2041
+ case "set_attribute":
2042
+ return await this.tools.setAttribute(args?.instancePath, args?.attributeName, args?.attributeValue, args?.valueType);
2043
+ case "get_attributes":
2044
+ return await this.tools.getAttributes(args?.instancePath);
2045
+ case "delete_attribute":
2046
+ return await this.tools.deleteAttribute(args?.instancePath, args?.attributeName);
2047
+ case "get_tags":
2048
+ return await this.tools.getTags(args?.instancePath);
2049
+ case "add_tag":
2050
+ return await this.tools.addTag(args?.instancePath, args?.tagName);
2051
+ case "remove_tag":
2052
+ return await this.tools.removeTag(args?.instancePath, args?.tagName);
2053
+ case "get_tagged":
2054
+ return await this.tools.getTagged(args?.tagName);
2055
+ case "get_selection":
2056
+ return await this.tools.getSelection();
2057
+ case "execute_luau":
2058
+ return await this.tools.executeLuau(args?.code);
2059
+ case "start_playtest":
2060
+ return await this.tools.startPlaytest(args?.mode);
2061
+ case "stop_playtest":
2062
+ return await this.tools.stopPlaytest();
2063
+ case "get_playtest_output":
2064
+ return await this.tools.getPlaytestOutput();
2065
+ case "export_build":
2066
+ return await this.tools.exportBuild(args?.instancePath, args?.outputId, args?.style);
2067
+ case "create_build":
2068
+ return await this.tools.createBuild(args?.id, args?.style, args?.palette, args?.parts, args?.bounds);
2069
+ case "generate_build":
2070
+ return await this.tools.generateBuild(args?.id, args?.style, args?.palette, args?.code, args?.seed);
2071
+ case "import_build":
2072
+ return await this.tools.importBuild(args?.buildData, args?.targetPath, args?.position);
2073
+ case "list_library":
2074
+ return await this.tools.listLibrary(args?.style);
2075
+ case "search_materials":
2076
+ return await this.tools.searchMaterials(args?.query, args?.maxResults);
2077
+ case "get_build":
2078
+ return await this.tools.getBuild(args?.id);
2079
+ case "import_scene":
2080
+ return await this.tools.importScene(args?.sceneData, args?.targetPath);
2081
+ case "undo":
2082
+ return await this.tools.undo();
2083
+ case "redo":
2084
+ return await this.tools.redo();
2085
+ case "search_assets":
2086
+ return await this.tools.searchAssets(args?.assetType, args?.query, args?.maxResults, args?.sortBy, args?.verifiedCreatorsOnly);
2087
+ case "get_asset_details":
2088
+ return await this.tools.getAssetDetails(args?.assetId);
2089
+ case "get_asset_thumbnail":
2090
+ return await this.tools.getAssetThumbnail(args?.assetId, args?.size);
2091
+ case "insert_asset":
2092
+ return await this.tools.insertAsset(args?.assetId, args?.parentPath, args?.position);
2093
+ case "preview_asset":
2094
+ return await this.tools.previewAsset(args?.assetId, args?.includeProperties, args?.maxDepth);
2095
+ default:
2096
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
2097
+ }
2098
+ } catch (error) {
2099
+ if (error instanceof McpError)
2100
+ throw error;
2101
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
2102
+ }
2103
+ });
2104
+ }
2105
+ async run() {
2106
+ const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
2107
+ const host = process.env.ROBLOX_STUDIO_HOST || "0.0.0.0";
2108
+ let bridgeMode = "primary";
2109
+ let httpHandle;
2110
+ let primaryApp;
2111
+ let boundPort = 0;
2112
+ let promotionInterval;
2113
+ try {
2114
+ primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames);
2115
+ const result = await listenWithRetry(primaryApp, host, basePort, 5);
2116
+ httpHandle = result.server;
2117
+ boundPort = result.port;
2118
+ console.error(`HTTP server listening on ${host}:${boundPort} for Studio plugin (primary mode)`);
2119
+ } catch {
2120
+ bridgeMode = "proxy";
2121
+ primaryApp = void 0;
2122
+ const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`);
2123
+ this.bridge = proxyBridge;
2124
+ this.tools = new RobloxStudioTools(this.bridge);
2125
+ console.error(`All ports ${basePort}-${basePort + 4} in use \u2014 entering proxy mode (forwarding to localhost:${basePort})`);
2126
+ const promotionIntervalMs = parseInt(process.env.ROBLOX_STUDIO_PROXY_PROMOTION_INTERVAL_MS || "5000");
2127
+ promotionInterval = setInterval(async () => {
2128
+ try {
2129
+ this.bridge = new BridgeService();
2130
+ this.tools = new RobloxStudioTools(this.bridge);
2131
+ primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames);
2132
+ const result = await listenWithRetry(primaryApp, host, basePort, 5);
2133
+ httpHandle = result.server;
2134
+ boundPort = result.port;
2135
+ bridgeMode = "primary";
2136
+ primaryApp.setMCPServerActive(true);
2137
+ console.error(`Promoted from proxy to primary on port ${boundPort}`);
2138
+ if (promotionInterval)
2139
+ clearInterval(promotionInterval);
2140
+ } catch {
2141
+ this.bridge = new ProxyBridgeService(`http://localhost:${basePort}`);
2142
+ this.tools = new RobloxStudioTools(this.bridge);
2143
+ primaryApp = void 0;
2144
+ }
2145
+ }, promotionIntervalMs);
2146
+ }
2147
+ const LEGACY_PORT = 3002;
2148
+ let legacyHandle;
2149
+ let legacyApp;
2150
+ if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
2151
+ legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames);
2152
+ try {
2153
+ const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
2154
+ legacyHandle = result.server;
2155
+ console.error(`Legacy HTTP server also listening on ${host}:${LEGACY_PORT} for old plugins`);
2156
+ legacyApp.setMCPServerActive(true);
2157
+ } catch {
2158
+ console.error(`Legacy port ${LEGACY_PORT} in use, skipping backward-compat listener`);
2159
+ }
2160
+ }
2161
+ const transport = new StdioServerTransport();
2162
+ await this.server.connect(transport);
2163
+ console.error(`${this.config.name} v${this.config.version} running on stdio`);
2164
+ if (primaryApp) {
2165
+ primaryApp.setMCPServerActive(true);
2166
+ }
2167
+ console.error(bridgeMode === "primary" ? "MCP server marked as active (primary mode)" : "MCP server active in proxy mode \u2014 forwarding requests to primary");
2168
+ console.error("Waiting for Studio plugin to connect...");
2169
+ const activityInterval = setInterval(() => {
2170
+ if (primaryApp)
2171
+ primaryApp.trackMCPActivity();
2172
+ if (legacyApp)
2173
+ legacyApp.trackMCPActivity();
2174
+ if (bridgeMode === "primary" && primaryApp) {
2175
+ const pluginConnected = primaryApp.isPluginConnected();
2176
+ const mcpActive = primaryApp.isMCPServerActive();
2177
+ if (pluginConnected && mcpActive) {
2178
+ } else if (pluginConnected && !mcpActive) {
2179
+ console.error("Studio plugin connected, but MCP server inactive");
2180
+ } else if (!pluginConnected && mcpActive) {
2181
+ console.error("MCP server active, waiting for Studio plugin...");
2182
+ } else {
2183
+ console.error("Waiting for connections...");
2184
+ }
2185
+ }
2186
+ }, 5e3);
2187
+ const cleanupInterval = setInterval(() => {
2188
+ this.bridge.cleanupOldRequests();
2189
+ }, 5e3);
2190
+ const shutdown = () => {
2191
+ console.error("Shutting down MCP server...");
2192
+ clearInterval(activityInterval);
2193
+ clearInterval(cleanupInterval);
2194
+ if (promotionInterval)
2195
+ clearInterval(promotionInterval);
2196
+ if (httpHandle)
2197
+ httpHandle.close();
2198
+ if (legacyHandle)
2199
+ legacyHandle.close();
2200
+ process.exit(0);
2201
+ };
2202
+ process.on("SIGTERM", shutdown);
2203
+ process.on("SIGINT", shutdown);
2204
+ process.on("SIGHUP", shutdown);
2205
+ process.stdin.on("end", shutdown);
2206
+ process.stdin.on("close", shutdown);
2207
+ }
2208
+ };
2209
+
2210
+ // ../../node_modules/@robloxstudio-mcp/core/dist/tools/definitions.js
2211
+ var TOOL_DEFINITIONS = [
2212
+ // === File & Instance Browsing ===
2213
+ {
2214
+ name: "get_file_tree",
2215
+ category: "read",
2216
+ description: "Get instance hierarchy tree from Studio",
2217
+ inputSchema: {
2218
+ type: "object",
2219
+ properties: {
2220
+ path: {
2221
+ type: "string",
2222
+ description: "Root path (default: game root)",
2223
+ default: ""
2224
+ }
2225
+ }
2226
+ }
2227
+ },
2228
+ {
2229
+ name: "search_files",
2230
+ category: "read",
2231
+ description: "Search instances by name, class, or script content",
2232
+ inputSchema: {
2233
+ type: "object",
2234
+ properties: {
2235
+ query: {
2236
+ type: "string",
2237
+ description: "Name, class, or code pattern"
2238
+ },
2239
+ searchType: {
2240
+ type: "string",
2241
+ enum: ["name", "type", "content"],
2242
+ description: "Search mode",
2243
+ default: "name"
2244
+ }
2245
+ },
2246
+ required: ["query"]
2247
+ }
2248
+ },
2249
+ // === Place & Service Info ===
2250
+ {
2251
+ name: "get_place_info",
2252
+ category: "read",
2253
+ description: "Get place ID, name, and game settings",
2254
+ inputSchema: {
2255
+ type: "object",
2256
+ properties: {}
2257
+ }
2258
+ },
2259
+ {
2260
+ name: "get_services",
2261
+ category: "read",
2262
+ description: "Get available services and their children",
2263
+ inputSchema: {
2264
+ type: "object",
2265
+ properties: {
2266
+ serviceName: {
2267
+ type: "string",
2268
+ description: "Specific service name"
2269
+ }
2270
+ }
2271
+ }
2272
+ },
2273
+ {
2274
+ name: "search_objects",
2275
+ category: "read",
2276
+ description: "Find instances by name, class, or properties",
2277
+ inputSchema: {
2278
+ type: "object",
2279
+ properties: {
2280
+ query: {
2281
+ type: "string",
2282
+ description: "Search query"
2283
+ },
2284
+ searchType: {
2285
+ type: "string",
2286
+ enum: ["name", "class", "property"],
2287
+ description: "Search mode",
2288
+ default: "name"
2289
+ },
2290
+ propertyName: {
2291
+ type: "string",
2292
+ description: 'Property name when searchType is "property"'
2293
+ }
2294
+ },
2295
+ required: ["query"]
2296
+ }
2297
+ },
2298
+ // === Instance Inspection ===
2299
+ {
2300
+ name: "get_instance_properties",
2301
+ category: "read",
2302
+ description: "Get all properties of an instance",
2303
+ inputSchema: {
2304
+ type: "object",
2305
+ properties: {
2306
+ instancePath: {
2307
+ type: "string",
2308
+ description: "Instance path (dot notation)"
2309
+ },
2310
+ excludeSource: {
2311
+ type: "boolean",
2312
+ description: "For scripts, return SourceLength/LineCount instead of full source (default: false)",
2313
+ default: false
2314
+ }
2315
+ },
2316
+ required: ["instancePath"]
2317
+ }
2318
+ },
2319
+ {
2320
+ name: "get_instance_children",
2321
+ category: "read",
2322
+ description: "Get children and their class types",
2323
+ inputSchema: {
2324
+ type: "object",
2325
+ properties: {
2326
+ instancePath: {
2327
+ type: "string",
2328
+ description: "Instance path (dot notation)"
2329
+ }
2330
+ },
2331
+ required: ["instancePath"]
2332
+ }
2333
+ },
2334
+ {
2335
+ name: "search_by_property",
2336
+ category: "read",
2337
+ description: "Find objects with specific property values",
2338
+ inputSchema: {
2339
+ type: "object",
2340
+ properties: {
2341
+ propertyName: {
2342
+ type: "string",
2343
+ description: "Property name"
2344
+ },
2345
+ propertyValue: {
2346
+ type: "string",
2347
+ description: "Value to match"
2348
+ }
2349
+ },
2350
+ required: ["propertyName", "propertyValue"]
2351
+ }
2352
+ },
2353
+ {
2354
+ name: "get_class_info",
2355
+ category: "read",
2356
+ description: "Get properties/methods for a class",
2357
+ inputSchema: {
2358
+ type: "object",
2359
+ properties: {
2360
+ className: {
2361
+ type: "string",
2362
+ description: "Roblox class name"
2363
+ }
2364
+ },
2365
+ required: ["className"]
2366
+ }
2367
+ },
2368
+ // === Project Structure ===
2369
+ {
2370
+ name: "get_project_structure",
2371
+ category: "read",
2372
+ description: "Get full game hierarchy tree. Increase maxDepth (default 3) for deeper traversal.",
2373
+ inputSchema: {
2374
+ type: "object",
2375
+ properties: {
2376
+ path: {
2377
+ type: "string",
2378
+ description: "Root path (default: workspace root)",
2379
+ default: ""
2380
+ },
2381
+ maxDepth: {
2382
+ type: "number",
2383
+ description: "Max traversal depth (default: 3)",
2384
+ default: 3
2385
+ },
2386
+ scriptsOnly: {
2387
+ type: "boolean",
2388
+ description: "Show only scripts",
2389
+ default: false
2390
+ }
2391
+ }
2392
+ }
2393
+ },
2394
+ // === Property Write ===
2395
+ {
2396
+ name: "set_property",
2397
+ category: "write",
2398
+ description: "Set a property on an instance",
2399
+ inputSchema: {
2400
+ type: "object",
2401
+ properties: {
2402
+ instancePath: {
2403
+ type: "string",
2404
+ description: "Instance path (dot notation)"
2405
+ },
2406
+ propertyName: {
2407
+ type: "string",
2408
+ description: "Property name"
2409
+ },
2410
+ propertyValue: {
2411
+ description: "Value to set"
2412
+ }
2413
+ },
2414
+ required: ["instancePath", "propertyName", "propertyValue"]
2415
+ }
2416
+ },
2417
+ {
2418
+ name: "mass_set_property",
2419
+ category: "write",
2420
+ description: "Set a property on multiple instances",
2421
+ inputSchema: {
2422
+ type: "object",
2423
+ properties: {
2424
+ paths: {
2425
+ type: "array",
2426
+ items: { type: "string" },
2427
+ description: "Instance paths"
2428
+ },
2429
+ propertyName: {
2430
+ type: "string",
2431
+ description: "Property name"
2432
+ },
2433
+ propertyValue: {
2434
+ description: "Value to set"
2435
+ }
2436
+ },
2437
+ required: ["paths", "propertyName", "propertyValue"]
2438
+ }
2439
+ },
2440
+ {
2441
+ name: "mass_get_property",
2442
+ category: "read",
2443
+ description: "Get a property from multiple instances",
2444
+ inputSchema: {
2445
+ type: "object",
2446
+ properties: {
2447
+ paths: {
2448
+ type: "array",
2449
+ items: { type: "string" },
2450
+ description: "Instance paths"
2451
+ },
2452
+ propertyName: {
2453
+ type: "string",
2454
+ description: "Property name"
2455
+ }
2456
+ },
2457
+ required: ["paths", "propertyName"]
2458
+ }
2459
+ },
2460
+ // === Object Creation/Deletion ===
2461
+ {
2462
+ name: "create_object",
2463
+ category: "write",
2464
+ description: "Create a new instance. Optionally set properties on creation.",
2465
+ inputSchema: {
2466
+ type: "object",
2467
+ properties: {
2468
+ className: {
2469
+ type: "string",
2470
+ description: "Roblox class name"
2471
+ },
2472
+ parent: {
2473
+ type: "string",
2474
+ description: "Parent instance path"
2475
+ },
2476
+ name: {
2477
+ type: "string",
2478
+ description: "Optional name"
2479
+ },
2480
+ properties: {
2481
+ type: "object",
2482
+ description: "Properties to set on creation"
2483
+ }
2484
+ },
2485
+ required: ["className", "parent"]
2486
+ }
2487
+ },
2488
+ {
2489
+ name: "mass_create_objects",
2490
+ category: "write",
2491
+ description: "Create multiple instances. Each can have optional properties.",
2492
+ inputSchema: {
2493
+ type: "object",
2494
+ properties: {
2495
+ objects: {
2496
+ type: "array",
2497
+ items: {
2498
+ type: "object",
2499
+ properties: {
2500
+ className: {
2501
+ type: "string",
2502
+ description: "Roblox class name"
2503
+ },
2504
+ parent: {
2505
+ type: "string",
2506
+ description: "Parent instance path"
2507
+ },
2508
+ name: {
2509
+ type: "string",
2510
+ description: "Optional name"
2511
+ },
2512
+ properties: {
2513
+ type: "object",
2514
+ description: "Properties to set on creation"
2515
+ }
2516
+ },
2517
+ required: ["className", "parent"]
2518
+ },
2519
+ description: "Objects to create"
2520
+ }
2521
+ },
2522
+ required: ["objects"]
2523
+ }
2524
+ },
2525
+ {
2526
+ name: "delete_object",
2527
+ category: "write",
2528
+ description: "Delete an instance",
2529
+ inputSchema: {
2530
+ type: "object",
2531
+ properties: {
2532
+ instancePath: {
2533
+ type: "string",
2534
+ description: "Instance path (dot notation)"
2535
+ }
2536
+ },
2537
+ required: ["instancePath"]
2538
+ }
2539
+ },
2540
+ // === Duplication ===
2541
+ {
2542
+ name: "smart_duplicate",
2543
+ category: "write",
2544
+ description: "Duplicate with naming, positioning, and property variations",
2545
+ inputSchema: {
2546
+ type: "object",
2547
+ properties: {
2548
+ instancePath: {
2549
+ type: "string",
2550
+ description: "Instance path (dot notation)"
2551
+ },
2552
+ count: {
2553
+ type: "number",
2554
+ description: "Number of duplicates"
2555
+ },
2556
+ options: {
2557
+ type: "object",
2558
+ properties: {
2559
+ namePattern: {
2560
+ type: "string",
2561
+ description: "Name pattern ({n} placeholder)"
2562
+ },
2563
+ positionOffset: {
2564
+ type: "array",
2565
+ items: { type: "number" },
2566
+ minItems: 3,
2567
+ maxItems: 3,
2568
+ description: "X, Y, Z offset per duplicate"
2569
+ },
2570
+ rotationOffset: {
2571
+ type: "array",
2572
+ items: { type: "number" },
2573
+ minItems: 3,
2574
+ maxItems: 3,
2575
+ description: "X, Y, Z rotation offset"
2576
+ },
2577
+ scaleOffset: {
2578
+ type: "array",
2579
+ items: { type: "number" },
2580
+ minItems: 3,
2581
+ maxItems: 3,
2582
+ description: "X, Y, Z scale multiplier"
2583
+ },
2584
+ propertyVariations: {
2585
+ type: "object",
2586
+ description: "Property name to array of values"
2587
+ },
2588
+ targetParents: {
2589
+ type: "array",
2590
+ items: { type: "string" },
2591
+ description: "Different parent per duplicate"
996
2592
  }
997
- catch (err) {
998
- if (err.code === 'EADDRINUSE') {
999
- console.error(`Port ${port} in use, trying next...`);
1000
- if (port === maxPort) {
1001
- throw new Error(`All ports ${basePort}-${maxPort} are in use. Stop some MCP server instances and retry.`);
1002
- }
1003
- continue;
2593
+ }
2594
+ }
2595
+ },
2596
+ required: ["instancePath", "count"]
2597
+ }
2598
+ },
2599
+ {
2600
+ name: "mass_duplicate",
2601
+ category: "write",
2602
+ description: "Batch smart_duplicate operations",
2603
+ inputSchema: {
2604
+ type: "object",
2605
+ properties: {
2606
+ duplications: {
2607
+ type: "array",
2608
+ items: {
2609
+ type: "object",
2610
+ properties: {
2611
+ instancePath: {
2612
+ type: "string",
2613
+ description: "Instance path (dot notation)"
2614
+ },
2615
+ count: {
2616
+ type: "number",
2617
+ description: "Number of duplicates"
2618
+ },
2619
+ options: {
2620
+ type: "object",
2621
+ properties: {
2622
+ namePattern: {
2623
+ type: "string",
2624
+ description: "Name pattern ({n} placeholder)"
2625
+ },
2626
+ positionOffset: {
2627
+ type: "array",
2628
+ items: { type: "number" },
2629
+ minItems: 3,
2630
+ maxItems: 3,
2631
+ description: "X, Y, Z offset per duplicate"
2632
+ },
2633
+ rotationOffset: {
2634
+ type: "array",
2635
+ items: { type: "number" },
2636
+ minItems: 3,
2637
+ maxItems: 3,
2638
+ description: "X, Y, Z rotation offset"
2639
+ },
2640
+ scaleOffset: {
2641
+ type: "array",
2642
+ items: { type: "number" },
2643
+ minItems: 3,
2644
+ maxItems: 3,
2645
+ description: "X, Y, Z scale multiplier"
2646
+ },
2647
+ propertyVariations: {
2648
+ type: "object",
2649
+ description: "Property name to array of values"
2650
+ },
2651
+ targetParents: {
2652
+ type: "array",
2653
+ items: { type: "string" },
2654
+ description: "Different parent per duplicate"
2655
+ }
1004
2656
  }
1005
- throw err;
1006
- }
2657
+ }
2658
+ },
2659
+ required: ["instancePath", "count"]
2660
+ },
2661
+ description: "Duplication operations"
1007
2662
  }
1008
- const LEGACY_PORT = 3002;
1009
- let legacyServer;
1010
- if (boundPort !== LEGACY_PORT) {
1011
- const legacy = createHttpServer(this.tools, this.bridge);
1012
- legacyServer = legacy;
1013
- try {
1014
- await new Promise((resolve, reject) => {
1015
- const onError = (err) => {
1016
- if (err.code === 'EADDRINUSE') {
1017
- legacy.removeListener('error', onError);
1018
- reject(err);
1019
- }
1020
- else {
1021
- reject(err);
1022
- }
1023
- };
1024
- legacy.once('error', onError);
1025
- legacy.listen(LEGACY_PORT, host, () => {
1026
- legacy.removeListener('error', onError);
1027
- console.error(`Legacy HTTP server also listening on ${host}:${LEGACY_PORT} for old plugins`);
1028
- resolve();
1029
- });
1030
- });
1031
- legacy.setMCPServerActive(true);
1032
- }
1033
- catch {
1034
- console.error(`Legacy port ${LEGACY_PORT} in use, skipping backward-compat listener`);
1035
- }
2663
+ },
2664
+ required: ["duplications"]
2665
+ }
2666
+ },
2667
+ // === Calculated/Relative Properties ===
2668
+ {
2669
+ name: "set_calculated_property",
2670
+ category: "write",
2671
+ description: 'Set properties via formula (e.g. "index * 50")',
2672
+ inputSchema: {
2673
+ type: "object",
2674
+ properties: {
2675
+ paths: {
2676
+ type: "array",
2677
+ items: { type: "string" },
2678
+ description: "Instance paths"
2679
+ },
2680
+ propertyName: {
2681
+ type: "string",
2682
+ description: "Property name"
2683
+ },
2684
+ formula: {
2685
+ type: "string",
2686
+ description: "Formula expression"
2687
+ },
2688
+ variables: {
2689
+ type: "object",
2690
+ description: "Additional formula variables"
1036
2691
  }
1037
- const transport = new StdioServerTransport();
1038
- await this.server.connect(transport);
1039
- console.error('Roblox Studio MCP server running on stdio');
1040
- httpServer.setMCPServerActive(true);
1041
- console.error('MCP server marked as active');
1042
- console.error('Waiting for Studio plugin to connect...');
1043
- setInterval(() => {
1044
- httpServer.trackMCPActivity();
1045
- if (legacyServer)
1046
- legacyServer.trackMCPActivity();
1047
- const pluginConnected = httpServer.isPluginConnected();
1048
- const mcpActive = httpServer.isMCPServerActive();
1049
- if (pluginConnected && mcpActive) {
1050
- }
1051
- else if (pluginConnected && !mcpActive) {
1052
- console.error('Studio plugin connected, but MCP server inactive');
1053
- }
1054
- else if (!pluginConnected && mcpActive) {
1055
- console.error('MCP server active, waiting for Studio plugin...');
1056
- }
1057
- else {
1058
- console.error('Waiting for connections...');
2692
+ },
2693
+ required: ["paths", "propertyName", "formula"]
2694
+ }
2695
+ },
2696
+ {
2697
+ name: "set_relative_property",
2698
+ category: "write",
2699
+ description: "Modify properties relative to current values",
2700
+ inputSchema: {
2701
+ type: "object",
2702
+ properties: {
2703
+ paths: {
2704
+ type: "array",
2705
+ items: { type: "string" },
2706
+ description: "Instance paths"
2707
+ },
2708
+ propertyName: {
2709
+ type: "string",
2710
+ description: "Property name"
2711
+ },
2712
+ operation: {
2713
+ type: "string",
2714
+ enum: ["add", "multiply", "divide", "subtract", "power"],
2715
+ description: "Operation"
2716
+ },
2717
+ value: {
2718
+ description: "Operand value"
2719
+ },
2720
+ component: {
2721
+ type: "string",
2722
+ enum: ["X", "Y", "Z", "XScale", "XOffset", "YScale", "YOffset"],
2723
+ description: "Vector3/UDim2 component"
2724
+ }
2725
+ },
2726
+ required: ["paths", "propertyName", "operation", "value"]
2727
+ }
2728
+ },
2729
+ // === Script Read/Write ===
2730
+ {
2731
+ name: "get_script_source",
2732
+ category: "read",
2733
+ description: 'Get script source. Returns "source" and "numberedSource" (line-numbered). Use startLine/endLine for large scripts.',
2734
+ inputSchema: {
2735
+ type: "object",
2736
+ properties: {
2737
+ instancePath: {
2738
+ type: "string",
2739
+ description: "Script instance path"
2740
+ },
2741
+ startLine: {
2742
+ type: "number",
2743
+ description: "Start line (1-indexed)"
2744
+ },
2745
+ endLine: {
2746
+ type: "number",
2747
+ description: "End line (inclusive)"
2748
+ }
2749
+ },
2750
+ required: ["instancePath"]
2751
+ }
2752
+ },
2753
+ {
2754
+ name: "set_script_source",
2755
+ category: "write",
2756
+ description: "Replace entire script source. For partial edits use edit/insert/delete_script_lines.",
2757
+ inputSchema: {
2758
+ type: "object",
2759
+ properties: {
2760
+ instancePath: {
2761
+ type: "string",
2762
+ description: "Script instance path"
2763
+ },
2764
+ source: {
2765
+ type: "string",
2766
+ description: "New source code"
2767
+ }
2768
+ },
2769
+ required: ["instancePath", "source"]
2770
+ }
2771
+ },
2772
+ {
2773
+ name: "edit_script_lines",
2774
+ category: "write",
2775
+ description: "Replace a range of lines. 1-indexed, inclusive. Use numberedSource for line numbers.",
2776
+ inputSchema: {
2777
+ type: "object",
2778
+ properties: {
2779
+ instancePath: {
2780
+ type: "string",
2781
+ description: "Script instance path"
2782
+ },
2783
+ startLine: {
2784
+ type: "number",
2785
+ description: "Start line (1-indexed)"
2786
+ },
2787
+ endLine: {
2788
+ type: "number",
2789
+ description: "End line (inclusive)"
2790
+ },
2791
+ newContent: {
2792
+ type: "string",
2793
+ description: "Replacement content"
2794
+ }
2795
+ },
2796
+ required: ["instancePath", "startLine", "endLine", "newContent"]
2797
+ }
2798
+ },
2799
+ {
2800
+ name: "insert_script_lines",
2801
+ category: "write",
2802
+ description: "Insert lines after a given line number (0 = beginning).",
2803
+ inputSchema: {
2804
+ type: "object",
2805
+ properties: {
2806
+ instancePath: {
2807
+ type: "string",
2808
+ description: "Script instance path"
2809
+ },
2810
+ afterLine: {
2811
+ type: "number",
2812
+ description: "Insert after this line (0 = beginning)",
2813
+ default: 0
2814
+ },
2815
+ newContent: {
2816
+ type: "string",
2817
+ description: "Content to insert"
2818
+ }
2819
+ },
2820
+ required: ["instancePath", "newContent"]
2821
+ }
2822
+ },
2823
+ {
2824
+ name: "delete_script_lines",
2825
+ category: "write",
2826
+ description: "Delete a range of lines. 1-indexed, inclusive.",
2827
+ inputSchema: {
2828
+ type: "object",
2829
+ properties: {
2830
+ instancePath: {
2831
+ type: "string",
2832
+ description: "Script instance path"
2833
+ },
2834
+ startLine: {
2835
+ type: "number",
2836
+ description: "Start line (1-indexed)"
2837
+ },
2838
+ endLine: {
2839
+ type: "number",
2840
+ description: "End line (inclusive)"
2841
+ }
2842
+ },
2843
+ required: ["instancePath", "startLine", "endLine"]
2844
+ }
2845
+ },
2846
+ // === Attributes ===
2847
+ {
2848
+ name: "get_attribute",
2849
+ category: "read",
2850
+ description: "Get an attribute value",
2851
+ inputSchema: {
2852
+ type: "object",
2853
+ properties: {
2854
+ instancePath: {
2855
+ type: "string",
2856
+ description: "Instance path (dot notation)"
2857
+ },
2858
+ attributeName: {
2859
+ type: "string",
2860
+ description: "Attribute name"
2861
+ }
2862
+ },
2863
+ required: ["instancePath", "attributeName"]
2864
+ }
2865
+ },
2866
+ {
2867
+ name: "set_attribute",
2868
+ category: "write",
2869
+ description: "Set an attribute. Supports primitives, Vector3, Color3, UDim2, BrickColor.",
2870
+ inputSchema: {
2871
+ type: "object",
2872
+ properties: {
2873
+ instancePath: {
2874
+ type: "string",
2875
+ description: "Instance path (dot notation)"
2876
+ },
2877
+ attributeName: {
2878
+ type: "string",
2879
+ description: "Attribute name"
2880
+ },
2881
+ attributeValue: {
2882
+ description: "Value. Objects for Vector3/Color3/UDim2."
2883
+ },
2884
+ valueType: {
2885
+ type: "string",
2886
+ description: "Type hint if needed"
2887
+ }
2888
+ },
2889
+ required: ["instancePath", "attributeName", "attributeValue"]
2890
+ }
2891
+ },
2892
+ {
2893
+ name: "get_attributes",
2894
+ category: "read",
2895
+ description: "Get all attributes on an instance",
2896
+ inputSchema: {
2897
+ type: "object",
2898
+ properties: {
2899
+ instancePath: {
2900
+ type: "string",
2901
+ description: "Instance path (dot notation)"
2902
+ }
2903
+ },
2904
+ required: ["instancePath"]
2905
+ }
2906
+ },
2907
+ {
2908
+ name: "delete_attribute",
2909
+ category: "write",
2910
+ description: "Delete an attribute",
2911
+ inputSchema: {
2912
+ type: "object",
2913
+ properties: {
2914
+ instancePath: {
2915
+ type: "string",
2916
+ description: "Instance path (dot notation)"
2917
+ },
2918
+ attributeName: {
2919
+ type: "string",
2920
+ description: "Attribute name"
2921
+ }
2922
+ },
2923
+ required: ["instancePath", "attributeName"]
2924
+ }
2925
+ },
2926
+ // === Tags ===
2927
+ {
2928
+ name: "get_tags",
2929
+ category: "read",
2930
+ description: "Get all tags on an instance",
2931
+ inputSchema: {
2932
+ type: "object",
2933
+ properties: {
2934
+ instancePath: {
2935
+ type: "string",
2936
+ description: "Instance path (dot notation)"
2937
+ }
2938
+ },
2939
+ required: ["instancePath"]
2940
+ }
2941
+ },
2942
+ {
2943
+ name: "add_tag",
2944
+ category: "write",
2945
+ description: "Add a tag",
2946
+ inputSchema: {
2947
+ type: "object",
2948
+ properties: {
2949
+ instancePath: {
2950
+ type: "string",
2951
+ description: "Instance path (dot notation)"
2952
+ },
2953
+ tagName: {
2954
+ type: "string",
2955
+ description: "Tag name"
2956
+ }
2957
+ },
2958
+ required: ["instancePath", "tagName"]
2959
+ }
2960
+ },
2961
+ {
2962
+ name: "remove_tag",
2963
+ category: "write",
2964
+ description: "Remove a tag",
2965
+ inputSchema: {
2966
+ type: "object",
2967
+ properties: {
2968
+ instancePath: {
2969
+ type: "string",
2970
+ description: "Instance path (dot notation)"
2971
+ },
2972
+ tagName: {
2973
+ type: "string",
2974
+ description: "Tag name"
2975
+ }
2976
+ },
2977
+ required: ["instancePath", "tagName"]
2978
+ }
2979
+ },
2980
+ {
2981
+ name: "get_tagged",
2982
+ category: "read",
2983
+ description: "Get all instances with a specific tag",
2984
+ inputSchema: {
2985
+ type: "object",
2986
+ properties: {
2987
+ tagName: {
2988
+ type: "string",
2989
+ description: "Tag name"
2990
+ }
2991
+ },
2992
+ required: ["tagName"]
2993
+ }
2994
+ },
2995
+ // === Selection ===
2996
+ {
2997
+ name: "get_selection",
2998
+ category: "read",
2999
+ description: "Get all currently selected objects",
3000
+ inputSchema: {
3001
+ type: "object",
3002
+ properties: {}
3003
+ }
3004
+ },
3005
+ // === Luau Execution ===
3006
+ {
3007
+ name: "execute_luau",
3008
+ category: "write",
3009
+ description: "Execute Luau code in plugin context. Use print()/warn() for output. Return value is captured.",
3010
+ inputSchema: {
3011
+ type: "object",
3012
+ properties: {
3013
+ code: {
3014
+ type: "string",
3015
+ description: "Luau code to execute"
3016
+ }
3017
+ },
3018
+ required: ["code"]
3019
+ }
3020
+ },
3021
+ // === Script Search ===
3022
+ {
3023
+ name: "grep_scripts",
3024
+ category: "read",
3025
+ description: "Ripgrep-inspired search across all script sources. Supports literal and Lua pattern matching, context lines, early termination, and results grouped by script with line/column numbers.",
3026
+ inputSchema: {
3027
+ type: "object",
3028
+ properties: {
3029
+ pattern: {
3030
+ type: "string",
3031
+ description: "Search pattern (literal string or Lua pattern)"
3032
+ },
3033
+ caseSensitive: {
3034
+ type: "boolean",
3035
+ description: "Case-sensitive search (default: false)",
3036
+ default: false
3037
+ },
3038
+ usePattern: {
3039
+ type: "boolean",
3040
+ description: "Use Lua pattern matching instead of literal (default: false)",
3041
+ default: false
3042
+ },
3043
+ contextLines: {
3044
+ type: "number",
3045
+ description: "Number of context lines before/after each match (like rg -C)",
3046
+ default: 0
3047
+ },
3048
+ maxResults: {
3049
+ type: "number",
3050
+ description: "Max total matches before stopping (default: 100)",
3051
+ default: 100
3052
+ },
3053
+ maxResultsPerScript: {
3054
+ type: "number",
3055
+ description: "Max matches per script (like rg -m)"
3056
+ },
3057
+ filesOnly: {
3058
+ type: "boolean",
3059
+ description: "Only return matching script paths, not line details (like rg -l)",
3060
+ default: false
3061
+ },
3062
+ path: {
3063
+ type: "string",
3064
+ description: 'Subtree to search (e.g. "game.ServerScriptService")'
3065
+ },
3066
+ classFilter: {
3067
+ type: "string",
3068
+ enum: ["Script", "LocalScript", "ModuleScript"],
3069
+ description: "Only search scripts of this class type"
3070
+ }
3071
+ },
3072
+ required: ["pattern"]
3073
+ }
3074
+ },
3075
+ // === Playtest ===
3076
+ {
3077
+ name: "start_playtest",
3078
+ category: "read",
3079
+ description: "Start playtest. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest.",
3080
+ inputSchema: {
3081
+ type: "object",
3082
+ properties: {
3083
+ mode: {
3084
+ type: "string",
3085
+ enum: ["play", "run"],
3086
+ description: "Play mode"
3087
+ }
3088
+ },
3089
+ required: ["mode"]
3090
+ }
3091
+ },
3092
+ {
3093
+ name: "stop_playtest",
3094
+ category: "read",
3095
+ description: "Stop playtest and return all captured output.",
3096
+ inputSchema: {
3097
+ type: "object",
3098
+ properties: {}
3099
+ }
3100
+ },
3101
+ {
3102
+ name: "get_playtest_output",
3103
+ category: "read",
3104
+ description: "Poll output buffer without stopping. Returns isRunning and captured messages.",
3105
+ inputSchema: {
3106
+ type: "object",
3107
+ properties: {}
3108
+ }
3109
+ },
3110
+ // === Undo/Redo ===
3111
+ {
3112
+ name: "undo",
3113
+ category: "write",
3114
+ description: "Undo the last change in Roblox Studio. Uses ChangeHistoryService to reverse the most recent operation.",
3115
+ inputSchema: {
3116
+ type: "object",
3117
+ properties: {}
3118
+ }
3119
+ },
3120
+ {
3121
+ name: "redo",
3122
+ category: "write",
3123
+ description: "Redo the last undone change in Roblox Studio. Uses ChangeHistoryService to reapply the most recently undone operation.",
3124
+ inputSchema: {
3125
+ type: "object",
3126
+ properties: {}
3127
+ }
3128
+ },
3129
+ // === Build Library ===
3130
+ {
3131
+ name: "export_build",
3132
+ category: "read",
3133
+ description: "Export a Model/Folder into a compact, token-efficient build JSON format and auto-save it to the local build library. The output contains a palette (unique BrickColor+Material combos mapped to short keys) and compact part arrays with positions normalized relative to the bounding box center. The file is saved to build-library/{style}/{id}.json automatically.",
3134
+ inputSchema: {
3135
+ type: "object",
3136
+ properties: {
3137
+ instancePath: {
3138
+ type: "string",
3139
+ description: "Path to the Model or Folder to export (dot notation)"
3140
+ },
3141
+ outputId: {
3142
+ type: "string",
3143
+ description: 'Build ID for the output (e.g. "medieval/cottage_01"). Defaults to style/instance_name.'
3144
+ },
3145
+ style: {
3146
+ type: "string",
3147
+ enum: ["medieval", "modern", "nature", "scifi", "misc"],
3148
+ description: "Style category for the build",
3149
+ default: "misc"
3150
+ }
3151
+ },
3152
+ required: ["instancePath"]
3153
+ }
3154
+ },
3155
+ {
3156
+ name: "create_build",
3157
+ category: "write",
3158
+ description: "Create a new build model from scratch and save it to the library. Define parts using compact arrays [posX, posY, posZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey, shape?, transparency?]. Palette maps short keys to [BrickColor, Material] pairs. The build is saved and can be referenced by import_build or import_scene.",
3159
+ inputSchema: {
3160
+ type: "object",
3161
+ properties: {
3162
+ id: {
3163
+ type: "string",
3164
+ description: 'Build ID including style prefix (e.g. "medieval/torch_01", "nature/bush_small")'
3165
+ },
3166
+ style: {
3167
+ type: "string",
3168
+ enum: ["medieval", "modern", "nature", "scifi", "misc"],
3169
+ description: "Style category"
3170
+ },
3171
+ palette: {
3172
+ type: "object",
3173
+ description: 'Map of short keys to [BrickColor, Material] or [BrickColor, Material, MaterialVariant] tuples. E.g. {"a": ["Dark stone grey", "Concrete"], "b": ["Brown", "Wood", "MyCustomWood"]}'
3174
+ },
3175
+ parts: {
3176
+ type: "array",
3177
+ description: "Array of part arrays. Each: [posX, posY, posZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey, shape?, transparency?]. Shapes: Block (default), Wedge, Cylinder, Ball, CornerWedge.",
3178
+ items: {
3179
+ type: "array",
3180
+ minItems: 10
3181
+ }
3182
+ },
3183
+ bounds: {
3184
+ type: "array",
3185
+ items: { type: "number" },
3186
+ minItems: 3,
3187
+ maxItems: 3,
3188
+ description: "Optional bounding box [X, Y, Z]. Auto-computed if omitted."
3189
+ }
3190
+ },
3191
+ required: ["id", "style", "palette", "parts"]
3192
+ }
3193
+ },
3194
+ {
3195
+ name: "generate_build",
3196
+ category: "write",
3197
+ description: `Procedurally generate a build via JS code. ALWAYS generate the entire scene in ONE call \u2014 never split into multiple small builds. PREFER high-level primitives over manual loops. No comments. No unnecessary variables. Maximize build detail per line.
3198
+
3199
+ EDITING: When modifying an existing build, call get_build first to retrieve the original code. Then make ONLY the targeted changes the user requested \u2014 do not rewrite unchanged code. Pass the modified code to generate_build.
3200
+
3201
+ HIGH-LEVEL (use these first \u2014 each replaces 5-20 lines):
3202
+ room(x,y,z, w,h,d, wallKey, floorKey?, ceilKey?, wallThickness?) - Complete enclosed room (floor+ceiling+4 walls)
3203
+ roof(x,y,z, w,d, style, key, overhang?) - style: "flat"|"gable"|"hip"
3204
+ stairs(x1,y1,z1, x2,y2,z2, width, key) - Auto-generates steps between two points
3205
+ column(x,y,z, height, radius, key, capKey?) - Cylinder with base+capital
3206
+ pew(x,y,z, w,d, seatKey, legKey?) - Bench with seat+backrest+legs
3207
+ arch(x,y,z, w,h, thickness, key, segments?) - Curved archway
3208
+ fence(x1,z1, x2,z2, y, key, postSpacing?) - Fence with posts+rails
3209
+
3210
+ BASIC:
3211
+ part(x,y,z, sx,sy,sz, key, shape?, transparency?)
3212
+ rpart(x,y,z, sx,sy,sz, rx,ry,rz, key, shape?, transparency?)
3213
+ wall(x1,z1, x2,z2, height, thickness, key) \u2014 vertical plane from (x1,z1) to (x2,z2)
3214
+ floor(x1,z1, x2,z2, y, thickness, key) \u2014 horizontal plane at height y, corners (x1,z1)-(x2,z2). NOT fill \u2014 only takes 2D corners+y, not 3D points
3215
+ fill(x1,y1,z1, x2,y2,z2, key, [ux,uy,uz]?) \u2014 3D volume between two 3D points
3216
+ beam(x1,y1,z1, x2,y2,z2, thickness, key)
3217
+
3218
+ IMPORTANT: Palette keys must match exactly. Use only keys defined in your palette object, not color names.
3219
+ CUSTOM MATERIALS: Use search_materials to find MaterialVariant names, then reference them as the 3rd palette element: {"a": ["Color", "BaseMaterial", "VariantName"]}.
3220
+
3221
+ REPETITION:
3222
+ row(x,y,z, count, spacingX, spacingZ, fn(i,cx,cy,cz))
3223
+ grid(x,y,z, countX, countZ, spacingX, spacingZ, fn(ix,iz,cx,cy,cz))
3224
+
3225
+ Shapes: Block(default), Wedge, Cylinder, Ball, CornerWedge. Max 10000 parts. Math and rng() available.
3226
+ CYLINDER AXIS: Roblox cylinders extend along the X axis. For upright cylinders, use size (height, diameter, diameter) with rz=90. The column() primitive handles this automatically.
3227
+
3228
+ EXAMPLE \u2014 compact cabin (17 lines):
3229
+ room(0,0,0,8,4,6,"a","b","a")
3230
+ roof(0,4,0,8,6,"gable","c")
3231
+ wall(-4,0,-2,4,0,-2,4,1,"a")
3232
+ part(0,2,3,3,3,0.3,"a","Block",0.4)
3233
+ row(-2,0,-1,3,0,2,(i,cx,cy,cz)=>{pew(cx,0,cz,3,2,"d")})
3234
+ column(-3,0,-2,4,0.5,"a","b")
3235
+ column(3,0,-2,4,0.5,"a","b")
3236
+ part(0,2,0,2,1,1,"b")`,
3237
+ inputSchema: {
3238
+ type: "object",
3239
+ properties: {
3240
+ id: {
3241
+ type: "string",
3242
+ description: 'Build ID including style prefix (e.g. "medieval/church_01")'
3243
+ },
3244
+ style: {
3245
+ type: "string",
3246
+ enum: ["medieval", "modern", "nature", "scifi", "misc"],
3247
+ description: "Style category"
3248
+ },
3249
+ palette: {
3250
+ type: "object",
3251
+ description: 'Map of short keys to [BrickColor, Material] or [BrickColor, Material, MaterialVariant] tuples. E.g. {"a": ["Dark stone grey", "Cobblestone"], "b": ["Brown", "WoodPlanks", "MyCustomWood"]}. MaterialVariant is optional \u2014 use it to reference custom materials from MaterialService.'
3252
+ },
3253
+ code: {
3254
+ type: "string",
3255
+ description: "JavaScript code using the primitives above to generate parts procedurally"
3256
+ },
3257
+ seed: {
3258
+ type: "number",
3259
+ description: "Optional seed for deterministic rng() output (default: 42)"
3260
+ }
3261
+ },
3262
+ required: ["id", "style", "palette", "code"]
3263
+ }
3264
+ },
3265
+ {
3266
+ name: "import_build",
3267
+ category: "write",
3268
+ description: 'Import a build into Roblox Studio. Accepts either a full build data object OR a library ID string (e.g. "medieval/church_01") to load from the build library. When using generate_build or create_build, pass the build ID string instead of the full data.',
3269
+ inputSchema: {
3270
+ type: "object",
3271
+ properties: {
3272
+ buildData: {
3273
+ description: 'Either a build data object (with palette, parts, etc.) OR a library ID string (e.g. "medieval/church_01") to load from the build library'
3274
+ },
3275
+ targetPath: {
3276
+ type: "string",
3277
+ description: "Parent instance path where the model will be created"
3278
+ },
3279
+ position: {
3280
+ type: "array",
3281
+ items: { type: "number" },
3282
+ minItems: 3,
3283
+ maxItems: 3,
3284
+ description: "World position offset [X, Y, Z]"
3285
+ }
3286
+ },
3287
+ required: ["buildData", "targetPath"]
3288
+ }
3289
+ },
3290
+ {
3291
+ name: "list_library",
3292
+ category: "read",
3293
+ description: "List available builds in the local build library. Returns build IDs, styles, bounds, and part counts. Optionally filter by style.",
3294
+ inputSchema: {
3295
+ type: "object",
3296
+ properties: {
3297
+ style: {
3298
+ type: "string",
3299
+ enum: ["medieval", "modern", "nature", "scifi", "misc"],
3300
+ description: "Filter by style category"
3301
+ }
3302
+ }
3303
+ }
3304
+ },
3305
+ {
3306
+ name: "search_materials",
3307
+ category: "read",
3308
+ description: "Search for MaterialVariant instances in MaterialService by name. Use this to find custom materials before using them in generate_build or create_build palettes. Returns material names and their base material types.",
3309
+ inputSchema: {
3310
+ type: "object",
3311
+ properties: {
3312
+ query: {
3313
+ type: "string",
3314
+ description: "Search query to match against material names (case-insensitive). Leave empty to list all."
3315
+ },
3316
+ maxResults: {
3317
+ type: "number",
3318
+ description: "Max results to return (default: 50)",
3319
+ default: 50
3320
+ }
3321
+ }
3322
+ }
3323
+ },
3324
+ {
3325
+ name: "get_build",
3326
+ category: "read",
3327
+ description: "Get a build from the library by ID. Returns metadata, palette, and generator code (if the build was created with generate_build). IMPORTANT: When the user asks to modify an existing build, ALWAYS call get_build first to retrieve the original code, then make targeted edits to only the relevant lines, and call generate_build with the modified code. Never rewrite the entire code from scratch \u2014 only change what the user asked to change.",
3328
+ inputSchema: {
3329
+ type: "object",
3330
+ properties: {
3331
+ id: {
3332
+ type: "string",
3333
+ description: 'Build ID (e.g. "medieval/church_01")'
3334
+ }
3335
+ },
3336
+ required: ["id"]
3337
+ }
3338
+ },
3339
+ {
3340
+ name: "import_scene",
3341
+ category: "write",
3342
+ description: "Import a full scene layout. Provide a scene with model references (resolved from library) and placement data. Each model is placed at the specified position/rotation. Can also include inline custom builds.",
3343
+ inputSchema: {
3344
+ type: "object",
3345
+ properties: {
3346
+ sceneData: {
3347
+ type: "object",
3348
+ description: "Scene layout object with: models (map of key to library build ID), place (array of [key, position, rotation?]), and optional custom (array of inline build objects with name, position, palette, parts)",
3349
+ properties: {
3350
+ models: {
3351
+ type: "object",
3352
+ description: 'Map of short keys to library build IDs (e.g. {"A": "medieval/cottage_01"})'
3353
+ },
3354
+ place: {
3355
+ type: "array",
3356
+ description: "Array of placements: [modelKey, [x,y,z], [rotX?,rotY?,rotZ?]]",
3357
+ items: { type: "array" }
3358
+ },
3359
+ custom: {
3360
+ type: "array",
3361
+ description: "Array of inline custom builds with {n: name, o: [x,y,z], palette: {...}, parts: [...]}",
3362
+ items: { type: "object" }
1059
3363
  }
1060
- }, 5000);
1061
- setInterval(() => {
1062
- this.bridge.cleanupOldRequests();
1063
- }, 5000);
3364
+ }
3365
+ },
3366
+ targetPath: {
3367
+ type: "string",
3368
+ description: "Parent instance path for the scene (default: game.Workspace)",
3369
+ default: "game.Workspace"
3370
+ }
3371
+ },
3372
+ required: ["sceneData"]
1064
3373
  }
1065
- }
1066
- const server = new RobloxStudioMCPServer();
3374
+ },
3375
+ // === Asset Tools ===
3376
+ {
3377
+ name: "search_assets",
3378
+ category: "read",
3379
+ description: "Search the Creator Store (Roblox marketplace) for assets by type and keywords. Requires ROBLOX_OPEN_CLOUD_API_KEY env var.",
3380
+ inputSchema: {
3381
+ type: "object",
3382
+ properties: {
3383
+ assetType: {
3384
+ type: "string",
3385
+ enum: ["Audio", "Model", "Decal", "Plugin", "MeshPart", "Video", "FontFamily"],
3386
+ description: "Type of asset to search for"
3387
+ },
3388
+ query: {
3389
+ type: "string",
3390
+ description: "Search keywords"
3391
+ },
3392
+ maxResults: {
3393
+ type: "number",
3394
+ description: "Max results to return (default: 25)",
3395
+ default: 25
3396
+ },
3397
+ sortBy: {
3398
+ type: "string",
3399
+ enum: ["Relevance", "Trending", "Top", "AudioDuration", "CreateTime", "UpdatedTime", "Ratings"],
3400
+ description: "Sort order (default: Relevance)",
3401
+ default: "Relevance"
3402
+ },
3403
+ verifiedCreatorsOnly: {
3404
+ type: "boolean",
3405
+ description: "Only show assets from verified creators",
3406
+ default: false
3407
+ }
3408
+ },
3409
+ required: ["assetType"]
3410
+ }
3411
+ },
3412
+ {
3413
+ name: "get_asset_details",
3414
+ category: "read",
3415
+ description: "Get detailed marketplace metadata for a specific asset (creator info, votes, description, pricing). Requires ROBLOX_OPEN_CLOUD_API_KEY env var.",
3416
+ inputSchema: {
3417
+ type: "object",
3418
+ properties: {
3419
+ assetId: {
3420
+ type: "number",
3421
+ description: "The Roblox asset ID"
3422
+ }
3423
+ },
3424
+ required: ["assetId"]
3425
+ }
3426
+ },
3427
+ {
3428
+ name: "get_asset_thumbnail",
3429
+ category: "read",
3430
+ description: "Get the thumbnail image for an asset as base64 PNG, suitable for vision LLMs. Requires ROBLOX_OPEN_CLOUD_API_KEY env var.",
3431
+ inputSchema: {
3432
+ type: "object",
3433
+ properties: {
3434
+ assetId: {
3435
+ type: "number",
3436
+ description: "The Roblox asset ID"
3437
+ },
3438
+ size: {
3439
+ type: "string",
3440
+ enum: ["150x150", "420x420", "768x432"],
3441
+ description: "Thumbnail size (default: 420x420)",
3442
+ default: "420x420"
3443
+ }
3444
+ },
3445
+ required: ["assetId"]
3446
+ }
3447
+ },
3448
+ {
3449
+ name: "insert_asset",
3450
+ category: "write",
3451
+ description: "Insert a Roblox asset into Studio by loading it via AssetService and parenting it to a target location. Optionally set position.",
3452
+ inputSchema: {
3453
+ type: "object",
3454
+ properties: {
3455
+ assetId: {
3456
+ type: "number",
3457
+ description: "The Roblox asset ID to insert"
3458
+ },
3459
+ parentPath: {
3460
+ type: "string",
3461
+ description: "Parent instance path (default: game.Workspace)",
3462
+ default: "game.Workspace"
3463
+ },
3464
+ position: {
3465
+ type: "object",
3466
+ properties: {
3467
+ x: { type: "number" },
3468
+ y: { type: "number" },
3469
+ z: { type: "number" }
3470
+ },
3471
+ description: "Optional world position to place the asset"
3472
+ }
3473
+ },
3474
+ required: ["assetId"]
3475
+ }
3476
+ },
3477
+ {
3478
+ name: "preview_asset",
3479
+ category: "read",
3480
+ description: "Preview a Roblox asset without permanently inserting it. Loads the asset, builds a hierarchy tree with properties and summary stats, then destroys it. Useful for inspecting asset contents before insertion.",
3481
+ inputSchema: {
3482
+ type: "object",
3483
+ properties: {
3484
+ assetId: {
3485
+ type: "number",
3486
+ description: "The Roblox asset ID to preview"
3487
+ },
3488
+ includeProperties: {
3489
+ type: "boolean",
3490
+ description: "Include detailed properties for each instance (default: true)",
3491
+ default: true
3492
+ },
3493
+ maxDepth: {
3494
+ type: "number",
3495
+ description: "Max hierarchy traversal depth (default: 10)",
3496
+ default: 10
3497
+ }
3498
+ },
3499
+ required: ["assetId"]
3500
+ }
3501
+ }
3502
+ ];
3503
+ var getAllTools = () => [...TOOL_DEFINITIONS];
3504
+
3505
+ // src/index.ts
3506
+ import { createRequire } from "module";
3507
+ var require2 = createRequire(import.meta.url);
3508
+ var { version: VERSION } = require2("../package.json");
3509
+ var server = new RobloxStudioMCPServer({
3510
+ name: "robloxstudio-mcp",
3511
+ version: VERSION,
3512
+ tools: getAllTools()
3513
+ });
1067
3514
  server.run().catch((error) => {
1068
- console.error('Server failed to start:', error);
1069
- process.exit(1);
3515
+ console.error("Server failed to start:", error);
3516
+ process.exit(1);
1070
3517
  });
1071
- //# sourceMappingURL=index.js.map