@el4cteo/rbx-studio-mcp 0.6.1 → 0.6.7

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 (75) hide show
  1. package/README.md +28 -2
  2. package/dist/bridge/console.js +182 -0
  3. package/dist/bridge/console.js.map +1 -1
  4. package/dist/index.js +8 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/cloudassets.js +233 -0
  7. package/dist/lib/cloudassets.js.map +1 -0
  8. package/dist/lib/credentials.js +180 -0
  9. package/dist/lib/credentials.js.map +1 -0
  10. package/dist/lib/livedata.js +325 -0
  11. package/dist/lib/livedata.js.map +1 -0
  12. package/dist/lib/liveluau.js +83 -0
  13. package/dist/lib/liveluau.js.map +1 -0
  14. package/dist/lib/liveops.js +358 -0
  15. package/dist/lib/liveops.js.map +1 -0
  16. package/dist/lib/opencloud.js +235 -0
  17. package/dist/lib/opencloud.js.map +1 -0
  18. package/dist/tools/anim.js +159 -0
  19. package/dist/tools/anim.js.map +1 -0
  20. package/dist/tools/audio.js +96 -0
  21. package/dist/tools/audio.js.map +1 -0
  22. package/dist/tools/character.js +95 -5
  23. package/dist/tools/character.js.map +1 -1
  24. package/dist/tools/data.js +292 -0
  25. package/dist/tools/data.js.map +1 -0
  26. package/dist/tools/device.js +77 -7
  27. package/dist/tools/device.js.map +1 -1
  28. package/dist/tools/discover.js +80 -4
  29. package/dist/tools/discover.js.map +1 -1
  30. package/dist/tools/exec.js +96 -2
  31. package/dist/tools/exec.js.map +1 -1
  32. package/dist/tools/input.js +35 -9
  33. package/dist/tools/input.js.map +1 -1
  34. package/dist/tools/perf.js +74 -7
  35. package/dist/tools/perf.js.map +1 -1
  36. package/dist/tools/scripts.js +162 -6
  37. package/dist/tools/scripts.js.map +1 -1
  38. package/dist/tools/spatial.js +135 -0
  39. package/dist/tools/spatial.js.map +1 -0
  40. package/dist/tools/universe.js +177 -0
  41. package/dist/tools/universe.js.map +1 -0
  42. package/dist/tools/upload.js +294 -0
  43. package/dist/tools/upload.js.map +1 -0
  44. package/dist/tools/world.js +675 -51
  45. package/dist/tools/world.js.map +1 -1
  46. package/package.json +2 -2
  47. package/plugin/src/Commands.luau +31 -7
  48. package/plugin/src/Config.luau +65 -65
  49. package/plugin/src/Console.luau +1909 -1843
  50. package/plugin/src/Emulation.luau +172 -0
  51. package/plugin/src/Phrase.luau +816 -618
  52. package/plugin/src/Png.luau +8 -4
  53. package/plugin/src/Prompt.luau +965 -961
  54. package/plugin/src/Secret.luau +86 -0
  55. package/plugin/src/Serialize.luau +440 -8
  56. package/plugin/src/Undo.luau +94 -6
  57. package/plugin/src/handlers/Anim.luau +897 -0
  58. package/plugin/src/handlers/Assets.luau +286 -2
  59. package/plugin/src/handlers/Audio.luau +411 -0
  60. package/plugin/src/handlers/Capture.luau +155 -20
  61. package/plugin/src/handlers/Character.luau +823 -361
  62. package/plugin/src/handlers/Data.luau +539 -0
  63. package/plugin/src/handlers/Device.luau +394 -139
  64. package/plugin/src/handlers/Discover.luau +685 -363
  65. package/plugin/src/handlers/Geometry.luau +722 -450
  66. package/plugin/src/handlers/Instances.luau +84 -4
  67. package/plugin/src/handlers/Perf.luau +227 -0
  68. package/plugin/src/handlers/Scripts.luau +673 -539
  69. package/plugin/src/handlers/Session.luau +3 -0
  70. package/plugin/src/handlers/Spatial.luau +334 -0
  71. package/plugin/src/handlers/Viewport.luau +268 -0
  72. package/plugin/src/handlers/World.luau +89 -15
  73. package/plugin/src/init.server.luau +9 -1
  74. package/scripts/build-plugin.mjs +20 -0
  75. package/scripts/check-plugin.mjs +171 -124
@@ -13,6 +13,7 @@
13
13
  ]]
14
14
 
15
15
  local AssetService = game:GetService("AssetService")
16
+ local InsertService = game:GetService("InsertService")
16
17
 
17
18
  local Dispatch = require(script.Parent.Parent.Dispatch)
18
19
  local Paths = require(script.Parent.Parent.Paths)
@@ -101,6 +102,54 @@ function Assets.insert(params: { [string]: any }): { [string]: any }
101
102
  end
102
103
  end
103
104
 
105
+ --[[
106
+ Scripts stripped BEFORE anything is parented.
107
+
108
+ The order is the whole point. A free model's scripts run the moment the
109
+ place is played, and one that is parented first and cleaned up a
110
+ fraction of a second later has already been given a foothold -- a
111
+ `Script` with `RunContext = Legacy` will not execute in edit mode, but
112
+ `Plugin` run context does, and nothing here should depend on knowing
113
+ which one an unknown model used.
114
+
115
+ This is the most-requested missing piece in Studio's own Toolbox: there
116
+ is no way to insert a model without its code, so people take the
117
+ geometry they want and the scripts they did not ask for together.
118
+ ]]
119
+ local stripped = 0
120
+ if params.stripScripts == true then
121
+ for _, child in children do
122
+ if child:IsA("LuaSourceContainer") then
123
+ child:Destroy()
124
+ stripped += 1
125
+ else
126
+ for _, descendant in child:GetDescendants() do
127
+ if descendant:IsA("LuaSourceContainer") then
128
+ descendant:Destroy()
129
+ stripped += 1
130
+ end
131
+ end
132
+ end
133
+ end
134
+ -- A model that was nothing BUT scripts is now empty, and parenting
135
+ -- nothing silently is worse than saying so.
136
+ local remaining: { Instance } = {}
137
+ for _, child in children do
138
+ if child.Parent ~= nil or child:IsDescendantOf(game) or child.Name ~= "" then
139
+ if not child:IsA("LuaSourceContainer") then
140
+ table.insert(remaining, child)
141
+ end
142
+ end
143
+ end
144
+ children = remaining
145
+ if #children == 0 then
146
+ Dispatch.fail(
147
+ "EMPTY_ASSET",
148
+ string.format("Asset %d was nothing but scripts, and stripScripts removed them all.", assetId)
149
+ )
150
+ end
151
+ end
152
+
104
153
  local inserted: { string } = {}
105
154
  local _, undoable = Undo.record("MCPInsertAsset", "MCP insert asset", function()
106
155
  for _, child in children do
@@ -132,8 +181,9 @@ function Assets.insert(params: { [string]: any }): { [string]: any }
132
181
  return {
133
182
  inserted = inserted,
134
183
  assetId = assetId,
135
- scriptCount = scriptCount,
136
- scripts = if scriptCount > 0 then scriptNames else nil,
184
+ scriptCount = if params.stripScripts == true then 0 else scriptCount,
185
+ scripts = if scriptCount > 0 and params.stripScripts ~= true then scriptNames else nil,
186
+ strippedScripts = if params.stripScripts == true then stripped else nil,
137
187
  undoable = undoable,
138
188
  }
139
189
  end
@@ -342,8 +392,242 @@ function Assets.bake(params: { [string]: any }): { [string]: any }
342
392
  }
343
393
  end
344
394
 
395
+ --[[
396
+ The audio library, which the Creator Store index answers badly.
397
+
398
+ `assets op="search" category="audio"` used to go through the same public
399
+ index the model search uses, and it came back with a name, a creator and a
400
+ vote ratio -- none of which is how anyone chooses a sound. The engine has a
401
+ purpose-built search for this, and it returns the fields that actually
402
+ decide: how long the clip is, whether it is music or a sound effect, who
403
+ performed it, and whether Roblox endorses it.
404
+
405
+ Measured against a live session: "footstep" returns 30 results carrying
406
+ Title, Artist, Duration, AudioType, Tags and IsEndorsed. Duration alone is
407
+ the difference between a footstep and a three-minute track that happens to
408
+ mention footsteps in its description -- and the old path could not tell them
409
+ apart.
410
+ ]]
411
+ function Assets.audio(params: { [string]: any }): { [string]: any }
412
+ local keyword = params.keyword
413
+ if typeof(keyword) ~= "string" or keyword == "" then
414
+ Dispatch.fail("BAD_PARAMS", "audio search needs a `keyword`.")
415
+ end
416
+ local limit = math.clamp(tonumber(params.limit) or 10, 1, 50)
417
+
418
+ local okParams, search = pcall(function()
419
+ local p = Instance.new("AudioSearchParams")
420
+ p.SearchKeyword = keyword
421
+ --[[
422
+ Both bounds are optional and both are worth passing through. A search
423
+ for a footstep wants something under two seconds; a search for
424
+ background music wants the opposite, and without a length filter the
425
+ two queries return the same list.
426
+ ]]
427
+ local minimum = tonumber(params.minDuration)
428
+ local maximum = tonumber(params.maxDuration)
429
+ if minimum ~= nil then
430
+ (p :: any).MinDuration = math.max(0, minimum)
431
+ end
432
+ if maximum ~= nil then
433
+ (p :: any).MaxDuration = math.max(0, maximum)
434
+ end
435
+ --[[
436
+ Always set, never left to the engine.
437
+
438
+ `AudioSubType` defaults to Music, and that default is a trap rather
439
+ than a preference: measured, a search for "footstep" with the default
440
+ returns "Silent Footsteps II" at 153 seconds, "Footsteps in the Dark"
441
+ at 268, and thirty more like them -- ambient tracks whose titles
442
+ mention footsteps. The same search as SoundEffect returns
443
+ "JjBg_Grass_Footstep_6" at one second.
444
+
445
+ Nothing about the empty result said "you searched the music library".
446
+ So the default here is SoundEffect, which is what someone typing a
447
+ noise into a game engine means, and Music is something you ask for.
448
+ ]]
449
+ local subType = if typeof(params.audioType) == "string" and params.audioType ~= ""
450
+ then params.audioType
451
+ else "SoundEffect"
452
+ ;(p :: any).AudioSubType = (Enum :: any).AudioSubType[subType]
453
+ return p
454
+ end)
455
+ if not okParams then
456
+ Dispatch.fail(
457
+ "BAD_PARAMS",
458
+ string.format("Those search parameters were refused: %s", tostring(search)),
459
+ 'audioType must be one of the Enum.AudioSubType names, e.g. "Music" or "SoundEffect".'
460
+ )
461
+ end
462
+
463
+ local okSearch, pages = pcall(function()
464
+ return (AssetService :: any):SearchAudioAsync(search)
465
+ end)
466
+ if not okSearch then
467
+ Dispatch.fail(
468
+ "AUDIO_SEARCH_FAILED",
469
+ string.format("The audio search failed: %s", tostring(pages))
470
+ )
471
+ end
472
+
473
+ local items: { { [string]: any } } = {}
474
+ local page = pages:GetCurrentPage()
475
+ for _, entry in page do
476
+ if #items >= limit then
477
+ break
478
+ end
479
+ local row = entry :: { [string]: any }
480
+ table.insert(items, {
481
+ assetId = tostring(row.Id),
482
+ title = tostring(row.Title),
483
+ artist = if row.Artist ~= nil and tostring(row.Artist) ~= "" then tostring(row.Artist) else nil,
484
+ -- Seconds, rounded. The API returns a float and nobody picking a
485
+ -- sound effect cares about the third decimal place.
486
+ duration = math.round(tonumber(row.Duration) or 0),
487
+ audioType = (tostring(row.AudioType):gsub("Enum%.AudioSubType%.", "")),
488
+ endorsed = row.IsEndorsed == true,
489
+ })
490
+ end
491
+
492
+ return {
493
+ items = items,
494
+ count = #items,
495
+ keyword = keyword,
496
+ audioType = if typeof(params.audioType) == "string" and params.audioType ~= ""
497
+ then params.audioType
498
+ else "SoundEffect",
499
+ }
500
+ end
501
+
502
+ --[[
503
+ Looks inside a published asset without putting it in the place.
504
+
505
+ `insert` already reports what came in, and reporting it afterwards is one
506
+ Ctrl+Z too late to be reassuring: the scripts are in the data model by then,
507
+ and the model may have parented things outside itself on the way in.
508
+ `LoadAssetAsync` builds the asset in memory instead, which is where it can be
509
+ read and then dropped.
510
+
511
+ So this is the safe half of the existing script warning. The Creator Store
512
+ index says whether an asset has scripts at all; this says which ones, what
513
+ class they are, and what else is in there -- before anything is committed.
514
+
515
+ The loaded container is destroyed on every path. It is never parented, so it
516
+ is invisible to the data model, to undo history and to the user.
517
+ ]]
518
+ function Assets.peek(params: { [string]: any }): { [string]: any }
519
+ local assetId = tonumber(params.assetId)
520
+ if assetId == nil or assetId <= 0 then
521
+ Dispatch.fail("BAD_PARAMS", "peek needs a numeric `assetId`.")
522
+ end
523
+
524
+ --[[
525
+ Loaded the way `insert` loads, not the way it is tempting to.
526
+
527
+ `AssetService:LoadAssetAsync` enforces ownership: it opens assets the
528
+ signed-in user owns or has been given, and refuses everything else with
529
+ "User is not authorized to access Asset". `InsertService:LoadAsset` does
530
+ not -- it is what the Toolbox itself uses, and it reaches any public
531
+ model.
532
+
533
+ Using the first one made this tool useless at the one job it exists for.
534
+ Measured: a free door from the Creator Store, two scripts inside it,
535
+ refused by `peek` and inserted by `insert` moments later. The SAFE
536
+ preview was weaker than the unsafe insert, so the only way to see what
537
+ was in a model was to put it in the place first -- which is precisely
538
+ what `peek` was written to avoid.
539
+
540
+ Nothing is parented here, so reaching further costs nothing: the model is
541
+ read in memory and destroyed before this function returns.
542
+ ]]
543
+ --[[
544
+ `game:GetObjects`, the same loader `insert` uses -- see the note there.
545
+
546
+ Two wrong loaders were tried before this one, and both failed the same
547
+ way: `AssetService:LoadAssetAsync` and `InsertService:LoadAsset` each
548
+ enforce ownership and refuse any Creator Store model with "User is not
549
+ authorized to access Asset". That made the SAFE preview weaker than the
550
+ unsafe insert -- measured, a free door with two scripts in it was refused
551
+ by `peek` and inserted by `insert` moments later, so the only way to see
552
+ inside a model was to put it in the place first.
553
+
554
+ `GetObjects` is the plugin-security path Studio's own toolbox uses and it
555
+ reaches everything `insert` reaches, which is the only correct answer
556
+ here: a preview that cannot see what the insert would bring in is not a
557
+ preview of anything.
558
+
559
+ It returns top-level instances rather than a wrapper model, so they are
560
+ walked directly.
561
+ ]]
562
+ local ok, loaded = pcall(function()
563
+ return game:GetObjects("rbxassetid://" .. assetId)
564
+ end)
565
+ if not ok then
566
+ Dispatch.fail(
567
+ "ASSET_UNAVAILABLE",
568
+ string.format("Could not load asset %d: %s", assetId, tostring(loaded)),
569
+ "The asset may be private, deleted, or not a model."
570
+ )
571
+ end
572
+
573
+ -- An array of roots, not one wrapper: `GetObjects` hands back whatever the
574
+ -- asset holds at its top level.
575
+ local roots = loaded :: { Instance }
576
+ local classes: { [string]: number } = {}
577
+ local scripts: { string } = {}
578
+ local total = 0
579
+
580
+ for _, root in roots do
581
+ total += 1
582
+ classes[root.ClassName] = (classes[root.ClassName] or 0) + 1
583
+ if root:IsA("LuaSourceContainer") and #scripts < 25 then
584
+ table.insert(scripts, string.format("%s (%s)", root.Name, root.ClassName))
585
+ end
586
+ for _, descendant in root:GetDescendants() do
587
+ total += 1
588
+ classes[descendant.ClassName] = (classes[descendant.ClassName] or 0) + 1
589
+ if descendant:IsA("LuaSourceContainer") and #scripts < 25 then
590
+ table.insert(scripts, string.format("%s (%s)", descendant:GetFullName(), descendant.ClassName))
591
+ end
592
+ end
593
+ end
594
+
595
+ local breakdown: { { [string]: any } } = {}
596
+ for className, count in classes do
597
+ table.insert(breakdown, { className = className, count = count })
598
+ end
599
+ table.sort(breakdown, function(a, b)
600
+ if a.count == b.count then
601
+ return a.className < b.className
602
+ end
603
+ return a.count > b.count
604
+ end)
605
+
606
+ local names: { string } = {}
607
+ for _, root in roots do
608
+ table.insert(names, string.format("%s (%s)", root.Name, root.ClassName))
609
+ end
610
+
611
+ -- Destroyed before returning, not after: the reply is built from plain
612
+ -- strings and numbers, so nothing in it outlives the instances it came from.
613
+ for _, root in roots do
614
+ root:Destroy()
615
+ end
616
+
617
+ return {
618
+ assetId = assetId,
619
+ roots = names,
620
+ descendants = total,
621
+ classes = breakdown,
622
+ scripts = scripts,
623
+ scriptCount = #scripts,
624
+ }
625
+ end
626
+
345
627
  function Assets.register()
346
628
  Dispatch.registerAll("assets", {
629
+ audio = Assets.audio,
630
+ peek = Assets.peek,
347
631
  insert = Assets.insert,
348
632
  bake = Assets.bake,
349
633
  })