@chrrxs/robloxstudio-mcp 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +261 -45
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +355 -81
- package/studio-plugin/MCPPlugin.rbxmx +355 -81
- package/studio-plugin/src/modules/ClientBroker.ts +5 -0
- package/studio-plugin/src/modules/Communication.ts +8 -1
- package/studio-plugin/src/modules/EvalBridges.ts +16 -53
- package/studio-plugin/src/modules/UI.ts +36 -0
- package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +44 -0
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +16 -1
- package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +172 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +1 -13
- package/studio-plugin/src/server/index.server.ts +6 -1
|
@@ -17,8 +17,16 @@ local RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer"
|
|
|
17
17
|
RuntimeLogBuffer.install()
|
|
18
18
|
UI.init(plugin)
|
|
19
19
|
local elements = UI.getElements()
|
|
20
|
+
local ICON_DISCONNECTED = "rbxassetid://75876056391496"
|
|
21
|
+
local ICON_CONNECTING = "rbxassetid://71302583919560"
|
|
22
|
+
local ICON_CONNECTED = "rbxassetid://130958234173611"
|
|
20
23
|
local toolbar = plugin:CreateToolbar("MCP Integration")
|
|
21
|
-
local button = toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration",
|
|
24
|
+
local button = toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration", ICON_DISCONNECTED)
|
|
25
|
+
UI.setToolbarButton(button, {
|
|
26
|
+
disconnected = ICON_DISCONNECTED,
|
|
27
|
+
connecting = ICON_CONNECTING,
|
|
28
|
+
connected = ICON_CONNECTED,
|
|
29
|
+
})
|
|
22
30
|
elements.connectButton.Activated:Connect(function()
|
|
23
31
|
local conn = State.getActiveConnection()
|
|
24
32
|
if conn and conn.isActive then
|
|
@@ -78,6 +86,7 @@ local Players = _services.Players
|
|
|
78
86
|
local ReplicatedStorage = _services.ReplicatedStorage
|
|
79
87
|
local RunService = _services.RunService
|
|
80
88
|
local RuntimeLogBuffer = TS.import(script, script.Parent, "RuntimeLogBuffer")
|
|
89
|
+
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
81
90
|
-- The client peer cannot reach the MCP HTTP server - Roblox forbids
|
|
82
91
|
-- HttpService:RequestAsync from the client DM even under PluginSecurity, and
|
|
83
92
|
-- HttpEnabled reads as false there regardless of identity. So the server peer
|
|
@@ -99,6 +108,7 @@ local BROKER_NAME = "__MCPClientBroker"
|
|
|
99
108
|
local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
100
109
|
["/api/execute-luau"] = true,
|
|
101
110
|
["/api/get-runtime-logs"] = true,
|
|
111
|
+
["/api/get-memory-breakdown"] = true,
|
|
102
112
|
}
|
|
103
113
|
-- Throttle re-ready calls per proxyId so a brief window of unknownInstance
|
|
104
114
|
-- polls doesn't cause a re-register stampede.
|
|
@@ -211,6 +221,9 @@ local function setupClientBroker()
|
|
|
211
221
|
if payload and payload.endpoint == "/api/get-runtime-logs" then
|
|
212
222
|
return handleGetRuntimeLogs(payload.data)
|
|
213
223
|
end
|
|
224
|
+
if payload and payload.endpoint == "/api/get-memory-breakdown" then
|
|
225
|
+
return MemoryHandlers.getMemoryBreakdown(payload.data or {})
|
|
226
|
+
end
|
|
214
227
|
if payload and payload.endpoint == "/api/execute-luau" then
|
|
215
228
|
return handleExecuteLuau(payload.data)
|
|
216
229
|
end
|
|
@@ -435,6 +448,8 @@ local AssetHandlers = TS.import(script, script.Parent, "handlers", "AssetHandler
|
|
|
435
448
|
local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
|
|
436
449
|
local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
|
|
437
450
|
local LogHandlers = TS.import(script, script.Parent, "handlers", "LogHandlers")
|
|
451
|
+
local SerializationHandlers = TS.import(script, script.Parent, "handlers", "SerializationHandlers")
|
|
452
|
+
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
438
453
|
local instanceId = HttpService:GenerateGUID(false)
|
|
439
454
|
local assignedRole
|
|
440
455
|
local function detectRole()
|
|
@@ -504,6 +519,9 @@ local routeMap = {
|
|
|
504
519
|
["/api/simulate-keyboard-input"] = InputHandlers.simulateKeyboardInput,
|
|
505
520
|
["/api/find-and-replace-in-scripts"] = ScriptHandlers.findAndReplaceInScripts,
|
|
506
521
|
["/api/get-runtime-logs"] = LogHandlers.getRuntimeLogs,
|
|
522
|
+
["/api/export-rbxm"] = SerializationHandlers.exportRbxm,
|
|
523
|
+
["/api/import-rbxm"] = SerializationHandlers.importRbxm,
|
|
524
|
+
["/api/get-memory-breakdown"] = MemoryHandlers.getMemoryBreakdown,
|
|
507
525
|
}
|
|
508
526
|
local function processRequest(request)
|
|
509
527
|
local endpoint = request.endpoint
|
|
@@ -603,6 +621,9 @@ local function pollForRequests(connIndex)
|
|
|
603
621
|
conn.isPolling = false
|
|
604
622
|
local ui = UI.getElements()
|
|
605
623
|
UI.updateTabDot(connIndex)
|
|
624
|
+
if connIndex == State.getActiveTabIndex() then
|
|
625
|
+
UI.updateToolbarIcon()
|
|
626
|
+
end
|
|
606
627
|
if success and (result.Success or result.StatusCode == 503) then
|
|
607
628
|
conn.consecutiveFailures = 0
|
|
608
629
|
conn.currentRetryDelay = 0.5
|
|
@@ -759,7 +780,6 @@ local function activatePlugin(connIndex)
|
|
|
759
780
|
conn.isActive = true
|
|
760
781
|
conn.consecutiveFailures = 0
|
|
761
782
|
conn.currentRetryDelay = 0.5
|
|
762
|
-
ui.screenGui.Enabled = true
|
|
763
783
|
if idx == State.getActiveTabIndex() then
|
|
764
784
|
conn.serverUrl = ui.urlInput.Text
|
|
765
785
|
local portStr = string.match(conn.serverUrl, ":(%d+)$")
|
|
@@ -889,15 +909,22 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
889
909
|
-- `require(SomeModule)` returns a fresh copy, not the one the running game
|
|
890
910
|
-- scripts hold. So runtime-mutated module state is invisible to probes.
|
|
891
911
|
--
|
|
892
|
-
-- These bridges fix that by living inside the user's game scripts
|
|
893
|
-
--
|
|
894
|
-
--
|
|
895
|
-
-- (
|
|
912
|
+
-- These bridges fix that by living inside the user's game scripts. Both
|
|
913
|
+
-- peers use the same symmetric shape:
|
|
914
|
+
-- - Server: a Script in ServerScriptService that creates a BindableFunction.
|
|
915
|
+
-- Plugin (server peer) invokes it with a fresh ModuleScript payload;
|
|
916
|
+
-- require() runs inside the Script VM so it shares the running server's
|
|
917
|
+
-- require cache.
|
|
896
918
|
-- - Client: a LocalScript in StarterPlayer.StarterPlayerScripts that
|
|
897
919
|
-- creates a BindableFunction. Plugin invokes it with a fresh ModuleScript
|
|
898
920
|
-- payload; require() runs inside the LocalScript VM so it shares the
|
|
899
921
|
-- game's require cache.
|
|
900
922
|
--
|
|
923
|
+
-- Why ModuleScript+require on both sides (no loadstring): require'd modules
|
|
924
|
+
-- run with the security level they were created at and don't need
|
|
925
|
+
-- ServerScriptService.LoadStringEnabled, so eval_server_runtime works even
|
|
926
|
+
-- when LoadStringEnabled=false (the default in fresh places).
|
|
927
|
+
--
|
|
901
928
|
-- Lifecycle: TestHandlers.startPlaytest inserts both scripts into the EDIT
|
|
902
929
|
-- DM right before ExecutePlayModeAsync. ExecutePlayModeAsync clones the
|
|
903
930
|
-- DataModel into the play DMs, so the scripts come along and run there.
|
|
@@ -928,7 +955,6 @@ local CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge"
|
|
|
928
955
|
local BRIDGE_NAMES = {
|
|
929
956
|
serverScript = SERVER_SCRIPT_NAME,
|
|
930
957
|
clientScript = CLIENT_SCRIPT_NAME,
|
|
931
|
-
serverRemote = "__MCP_ServerEvalRemote",
|
|
932
958
|
serverLocal = "__MCP_ServerEvalLocal",
|
|
933
959
|
clientLocal = "__MCP_ClientEvalBridge",
|
|
934
960
|
}
|
|
@@ -939,7 +965,6 @@ local SERVER_BRIDGE_SOURCE = `\
|
|
|
939
965
|
-- stop_playtest. Provides shared-require-cache eval on the server peer for\
|
|
940
966
|
-- the eval_server_runtime MCP tool.\
|
|
941
967
|
\
|
|
942
|
-
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
943
968
|
local ServerScriptService = game:GetService("ServerScriptService")\
|
|
944
969
|
local RunService = game:GetService("RunService")\
|
|
945
970
|
\
|
|
@@ -947,49 +972,18 @@ if not RunService:IsStudio() then\
|
|
|
947
972
|
return\
|
|
948
973
|
end\
|
|
949
974
|
\
|
|
950
|
-
local function evalCode(source)\
|
|
951
|
-
if type(source) ~= "string" then\
|
|
952
|
-
return false, "source must be a string"\
|
|
953
|
-
end\
|
|
954
|
-
local fn, compileErr = loadstring(source, "MCPServerEval")\
|
|
955
|
-
if not fn then\
|
|
956
|
-
local errStr = tostring(compileErr or "loadstring returned nil")\
|
|
957
|
-
-- Roblox returns nil from loadstring when LoadStringEnabled=false.\
|
|
958
|
-
-- Surface a clear, actionable error.\
|
|
959
|
-
if string.find(errStr, "not enabled", 1, true)\
|
|
960
|
-
or string.find(errStr, "disabled", 1, true)\
|
|
961
|
-
or errStr == "loadstring returned nil"\
|
|
962
|
-
then\
|
|
963
|
-
return false,\
|
|
964
|
-
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime requires it. "\
|
|
965
|
-
.. "Enable it in Studio (ServerScriptService > Properties > LoadStringEnabled = true) "\
|
|
966
|
-
.. "and restart the playtest."\
|
|
967
|
-
end\
|
|
968
|
-
return false, errStr\
|
|
969
|
-
end\
|
|
970
|
-
return pcall(fn)\
|
|
971
|
-
end\
|
|
972
|
-
\
|
|
973
|
-
-- Defensive cleanup of stale instances from a prior session.\
|
|
974
|
-
local prevRf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.serverRemote}")\
|
|
975
|
-
if prevRf then prevRf:Destroy() end\
|
|
976
975
|
local prevBf = ServerScriptService:FindFirstChild("{BRIDGE_NAMES.serverLocal}")\
|
|
977
976
|
if prevBf then prevBf:Destroy() end\
|
|
978
977
|
\
|
|
979
|
-
local rf = Instance.new("RemoteFunction")\
|
|
980
|
-
rf.Name = "{BRIDGE_NAMES.serverRemote}"\
|
|
981
|
-
rf.Archivable = false\
|
|
982
|
-
rf.Parent = ReplicatedStorage\
|
|
983
|
-
rf.OnServerInvoke = function(_player, source)\
|
|
984
|
-
return evalCode(source)\
|
|
985
|
-
end\
|
|
986
|
-
\
|
|
987
978
|
local bf = Instance.new("BindableFunction")\
|
|
988
979
|
bf.Name = "{BRIDGE_NAMES.serverLocal}"\
|
|
989
980
|
bf.Archivable = false\
|
|
990
981
|
bf.Parent = ServerScriptService\
|
|
991
|
-
bf.OnInvoke = function(
|
|
992
|
-
|
|
982
|
+
bf.OnInvoke = function(payload)\
|
|
983
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then\
|
|
984
|
+
return false, "payload must be a ModuleScript instance"\
|
|
985
|
+
end\
|
|
986
|
+
return pcall(require, payload)\
|
|
993
987
|
end\
|
|
994
988
|
`
|
|
995
989
|
local CLIENT_BRIDGE_SOURCE = `\
|
|
@@ -1086,20 +1080,9 @@ local function installBridges()
|
|
|
1086
1080
|
installed = true,
|
|
1087
1081
|
}
|
|
1088
1082
|
end
|
|
1089
|
-
-- Heuristic check so start_playtest can surface a warning when
|
|
1090
|
-
-- LoadStringEnabled is false (eval_server_runtime won't work in that mode).
|
|
1091
|
-
-- We can't import the runtime LoadStringEnabled value cleanly without
|
|
1092
|
-
-- pulling in the type — read defensively.
|
|
1093
|
-
local function loadStringEnabled()
|
|
1094
|
-
local ok, value = pcall(function()
|
|
1095
|
-
return ServerScriptService.LoadStringEnabled
|
|
1096
|
-
end)
|
|
1097
|
-
return ok and value == true
|
|
1098
|
-
end
|
|
1099
1083
|
return {
|
|
1100
1084
|
cleanupBridges = cleanupBridges,
|
|
1101
1085
|
installBridges = installBridges,
|
|
1102
|
-
loadStringEnabled = loadStringEnabled,
|
|
1103
1086
|
BRIDGE_NAMES = BRIDGE_NAMES,
|
|
1104
1087
|
}
|
|
1105
1088
|
]]></string>
|
|
@@ -2815,6 +2798,74 @@ return {
|
|
|
2815
2798
|
</Properties>
|
|
2816
2799
|
</Item>
|
|
2817
2800
|
<Item class="ModuleScript" referent="12">
|
|
2801
|
+
<Properties>
|
|
2802
|
+
<string name="Name">MemoryHandlers</string>
|
|
2803
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
2804
|
+
local Stats = game:GetService("Stats")
|
|
2805
|
+
-- GetMemoryUsageMbAllCategories is gated by capability "InternalTest" and not
|
|
2806
|
+
-- callable from plugin context. GetMemoryUsageMbForTag is not - so we iterate
|
|
2807
|
+
-- Enum.DeveloperMemoryTag and ask per-tag.
|
|
2808
|
+
local function getMemoryBreakdown(requestData)
|
|
2809
|
+
if not Stats.MemoryTrackingEnabled then
|
|
2810
|
+
return {
|
|
2811
|
+
error = "MemoryTrackingEnabled is false on this peer",
|
|
2812
|
+
}
|
|
2813
|
+
end
|
|
2814
|
+
local requested = requestData.tags
|
|
2815
|
+
local _result
|
|
2816
|
+
if requested and #requested > 0 then
|
|
2817
|
+
local _set = {}
|
|
2818
|
+
for _, _v in requested do
|
|
2819
|
+
_set[_v] = true
|
|
2820
|
+
end
|
|
2821
|
+
_result = _set
|
|
2822
|
+
else
|
|
2823
|
+
_result = nil
|
|
2824
|
+
end
|
|
2825
|
+
local requestedSet = _result
|
|
2826
|
+
local categories = {}
|
|
2827
|
+
for _, item in Enum.DeveloperMemoryTag:GetEnumItems() do
|
|
2828
|
+
local name = item.Name
|
|
2829
|
+
if requestedSet and not (requestedSet[name] ~= nil) then
|
|
2830
|
+
continue
|
|
2831
|
+
end
|
|
2832
|
+
local ok, mb = pcall(function()
|
|
2833
|
+
return Stats:GetMemoryUsageMbForTag(item)
|
|
2834
|
+
end)
|
|
2835
|
+
categories[name] = if ok then mb else 0
|
|
2836
|
+
end
|
|
2837
|
+
local unknownTags = {}
|
|
2838
|
+
if requestedSet then
|
|
2839
|
+
local known = {}
|
|
2840
|
+
for _, i in Enum.DeveloperMemoryTag:GetEnumItems() do
|
|
2841
|
+
local _name = i.Name
|
|
2842
|
+
known[_name] = true
|
|
2843
|
+
end
|
|
2844
|
+
for t in requestedSet do
|
|
2845
|
+
if not (known[t] ~= nil) then
|
|
2846
|
+
table.insert(unknownTags, t)
|
|
2847
|
+
categories[t] = 0
|
|
2848
|
+
end
|
|
2849
|
+
end
|
|
2850
|
+
end
|
|
2851
|
+
local result = {
|
|
2852
|
+
total_mb = Stats:GetTotalMemoryUsageMb(),
|
|
2853
|
+
categories = categories,
|
|
2854
|
+
memory_tracking_enabled = true,
|
|
2855
|
+
timestamp = DateTime.now().UnixTimestampMillis,
|
|
2856
|
+
}
|
|
2857
|
+
if #unknownTags > 0 then
|
|
2858
|
+
result.unknown_tags = unknownTags
|
|
2859
|
+
end
|
|
2860
|
+
return result
|
|
2861
|
+
end
|
|
2862
|
+
return {
|
|
2863
|
+
getMemoryBreakdown = getMemoryBreakdown,
|
|
2864
|
+
}
|
|
2865
|
+
]]></string>
|
|
2866
|
+
</Properties>
|
|
2867
|
+
</Item>
|
|
2868
|
+
<Item class="ModuleScript" referent="13">
|
|
2818
2869
|
<Properties>
|
|
2819
2870
|
<string name="Name">MetadataHandlers</string>
|
|
2820
2871
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3451,7 +3502,7 @@ return {
|
|
|
3451
3502
|
]]></string>
|
|
3452
3503
|
</Properties>
|
|
3453
3504
|
</Item>
|
|
3454
|
-
<Item class="ModuleScript" referent="
|
|
3505
|
+
<Item class="ModuleScript" referent="14">
|
|
3455
3506
|
<Properties>
|
|
3456
3507
|
<string name="Name">PropertyHandlers</string>
|
|
3457
3508
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3703,7 +3754,7 @@ return {
|
|
|
3703
3754
|
]]></string>
|
|
3704
3755
|
</Properties>
|
|
3705
3756
|
</Item>
|
|
3706
|
-
<Item class="ModuleScript" referent="
|
|
3757
|
+
<Item class="ModuleScript" referent="15">
|
|
3707
3758
|
<Properties>
|
|
3708
3759
|
<string name="Name">QueryHandlers</string>
|
|
3709
3760
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3811,8 +3862,23 @@ local function searchFiles(requestData)
|
|
|
3811
3862
|
}
|
|
3812
3863
|
end
|
|
3813
3864
|
local function getPlaceInfo(_requestData)
|
|
3865
|
+
local dataModelName = game.Name
|
|
3866
|
+
local placeName = dataModelName
|
|
3867
|
+
if game.PlaceId > 0 then
|
|
3868
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
3869
|
+
local ok, info = pcall(function()
|
|
3870
|
+
return MarketplaceService:GetProductInfo(game.PlaceId)
|
|
3871
|
+
end)
|
|
3872
|
+
if ok and info ~= nil then
|
|
3873
|
+
local name = info.Name
|
|
3874
|
+
if type(name) == "string" and name ~= "" then
|
|
3875
|
+
placeName = name
|
|
3876
|
+
end
|
|
3877
|
+
end
|
|
3878
|
+
end
|
|
3814
3879
|
return {
|
|
3815
|
-
placeName =
|
|
3880
|
+
placeName = placeName,
|
|
3881
|
+
dataModelName = dataModelName,
|
|
3816
3882
|
placeId = game.PlaceId,
|
|
3817
3883
|
gameId = game.GameId,
|
|
3818
3884
|
jobId = game.JobId,
|
|
@@ -4730,7 +4796,7 @@ return {
|
|
|
4730
4796
|
]]></string>
|
|
4731
4797
|
</Properties>
|
|
4732
4798
|
</Item>
|
|
4733
|
-
<Item class="ModuleScript" referent="
|
|
4799
|
+
<Item class="ModuleScript" referent="16">
|
|
4734
4800
|
<Properties>
|
|
4735
4801
|
<string name="Name">ScriptHandlers</string>
|
|
4736
4802
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5426,7 +5492,193 @@ return {
|
|
|
5426
5492
|
]]></string>
|
|
5427
5493
|
</Properties>
|
|
5428
5494
|
</Item>
|
|
5429
|
-
<Item class="ModuleScript" referent="
|
|
5495
|
+
<Item class="ModuleScript" referent="17">
|
|
5496
|
+
<Properties>
|
|
5497
|
+
<string name="Name">SerializationHandlers</string>
|
|
5498
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5499
|
+
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
5500
|
+
local RunService = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services").RunService
|
|
5501
|
+
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
5502
|
+
local Recording = TS.import(script, script.Parent.Parent, "Recording")
|
|
5503
|
+
-- SerializationService:SerializeInstancesAsync / DeserializeInstancesAsync were
|
|
5504
|
+
-- added in engine v668 and are PluginSecurity. They are not in @rbxts/types yet,
|
|
5505
|
+
-- so we resolve the service through an untyped GetService cast and treat the
|
|
5506
|
+
-- methods as opaque (buffer in / buffer out).
|
|
5507
|
+
local SerializationService = game:GetService("SerializationService")
|
|
5508
|
+
-- EncodingService:Base64Encode / Base64Decode take and return `buffer` (not
|
|
5509
|
+
-- `string`). The signature is in @rbxts/types under None.d.ts so a normal
|
|
5510
|
+
-- GetService("EncodingService") would already give correct types, but @rbxts
|
|
5511
|
+
-- generates a per-service nominal interface and roblox.d.ts doesn't re-export
|
|
5512
|
+
-- EncodingService from the services barrel module - so the typed cast below
|
|
5513
|
+
-- matches what GetService would give us if it did.
|
|
5514
|
+
local EncodingService = game:GetService("EncodingService")
|
|
5515
|
+
local _binding = Utils
|
|
5516
|
+
local getInstanceByPath = _binding.getInstanceByPath
|
|
5517
|
+
local getInstancePath = _binding.getInstancePath
|
|
5518
|
+
local _binding_1 = Recording
|
|
5519
|
+
local beginRecording = _binding_1.beginRecording
|
|
5520
|
+
local finishRecording = _binding_1.finishRecording
|
|
5521
|
+
local function exportRbxm(requestData)
|
|
5522
|
+
local instancePaths = requestData.instance_paths
|
|
5523
|
+
if not instancePaths or not (type(instancePaths) == "table") or #instancePaths == 0 then
|
|
5524
|
+
return {
|
|
5525
|
+
error = "instance_paths must be a non-empty array",
|
|
5526
|
+
}
|
|
5527
|
+
end
|
|
5528
|
+
local instances = {}
|
|
5529
|
+
for _, p in instancePaths do
|
|
5530
|
+
local inst = getInstanceByPath(p)
|
|
5531
|
+
if not inst then
|
|
5532
|
+
return {
|
|
5533
|
+
error = `instance not found: {p}`,
|
|
5534
|
+
}
|
|
5535
|
+
end
|
|
5536
|
+
table.insert(instances, inst)
|
|
5537
|
+
end
|
|
5538
|
+
local serializeOk, serializeResult = pcall(function()
|
|
5539
|
+
return SerializationService:SerializeInstancesAsync(instances)
|
|
5540
|
+
end)
|
|
5541
|
+
if not serializeOk then
|
|
5542
|
+
return {
|
|
5543
|
+
error = `SerializeInstancesAsync failed: {tostring(serializeResult)}`,
|
|
5544
|
+
}
|
|
5545
|
+
end
|
|
5546
|
+
local buf = serializeResult
|
|
5547
|
+
local encodeOk, encodeResult = pcall(function()
|
|
5548
|
+
return EncodingService:Base64Encode(buf)
|
|
5549
|
+
end)
|
|
5550
|
+
if not encodeOk then
|
|
5551
|
+
return {
|
|
5552
|
+
error = `EncodingService:Base64Encode failed: {tostring(encodeResult)}`,
|
|
5553
|
+
}
|
|
5554
|
+
end
|
|
5555
|
+
-- Base64Encode returns a buffer of ASCII bytes; convert to a Lua string so
|
|
5556
|
+
-- HttpService:JSONEncode (called by the harness in Communication.ts) accepts
|
|
5557
|
+
-- it. Base64 is by definition pure ASCII so this round-trips cleanly.
|
|
5558
|
+
local base64Str = buffer.tostring(encodeResult)
|
|
5559
|
+
return {
|
|
5560
|
+
base64 = base64Str,
|
|
5561
|
+
instance_count = #instances,
|
|
5562
|
+
}
|
|
5563
|
+
end
|
|
5564
|
+
local function importRbxm(requestData)
|
|
5565
|
+
local b64 = requestData.base64
|
|
5566
|
+
local parentPath = requestData.parent_path
|
|
5567
|
+
local _condition = (requestData.source_label)
|
|
5568
|
+
if _condition == nil then
|
|
5569
|
+
_condition = "unknown"
|
|
5570
|
+
end
|
|
5571
|
+
local sourceLabel = _condition
|
|
5572
|
+
if not (b64 ~= "" and b64) or not (type(b64) == "string") then
|
|
5573
|
+
return {
|
|
5574
|
+
error = "base64 payload is required",
|
|
5575
|
+
}
|
|
5576
|
+
end
|
|
5577
|
+
if not (parentPath ~= "" and parentPath) or not (type(parentPath) == "string") then
|
|
5578
|
+
return {
|
|
5579
|
+
error = "parent_path is required",
|
|
5580
|
+
}
|
|
5581
|
+
end
|
|
5582
|
+
local parentInstance = getInstanceByPath(parentPath)
|
|
5583
|
+
if not parentInstance then
|
|
5584
|
+
return {
|
|
5585
|
+
error = `parent instance not found: {parentPath}`,
|
|
5586
|
+
}
|
|
5587
|
+
end
|
|
5588
|
+
-- b64 is an ASCII-only Lua string from the wire; lift it into a buffer for
|
|
5589
|
+
-- EncodingService:Base64Decode, which returns a buffer of raw rbxm bytes
|
|
5590
|
+
-- ready for DeserializeInstancesAsync.
|
|
5591
|
+
local b64BufOk, b64BufResult = pcall(function()
|
|
5592
|
+
return buffer.fromstring(b64)
|
|
5593
|
+
end)
|
|
5594
|
+
if not b64BufOk then
|
|
5595
|
+
return {
|
|
5596
|
+
error = `buffer.fromstring(base64) failed: {tostring(b64BufResult)}`,
|
|
5597
|
+
}
|
|
5598
|
+
end
|
|
5599
|
+
local decodeOk, decodeResult = pcall(function()
|
|
5600
|
+
return EncodingService:Base64Decode(b64BufResult)
|
|
5601
|
+
end)
|
|
5602
|
+
if not decodeOk then
|
|
5603
|
+
return {
|
|
5604
|
+
error = `EncodingService:Base64Decode failed: {tostring(decodeResult)}`,
|
|
5605
|
+
}
|
|
5606
|
+
end
|
|
5607
|
+
local buf = decodeResult
|
|
5608
|
+
local deserOk, deserResult = pcall(function()
|
|
5609
|
+
return SerializationService:DeserializeInstancesAsync(buf)
|
|
5610
|
+
end)
|
|
5611
|
+
if not deserOk then
|
|
5612
|
+
return {
|
|
5613
|
+
error = `DeserializeInstancesAsync failed: {tostring(deserResult)}`,
|
|
5614
|
+
}
|
|
5615
|
+
end
|
|
5616
|
+
local deserialized = deserResult
|
|
5617
|
+
-- All-or-nothing parenting. Track every instance we've attached and roll back
|
|
5618
|
+
-- (unparent + Destroy) if any later one fails - partial imports leave the DM
|
|
5619
|
+
-- in a worse state than failing cleanly.
|
|
5620
|
+
local isEdit = not RunService:IsRunning()
|
|
5621
|
+
local recordingId = if isEdit then beginRecording(`Import rbxm`) else nil
|
|
5622
|
+
local attached = {}
|
|
5623
|
+
local failureMessage
|
|
5624
|
+
for _, inst in deserialized do
|
|
5625
|
+
local parentOk, parentErr = pcall(function()
|
|
5626
|
+
inst.Parent = parentInstance
|
|
5627
|
+
end)
|
|
5628
|
+
if not parentOk then
|
|
5629
|
+
failureMessage = `failed to parent {inst.Name} ({inst.ClassName}) under {parentPath}: {tostring(parentErr)}`
|
|
5630
|
+
break
|
|
5631
|
+
end
|
|
5632
|
+
table.insert(attached, inst)
|
|
5633
|
+
end
|
|
5634
|
+
if failureMessage ~= nil then
|
|
5635
|
+
for _, inst in attached do
|
|
5636
|
+
pcall(function()
|
|
5637
|
+
inst.Parent = nil
|
|
5638
|
+
inst:Destroy()
|
|
5639
|
+
end)
|
|
5640
|
+
end
|
|
5641
|
+
-- Also destroy any unparented deserialized instances so they don't leak.
|
|
5642
|
+
for _, inst in deserialized do
|
|
5643
|
+
if inst.Parent == nil then
|
|
5644
|
+
pcall(function()
|
|
5645
|
+
return inst:Destroy()
|
|
5646
|
+
end)
|
|
5647
|
+
end
|
|
5648
|
+
end
|
|
5649
|
+
finishRecording(recordingId, false)
|
|
5650
|
+
return {
|
|
5651
|
+
error = failureMessage,
|
|
5652
|
+
}
|
|
5653
|
+
end
|
|
5654
|
+
local names = {}
|
|
5655
|
+
local paths = {}
|
|
5656
|
+
for _, inst in attached do
|
|
5657
|
+
local _name = inst.Name
|
|
5658
|
+
table.insert(names, _name)
|
|
5659
|
+
local _arg0 = getInstancePath(inst)
|
|
5660
|
+
table.insert(paths, _arg0)
|
|
5661
|
+
end
|
|
5662
|
+
-- The recording shows "MCP: Import rbxm" in Studio's undo stack -
|
|
5663
|
+
-- ChangeHistoryService doesn't expose a way to set a richer displayName
|
|
5664
|
+
-- after TryBeginRecording, so the count/source only land in the JSON response.
|
|
5665
|
+
finishRecording(recordingId, true)
|
|
5666
|
+
return {
|
|
5667
|
+
instance_count = #attached,
|
|
5668
|
+
instance_names = names,
|
|
5669
|
+
instance_paths = paths,
|
|
5670
|
+
parent_path = parentPath,
|
|
5671
|
+
source = sourceLabel,
|
|
5672
|
+
}
|
|
5673
|
+
end
|
|
5674
|
+
return {
|
|
5675
|
+
exportRbxm = exportRbxm,
|
|
5676
|
+
importRbxm = importRbxm,
|
|
5677
|
+
}
|
|
5678
|
+
]]></string>
|
|
5679
|
+
</Properties>
|
|
5680
|
+
</Item>
|
|
5681
|
+
<Item class="ModuleScript" referent="18">
|
|
5430
5682
|
<Properties>
|
|
5431
5683
|
<string name="Name">TestHandlers</string>
|
|
5432
5684
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5437,7 +5689,6 @@ local LogService = _services.LogService
|
|
|
5437
5689
|
local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
|
|
5438
5690
|
local installBridges = _EvalBridges.installBridges
|
|
5439
5691
|
local cleanupBridges = _EvalBridges.cleanupBridges
|
|
5440
|
-
local loadStringEnabled = _EvalBridges.loadStringEnabled
|
|
5441
5692
|
local StudioTestService = game:GetService("StudioTestService")
|
|
5442
5693
|
local ServerScriptService = game:GetService("ServerScriptService")
|
|
5443
5694
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
@@ -5598,7 +5849,6 @@ local function startPlaytest(requestData)
|
|
|
5598
5849
|
-- so eval_server_runtime / eval_client_runtime work without manual setup.
|
|
5599
5850
|
-- Bridges are cleaned up from the edit DM after the play DMs tear down.
|
|
5600
5851
|
local bridgeInstall = installBridges()
|
|
5601
|
-
local hasLoadString = loadStringEnabled()
|
|
5602
5852
|
if not bridgeInstall.installed then
|
|
5603
5853
|
warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
|
|
5604
5854
|
end
|
|
@@ -5632,13 +5882,6 @@ local function startPlaytest(requestData)
|
|
|
5632
5882
|
message = msg,
|
|
5633
5883
|
evalBridges = if bridgeInstall.installed then "installed" else `failed: {bridgeInstall.error}`,
|
|
5634
5884
|
}
|
|
5635
|
-
-- Surface loadstring availability up-front so callers know whether
|
|
5636
|
-
-- eval_server_runtime will work before they try it. eval_client_runtime
|
|
5637
|
-
-- doesn't need loadstring (it uses ModuleScript+require), so this only
|
|
5638
|
-
-- affects the server bridge.
|
|
5639
|
-
if not hasLoadString then
|
|
5640
|
-
response.serverEvalNote = "ServerScriptService.LoadStringEnabled is false. eval_server_runtime will not work " .. "until you enable it (ServerScriptService > Properties > LoadStringEnabled = true) " .. "and restart the playtest. eval_client_runtime is unaffected."
|
|
5641
|
-
end
|
|
5642
5885
|
return response
|
|
5643
5886
|
end
|
|
5644
5887
|
local function stopPlaytest(_requestData)
|
|
@@ -5745,7 +5988,7 @@ return {
|
|
|
5745
5988
|
</Properties>
|
|
5746
5989
|
</Item>
|
|
5747
5990
|
</Item>
|
|
5748
|
-
<Item class="ModuleScript" referent="
|
|
5991
|
+
<Item class="ModuleScript" referent="19">
|
|
5749
5992
|
<Properties>
|
|
5750
5993
|
<string name="Name">Recording</string>
|
|
5751
5994
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5775,7 +6018,7 @@ return {
|
|
|
5775
6018
|
]]></string>
|
|
5776
6019
|
</Properties>
|
|
5777
6020
|
</Item>
|
|
5778
|
-
<Item class="ModuleScript" referent="
|
|
6021
|
+
<Item class="ModuleScript" referent="20">
|
|
5779
6022
|
<Properties>
|
|
5780
6023
|
<string name="Name">RuntimeLogBuffer</string>
|
|
5781
6024
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5956,11 +6199,11 @@ return {
|
|
|
5956
6199
|
]]></string>
|
|
5957
6200
|
</Properties>
|
|
5958
6201
|
</Item>
|
|
5959
|
-
<Item class="ModuleScript" referent="
|
|
6202
|
+
<Item class="ModuleScript" referent="21">
|
|
5960
6203
|
<Properties>
|
|
5961
6204
|
<string name="Name">State</string>
|
|
5962
6205
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5963
|
-
local CURRENT_VERSION = "2.
|
|
6206
|
+
local CURRENT_VERSION = "2.11.0"
|
|
5964
6207
|
local MAX_CONNECTIONS = 5
|
|
5965
6208
|
local BASE_PORT = 58741
|
|
5966
6209
|
local activeTabIndex = 0
|
|
@@ -6052,7 +6295,7 @@ return {
|
|
|
6052
6295
|
]]></string>
|
|
6053
6296
|
</Properties>
|
|
6054
6297
|
</Item>
|
|
6055
|
-
<Item class="ModuleScript" referent="
|
|
6298
|
+
<Item class="ModuleScript" referent="22">
|
|
6056
6299
|
<Properties>
|
|
6057
6300
|
<string name="Name">UI</string>
|
|
6058
6301
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6062,6 +6305,34 @@ local State = TS.import(script, script.Parent, "State")
|
|
|
6062
6305
|
local elements = nil
|
|
6063
6306
|
local pulseAnimation
|
|
6064
6307
|
local buttonHover = false
|
|
6308
|
+
local toolbarButton
|
|
6309
|
+
local toolbarIcons
|
|
6310
|
+
local lastToolbarIcon
|
|
6311
|
+
local updateToolbarIcon
|
|
6312
|
+
local function setToolbarButton(btn, icons)
|
|
6313
|
+
toolbarButton = btn
|
|
6314
|
+
toolbarIcons = icons
|
|
6315
|
+
lastToolbarIcon = nil
|
|
6316
|
+
updateToolbarIcon()
|
|
6317
|
+
end
|
|
6318
|
+
function updateToolbarIcon()
|
|
6319
|
+
if not toolbarButton or not toolbarIcons then
|
|
6320
|
+
return nil
|
|
6321
|
+
end
|
|
6322
|
+
local conn = State.getActiveConnection()
|
|
6323
|
+
local nextIcon
|
|
6324
|
+
if not conn or not conn.isActive then
|
|
6325
|
+
nextIcon = toolbarIcons.disconnected
|
|
6326
|
+
elseif conn.lastHttpOk and conn.lastMcpOk then
|
|
6327
|
+
nextIcon = toolbarIcons.connected
|
|
6328
|
+
else
|
|
6329
|
+
nextIcon = toolbarIcons.connecting
|
|
6330
|
+
end
|
|
6331
|
+
if nextIcon ~= lastToolbarIcon then
|
|
6332
|
+
toolbarButton.Icon = nextIcon
|
|
6333
|
+
lastToolbarIcon = nextIcon
|
|
6334
|
+
end
|
|
6335
|
+
end
|
|
6065
6336
|
local tabButtons = {}
|
|
6066
6337
|
local TWEEN_QUICK = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
|
|
6067
6338
|
local function tweenProp(instance, props)
|
|
@@ -6640,6 +6911,7 @@ local function init(pluginRef)
|
|
|
6640
6911
|
refreshTabBar()
|
|
6641
6912
|
end
|
|
6642
6913
|
function updateUIState()
|
|
6914
|
+
updateToolbarIcon()
|
|
6643
6915
|
local conn = State.getActiveConnection()
|
|
6644
6916
|
if not conn then
|
|
6645
6917
|
return nil
|
|
@@ -6765,6 +7037,8 @@ return {
|
|
|
6765
7037
|
updateTabLabel = updateTabLabel,
|
|
6766
7038
|
stopPulseAnimation = stopPulseAnimation,
|
|
6767
7039
|
startPulseAnimation = startPulseAnimation,
|
|
7040
|
+
setToolbarButton = setToolbarButton,
|
|
7041
|
+
updateToolbarIcon = updateToolbarIcon,
|
|
6768
7042
|
getElements = function()
|
|
6769
7043
|
return elements
|
|
6770
7044
|
end,
|
|
@@ -6772,7 +7046,7 @@ return {
|
|
|
6772
7046
|
]]></string>
|
|
6773
7047
|
</Properties>
|
|
6774
7048
|
</Item>
|
|
6775
|
-
<Item class="ModuleScript" referent="
|
|
7049
|
+
<Item class="ModuleScript" referent="23">
|
|
6776
7050
|
<Properties>
|
|
6777
7051
|
<string name="Name">Utils</string>
|
|
6778
7052
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7302,11 +7576,11 @@ return {
|
|
|
7302
7576
|
</Properties>
|
|
7303
7577
|
</Item>
|
|
7304
7578
|
</Item>
|
|
7305
|
-
<Item class="Folder" referent="
|
|
7579
|
+
<Item class="Folder" referent="27">
|
|
7306
7580
|
<Properties>
|
|
7307
7581
|
<string name="Name">include</string>
|
|
7308
7582
|
</Properties>
|
|
7309
|
-
<Item class="ModuleScript" referent="
|
|
7583
|
+
<Item class="ModuleScript" referent="24">
|
|
7310
7584
|
<Properties>
|
|
7311
7585
|
<string name="Name">Promise</string>
|
|
7312
7586
|
<string name="Source"><![CDATA[--[[
|
|
@@ -9380,7 +9654,7 @@ return Promise
|
|
|
9380
9654
|
]]></string>
|
|
9381
9655
|
</Properties>
|
|
9382
9656
|
</Item>
|
|
9383
|
-
<Item class="ModuleScript" referent="
|
|
9657
|
+
<Item class="ModuleScript" referent="25">
|
|
9384
9658
|
<Properties>
|
|
9385
9659
|
<string name="Name">RuntimeLib</string>
|
|
9386
9660
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -9647,15 +9921,15 @@ return TS
|
|
|
9647
9921
|
</Properties>
|
|
9648
9922
|
</Item>
|
|
9649
9923
|
</Item>
|
|
9650
|
-
<Item class="Folder" referent="
|
|
9924
|
+
<Item class="Folder" referent="28">
|
|
9651
9925
|
<Properties>
|
|
9652
9926
|
<string name="Name">node_modules</string>
|
|
9653
9927
|
</Properties>
|
|
9654
|
-
<Item class="Folder" referent="
|
|
9928
|
+
<Item class="Folder" referent="29">
|
|
9655
9929
|
<Properties>
|
|
9656
9930
|
<string name="Name">@rbxts</string>
|
|
9657
9931
|
</Properties>
|
|
9658
|
-
<Item class="ModuleScript" referent="
|
|
9932
|
+
<Item class="ModuleScript" referent="26">
|
|
9659
9933
|
<Properties>
|
|
9660
9934
|
<string name="Name">services</string>
|
|
9661
9935
|
<string name="Source"><![CDATA[return setmetatable({}, {
|