@chrrxs/robloxstudio-mcp-inspector 2.8.1 → 2.9.1
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 +134 -1
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +316 -23
- package/studio-plugin/MCPPlugin.rbxmx +316 -23
- package/studio-plugin/src/modules/EvalBridges.ts +215 -0
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +46 -2
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +30 -1
package/dist/index.js
CHANGED
|
@@ -335,6 +335,8 @@ var init_http_server = __esm({
|
|
|
335
335
|
get_tagged: (tools, body) => tools.getTagged(body.tagName),
|
|
336
336
|
get_selection: (tools) => tools.getSelection(),
|
|
337
337
|
execute_luau: (tools, body) => tools.executeLuau(body.code, body.target),
|
|
338
|
+
eval_server_runtime: (tools, body) => tools.evalServerRuntime(body.code),
|
|
339
|
+
eval_client_runtime: (tools, body) => tools.evalClientRuntime(body.code, body.target),
|
|
338
340
|
start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers),
|
|
339
341
|
stop_playtest: (tools) => tools.stopPlaytest(),
|
|
340
342
|
get_playtest_output: (tools, body) => tools.getPlaytestOutput(body.target),
|
|
@@ -1195,7 +1197,27 @@ function encodePngFromRgbaResponse(response) {
|
|
|
1195
1197
|
const rgbaBuffer = Buffer.from(response.data, "base64");
|
|
1196
1198
|
return rgbaToPng(rgbaBuffer, response.width, response.height);
|
|
1197
1199
|
}
|
|
1198
|
-
|
|
1200
|
+
function luaLongQuote(s) {
|
|
1201
|
+
let level = 0;
|
|
1202
|
+
while (s.includes(`]${"=".repeat(level)}]`))
|
|
1203
|
+
level++;
|
|
1204
|
+
const eq = "=".repeat(level);
|
|
1205
|
+
return `[${eq}[
|
|
1206
|
+
${s}
|
|
1207
|
+
]${eq}]`;
|
|
1208
|
+
}
|
|
1209
|
+
function parseBridgeResponse(response) {
|
|
1210
|
+
const r = response;
|
|
1211
|
+
if (r && typeof r.returnValue === "string") {
|
|
1212
|
+
try {
|
|
1213
|
+
const parsed = JSON.parse(r.returnValue);
|
|
1214
|
+
return JSON.stringify(parsed);
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return JSON.stringify(response);
|
|
1219
|
+
}
|
|
1220
|
+
var SERVER_LOCAL_NAME, CLIENT_LOCAL_NAME, RobloxStudioTools;
|
|
1199
1221
|
var init_tools = __esm({
|
|
1200
1222
|
"../core/dist/tools/index.js"() {
|
|
1201
1223
|
"use strict";
|
|
@@ -1204,6 +1226,8 @@ var init_tools = __esm({
|
|
|
1204
1226
|
init_opencloud_client();
|
|
1205
1227
|
init_roblox_cookie_client();
|
|
1206
1228
|
init_png_encoder();
|
|
1229
|
+
SERVER_LOCAL_NAME = "__MCP_ServerEvalLocal";
|
|
1230
|
+
CLIENT_LOCAL_NAME = "__MCP_ClientEvalBridge";
|
|
1207
1231
|
RobloxStudioTools = class _RobloxStudioTools {
|
|
1208
1232
|
client;
|
|
1209
1233
|
bridge;
|
|
@@ -1737,6 +1761,81 @@ ${code}`
|
|
|
1737
1761
|
]
|
|
1738
1762
|
};
|
|
1739
1763
|
}
|
|
1764
|
+
async evalServerRuntime(code) {
|
|
1765
|
+
if (!code) {
|
|
1766
|
+
throw new Error("Code is required for eval_server_runtime");
|
|
1767
|
+
}
|
|
1768
|
+
const wrapper = `
|
|
1769
|
+
local HttpService = game:GetService("HttpService")
|
|
1770
|
+
local bf = game:GetService("ServerScriptService"):FindFirstChild("${SERVER_LOCAL_NAME}")
|
|
1771
|
+
if not bf then
|
|
1772
|
+
return HttpService:JSONEncode({
|
|
1773
|
+
bridge = "missing",
|
|
1774
|
+
error = "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
|
|
1775
|
+
})
|
|
1776
|
+
end
|
|
1777
|
+
local USER_CODE = ${luaLongQuote(code)}
|
|
1778
|
+
local ok, result = bf:Invoke(USER_CODE)
|
|
1779
|
+
return HttpService:JSONEncode({
|
|
1780
|
+
bridge = "ok",
|
|
1781
|
+
ok = ok,
|
|
1782
|
+
result = if result == nil then nil else tostring(result),
|
|
1783
|
+
})
|
|
1784
|
+
`;
|
|
1785
|
+
const response = await this.client.request("/api/execute-luau", { code: wrapper }, "server");
|
|
1786
|
+
return {
|
|
1787
|
+
content: [
|
|
1788
|
+
{
|
|
1789
|
+
type: "text",
|
|
1790
|
+
text: parseBridgeResponse(response)
|
|
1791
|
+
}
|
|
1792
|
+
]
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
async evalClientRuntime(code, target) {
|
|
1796
|
+
if (!code) {
|
|
1797
|
+
throw new Error("Code is required for eval_client_runtime");
|
|
1798
|
+
}
|
|
1799
|
+
const clientTarget = target || "client-1";
|
|
1800
|
+
if (!clientTarget.startsWith("client-")) {
|
|
1801
|
+
throw new Error(`eval_client_runtime requires target=client-N (got: ${clientTarget})`);
|
|
1802
|
+
}
|
|
1803
|
+
const wrapper = `
|
|
1804
|
+
local HttpService = game:GetService("HttpService")
|
|
1805
|
+
local bf = game:GetService("ReplicatedStorage"):FindFirstChild("${CLIENT_LOCAL_NAME}")
|
|
1806
|
+
if not bf then
|
|
1807
|
+
return HttpService:JSONEncode({
|
|
1808
|
+
bridge = "missing",
|
|
1809
|
+
error = "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
|
|
1810
|
+
})
|
|
1811
|
+
end
|
|
1812
|
+
local USER_CODE = ${luaLongQuote(code)}
|
|
1813
|
+
local m = Instance.new("ModuleScript")
|
|
1814
|
+
m.Name = "__MCPEvalPayload"
|
|
1815
|
+
local okSet, setErr = pcall(function() m.Source = USER_CODE end)
|
|
1816
|
+
if not okSet then
|
|
1817
|
+
m:Destroy()
|
|
1818
|
+
return HttpService:JSONEncode({ bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) })
|
|
1819
|
+
end
|
|
1820
|
+
m.Parent = workspace
|
|
1821
|
+
local ok, result = bf:Invoke(m)
|
|
1822
|
+
m:Destroy()
|
|
1823
|
+
return HttpService:JSONEncode({
|
|
1824
|
+
bridge = "ok",
|
|
1825
|
+
ok = ok,
|
|
1826
|
+
result = if result == nil then nil else tostring(result),
|
|
1827
|
+
})
|
|
1828
|
+
`;
|
|
1829
|
+
const response = await this.client.request("/api/execute-luau", { code: wrapper }, clientTarget);
|
|
1830
|
+
return {
|
|
1831
|
+
content: [
|
|
1832
|
+
{
|
|
1833
|
+
type: "text",
|
|
1834
|
+
text: parseBridgeResponse(response)
|
|
1835
|
+
}
|
|
1836
|
+
]
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1740
1839
|
async startPlaytest(mode, numPlayers) {
|
|
1741
1840
|
if (mode !== "play" && mode !== "run") {
|
|
1742
1841
|
throw new Error('mode must be "play" or "run"');
|
|
@@ -3730,6 +3829,40 @@ var init_definitions = __esm({
|
|
|
3730
3829
|
required: ["code"]
|
|
3731
3830
|
}
|
|
3732
3831
|
},
|
|
3832
|
+
{
|
|
3833
|
+
name: "eval_server_runtime",
|
|
3834
|
+
category: "write",
|
|
3835
|
+
description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts). Use this instead of execute_luau target=server when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest. Requires ServerScriptService.LoadStringEnabled=true.",
|
|
3836
|
+
inputSchema: {
|
|
3837
|
+
type: "object",
|
|
3838
|
+
properties: {
|
|
3839
|
+
code: {
|
|
3840
|
+
type: "string",
|
|
3841
|
+
description: "Luau code to execute. Use return ... to get a value back."
|
|
3842
|
+
}
|
|
3843
|
+
},
|
|
3844
|
+
required: ["code"]
|
|
3845
|
+
}
|
|
3846
|
+
},
|
|
3847
|
+
{
|
|
3848
|
+
name: "eval_client_runtime",
|
|
3849
|
+
category: "write",
|
|
3850
|
+
description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts). Use this instead of execute_luau target=client-N when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest. Does not require LoadStringEnabled.",
|
|
3851
|
+
inputSchema: {
|
|
3852
|
+
type: "object",
|
|
3853
|
+
properties: {
|
|
3854
|
+
code: {
|
|
3855
|
+
type: "string",
|
|
3856
|
+
description: "Luau code to execute. Use return ... to get a value back."
|
|
3857
|
+
},
|
|
3858
|
+
target: {
|
|
3859
|
+
type: "string",
|
|
3860
|
+
description: 'Client target: "client-1" (default), "client-2", etc.'
|
|
3861
|
+
}
|
|
3862
|
+
},
|
|
3863
|
+
required: ["code"]
|
|
3864
|
+
}
|
|
3865
|
+
},
|
|
3733
3866
|
// === Script Search ===
|
|
3734
3867
|
{
|
|
3735
3868
|
name: "grep_scripts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrrxs/robloxstudio-mcp-inspector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
4
4
|
"description": "Read-only MCP Server for Roblox Studio (fork of boshyxd/robloxstudio-mcp-inspector with per-peer execute_luau fixes baked in)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -770,11 +770,239 @@ return {
|
|
|
770
770
|
]]></string>
|
|
771
771
|
</Properties>
|
|
772
772
|
</Item>
|
|
773
|
-
<Item class="
|
|
773
|
+
<Item class="ModuleScript" referent="4">
|
|
774
|
+
<Properties>
|
|
775
|
+
<string name="Name">EvalBridges</string>
|
|
776
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
777
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
778
|
+
-- Game-VM eval bridges, ported from chrrxs/roblox-mcp-primitives.
|
|
779
|
+
--
|
|
780
|
+
-- Our standard `execute_luau target=server/client-N` runs in the plugin VM
|
|
781
|
+
-- with a fresh ModuleScript per call. That gives a clean sandbox but means
|
|
782
|
+
-- `require(SomeModule)` returns a fresh copy, not the one the running game
|
|
783
|
+
-- scripts hold. So runtime-mutated module state is invisible to probes.
|
|
784
|
+
--
|
|
785
|
+
-- These bridges fix that by living inside the user's game scripts:
|
|
786
|
+
-- - Server: a Script in ServerScriptService that creates a BindableFunction
|
|
787
|
+
-- (for our server-peer plugin to invoke directly) plus a RemoteFunction
|
|
788
|
+
-- (kept for parity with the upstream primitive's client-callable shape).
|
|
789
|
+
-- - Client: a LocalScript in StarterPlayer.StarterPlayerScripts that
|
|
790
|
+
-- creates a BindableFunction. Plugin invokes it with a fresh ModuleScript
|
|
791
|
+
-- payload; require() runs inside the LocalScript VM so it shares the
|
|
792
|
+
-- game's require cache.
|
|
793
|
+
--
|
|
794
|
+
-- Lifecycle: TestHandlers.startPlaytest inserts both scripts into the EDIT
|
|
795
|
+
-- DM right before ExecutePlayModeAsync. ExecutePlayModeAsync clones the
|
|
796
|
+
-- DataModel into the play DMs, so the scripts come along and run there.
|
|
797
|
+
-- TestHandlers cleans them up from the edit DM when ExecutePlayModeAsync
|
|
798
|
+
-- returns (test ended for any reason: stop_playtest, manual close, EndTest).
|
|
799
|
+
--
|
|
800
|
+
-- Archivable handling: ExecutePlayModeAsync's deep-clone SKIPS instances
|
|
801
|
+
-- with Archivable=false (verified empirically in v2.9.0 testing - bridges
|
|
802
|
+
-- never reached the play DMs because we'd set them to false). We now keep
|
|
803
|
+
-- Archivable=true so the clone works, and rely on cleanupBridges() to
|
|
804
|
+
-- remove the scripts from the edit DM when the test ends. The only failure
|
|
805
|
+
-- mode is the user saving DURING an active playtest, which would persist
|
|
806
|
+
-- the bridges to the .rbxl - that's a no-op next session because
|
|
807
|
+
-- installBridges() always calls cleanupBridges() first to clear stale
|
|
808
|
+
-- instances. The RemoteFunction/BindableFunction that the bridge scripts
|
|
809
|
+
-- CREATE at runtime stay Archivable=false (they're runtime-only and should
|
|
810
|
+
-- never appear in a save).
|
|
811
|
+
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
812
|
+
local ServerScriptService = _services.ServerScriptService
|
|
813
|
+
local StarterPlayer = _services.StarterPlayer
|
|
814
|
+
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
815
|
+
local function getStarterPlayerScripts()
|
|
816
|
+
return StarterPlayer:FindFirstChild("StarterPlayerScripts")
|
|
817
|
+
end
|
|
818
|
+
local SERVER_SCRIPT_NAME = "__MCP_ServerEvalBridge"
|
|
819
|
+
local CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge"
|
|
820
|
+
-- Public so the eval_*_runtime tool wrappers can reference the same names.
|
|
821
|
+
local BRIDGE_NAMES = {
|
|
822
|
+
serverScript = SERVER_SCRIPT_NAME,
|
|
823
|
+
clientScript = CLIENT_SCRIPT_NAME,
|
|
824
|
+
serverRemote = "__MCP_ServerEvalRemote",
|
|
825
|
+
serverLocal = "__MCP_ServerEvalLocal",
|
|
826
|
+
clientLocal = "__MCP_ClientEvalBridge",
|
|
827
|
+
}
|
|
828
|
+
-- Embedded Luau. The double `${...}` references our exported names so a
|
|
829
|
+
-- rename here propagates to both the script source and the tool wrappers.
|
|
830
|
+
local SERVER_BRIDGE_SOURCE = `\
|
|
831
|
+
-- Auto-installed by @chrrxs/robloxstudio-mcp at start_playtest, removed at\
|
|
832
|
+
-- stop_playtest. Provides shared-require-cache eval on the server peer for\
|
|
833
|
+
-- the eval_server_runtime MCP tool.\
|
|
834
|
+
\
|
|
835
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
836
|
+
local ServerScriptService = game:GetService("ServerScriptService")\
|
|
837
|
+
local RunService = game:GetService("RunService")\
|
|
838
|
+
\
|
|
839
|
+
if not RunService:IsStudio() then\
|
|
840
|
+
return\
|
|
841
|
+
end\
|
|
842
|
+
\
|
|
843
|
+
local function evalCode(source)\
|
|
844
|
+
if type(source) ~= "string" then\
|
|
845
|
+
return false, "source must be a string"\
|
|
846
|
+
end\
|
|
847
|
+
local fn, compileErr = loadstring(source, "MCPServerEval")\
|
|
848
|
+
if not fn then\
|
|
849
|
+
local errStr = tostring(compileErr or "loadstring returned nil")\
|
|
850
|
+
-- Roblox returns nil from loadstring when LoadStringEnabled=false.\
|
|
851
|
+
-- Surface a clear, actionable error.\
|
|
852
|
+
if string.find(errStr, "not enabled", 1, true)\
|
|
853
|
+
or string.find(errStr, "disabled", 1, true)\
|
|
854
|
+
or errStr == "loadstring returned nil"\
|
|
855
|
+
then\
|
|
856
|
+
return false,\
|
|
857
|
+
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime requires it. "\
|
|
858
|
+
.. "Enable it in Studio (ServerScriptService > Properties > LoadStringEnabled = true) "\
|
|
859
|
+
.. "and restart the playtest."\
|
|
860
|
+
end\
|
|
861
|
+
return false, errStr\
|
|
862
|
+
end\
|
|
863
|
+
return pcall(fn)\
|
|
864
|
+
end\
|
|
865
|
+
\
|
|
866
|
+
-- Defensive cleanup of stale instances from a prior session.\
|
|
867
|
+
local prevRf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.serverRemote}")\
|
|
868
|
+
if prevRf then prevRf:Destroy() end\
|
|
869
|
+
local prevBf = ServerScriptService:FindFirstChild("{BRIDGE_NAMES.serverLocal}")\
|
|
870
|
+
if prevBf then prevBf:Destroy() end\
|
|
871
|
+
\
|
|
872
|
+
local rf = Instance.new("RemoteFunction")\
|
|
873
|
+
rf.Name = "{BRIDGE_NAMES.serverRemote}"\
|
|
874
|
+
rf.Archivable = false\
|
|
875
|
+
rf.Parent = ReplicatedStorage\
|
|
876
|
+
rf.OnServerInvoke = function(_player, source)\
|
|
877
|
+
return evalCode(source)\
|
|
878
|
+
end\
|
|
879
|
+
\
|
|
880
|
+
local bf = Instance.new("BindableFunction")\
|
|
881
|
+
bf.Name = "{BRIDGE_NAMES.serverLocal}"\
|
|
882
|
+
bf.Archivable = false\
|
|
883
|
+
bf.Parent = ServerScriptService\
|
|
884
|
+
bf.OnInvoke = function(source)\
|
|
885
|
+
return evalCode(source)\
|
|
886
|
+
end\
|
|
887
|
+
`
|
|
888
|
+
local CLIENT_BRIDGE_SOURCE = `\
|
|
889
|
+
-- Auto-installed by @chrrxs/robloxstudio-mcp at start_playtest, removed at\
|
|
890
|
+
-- stop_playtest. Provides shared-require-cache eval on the client peer for\
|
|
891
|
+
-- the eval_client_runtime MCP tool.\
|
|
892
|
+
\
|
|
893
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
894
|
+
local RunService = game:GetService("RunService")\
|
|
895
|
+
\
|
|
896
|
+
if not RunService:IsStudio() then\
|
|
897
|
+
return\
|
|
898
|
+
end\
|
|
899
|
+
\
|
|
900
|
+
local prevBf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.clientLocal}")\
|
|
901
|
+
if prevBf then prevBf:Destroy() end\
|
|
902
|
+
\
|
|
903
|
+
local bf = Instance.new("BindableFunction")\
|
|
904
|
+
bf.Name = "{BRIDGE_NAMES.clientLocal}"\
|
|
905
|
+
bf.Archivable = false\
|
|
906
|
+
bf.Parent = ReplicatedStorage\
|
|
907
|
+
bf.OnInvoke = function(payload)\
|
|
908
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then\
|
|
909
|
+
return false, "payload must be a ModuleScript instance"\
|
|
910
|
+
end\
|
|
911
|
+
return pcall(require, payload)\
|
|
912
|
+
end\
|
|
913
|
+
`
|
|
914
|
+
local function setSource(scriptInst, source)
|
|
915
|
+
-- ScriptEditorService is the cleaner API and integrates with Studio's
|
|
916
|
+
-- edit history; fall back to direct Source mutation (allowed in plugin
|
|
917
|
+
-- context with PluginSecurity) if the edit service rejects the call.
|
|
918
|
+
local seOk = pcall(function()
|
|
919
|
+
ScriptEditorService:UpdateSourceAsync(scriptInst, function()
|
|
920
|
+
return source
|
|
921
|
+
end)
|
|
922
|
+
end)
|
|
923
|
+
if not seOk then
|
|
924
|
+
scriptInst.Source = source
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
local function findBridges()
|
|
928
|
+
local sps = getStarterPlayerScripts()
|
|
929
|
+
return {
|
|
930
|
+
server = ServerScriptService:FindFirstChild(SERVER_SCRIPT_NAME),
|
|
931
|
+
client = if sps then sps:FindFirstChild(CLIENT_SCRIPT_NAME) else nil,
|
|
932
|
+
}
|
|
933
|
+
end
|
|
934
|
+
local function cleanupBridges()
|
|
935
|
+
local _binding = findBridges()
|
|
936
|
+
local server = _binding.server
|
|
937
|
+
local client = _binding.client
|
|
938
|
+
if server then
|
|
939
|
+
pcall(function()
|
|
940
|
+
return server:Destroy()
|
|
941
|
+
end)
|
|
942
|
+
end
|
|
943
|
+
if client then
|
|
944
|
+
pcall(function()
|
|
945
|
+
return client:Destroy()
|
|
946
|
+
end)
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
local function installBridges()
|
|
950
|
+
-- Defensive: clear any stale bridges from a prior unclean exit before
|
|
951
|
+
-- inserting fresh. The injected script also self-cleans its
|
|
952
|
+
-- ReplicatedStorage/ServerScriptService children at startup, but the
|
|
953
|
+
-- containing Script/LocalScript objects themselves we must clear here.
|
|
954
|
+
cleanupBridges()
|
|
955
|
+
local ok, err = pcall(function()
|
|
956
|
+
local serverScript = Instance.new("Script")
|
|
957
|
+
serverScript.Name = SERVER_SCRIPT_NAME
|
|
958
|
+
-- Archivable=true so ExecutePlayModeAsync's deep-clone includes the
|
|
959
|
+
-- script. cleanupBridges() removes it from the edit DM when the
|
|
960
|
+
-- playtest ends.
|
|
961
|
+
setSource(serverScript, SERVER_BRIDGE_SOURCE)
|
|
962
|
+
serverScript.Parent = ServerScriptService
|
|
963
|
+
local sps = getStarterPlayerScripts()
|
|
964
|
+
if not sps then
|
|
965
|
+
error("StarterPlayer.StarterPlayerScripts not found - cannot install client eval bridge")
|
|
966
|
+
end
|
|
967
|
+
local clientScript = Instance.new("LocalScript")
|
|
968
|
+
clientScript.Name = CLIENT_SCRIPT_NAME
|
|
969
|
+
setSource(clientScript, CLIENT_BRIDGE_SOURCE)
|
|
970
|
+
clientScript.Parent = sps
|
|
971
|
+
end)
|
|
972
|
+
if not ok then
|
|
973
|
+
return {
|
|
974
|
+
installed = false,
|
|
975
|
+
error = tostring(err),
|
|
976
|
+
}
|
|
977
|
+
end
|
|
978
|
+
return {
|
|
979
|
+
installed = true,
|
|
980
|
+
}
|
|
981
|
+
end
|
|
982
|
+
-- Heuristic check so start_playtest can surface a warning when
|
|
983
|
+
-- LoadStringEnabled is false (eval_server_runtime won't work in that mode).
|
|
984
|
+
-- We can't import the runtime LoadStringEnabled value cleanly without
|
|
985
|
+
-- pulling in the type — read defensively.
|
|
986
|
+
local function loadStringEnabled()
|
|
987
|
+
local ok, value = pcall(function()
|
|
988
|
+
return ServerScriptService.LoadStringEnabled
|
|
989
|
+
end)
|
|
990
|
+
return ok and value == true
|
|
991
|
+
end
|
|
992
|
+
return {
|
|
993
|
+
cleanupBridges = cleanupBridges,
|
|
994
|
+
installBridges = installBridges,
|
|
995
|
+
loadStringEnabled = loadStringEnabled,
|
|
996
|
+
BRIDGE_NAMES = BRIDGE_NAMES,
|
|
997
|
+
}
|
|
998
|
+
]]></string>
|
|
999
|
+
</Properties>
|
|
1000
|
+
</Item>
|
|
1001
|
+
<Item class="Folder" referent="5">
|
|
774
1002
|
<Properties>
|
|
775
1003
|
<string name="Name">handlers</string>
|
|
776
1004
|
</Properties>
|
|
777
|
-
<Item class="ModuleScript" referent="
|
|
1005
|
+
<Item class="ModuleScript" referent="6">
|
|
778
1006
|
<Properties>
|
|
779
1007
|
<string name="Name">AssetHandlers</string>
|
|
780
1008
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1060,7 +1288,7 @@ return {
|
|
|
1060
1288
|
]]></string>
|
|
1061
1289
|
</Properties>
|
|
1062
1290
|
</Item>
|
|
1063
|
-
<Item class="ModuleScript" referent="
|
|
1291
|
+
<Item class="ModuleScript" referent="7">
|
|
1064
1292
|
<Properties>
|
|
1065
1293
|
<string name="Name">BuildHandlers</string>
|
|
1066
1294
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1625,7 +1853,7 @@ return {
|
|
|
1625
1853
|
]]></string>
|
|
1626
1854
|
</Properties>
|
|
1627
1855
|
</Item>
|
|
1628
|
-
<Item class="ModuleScript" referent="
|
|
1856
|
+
<Item class="ModuleScript" referent="8">
|
|
1629
1857
|
<Properties>
|
|
1630
1858
|
<string name="Name">CaptureHandlers</string>
|
|
1631
1859
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1791,7 +2019,7 @@ return {
|
|
|
1791
2019
|
]]></string>
|
|
1792
2020
|
</Properties>
|
|
1793
2021
|
</Item>
|
|
1794
|
-
<Item class="ModuleScript" referent="
|
|
2022
|
+
<Item class="ModuleScript" referent="9">
|
|
1795
2023
|
<Properties>
|
|
1796
2024
|
<string name="Name">InputHandlers</string>
|
|
1797
2025
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1945,7 +2173,7 @@ return {
|
|
|
1945
2173
|
]]></string>
|
|
1946
2174
|
</Properties>
|
|
1947
2175
|
</Item>
|
|
1948
|
-
<Item class="ModuleScript" referent="
|
|
2176
|
+
<Item class="ModuleScript" referent="10">
|
|
1949
2177
|
<Properties>
|
|
1950
2178
|
<string name="Name">InstanceHandlers</string>
|
|
1951
2179
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -2452,7 +2680,7 @@ return {
|
|
|
2452
2680
|
]]></string>
|
|
2453
2681
|
</Properties>
|
|
2454
2682
|
</Item>
|
|
2455
|
-
<Item class="ModuleScript" referent="
|
|
2683
|
+
<Item class="ModuleScript" referent="11">
|
|
2456
2684
|
<Properties>
|
|
2457
2685
|
<string name="Name">MetadataHandlers</string>
|
|
2458
2686
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -2911,13 +3139,56 @@ local function executeLuau(requestData)
|
|
|
2911
3139
|
table.insert(output, _arg0)
|
|
2912
3140
|
oldWarn(unpack(args))
|
|
2913
3141
|
end
|
|
3142
|
+
-- Try loadstring first (preserves print/warn interception). When
|
|
3143
|
+
-- ServerScriptService.LoadStringEnabled=false AND the plugin runs in a
|
|
3144
|
+
-- peer where the engine respects that gate (notably the play-server DM
|
|
3145
|
+
-- in some Studio configurations), loadstring either returns nil with a
|
|
3146
|
+
-- "loadstring() is not available" message OR throws that same message
|
|
3147
|
+
-- directly. Both paths must trigger the ModuleScript + require
|
|
3148
|
+
-- fallback. The fallback can't intercept print/warn since the
|
|
3149
|
+
-- ModuleScript runs in its own environment, so the output array stays
|
|
3150
|
+
-- empty in that branch - the playtest log buffer already captures
|
|
3151
|
+
-- prints separately via LogService.MessageOut.
|
|
3152
|
+
local runViaModuleScript = function()
|
|
3153
|
+
local m = Instance.new("ModuleScript")
|
|
3154
|
+
m.Name = "__MCPExecLuauPayload"
|
|
3155
|
+
local okSet, setErr = pcall(function()
|
|
3156
|
+
m.Source = code
|
|
3157
|
+
end)
|
|
3158
|
+
if not okSet then
|
|
3159
|
+
m:Destroy()
|
|
3160
|
+
error(`ModuleScript Source set failed: {tostring(setErr)}`)
|
|
3161
|
+
end
|
|
3162
|
+
m.Parent = game:GetService("Workspace")
|
|
3163
|
+
local okReq, reqResult = pcall(function()
|
|
3164
|
+
return require(m)
|
|
3165
|
+
end)
|
|
3166
|
+
m:Destroy()
|
|
3167
|
+
if not okReq then
|
|
3168
|
+
error(tostring(reqResult))
|
|
3169
|
+
end
|
|
3170
|
+
return reqResult
|
|
3171
|
+
end
|
|
3172
|
+
local isLoadstringUnavailable = function(err)
|
|
3173
|
+
local errStr = tostring(err)
|
|
3174
|
+
local matchStart = string.find(errStr, "not available", 1, true)
|
|
3175
|
+
return matchStart ~= nil
|
|
3176
|
+
end
|
|
2914
3177
|
local success, result = pcall(function()
|
|
2915
3178
|
local fn, compileError = loadstring(code)
|
|
2916
3179
|
if not fn then
|
|
3180
|
+
if isLoadstringUnavailable(compileError) then
|
|
3181
|
+
return runViaModuleScript()
|
|
3182
|
+
end
|
|
2917
3183
|
error(`Compile error: {compileError}`)
|
|
2918
3184
|
end
|
|
2919
3185
|
return fn()
|
|
2920
3186
|
end)
|
|
3187
|
+
-- loadstring throws (not returns nil) in some plugin contexts when
|
|
3188
|
+
-- LoadStringEnabled=false. Catch that here as a second-chance fallback.
|
|
3189
|
+
if not success and isLoadstringUnavailable(result) then
|
|
3190
|
+
success, result = pcall(runViaModuleScript)
|
|
3191
|
+
end
|
|
2921
3192
|
env.print = oldPrint
|
|
2922
3193
|
env.warn = oldWarn
|
|
2923
3194
|
if success then
|
|
@@ -3035,7 +3306,7 @@ return {
|
|
|
3035
3306
|
]]></string>
|
|
3036
3307
|
</Properties>
|
|
3037
3308
|
</Item>
|
|
3038
|
-
<Item class="ModuleScript" referent="
|
|
3309
|
+
<Item class="ModuleScript" referent="12">
|
|
3039
3310
|
<Properties>
|
|
3040
3311
|
<string name="Name">PropertyHandlers</string>
|
|
3041
3312
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3287,7 +3558,7 @@ return {
|
|
|
3287
3558
|
]]></string>
|
|
3288
3559
|
</Properties>
|
|
3289
3560
|
</Item>
|
|
3290
|
-
<Item class="ModuleScript" referent="
|
|
3561
|
+
<Item class="ModuleScript" referent="13">
|
|
3291
3562
|
<Properties>
|
|
3292
3563
|
<string name="Name">QueryHandlers</string>
|
|
3293
3564
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -4314,7 +4585,7 @@ return {
|
|
|
4314
4585
|
]]></string>
|
|
4315
4586
|
</Properties>
|
|
4316
4587
|
</Item>
|
|
4317
|
-
<Item class="ModuleScript" referent="
|
|
4588
|
+
<Item class="ModuleScript" referent="14">
|
|
4318
4589
|
<Properties>
|
|
4319
4590
|
<string name="Name">ScriptHandlers</string>
|
|
4320
4591
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5010,7 +5281,7 @@ return {
|
|
|
5010
5281
|
]]></string>
|
|
5011
5282
|
</Properties>
|
|
5012
5283
|
</Item>
|
|
5013
|
-
<Item class="ModuleScript" referent="
|
|
5284
|
+
<Item class="ModuleScript" referent="15">
|
|
5014
5285
|
<Properties>
|
|
5015
5286
|
<string name="Name">TestHandlers</string>
|
|
5016
5287
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5018,6 +5289,10 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
|
5018
5289
|
local _services = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
5019
5290
|
local HttpService = _services.HttpService
|
|
5020
5291
|
local LogService = _services.LogService
|
|
5292
|
+
local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
|
|
5293
|
+
local installBridges = _EvalBridges.installBridges
|
|
5294
|
+
local cleanupBridges = _EvalBridges.cleanupBridges
|
|
5295
|
+
local loadStringEnabled = _EvalBridges.loadStringEnabled
|
|
5021
5296
|
local StudioTestService = game:GetService("StudioTestService")
|
|
5022
5297
|
local ServerScriptService = game:GetService("ServerScriptService")
|
|
5023
5298
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
@@ -5174,6 +5449,14 @@ local function startPlaytest(requestData)
|
|
|
5174
5449
|
if not injected then
|
|
5175
5450
|
warn(`[MCP] Failed to inject stop listener: {injErr}`)
|
|
5176
5451
|
end
|
|
5452
|
+
-- Auto-install the game-VM eval bridges (ServerEvalBridge + ClientEvalBridge)
|
|
5453
|
+
-- so eval_server_runtime / eval_client_runtime work without manual setup.
|
|
5454
|
+
-- Bridges are cleaned up from the edit DM after the play DMs tear down.
|
|
5455
|
+
local bridgeInstall = installBridges()
|
|
5456
|
+
local hasLoadString = loadStringEnabled()
|
|
5457
|
+
if not bridgeInstall.installed then
|
|
5458
|
+
warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
|
|
5459
|
+
end
|
|
5177
5460
|
if numPlayers ~= nil and mode == "run" then
|
|
5178
5461
|
local TestService = game:GetService("TestService")
|
|
5179
5462
|
TestService.NumberOfPlayers = math.clamp(numPlayers, 1, 8)
|
|
@@ -5196,12 +5479,22 @@ local function startPlaytest(requestData)
|
|
|
5196
5479
|
end
|
|
5197
5480
|
testRunning = false
|
|
5198
5481
|
cleanupStopListener()
|
|
5482
|
+
cleanupBridges()
|
|
5199
5483
|
end)
|
|
5200
5484
|
local msg = if numPlayers ~= nil then `Playtest started in {mode} mode with {numPlayers} player(s)` else `Playtest started in {mode} mode`
|
|
5201
|
-
|
|
5485
|
+
local response = {
|
|
5202
5486
|
success = true,
|
|
5203
5487
|
message = msg,
|
|
5488
|
+
evalBridges = if bridgeInstall.installed then "installed" else `failed: {bridgeInstall.error}`,
|
|
5204
5489
|
}
|
|
5490
|
+
-- Surface loadstring availability up-front so callers know whether
|
|
5491
|
+
-- eval_server_runtime will work before they try it. eval_client_runtime
|
|
5492
|
+
-- doesn't need loadstring (it uses ModuleScript+require), so this only
|
|
5493
|
+
-- affects the server bridge.
|
|
5494
|
+
if not hasLoadString then
|
|
5495
|
+
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."
|
|
5496
|
+
end
|
|
5497
|
+
return response
|
|
5205
5498
|
end
|
|
5206
5499
|
local function stopPlaytest(_requestData)
|
|
5207
5500
|
-- Server-side routing (tools/index.ts:stopPlaytest) sends /api/stop-playtest
|
|
@@ -5307,7 +5600,7 @@ return {
|
|
|
5307
5600
|
</Properties>
|
|
5308
5601
|
</Item>
|
|
5309
5602
|
</Item>
|
|
5310
|
-
<Item class="ModuleScript" referent="
|
|
5603
|
+
<Item class="ModuleScript" referent="16">
|
|
5311
5604
|
<Properties>
|
|
5312
5605
|
<string name="Name">Recording</string>
|
|
5313
5606
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5337,11 +5630,11 @@ return {
|
|
|
5337
5630
|
]]></string>
|
|
5338
5631
|
</Properties>
|
|
5339
5632
|
</Item>
|
|
5340
|
-
<Item class="ModuleScript" referent="
|
|
5633
|
+
<Item class="ModuleScript" referent="17">
|
|
5341
5634
|
<Properties>
|
|
5342
5635
|
<string name="Name">State</string>
|
|
5343
5636
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5344
|
-
local CURRENT_VERSION = "2.
|
|
5637
|
+
local CURRENT_VERSION = "2.9.1"
|
|
5345
5638
|
local MAX_CONNECTIONS = 5
|
|
5346
5639
|
local BASE_PORT = 58741
|
|
5347
5640
|
local activeTabIndex = 0
|
|
@@ -5433,7 +5726,7 @@ return {
|
|
|
5433
5726
|
]]></string>
|
|
5434
5727
|
</Properties>
|
|
5435
5728
|
</Item>
|
|
5436
|
-
<Item class="ModuleScript" referent="
|
|
5729
|
+
<Item class="ModuleScript" referent="18">
|
|
5437
5730
|
<Properties>
|
|
5438
5731
|
<string name="Name">UI</string>
|
|
5439
5732
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6153,7 +6446,7 @@ return {
|
|
|
6153
6446
|
]]></string>
|
|
6154
6447
|
</Properties>
|
|
6155
6448
|
</Item>
|
|
6156
|
-
<Item class="ModuleScript" referent="
|
|
6449
|
+
<Item class="ModuleScript" referent="19">
|
|
6157
6450
|
<Properties>
|
|
6158
6451
|
<string name="Name">Utils</string>
|
|
6159
6452
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6683,11 +6976,11 @@ return {
|
|
|
6683
6976
|
</Properties>
|
|
6684
6977
|
</Item>
|
|
6685
6978
|
</Item>
|
|
6686
|
-
<Item class="Folder" referent="
|
|
6979
|
+
<Item class="Folder" referent="23">
|
|
6687
6980
|
<Properties>
|
|
6688
6981
|
<string name="Name">include</string>
|
|
6689
6982
|
</Properties>
|
|
6690
|
-
<Item class="ModuleScript" referent="
|
|
6983
|
+
<Item class="ModuleScript" referent="20">
|
|
6691
6984
|
<Properties>
|
|
6692
6985
|
<string name="Name">Promise</string>
|
|
6693
6986
|
<string name="Source"><![CDATA[--[[
|
|
@@ -8761,7 +9054,7 @@ return Promise
|
|
|
8761
9054
|
]]></string>
|
|
8762
9055
|
</Properties>
|
|
8763
9056
|
</Item>
|
|
8764
|
-
<Item class="ModuleScript" referent="
|
|
9057
|
+
<Item class="ModuleScript" referent="21">
|
|
8765
9058
|
<Properties>
|
|
8766
9059
|
<string name="Name">RuntimeLib</string>
|
|
8767
9060
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -9028,15 +9321,15 @@ return TS
|
|
|
9028
9321
|
</Properties>
|
|
9029
9322
|
</Item>
|
|
9030
9323
|
</Item>
|
|
9031
|
-
<Item class="Folder" referent="
|
|
9324
|
+
<Item class="Folder" referent="24">
|
|
9032
9325
|
<Properties>
|
|
9033
9326
|
<string name="Name">node_modules</string>
|
|
9034
9327
|
</Properties>
|
|
9035
|
-
<Item class="Folder" referent="
|
|
9328
|
+
<Item class="Folder" referent="25">
|
|
9036
9329
|
<Properties>
|
|
9037
9330
|
<string name="Name">@rbxts</string>
|
|
9038
9331
|
</Properties>
|
|
9039
|
-
<Item class="ModuleScript" referent="
|
|
9332
|
+
<Item class="ModuleScript" referent="22">
|
|
9040
9333
|
<Properties>
|
|
9041
9334
|
<string name="Name">services</string>
|
|
9042
9335
|
<string name="Source"><![CDATA[return setmetatable({}, {
|