@chrrxs/robloxstudio-mcp-inspector 2.8.1 → 2.9.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
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,16 @@ 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
|
+
var SERVER_LOCAL_NAME, CLIENT_LOCAL_NAME, RobloxStudioTools;
|
|
1199
1210
|
var init_tools = __esm({
|
|
1200
1211
|
"../core/dist/tools/index.js"() {
|
|
1201
1212
|
"use strict";
|
|
@@ -1204,6 +1215,8 @@ var init_tools = __esm({
|
|
|
1204
1215
|
init_opencloud_client();
|
|
1205
1216
|
init_roblox_cookie_client();
|
|
1206
1217
|
init_png_encoder();
|
|
1218
|
+
SERVER_LOCAL_NAME = "__MCP_ServerEvalLocal";
|
|
1219
|
+
CLIENT_LOCAL_NAME = "__MCP_ClientEvalBridge";
|
|
1207
1220
|
RobloxStudioTools = class _RobloxStudioTools {
|
|
1208
1221
|
client;
|
|
1209
1222
|
bridge;
|
|
@@ -1737,6 +1750,79 @@ ${code}`
|
|
|
1737
1750
|
]
|
|
1738
1751
|
};
|
|
1739
1752
|
}
|
|
1753
|
+
async evalServerRuntime(code) {
|
|
1754
|
+
if (!code) {
|
|
1755
|
+
throw new Error("Code is required for eval_server_runtime");
|
|
1756
|
+
}
|
|
1757
|
+
const wrapper = `
|
|
1758
|
+
local bf = game:GetService("ServerScriptService"):FindFirstChild("${SERVER_LOCAL_NAME}")
|
|
1759
|
+
if not bf then
|
|
1760
|
+
return {
|
|
1761
|
+
bridge = "missing",
|
|
1762
|
+
error = "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
|
|
1763
|
+
}
|
|
1764
|
+
end
|
|
1765
|
+
local USER_CODE = ${luaLongQuote(code)}
|
|
1766
|
+
local ok, result = bf:Invoke(USER_CODE)
|
|
1767
|
+
return {
|
|
1768
|
+
bridge = "ok",
|
|
1769
|
+
ok = ok,
|
|
1770
|
+
result = if result == nil then nil else tostring(result),
|
|
1771
|
+
}
|
|
1772
|
+
`;
|
|
1773
|
+
const response = await this.client.request("/api/execute-luau", { code: wrapper }, "server");
|
|
1774
|
+
return {
|
|
1775
|
+
content: [
|
|
1776
|
+
{
|
|
1777
|
+
type: "text",
|
|
1778
|
+
text: JSON.stringify(response)
|
|
1779
|
+
}
|
|
1780
|
+
]
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
async evalClientRuntime(code, target) {
|
|
1784
|
+
if (!code) {
|
|
1785
|
+
throw new Error("Code is required for eval_client_runtime");
|
|
1786
|
+
}
|
|
1787
|
+
const clientTarget = target || "client-1";
|
|
1788
|
+
if (!clientTarget.startsWith("client-")) {
|
|
1789
|
+
throw new Error(`eval_client_runtime requires target=client-N (got: ${clientTarget})`);
|
|
1790
|
+
}
|
|
1791
|
+
const wrapper = `
|
|
1792
|
+
local bf = game:GetService("ReplicatedStorage"):FindFirstChild("${CLIENT_LOCAL_NAME}")
|
|
1793
|
+
if not bf then
|
|
1794
|
+
return {
|
|
1795
|
+
bridge = "missing",
|
|
1796
|
+
error = "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
|
|
1797
|
+
}
|
|
1798
|
+
end
|
|
1799
|
+
local USER_CODE = ${luaLongQuote(code)}
|
|
1800
|
+
local m = Instance.new("ModuleScript")
|
|
1801
|
+
m.Name = "__MCPEvalPayload"
|
|
1802
|
+
local okSet, setErr = pcall(function() m.Source = USER_CODE end)
|
|
1803
|
+
if not okSet then
|
|
1804
|
+
m:Destroy()
|
|
1805
|
+
return { bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) }
|
|
1806
|
+
end
|
|
1807
|
+
m.Parent = workspace
|
|
1808
|
+
local ok, result = bf:Invoke(m)
|
|
1809
|
+
m:Destroy()
|
|
1810
|
+
return {
|
|
1811
|
+
bridge = "ok",
|
|
1812
|
+
ok = ok,
|
|
1813
|
+
result = if result == nil then nil else tostring(result),
|
|
1814
|
+
}
|
|
1815
|
+
`;
|
|
1816
|
+
const response = await this.client.request("/api/execute-luau", { code: wrapper }, clientTarget);
|
|
1817
|
+
return {
|
|
1818
|
+
content: [
|
|
1819
|
+
{
|
|
1820
|
+
type: "text",
|
|
1821
|
+
text: JSON.stringify(response)
|
|
1822
|
+
}
|
|
1823
|
+
]
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1740
1826
|
async startPlaytest(mode, numPlayers) {
|
|
1741
1827
|
if (mode !== "play" && mode !== "run") {
|
|
1742
1828
|
throw new Error('mode must be "play" or "run"');
|
|
@@ -3730,6 +3816,40 @@ var init_definitions = __esm({
|
|
|
3730
3816
|
required: ["code"]
|
|
3731
3817
|
}
|
|
3732
3818
|
},
|
|
3819
|
+
{
|
|
3820
|
+
name: "eval_server_runtime",
|
|
3821
|
+
category: "write",
|
|
3822
|
+
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.",
|
|
3823
|
+
inputSchema: {
|
|
3824
|
+
type: "object",
|
|
3825
|
+
properties: {
|
|
3826
|
+
code: {
|
|
3827
|
+
type: "string",
|
|
3828
|
+
description: "Luau code to execute. Use return ... to get a value back."
|
|
3829
|
+
}
|
|
3830
|
+
},
|
|
3831
|
+
required: ["code"]
|
|
3832
|
+
}
|
|
3833
|
+
},
|
|
3834
|
+
{
|
|
3835
|
+
name: "eval_client_runtime",
|
|
3836
|
+
category: "write",
|
|
3837
|
+
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.",
|
|
3838
|
+
inputSchema: {
|
|
3839
|
+
type: "object",
|
|
3840
|
+
properties: {
|
|
3841
|
+
code: {
|
|
3842
|
+
type: "string",
|
|
3843
|
+
description: "Luau code to execute. Use return ... to get a value back."
|
|
3844
|
+
},
|
|
3845
|
+
target: {
|
|
3846
|
+
type: "string",
|
|
3847
|
+
description: 'Client target: "client-1" (default), "client-2", etc.'
|
|
3848
|
+
}
|
|
3849
|
+
},
|
|
3850
|
+
required: ["code"]
|
|
3851
|
+
}
|
|
3852
|
+
},
|
|
3733
3853
|
// === Script Search ===
|
|
3734
3854
|
{
|
|
3735
3855
|
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.0",
|
|
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,227 @@ 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
|
+
-- Both scripts have Archivable=false so a user save doesn't persist them.
|
|
800
|
+
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
801
|
+
local ServerScriptService = _services.ServerScriptService
|
|
802
|
+
local StarterPlayer = _services.StarterPlayer
|
|
803
|
+
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
804
|
+
local function getStarterPlayerScripts()
|
|
805
|
+
return StarterPlayer:FindFirstChild("StarterPlayerScripts")
|
|
806
|
+
end
|
|
807
|
+
local SERVER_SCRIPT_NAME = "__MCP_ServerEvalBridge"
|
|
808
|
+
local CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge"
|
|
809
|
+
-- Public so the eval_*_runtime tool wrappers can reference the same names.
|
|
810
|
+
local BRIDGE_NAMES = {
|
|
811
|
+
serverScript = SERVER_SCRIPT_NAME,
|
|
812
|
+
clientScript = CLIENT_SCRIPT_NAME,
|
|
813
|
+
serverRemote = "__MCP_ServerEvalRemote",
|
|
814
|
+
serverLocal = "__MCP_ServerEvalLocal",
|
|
815
|
+
clientLocal = "__MCP_ClientEvalBridge",
|
|
816
|
+
}
|
|
817
|
+
-- Embedded Luau. The double `${...}` references our exported names so a
|
|
818
|
+
-- rename here propagates to both the script source and the tool wrappers.
|
|
819
|
+
local SERVER_BRIDGE_SOURCE = `\
|
|
820
|
+
-- Auto-installed by @chrrxs/robloxstudio-mcp at start_playtest, removed at\
|
|
821
|
+
-- stop_playtest. Provides shared-require-cache eval on the server peer for\
|
|
822
|
+
-- the eval_server_runtime MCP tool.\
|
|
823
|
+
\
|
|
824
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
825
|
+
local ServerScriptService = game:GetService("ServerScriptService")\
|
|
826
|
+
local RunService = game:GetService("RunService")\
|
|
827
|
+
\
|
|
828
|
+
if not RunService:IsStudio() then\
|
|
829
|
+
return\
|
|
830
|
+
end\
|
|
831
|
+
\
|
|
832
|
+
local function evalCode(source)\
|
|
833
|
+
if type(source) ~= "string" then\
|
|
834
|
+
return false, "source must be a string"\
|
|
835
|
+
end\
|
|
836
|
+
local fn, compileErr = loadstring(source, "MCPServerEval")\
|
|
837
|
+
if not fn then\
|
|
838
|
+
local errStr = tostring(compileErr or "loadstring returned nil")\
|
|
839
|
+
-- Roblox returns nil from loadstring when LoadStringEnabled=false.\
|
|
840
|
+
-- Surface a clear, actionable error.\
|
|
841
|
+
if string.find(errStr, "not enabled", 1, true)\
|
|
842
|
+
or string.find(errStr, "disabled", 1, true)\
|
|
843
|
+
or errStr == "loadstring returned nil"\
|
|
844
|
+
then\
|
|
845
|
+
return false,\
|
|
846
|
+
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime requires it. "\
|
|
847
|
+
.. "Enable it in Studio (ServerScriptService > Properties > LoadStringEnabled = true) "\
|
|
848
|
+
.. "and restart the playtest."\
|
|
849
|
+
end\
|
|
850
|
+
return false, errStr\
|
|
851
|
+
end\
|
|
852
|
+
return pcall(fn)\
|
|
853
|
+
end\
|
|
854
|
+
\
|
|
855
|
+
-- Defensive cleanup of stale instances from a prior session.\
|
|
856
|
+
local prevRf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.serverRemote}")\
|
|
857
|
+
if prevRf then prevRf:Destroy() end\
|
|
858
|
+
local prevBf = ServerScriptService:FindFirstChild("{BRIDGE_NAMES.serverLocal}")\
|
|
859
|
+
if prevBf then prevBf:Destroy() end\
|
|
860
|
+
\
|
|
861
|
+
local rf = Instance.new("RemoteFunction")\
|
|
862
|
+
rf.Name = "{BRIDGE_NAMES.serverRemote}"\
|
|
863
|
+
rf.Archivable = false\
|
|
864
|
+
rf.Parent = ReplicatedStorage\
|
|
865
|
+
rf.OnServerInvoke = function(_player, source)\
|
|
866
|
+
return evalCode(source)\
|
|
867
|
+
end\
|
|
868
|
+
\
|
|
869
|
+
local bf = Instance.new("BindableFunction")\
|
|
870
|
+
bf.Name = "{BRIDGE_NAMES.serverLocal}"\
|
|
871
|
+
bf.Archivable = false\
|
|
872
|
+
bf.Parent = ServerScriptService\
|
|
873
|
+
bf.OnInvoke = function(source)\
|
|
874
|
+
return evalCode(source)\
|
|
875
|
+
end\
|
|
876
|
+
`
|
|
877
|
+
local CLIENT_BRIDGE_SOURCE = `\
|
|
878
|
+
-- Auto-installed by @chrrxs/robloxstudio-mcp at start_playtest, removed at\
|
|
879
|
+
-- stop_playtest. Provides shared-require-cache eval on the client peer for\
|
|
880
|
+
-- the eval_client_runtime MCP tool.\
|
|
881
|
+
\
|
|
882
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
883
|
+
local RunService = game:GetService("RunService")\
|
|
884
|
+
\
|
|
885
|
+
if not RunService:IsStudio() then\
|
|
886
|
+
return\
|
|
887
|
+
end\
|
|
888
|
+
\
|
|
889
|
+
local prevBf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.clientLocal}")\
|
|
890
|
+
if prevBf then prevBf:Destroy() end\
|
|
891
|
+
\
|
|
892
|
+
local bf = Instance.new("BindableFunction")\
|
|
893
|
+
bf.Name = "{BRIDGE_NAMES.clientLocal}"\
|
|
894
|
+
bf.Archivable = false\
|
|
895
|
+
bf.Parent = ReplicatedStorage\
|
|
896
|
+
bf.OnInvoke = function(payload)\
|
|
897
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then\
|
|
898
|
+
return false, "payload must be a ModuleScript instance"\
|
|
899
|
+
end\
|
|
900
|
+
return pcall(require, payload)\
|
|
901
|
+
end\
|
|
902
|
+
`
|
|
903
|
+
local function setSource(scriptInst, source)
|
|
904
|
+
-- ScriptEditorService is the cleaner API and integrates with Studio's
|
|
905
|
+
-- edit history; fall back to direct Source mutation (allowed in plugin
|
|
906
|
+
-- context with PluginSecurity) if the edit service rejects the call.
|
|
907
|
+
local seOk = pcall(function()
|
|
908
|
+
ScriptEditorService:UpdateSourceAsync(scriptInst, function()
|
|
909
|
+
return source
|
|
910
|
+
end)
|
|
911
|
+
end)
|
|
912
|
+
if not seOk then
|
|
913
|
+
scriptInst.Source = source
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
local function findBridges()
|
|
917
|
+
local sps = getStarterPlayerScripts()
|
|
918
|
+
return {
|
|
919
|
+
server = ServerScriptService:FindFirstChild(SERVER_SCRIPT_NAME),
|
|
920
|
+
client = if sps then sps:FindFirstChild(CLIENT_SCRIPT_NAME) else nil,
|
|
921
|
+
}
|
|
922
|
+
end
|
|
923
|
+
local function cleanupBridges()
|
|
924
|
+
local _binding = findBridges()
|
|
925
|
+
local server = _binding.server
|
|
926
|
+
local client = _binding.client
|
|
927
|
+
if server then
|
|
928
|
+
pcall(function()
|
|
929
|
+
return server:Destroy()
|
|
930
|
+
end)
|
|
931
|
+
end
|
|
932
|
+
if client then
|
|
933
|
+
pcall(function()
|
|
934
|
+
return client:Destroy()
|
|
935
|
+
end)
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
local function installBridges()
|
|
939
|
+
-- Defensive: clear any stale bridges from a prior unclean exit before
|
|
940
|
+
-- inserting fresh. The injected script also self-cleans its
|
|
941
|
+
-- ReplicatedStorage/ServerScriptService children at startup, but the
|
|
942
|
+
-- containing Script/LocalScript objects themselves we must clear here.
|
|
943
|
+
cleanupBridges()
|
|
944
|
+
local ok, err = pcall(function()
|
|
945
|
+
local serverScript = Instance.new("Script")
|
|
946
|
+
serverScript.Name = SERVER_SCRIPT_NAME
|
|
947
|
+
serverScript.Archivable = false
|
|
948
|
+
setSource(serverScript, SERVER_BRIDGE_SOURCE)
|
|
949
|
+
serverScript.Parent = ServerScriptService
|
|
950
|
+
local sps = getStarterPlayerScripts()
|
|
951
|
+
if not sps then
|
|
952
|
+
error("StarterPlayer.StarterPlayerScripts not found - cannot install client eval bridge")
|
|
953
|
+
end
|
|
954
|
+
local clientScript = Instance.new("LocalScript")
|
|
955
|
+
clientScript.Name = CLIENT_SCRIPT_NAME
|
|
956
|
+
clientScript.Archivable = false
|
|
957
|
+
setSource(clientScript, CLIENT_BRIDGE_SOURCE)
|
|
958
|
+
clientScript.Parent = sps
|
|
959
|
+
end)
|
|
960
|
+
if not ok then
|
|
961
|
+
return {
|
|
962
|
+
installed = false,
|
|
963
|
+
error = tostring(err),
|
|
964
|
+
}
|
|
965
|
+
end
|
|
966
|
+
return {
|
|
967
|
+
installed = true,
|
|
968
|
+
}
|
|
969
|
+
end
|
|
970
|
+
-- Heuristic check so start_playtest can surface a warning when
|
|
971
|
+
-- LoadStringEnabled is false (eval_server_runtime won't work in that mode).
|
|
972
|
+
-- We can't import the runtime LoadStringEnabled value cleanly without
|
|
973
|
+
-- pulling in the type — read defensively.
|
|
974
|
+
local function loadStringEnabled()
|
|
975
|
+
local ok, value = pcall(function()
|
|
976
|
+
return ServerScriptService.LoadStringEnabled
|
|
977
|
+
end)
|
|
978
|
+
return ok and value == true
|
|
979
|
+
end
|
|
980
|
+
return {
|
|
981
|
+
cleanupBridges = cleanupBridges,
|
|
982
|
+
installBridges = installBridges,
|
|
983
|
+
loadStringEnabled = loadStringEnabled,
|
|
984
|
+
BRIDGE_NAMES = BRIDGE_NAMES,
|
|
985
|
+
}
|
|
986
|
+
]]></string>
|
|
987
|
+
</Properties>
|
|
988
|
+
</Item>
|
|
989
|
+
<Item class="Folder" referent="5">
|
|
774
990
|
<Properties>
|
|
775
991
|
<string name="Name">handlers</string>
|
|
776
992
|
</Properties>
|
|
777
|
-
<Item class="ModuleScript" referent="
|
|
993
|
+
<Item class="ModuleScript" referent="6">
|
|
778
994
|
<Properties>
|
|
779
995
|
<string name="Name">AssetHandlers</string>
|
|
780
996
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1060,7 +1276,7 @@ return {
|
|
|
1060
1276
|
]]></string>
|
|
1061
1277
|
</Properties>
|
|
1062
1278
|
</Item>
|
|
1063
|
-
<Item class="ModuleScript" referent="
|
|
1279
|
+
<Item class="ModuleScript" referent="7">
|
|
1064
1280
|
<Properties>
|
|
1065
1281
|
<string name="Name">BuildHandlers</string>
|
|
1066
1282
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1625,7 +1841,7 @@ return {
|
|
|
1625
1841
|
]]></string>
|
|
1626
1842
|
</Properties>
|
|
1627
1843
|
</Item>
|
|
1628
|
-
<Item class="ModuleScript" referent="
|
|
1844
|
+
<Item class="ModuleScript" referent="8">
|
|
1629
1845
|
<Properties>
|
|
1630
1846
|
<string name="Name">CaptureHandlers</string>
|
|
1631
1847
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1791,7 +2007,7 @@ return {
|
|
|
1791
2007
|
]]></string>
|
|
1792
2008
|
</Properties>
|
|
1793
2009
|
</Item>
|
|
1794
|
-
<Item class="ModuleScript" referent="
|
|
2010
|
+
<Item class="ModuleScript" referent="9">
|
|
1795
2011
|
<Properties>
|
|
1796
2012
|
<string name="Name">InputHandlers</string>
|
|
1797
2013
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1945,7 +2161,7 @@ return {
|
|
|
1945
2161
|
]]></string>
|
|
1946
2162
|
</Properties>
|
|
1947
2163
|
</Item>
|
|
1948
|
-
<Item class="ModuleScript" referent="
|
|
2164
|
+
<Item class="ModuleScript" referent="10">
|
|
1949
2165
|
<Properties>
|
|
1950
2166
|
<string name="Name">InstanceHandlers</string>
|
|
1951
2167
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -2452,7 +2668,7 @@ return {
|
|
|
2452
2668
|
]]></string>
|
|
2453
2669
|
</Properties>
|
|
2454
2670
|
</Item>
|
|
2455
|
-
<Item class="ModuleScript" referent="
|
|
2671
|
+
<Item class="ModuleScript" referent="11">
|
|
2456
2672
|
<Properties>
|
|
2457
2673
|
<string name="Name">MetadataHandlers</string>
|
|
2458
2674
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3035,7 +3251,7 @@ return {
|
|
|
3035
3251
|
]]></string>
|
|
3036
3252
|
</Properties>
|
|
3037
3253
|
</Item>
|
|
3038
|
-
<Item class="ModuleScript" referent="
|
|
3254
|
+
<Item class="ModuleScript" referent="12">
|
|
3039
3255
|
<Properties>
|
|
3040
3256
|
<string name="Name">PropertyHandlers</string>
|
|
3041
3257
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3287,7 +3503,7 @@ return {
|
|
|
3287
3503
|
]]></string>
|
|
3288
3504
|
</Properties>
|
|
3289
3505
|
</Item>
|
|
3290
|
-
<Item class="ModuleScript" referent="
|
|
3506
|
+
<Item class="ModuleScript" referent="13">
|
|
3291
3507
|
<Properties>
|
|
3292
3508
|
<string name="Name">QueryHandlers</string>
|
|
3293
3509
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -4314,7 +4530,7 @@ return {
|
|
|
4314
4530
|
]]></string>
|
|
4315
4531
|
</Properties>
|
|
4316
4532
|
</Item>
|
|
4317
|
-
<Item class="ModuleScript" referent="
|
|
4533
|
+
<Item class="ModuleScript" referent="14">
|
|
4318
4534
|
<Properties>
|
|
4319
4535
|
<string name="Name">ScriptHandlers</string>
|
|
4320
4536
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5010,7 +5226,7 @@ return {
|
|
|
5010
5226
|
]]></string>
|
|
5011
5227
|
</Properties>
|
|
5012
5228
|
</Item>
|
|
5013
|
-
<Item class="ModuleScript" referent="
|
|
5229
|
+
<Item class="ModuleScript" referent="15">
|
|
5014
5230
|
<Properties>
|
|
5015
5231
|
<string name="Name">TestHandlers</string>
|
|
5016
5232
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5018,6 +5234,10 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
|
5018
5234
|
local _services = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
5019
5235
|
local HttpService = _services.HttpService
|
|
5020
5236
|
local LogService = _services.LogService
|
|
5237
|
+
local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
|
|
5238
|
+
local installBridges = _EvalBridges.installBridges
|
|
5239
|
+
local cleanupBridges = _EvalBridges.cleanupBridges
|
|
5240
|
+
local loadStringEnabled = _EvalBridges.loadStringEnabled
|
|
5021
5241
|
local StudioTestService = game:GetService("StudioTestService")
|
|
5022
5242
|
local ServerScriptService = game:GetService("ServerScriptService")
|
|
5023
5243
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
@@ -5174,6 +5394,14 @@ local function startPlaytest(requestData)
|
|
|
5174
5394
|
if not injected then
|
|
5175
5395
|
warn(`[MCP] Failed to inject stop listener: {injErr}`)
|
|
5176
5396
|
end
|
|
5397
|
+
-- Auto-install the game-VM eval bridges (ServerEvalBridge + ClientEvalBridge)
|
|
5398
|
+
-- so eval_server_runtime / eval_client_runtime work without manual setup.
|
|
5399
|
+
-- Bridges are cleaned up from the edit DM after the play DMs tear down.
|
|
5400
|
+
local bridgeInstall = installBridges()
|
|
5401
|
+
local hasLoadString = loadStringEnabled()
|
|
5402
|
+
if not bridgeInstall.installed then
|
|
5403
|
+
warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
|
|
5404
|
+
end
|
|
5177
5405
|
if numPlayers ~= nil and mode == "run" then
|
|
5178
5406
|
local TestService = game:GetService("TestService")
|
|
5179
5407
|
TestService.NumberOfPlayers = math.clamp(numPlayers, 1, 8)
|
|
@@ -5196,12 +5424,22 @@ local function startPlaytest(requestData)
|
|
|
5196
5424
|
end
|
|
5197
5425
|
testRunning = false
|
|
5198
5426
|
cleanupStopListener()
|
|
5427
|
+
cleanupBridges()
|
|
5199
5428
|
end)
|
|
5200
5429
|
local msg = if numPlayers ~= nil then `Playtest started in {mode} mode with {numPlayers} player(s)` else `Playtest started in {mode} mode`
|
|
5201
|
-
|
|
5430
|
+
local response = {
|
|
5202
5431
|
success = true,
|
|
5203
5432
|
message = msg,
|
|
5433
|
+
evalBridges = if bridgeInstall.installed then "installed" else `failed: {bridgeInstall.error}`,
|
|
5204
5434
|
}
|
|
5435
|
+
-- Surface loadstring availability up-front so callers know whether
|
|
5436
|
+
-- eval_server_runtime will work before they try it. eval_client_runtime
|
|
5437
|
+
-- doesn't need loadstring (it uses ModuleScript+require), so this only
|
|
5438
|
+
-- affects the server bridge.
|
|
5439
|
+
if not hasLoadString then
|
|
5440
|
+
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."
|
|
5441
|
+
end
|
|
5442
|
+
return response
|
|
5205
5443
|
end
|
|
5206
5444
|
local function stopPlaytest(_requestData)
|
|
5207
5445
|
-- Server-side routing (tools/index.ts:stopPlaytest) sends /api/stop-playtest
|
|
@@ -5307,7 +5545,7 @@ return {
|
|
|
5307
5545
|
</Properties>
|
|
5308
5546
|
</Item>
|
|
5309
5547
|
</Item>
|
|
5310
|
-
<Item class="ModuleScript" referent="
|
|
5548
|
+
<Item class="ModuleScript" referent="16">
|
|
5311
5549
|
<Properties>
|
|
5312
5550
|
<string name="Name">Recording</string>
|
|
5313
5551
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5337,11 +5575,11 @@ return {
|
|
|
5337
5575
|
]]></string>
|
|
5338
5576
|
</Properties>
|
|
5339
5577
|
</Item>
|
|
5340
|
-
<Item class="ModuleScript" referent="
|
|
5578
|
+
<Item class="ModuleScript" referent="17">
|
|
5341
5579
|
<Properties>
|
|
5342
5580
|
<string name="Name">State</string>
|
|
5343
5581
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5344
|
-
local CURRENT_VERSION = "2.
|
|
5582
|
+
local CURRENT_VERSION = "2.9.0"
|
|
5345
5583
|
local MAX_CONNECTIONS = 5
|
|
5346
5584
|
local BASE_PORT = 58741
|
|
5347
5585
|
local activeTabIndex = 0
|
|
@@ -5433,7 +5671,7 @@ return {
|
|
|
5433
5671
|
]]></string>
|
|
5434
5672
|
</Properties>
|
|
5435
5673
|
</Item>
|
|
5436
|
-
<Item class="ModuleScript" referent="
|
|
5674
|
+
<Item class="ModuleScript" referent="18">
|
|
5437
5675
|
<Properties>
|
|
5438
5676
|
<string name="Name">UI</string>
|
|
5439
5677
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6153,7 +6391,7 @@ return {
|
|
|
6153
6391
|
]]></string>
|
|
6154
6392
|
</Properties>
|
|
6155
6393
|
</Item>
|
|
6156
|
-
<Item class="ModuleScript" referent="
|
|
6394
|
+
<Item class="ModuleScript" referent="19">
|
|
6157
6395
|
<Properties>
|
|
6158
6396
|
<string name="Name">Utils</string>
|
|
6159
6397
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6683,11 +6921,11 @@ return {
|
|
|
6683
6921
|
</Properties>
|
|
6684
6922
|
</Item>
|
|
6685
6923
|
</Item>
|
|
6686
|
-
<Item class="Folder" referent="
|
|
6924
|
+
<Item class="Folder" referent="23">
|
|
6687
6925
|
<Properties>
|
|
6688
6926
|
<string name="Name">include</string>
|
|
6689
6927
|
</Properties>
|
|
6690
|
-
<Item class="ModuleScript" referent="
|
|
6928
|
+
<Item class="ModuleScript" referent="20">
|
|
6691
6929
|
<Properties>
|
|
6692
6930
|
<string name="Name">Promise</string>
|
|
6693
6931
|
<string name="Source"><![CDATA[--[[
|
|
@@ -8761,7 +8999,7 @@ return Promise
|
|
|
8761
8999
|
]]></string>
|
|
8762
9000
|
</Properties>
|
|
8763
9001
|
</Item>
|
|
8764
|
-
<Item class="ModuleScript" referent="
|
|
9002
|
+
<Item class="ModuleScript" referent="21">
|
|
8765
9003
|
<Properties>
|
|
8766
9004
|
<string name="Name">RuntimeLib</string>
|
|
8767
9005
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -9028,15 +9266,15 @@ return TS
|
|
|
9028
9266
|
</Properties>
|
|
9029
9267
|
</Item>
|
|
9030
9268
|
</Item>
|
|
9031
|
-
<Item class="Folder" referent="
|
|
9269
|
+
<Item class="Folder" referent="24">
|
|
9032
9270
|
<Properties>
|
|
9033
9271
|
<string name="Name">node_modules</string>
|
|
9034
9272
|
</Properties>
|
|
9035
|
-
<Item class="Folder" referent="
|
|
9273
|
+
<Item class="Folder" referent="25">
|
|
9036
9274
|
<Properties>
|
|
9037
9275
|
<string name="Name">@rbxts</string>
|
|
9038
9276
|
</Properties>
|
|
9039
|
-
<Item class="ModuleScript" referent="
|
|
9277
|
+
<Item class="ModuleScript" referent="22">
|
|
9040
9278
|
<Properties>
|
|
9041
9279
|
<string name="Name">services</string>
|
|
9042
9280
|
<string name="Source"><![CDATA[return setmetatable({}, {
|
|
@@ -770,11 +770,227 @@ 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
|
+
-- Both scripts have Archivable=false so a user save doesn't persist them.
|
|
800
|
+
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
801
|
+
local ServerScriptService = _services.ServerScriptService
|
|
802
|
+
local StarterPlayer = _services.StarterPlayer
|
|
803
|
+
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
804
|
+
local function getStarterPlayerScripts()
|
|
805
|
+
return StarterPlayer:FindFirstChild("StarterPlayerScripts")
|
|
806
|
+
end
|
|
807
|
+
local SERVER_SCRIPT_NAME = "__MCP_ServerEvalBridge"
|
|
808
|
+
local CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge"
|
|
809
|
+
-- Public so the eval_*_runtime tool wrappers can reference the same names.
|
|
810
|
+
local BRIDGE_NAMES = {
|
|
811
|
+
serverScript = SERVER_SCRIPT_NAME,
|
|
812
|
+
clientScript = CLIENT_SCRIPT_NAME,
|
|
813
|
+
serverRemote = "__MCP_ServerEvalRemote",
|
|
814
|
+
serverLocal = "__MCP_ServerEvalLocal",
|
|
815
|
+
clientLocal = "__MCP_ClientEvalBridge",
|
|
816
|
+
}
|
|
817
|
+
-- Embedded Luau. The double `${...}` references our exported names so a
|
|
818
|
+
-- rename here propagates to both the script source and the tool wrappers.
|
|
819
|
+
local SERVER_BRIDGE_SOURCE = `\
|
|
820
|
+
-- Auto-installed by @chrrxs/robloxstudio-mcp at start_playtest, removed at\
|
|
821
|
+
-- stop_playtest. Provides shared-require-cache eval on the server peer for\
|
|
822
|
+
-- the eval_server_runtime MCP tool.\
|
|
823
|
+
\
|
|
824
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
825
|
+
local ServerScriptService = game:GetService("ServerScriptService")\
|
|
826
|
+
local RunService = game:GetService("RunService")\
|
|
827
|
+
\
|
|
828
|
+
if not RunService:IsStudio() then\
|
|
829
|
+
return\
|
|
830
|
+
end\
|
|
831
|
+
\
|
|
832
|
+
local function evalCode(source)\
|
|
833
|
+
if type(source) ~= "string" then\
|
|
834
|
+
return false, "source must be a string"\
|
|
835
|
+
end\
|
|
836
|
+
local fn, compileErr = loadstring(source, "MCPServerEval")\
|
|
837
|
+
if not fn then\
|
|
838
|
+
local errStr = tostring(compileErr or "loadstring returned nil")\
|
|
839
|
+
-- Roblox returns nil from loadstring when LoadStringEnabled=false.\
|
|
840
|
+
-- Surface a clear, actionable error.\
|
|
841
|
+
if string.find(errStr, "not enabled", 1, true)\
|
|
842
|
+
or string.find(errStr, "disabled", 1, true)\
|
|
843
|
+
or errStr == "loadstring returned nil"\
|
|
844
|
+
then\
|
|
845
|
+
return false,\
|
|
846
|
+
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime requires it. "\
|
|
847
|
+
.. "Enable it in Studio (ServerScriptService > Properties > LoadStringEnabled = true) "\
|
|
848
|
+
.. "and restart the playtest."\
|
|
849
|
+
end\
|
|
850
|
+
return false, errStr\
|
|
851
|
+
end\
|
|
852
|
+
return pcall(fn)\
|
|
853
|
+
end\
|
|
854
|
+
\
|
|
855
|
+
-- Defensive cleanup of stale instances from a prior session.\
|
|
856
|
+
local prevRf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.serverRemote}")\
|
|
857
|
+
if prevRf then prevRf:Destroy() end\
|
|
858
|
+
local prevBf = ServerScriptService:FindFirstChild("{BRIDGE_NAMES.serverLocal}")\
|
|
859
|
+
if prevBf then prevBf:Destroy() end\
|
|
860
|
+
\
|
|
861
|
+
local rf = Instance.new("RemoteFunction")\
|
|
862
|
+
rf.Name = "{BRIDGE_NAMES.serverRemote}"\
|
|
863
|
+
rf.Archivable = false\
|
|
864
|
+
rf.Parent = ReplicatedStorage\
|
|
865
|
+
rf.OnServerInvoke = function(_player, source)\
|
|
866
|
+
return evalCode(source)\
|
|
867
|
+
end\
|
|
868
|
+
\
|
|
869
|
+
local bf = Instance.new("BindableFunction")\
|
|
870
|
+
bf.Name = "{BRIDGE_NAMES.serverLocal}"\
|
|
871
|
+
bf.Archivable = false\
|
|
872
|
+
bf.Parent = ServerScriptService\
|
|
873
|
+
bf.OnInvoke = function(source)\
|
|
874
|
+
return evalCode(source)\
|
|
875
|
+
end\
|
|
876
|
+
`
|
|
877
|
+
local CLIENT_BRIDGE_SOURCE = `\
|
|
878
|
+
-- Auto-installed by @chrrxs/robloxstudio-mcp at start_playtest, removed at\
|
|
879
|
+
-- stop_playtest. Provides shared-require-cache eval on the client peer for\
|
|
880
|
+
-- the eval_client_runtime MCP tool.\
|
|
881
|
+
\
|
|
882
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
883
|
+
local RunService = game:GetService("RunService")\
|
|
884
|
+
\
|
|
885
|
+
if not RunService:IsStudio() then\
|
|
886
|
+
return\
|
|
887
|
+
end\
|
|
888
|
+
\
|
|
889
|
+
local prevBf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.clientLocal}")\
|
|
890
|
+
if prevBf then prevBf:Destroy() end\
|
|
891
|
+
\
|
|
892
|
+
local bf = Instance.new("BindableFunction")\
|
|
893
|
+
bf.Name = "{BRIDGE_NAMES.clientLocal}"\
|
|
894
|
+
bf.Archivable = false\
|
|
895
|
+
bf.Parent = ReplicatedStorage\
|
|
896
|
+
bf.OnInvoke = function(payload)\
|
|
897
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then\
|
|
898
|
+
return false, "payload must be a ModuleScript instance"\
|
|
899
|
+
end\
|
|
900
|
+
return pcall(require, payload)\
|
|
901
|
+
end\
|
|
902
|
+
`
|
|
903
|
+
local function setSource(scriptInst, source)
|
|
904
|
+
-- ScriptEditorService is the cleaner API and integrates with Studio's
|
|
905
|
+
-- edit history; fall back to direct Source mutation (allowed in plugin
|
|
906
|
+
-- context with PluginSecurity) if the edit service rejects the call.
|
|
907
|
+
local seOk = pcall(function()
|
|
908
|
+
ScriptEditorService:UpdateSourceAsync(scriptInst, function()
|
|
909
|
+
return source
|
|
910
|
+
end)
|
|
911
|
+
end)
|
|
912
|
+
if not seOk then
|
|
913
|
+
scriptInst.Source = source
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
local function findBridges()
|
|
917
|
+
local sps = getStarterPlayerScripts()
|
|
918
|
+
return {
|
|
919
|
+
server = ServerScriptService:FindFirstChild(SERVER_SCRIPT_NAME),
|
|
920
|
+
client = if sps then sps:FindFirstChild(CLIENT_SCRIPT_NAME) else nil,
|
|
921
|
+
}
|
|
922
|
+
end
|
|
923
|
+
local function cleanupBridges()
|
|
924
|
+
local _binding = findBridges()
|
|
925
|
+
local server = _binding.server
|
|
926
|
+
local client = _binding.client
|
|
927
|
+
if server then
|
|
928
|
+
pcall(function()
|
|
929
|
+
return server:Destroy()
|
|
930
|
+
end)
|
|
931
|
+
end
|
|
932
|
+
if client then
|
|
933
|
+
pcall(function()
|
|
934
|
+
return client:Destroy()
|
|
935
|
+
end)
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
local function installBridges()
|
|
939
|
+
-- Defensive: clear any stale bridges from a prior unclean exit before
|
|
940
|
+
-- inserting fresh. The injected script also self-cleans its
|
|
941
|
+
-- ReplicatedStorage/ServerScriptService children at startup, but the
|
|
942
|
+
-- containing Script/LocalScript objects themselves we must clear here.
|
|
943
|
+
cleanupBridges()
|
|
944
|
+
local ok, err = pcall(function()
|
|
945
|
+
local serverScript = Instance.new("Script")
|
|
946
|
+
serverScript.Name = SERVER_SCRIPT_NAME
|
|
947
|
+
serverScript.Archivable = false
|
|
948
|
+
setSource(serverScript, SERVER_BRIDGE_SOURCE)
|
|
949
|
+
serverScript.Parent = ServerScriptService
|
|
950
|
+
local sps = getStarterPlayerScripts()
|
|
951
|
+
if not sps then
|
|
952
|
+
error("StarterPlayer.StarterPlayerScripts not found - cannot install client eval bridge")
|
|
953
|
+
end
|
|
954
|
+
local clientScript = Instance.new("LocalScript")
|
|
955
|
+
clientScript.Name = CLIENT_SCRIPT_NAME
|
|
956
|
+
clientScript.Archivable = false
|
|
957
|
+
setSource(clientScript, CLIENT_BRIDGE_SOURCE)
|
|
958
|
+
clientScript.Parent = sps
|
|
959
|
+
end)
|
|
960
|
+
if not ok then
|
|
961
|
+
return {
|
|
962
|
+
installed = false,
|
|
963
|
+
error = tostring(err),
|
|
964
|
+
}
|
|
965
|
+
end
|
|
966
|
+
return {
|
|
967
|
+
installed = true,
|
|
968
|
+
}
|
|
969
|
+
end
|
|
970
|
+
-- Heuristic check so start_playtest can surface a warning when
|
|
971
|
+
-- LoadStringEnabled is false (eval_server_runtime won't work in that mode).
|
|
972
|
+
-- We can't import the runtime LoadStringEnabled value cleanly without
|
|
973
|
+
-- pulling in the type — read defensively.
|
|
974
|
+
local function loadStringEnabled()
|
|
975
|
+
local ok, value = pcall(function()
|
|
976
|
+
return ServerScriptService.LoadStringEnabled
|
|
977
|
+
end)
|
|
978
|
+
return ok and value == true
|
|
979
|
+
end
|
|
980
|
+
return {
|
|
981
|
+
cleanupBridges = cleanupBridges,
|
|
982
|
+
installBridges = installBridges,
|
|
983
|
+
loadStringEnabled = loadStringEnabled,
|
|
984
|
+
BRIDGE_NAMES = BRIDGE_NAMES,
|
|
985
|
+
}
|
|
986
|
+
]]></string>
|
|
987
|
+
</Properties>
|
|
988
|
+
</Item>
|
|
989
|
+
<Item class="Folder" referent="5">
|
|
774
990
|
<Properties>
|
|
775
991
|
<string name="Name">handlers</string>
|
|
776
992
|
</Properties>
|
|
777
|
-
<Item class="ModuleScript" referent="
|
|
993
|
+
<Item class="ModuleScript" referent="6">
|
|
778
994
|
<Properties>
|
|
779
995
|
<string name="Name">AssetHandlers</string>
|
|
780
996
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1060,7 +1276,7 @@ return {
|
|
|
1060
1276
|
]]></string>
|
|
1061
1277
|
</Properties>
|
|
1062
1278
|
</Item>
|
|
1063
|
-
<Item class="ModuleScript" referent="
|
|
1279
|
+
<Item class="ModuleScript" referent="7">
|
|
1064
1280
|
<Properties>
|
|
1065
1281
|
<string name="Name">BuildHandlers</string>
|
|
1066
1282
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1625,7 +1841,7 @@ return {
|
|
|
1625
1841
|
]]></string>
|
|
1626
1842
|
</Properties>
|
|
1627
1843
|
</Item>
|
|
1628
|
-
<Item class="ModuleScript" referent="
|
|
1844
|
+
<Item class="ModuleScript" referent="8">
|
|
1629
1845
|
<Properties>
|
|
1630
1846
|
<string name="Name">CaptureHandlers</string>
|
|
1631
1847
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1791,7 +2007,7 @@ return {
|
|
|
1791
2007
|
]]></string>
|
|
1792
2008
|
</Properties>
|
|
1793
2009
|
</Item>
|
|
1794
|
-
<Item class="ModuleScript" referent="
|
|
2010
|
+
<Item class="ModuleScript" referent="9">
|
|
1795
2011
|
<Properties>
|
|
1796
2012
|
<string name="Name">InputHandlers</string>
|
|
1797
2013
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -1945,7 +2161,7 @@ return {
|
|
|
1945
2161
|
]]></string>
|
|
1946
2162
|
</Properties>
|
|
1947
2163
|
</Item>
|
|
1948
|
-
<Item class="ModuleScript" referent="
|
|
2164
|
+
<Item class="ModuleScript" referent="10">
|
|
1949
2165
|
<Properties>
|
|
1950
2166
|
<string name="Name">InstanceHandlers</string>
|
|
1951
2167
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -2452,7 +2668,7 @@ return {
|
|
|
2452
2668
|
]]></string>
|
|
2453
2669
|
</Properties>
|
|
2454
2670
|
</Item>
|
|
2455
|
-
<Item class="ModuleScript" referent="
|
|
2671
|
+
<Item class="ModuleScript" referent="11">
|
|
2456
2672
|
<Properties>
|
|
2457
2673
|
<string name="Name">MetadataHandlers</string>
|
|
2458
2674
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3035,7 +3251,7 @@ return {
|
|
|
3035
3251
|
]]></string>
|
|
3036
3252
|
</Properties>
|
|
3037
3253
|
</Item>
|
|
3038
|
-
<Item class="ModuleScript" referent="
|
|
3254
|
+
<Item class="ModuleScript" referent="12">
|
|
3039
3255
|
<Properties>
|
|
3040
3256
|
<string name="Name">PropertyHandlers</string>
|
|
3041
3257
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3287,7 +3503,7 @@ return {
|
|
|
3287
3503
|
]]></string>
|
|
3288
3504
|
</Properties>
|
|
3289
3505
|
</Item>
|
|
3290
|
-
<Item class="ModuleScript" referent="
|
|
3506
|
+
<Item class="ModuleScript" referent="13">
|
|
3291
3507
|
<Properties>
|
|
3292
3508
|
<string name="Name">QueryHandlers</string>
|
|
3293
3509
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -4314,7 +4530,7 @@ return {
|
|
|
4314
4530
|
]]></string>
|
|
4315
4531
|
</Properties>
|
|
4316
4532
|
</Item>
|
|
4317
|
-
<Item class="ModuleScript" referent="
|
|
4533
|
+
<Item class="ModuleScript" referent="14">
|
|
4318
4534
|
<Properties>
|
|
4319
4535
|
<string name="Name">ScriptHandlers</string>
|
|
4320
4536
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5010,7 +5226,7 @@ return {
|
|
|
5010
5226
|
]]></string>
|
|
5011
5227
|
</Properties>
|
|
5012
5228
|
</Item>
|
|
5013
|
-
<Item class="ModuleScript" referent="
|
|
5229
|
+
<Item class="ModuleScript" referent="15">
|
|
5014
5230
|
<Properties>
|
|
5015
5231
|
<string name="Name">TestHandlers</string>
|
|
5016
5232
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5018,6 +5234,10 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
|
5018
5234
|
local _services = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
5019
5235
|
local HttpService = _services.HttpService
|
|
5020
5236
|
local LogService = _services.LogService
|
|
5237
|
+
local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
|
|
5238
|
+
local installBridges = _EvalBridges.installBridges
|
|
5239
|
+
local cleanupBridges = _EvalBridges.cleanupBridges
|
|
5240
|
+
local loadStringEnabled = _EvalBridges.loadStringEnabled
|
|
5021
5241
|
local StudioTestService = game:GetService("StudioTestService")
|
|
5022
5242
|
local ServerScriptService = game:GetService("ServerScriptService")
|
|
5023
5243
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
@@ -5174,6 +5394,14 @@ local function startPlaytest(requestData)
|
|
|
5174
5394
|
if not injected then
|
|
5175
5395
|
warn(`[MCP] Failed to inject stop listener: {injErr}`)
|
|
5176
5396
|
end
|
|
5397
|
+
-- Auto-install the game-VM eval bridges (ServerEvalBridge + ClientEvalBridge)
|
|
5398
|
+
-- so eval_server_runtime / eval_client_runtime work without manual setup.
|
|
5399
|
+
-- Bridges are cleaned up from the edit DM after the play DMs tear down.
|
|
5400
|
+
local bridgeInstall = installBridges()
|
|
5401
|
+
local hasLoadString = loadStringEnabled()
|
|
5402
|
+
if not bridgeInstall.installed then
|
|
5403
|
+
warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
|
|
5404
|
+
end
|
|
5177
5405
|
if numPlayers ~= nil and mode == "run" then
|
|
5178
5406
|
local TestService = game:GetService("TestService")
|
|
5179
5407
|
TestService.NumberOfPlayers = math.clamp(numPlayers, 1, 8)
|
|
@@ -5196,12 +5424,22 @@ local function startPlaytest(requestData)
|
|
|
5196
5424
|
end
|
|
5197
5425
|
testRunning = false
|
|
5198
5426
|
cleanupStopListener()
|
|
5427
|
+
cleanupBridges()
|
|
5199
5428
|
end)
|
|
5200
5429
|
local msg = if numPlayers ~= nil then `Playtest started in {mode} mode with {numPlayers} player(s)` else `Playtest started in {mode} mode`
|
|
5201
|
-
|
|
5430
|
+
local response = {
|
|
5202
5431
|
success = true,
|
|
5203
5432
|
message = msg,
|
|
5433
|
+
evalBridges = if bridgeInstall.installed then "installed" else `failed: {bridgeInstall.error}`,
|
|
5204
5434
|
}
|
|
5435
|
+
-- Surface loadstring availability up-front so callers know whether
|
|
5436
|
+
-- eval_server_runtime will work before they try it. eval_client_runtime
|
|
5437
|
+
-- doesn't need loadstring (it uses ModuleScript+require), so this only
|
|
5438
|
+
-- affects the server bridge.
|
|
5439
|
+
if not hasLoadString then
|
|
5440
|
+
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."
|
|
5441
|
+
end
|
|
5442
|
+
return response
|
|
5205
5443
|
end
|
|
5206
5444
|
local function stopPlaytest(_requestData)
|
|
5207
5445
|
-- Server-side routing (tools/index.ts:stopPlaytest) sends /api/stop-playtest
|
|
@@ -5307,7 +5545,7 @@ return {
|
|
|
5307
5545
|
</Properties>
|
|
5308
5546
|
</Item>
|
|
5309
5547
|
</Item>
|
|
5310
|
-
<Item class="ModuleScript" referent="
|
|
5548
|
+
<Item class="ModuleScript" referent="16">
|
|
5311
5549
|
<Properties>
|
|
5312
5550
|
<string name="Name">Recording</string>
|
|
5313
5551
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5337,11 +5575,11 @@ return {
|
|
|
5337
5575
|
]]></string>
|
|
5338
5576
|
</Properties>
|
|
5339
5577
|
</Item>
|
|
5340
|
-
<Item class="ModuleScript" referent="
|
|
5578
|
+
<Item class="ModuleScript" referent="17">
|
|
5341
5579
|
<Properties>
|
|
5342
5580
|
<string name="Name">State</string>
|
|
5343
5581
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5344
|
-
local CURRENT_VERSION = "2.
|
|
5582
|
+
local CURRENT_VERSION = "2.9.0"
|
|
5345
5583
|
local MAX_CONNECTIONS = 5
|
|
5346
5584
|
local BASE_PORT = 58741
|
|
5347
5585
|
local activeTabIndex = 0
|
|
@@ -5433,7 +5671,7 @@ return {
|
|
|
5433
5671
|
]]></string>
|
|
5434
5672
|
</Properties>
|
|
5435
5673
|
</Item>
|
|
5436
|
-
<Item class="ModuleScript" referent="
|
|
5674
|
+
<Item class="ModuleScript" referent="18">
|
|
5437
5675
|
<Properties>
|
|
5438
5676
|
<string name="Name">UI</string>
|
|
5439
5677
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6153,7 +6391,7 @@ return {
|
|
|
6153
6391
|
]]></string>
|
|
6154
6392
|
</Properties>
|
|
6155
6393
|
</Item>
|
|
6156
|
-
<Item class="ModuleScript" referent="
|
|
6394
|
+
<Item class="ModuleScript" referent="19">
|
|
6157
6395
|
<Properties>
|
|
6158
6396
|
<string name="Name">Utils</string>
|
|
6159
6397
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6683,11 +6921,11 @@ return {
|
|
|
6683
6921
|
</Properties>
|
|
6684
6922
|
</Item>
|
|
6685
6923
|
</Item>
|
|
6686
|
-
<Item class="Folder" referent="
|
|
6924
|
+
<Item class="Folder" referent="23">
|
|
6687
6925
|
<Properties>
|
|
6688
6926
|
<string name="Name">include</string>
|
|
6689
6927
|
</Properties>
|
|
6690
|
-
<Item class="ModuleScript" referent="
|
|
6928
|
+
<Item class="ModuleScript" referent="20">
|
|
6691
6929
|
<Properties>
|
|
6692
6930
|
<string name="Name">Promise</string>
|
|
6693
6931
|
<string name="Source"><![CDATA[--[[
|
|
@@ -8761,7 +8999,7 @@ return Promise
|
|
|
8761
8999
|
]]></string>
|
|
8762
9000
|
</Properties>
|
|
8763
9001
|
</Item>
|
|
8764
|
-
<Item class="ModuleScript" referent="
|
|
9002
|
+
<Item class="ModuleScript" referent="21">
|
|
8765
9003
|
<Properties>
|
|
8766
9004
|
<string name="Name">RuntimeLib</string>
|
|
8767
9005
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -9028,15 +9266,15 @@ return TS
|
|
|
9028
9266
|
</Properties>
|
|
9029
9267
|
</Item>
|
|
9030
9268
|
</Item>
|
|
9031
|
-
<Item class="Folder" referent="
|
|
9269
|
+
<Item class="Folder" referent="24">
|
|
9032
9270
|
<Properties>
|
|
9033
9271
|
<string name="Name">node_modules</string>
|
|
9034
9272
|
</Properties>
|
|
9035
|
-
<Item class="Folder" referent="
|
|
9273
|
+
<Item class="Folder" referent="25">
|
|
9036
9274
|
<Properties>
|
|
9037
9275
|
<string name="Name">@rbxts</string>
|
|
9038
9276
|
</Properties>
|
|
9039
|
-
<Item class="ModuleScript" referent="
|
|
9277
|
+
<Item class="ModuleScript" referent="22">
|
|
9040
9278
|
<Properties>
|
|
9041
9279
|
<string name="Name">services</string>
|
|
9042
9280
|
<string name="Source"><![CDATA[return setmetatable({}, {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Game-VM eval bridges, ported from chrrxs/roblox-mcp-primitives.
|
|
2
|
+
//
|
|
3
|
+
// Our standard `execute_luau target=server/client-N` runs in the plugin VM
|
|
4
|
+
// with a fresh ModuleScript per call. That gives a clean sandbox but means
|
|
5
|
+
// `require(SomeModule)` returns a fresh copy, not the one the running game
|
|
6
|
+
// scripts hold. So runtime-mutated module state is invisible to probes.
|
|
7
|
+
//
|
|
8
|
+
// These bridges fix that by living inside the user's game scripts:
|
|
9
|
+
// - Server: a Script in ServerScriptService that creates a BindableFunction
|
|
10
|
+
// (for our server-peer plugin to invoke directly) plus a RemoteFunction
|
|
11
|
+
// (kept for parity with the upstream primitive's client-callable shape).
|
|
12
|
+
// - Client: a LocalScript in StarterPlayer.StarterPlayerScripts that
|
|
13
|
+
// creates a BindableFunction. Plugin invokes it with a fresh ModuleScript
|
|
14
|
+
// payload; require() runs inside the LocalScript VM so it shares the
|
|
15
|
+
// game's require cache.
|
|
16
|
+
//
|
|
17
|
+
// Lifecycle: TestHandlers.startPlaytest inserts both scripts into the EDIT
|
|
18
|
+
// DM right before ExecutePlayModeAsync. ExecutePlayModeAsync clones the
|
|
19
|
+
// DataModel into the play DMs, so the scripts come along and run there.
|
|
20
|
+
// TestHandlers cleans them up from the edit DM when ExecutePlayModeAsync
|
|
21
|
+
// returns (test ended for any reason: stop_playtest, manual close, EndTest).
|
|
22
|
+
// Both scripts have Archivable=false so a user save doesn't persist them.
|
|
23
|
+
|
|
24
|
+
import { ServerScriptService, StarterPlayer } from "@rbxts/services";
|
|
25
|
+
|
|
26
|
+
const ScriptEditorService = game.GetService("ScriptEditorService");
|
|
27
|
+
|
|
28
|
+
function getStarterPlayerScripts(): Instance | undefined {
|
|
29
|
+
return StarterPlayer.FindFirstChild("StarterPlayerScripts");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SERVER_SCRIPT_NAME = "__MCP_ServerEvalBridge";
|
|
33
|
+
const CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge";
|
|
34
|
+
|
|
35
|
+
// Public so the eval_*_runtime tool wrappers can reference the same names.
|
|
36
|
+
export const BRIDGE_NAMES = {
|
|
37
|
+
serverScript: SERVER_SCRIPT_NAME,
|
|
38
|
+
clientScript: CLIENT_SCRIPT_NAME,
|
|
39
|
+
serverRemote: "__MCP_ServerEvalRemote",
|
|
40
|
+
serverLocal: "__MCP_ServerEvalLocal",
|
|
41
|
+
clientLocal: "__MCP_ClientEvalBridge",
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
// Embedded Luau. The double `${...}` references our exported names so a
|
|
45
|
+
// rename here propagates to both the script source and the tool wrappers.
|
|
46
|
+
const SERVER_BRIDGE_SOURCE = `
|
|
47
|
+
-- Auto-installed by @chrrxs/robloxstudio-mcp at start_playtest, removed at
|
|
48
|
+
-- stop_playtest. Provides shared-require-cache eval on the server peer for
|
|
49
|
+
-- the eval_server_runtime MCP tool.
|
|
50
|
+
|
|
51
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
52
|
+
local ServerScriptService = game:GetService("ServerScriptService")
|
|
53
|
+
local RunService = game:GetService("RunService")
|
|
54
|
+
|
|
55
|
+
if not RunService:IsStudio() then
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
local function evalCode(source)
|
|
60
|
+
if type(source) ~= "string" then
|
|
61
|
+
return false, "source must be a string"
|
|
62
|
+
end
|
|
63
|
+
local fn, compileErr = loadstring(source, "MCPServerEval")
|
|
64
|
+
if not fn then
|
|
65
|
+
local errStr = tostring(compileErr or "loadstring returned nil")
|
|
66
|
+
-- Roblox returns nil from loadstring when LoadStringEnabled=false.
|
|
67
|
+
-- Surface a clear, actionable error.
|
|
68
|
+
if string.find(errStr, "not enabled", 1, true)
|
|
69
|
+
or string.find(errStr, "disabled", 1, true)
|
|
70
|
+
or errStr == "loadstring returned nil"
|
|
71
|
+
then
|
|
72
|
+
return false,
|
|
73
|
+
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime requires it. "
|
|
74
|
+
.. "Enable it in Studio (ServerScriptService > Properties > LoadStringEnabled = true) "
|
|
75
|
+
.. "and restart the playtest."
|
|
76
|
+
end
|
|
77
|
+
return false, errStr
|
|
78
|
+
end
|
|
79
|
+
return pcall(fn)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
-- Defensive cleanup of stale instances from a prior session.
|
|
83
|
+
local prevRf = ReplicatedStorage:FindFirstChild("${BRIDGE_NAMES.serverRemote}")
|
|
84
|
+
if prevRf then prevRf:Destroy() end
|
|
85
|
+
local prevBf = ServerScriptService:FindFirstChild("${BRIDGE_NAMES.serverLocal}")
|
|
86
|
+
if prevBf then prevBf:Destroy() end
|
|
87
|
+
|
|
88
|
+
local rf = Instance.new("RemoteFunction")
|
|
89
|
+
rf.Name = "${BRIDGE_NAMES.serverRemote}"
|
|
90
|
+
rf.Archivable = false
|
|
91
|
+
rf.Parent = ReplicatedStorage
|
|
92
|
+
rf.OnServerInvoke = function(_player, source)
|
|
93
|
+
return evalCode(source)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
local bf = Instance.new("BindableFunction")
|
|
97
|
+
bf.Name = "${BRIDGE_NAMES.serverLocal}"
|
|
98
|
+
bf.Archivable = false
|
|
99
|
+
bf.Parent = ServerScriptService
|
|
100
|
+
bf.OnInvoke = function(source)
|
|
101
|
+
return evalCode(source)
|
|
102
|
+
end
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
const CLIENT_BRIDGE_SOURCE = `
|
|
106
|
+
-- Auto-installed by @chrrxs/robloxstudio-mcp at start_playtest, removed at
|
|
107
|
+
-- stop_playtest. Provides shared-require-cache eval on the client peer for
|
|
108
|
+
-- the eval_client_runtime MCP tool.
|
|
109
|
+
|
|
110
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
111
|
+
local RunService = game:GetService("RunService")
|
|
112
|
+
|
|
113
|
+
if not RunService:IsStudio() then
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
local prevBf = ReplicatedStorage:FindFirstChild("${BRIDGE_NAMES.clientLocal}")
|
|
118
|
+
if prevBf then prevBf:Destroy() end
|
|
119
|
+
|
|
120
|
+
local bf = Instance.new("BindableFunction")
|
|
121
|
+
bf.Name = "${BRIDGE_NAMES.clientLocal}"
|
|
122
|
+
bf.Archivable = false
|
|
123
|
+
bf.Parent = ReplicatedStorage
|
|
124
|
+
bf.OnInvoke = function(payload)
|
|
125
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then
|
|
126
|
+
return false, "payload must be a ModuleScript instance"
|
|
127
|
+
end
|
|
128
|
+
return pcall(require, payload)
|
|
129
|
+
end
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
function setSource(scriptInst: Script | LocalScript, source: string): void {
|
|
133
|
+
// ScriptEditorService is the cleaner API and integrates with Studio's
|
|
134
|
+
// edit history; fall back to direct Source mutation (allowed in plugin
|
|
135
|
+
// context with PluginSecurity) if the edit service rejects the call.
|
|
136
|
+
const [seOk] = pcall(() => {
|
|
137
|
+
ScriptEditorService.UpdateSourceAsync(scriptInst, () => source);
|
|
138
|
+
});
|
|
139
|
+
if (!seOk) {
|
|
140
|
+
(scriptInst as unknown as { Source: string }).Source = source;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function findBridges(): { server?: Instance; client?: Instance } {
|
|
145
|
+
const sps = getStarterPlayerScripts();
|
|
146
|
+
return {
|
|
147
|
+
server: ServerScriptService.FindFirstChild(SERVER_SCRIPT_NAME),
|
|
148
|
+
client: sps ? sps.FindFirstChild(CLIENT_SCRIPT_NAME) : undefined,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function cleanupBridges(): void {
|
|
153
|
+
const { server, client } = findBridges();
|
|
154
|
+
if (server) {
|
|
155
|
+
pcall(() => server.Destroy());
|
|
156
|
+
}
|
|
157
|
+
if (client) {
|
|
158
|
+
pcall(() => client.Destroy());
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function installBridges(): { installed: boolean; error?: string } {
|
|
163
|
+
// Defensive: clear any stale bridges from a prior unclean exit before
|
|
164
|
+
// inserting fresh. The injected script also self-cleans its
|
|
165
|
+
// ReplicatedStorage/ServerScriptService children at startup, but the
|
|
166
|
+
// containing Script/LocalScript objects themselves we must clear here.
|
|
167
|
+
cleanupBridges();
|
|
168
|
+
|
|
169
|
+
const [ok, err] = pcall(() => {
|
|
170
|
+
const serverScript = new Instance("Script");
|
|
171
|
+
serverScript.Name = SERVER_SCRIPT_NAME;
|
|
172
|
+
serverScript.Archivable = false;
|
|
173
|
+
setSource(serverScript, SERVER_BRIDGE_SOURCE);
|
|
174
|
+
serverScript.Parent = ServerScriptService;
|
|
175
|
+
|
|
176
|
+
const sps = getStarterPlayerScripts();
|
|
177
|
+
if (!sps) {
|
|
178
|
+
error("StarterPlayer.StarterPlayerScripts not found - cannot install client eval bridge");
|
|
179
|
+
}
|
|
180
|
+
const clientScript = new Instance("LocalScript");
|
|
181
|
+
clientScript.Name = CLIENT_SCRIPT_NAME;
|
|
182
|
+
clientScript.Archivable = false;
|
|
183
|
+
setSource(clientScript, CLIENT_BRIDGE_SOURCE);
|
|
184
|
+
clientScript.Parent = sps;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!ok) {
|
|
188
|
+
return { installed: false, error: tostring(err) };
|
|
189
|
+
}
|
|
190
|
+
return { installed: true };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Heuristic check so start_playtest can surface a warning when
|
|
194
|
+
// LoadStringEnabled is false (eval_server_runtime won't work in that mode).
|
|
195
|
+
// We can't import the runtime LoadStringEnabled value cleanly without
|
|
196
|
+
// pulling in the type — read defensively.
|
|
197
|
+
export function loadStringEnabled(): boolean {
|
|
198
|
+
const [ok, value] = pcall(
|
|
199
|
+
() => (ServerScriptService as unknown as { LoadStringEnabled: boolean }).LoadStringEnabled,
|
|
200
|
+
);
|
|
201
|
+
return ok && value === true;
|
|
202
|
+
}
|
|
203
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HttpService, LogService } from "@rbxts/services";
|
|
2
|
+
import { installBridges, cleanupBridges, loadStringEnabled } from "../EvalBridges";
|
|
2
3
|
|
|
3
4
|
const StudioTestService = game.GetService("StudioTestService");
|
|
4
5
|
const ServerScriptService = game.GetService("ServerScriptService");
|
|
@@ -154,6 +155,15 @@ function startPlaytest(requestData: Record<string, unknown>) {
|
|
|
154
155
|
warn(`[MCP] Failed to inject stop listener: ${injErr}`);
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// Auto-install the game-VM eval bridges (ServerEvalBridge + ClientEvalBridge)
|
|
159
|
+
// so eval_server_runtime / eval_client_runtime work without manual setup.
|
|
160
|
+
// Bridges are cleaned up from the edit DM after the play DMs tear down.
|
|
161
|
+
const bridgeInstall = installBridges();
|
|
162
|
+
const hasLoadString = loadStringEnabled();
|
|
163
|
+
if (!bridgeInstall.installed) {
|
|
164
|
+
warn(`[MCP] Eval bridge install failed: ${bridgeInstall.error}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
if (numPlayers !== undefined && mode === "run") {
|
|
158
168
|
const TestService = game.GetService("TestService") as TestService & { NumberOfPlayers: number };
|
|
159
169
|
TestService.NumberOfPlayers = math.clamp(numPlayers, 1, 8);
|
|
@@ -180,12 +190,31 @@ function startPlaytest(requestData: Record<string, unknown>) {
|
|
|
180
190
|
testRunning = false;
|
|
181
191
|
|
|
182
192
|
cleanupStopListener();
|
|
193
|
+
cleanupBridges();
|
|
183
194
|
});
|
|
184
195
|
|
|
185
196
|
const msg = numPlayers !== undefined
|
|
186
197
|
? `Playtest started in ${mode} mode with ${numPlayers} player(s)`
|
|
187
198
|
: `Playtest started in ${mode} mode`;
|
|
188
|
-
|
|
199
|
+
|
|
200
|
+
const response: Record<string, unknown> = {
|
|
201
|
+
success: true,
|
|
202
|
+
message: msg,
|
|
203
|
+
evalBridges: bridgeInstall.installed ? "installed" : `failed: ${bridgeInstall.error}`,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Surface loadstring availability up-front so callers know whether
|
|
207
|
+
// eval_server_runtime will work before they try it. eval_client_runtime
|
|
208
|
+
// doesn't need loadstring (it uses ModuleScript+require), so this only
|
|
209
|
+
// affects the server bridge.
|
|
210
|
+
if (!hasLoadString) {
|
|
211
|
+
response.serverEvalNote =
|
|
212
|
+
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime will not work " +
|
|
213
|
+
"until you enable it (ServerScriptService > Properties > LoadStringEnabled = true) " +
|
|
214
|
+
"and restart the playtest. eval_client_runtime is unaffected.";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return response;
|
|
189
218
|
}
|
|
190
219
|
|
|
191
220
|
function stopPlaytest(_requestData: Record<string, unknown>) {
|