@chrrxs/robloxstudio-mcp-inspector 2.10.0 → 2.10.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 +45 -45
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +76 -66
- package/studio-plugin/MCPPlugin.rbxmx +76 -66
- package/studio-plugin/src/modules/Communication.ts +1 -1
- package/studio-plugin/src/modules/EvalBridges.ts +16 -53
- package/studio-plugin/src/modules/UI.ts +36 -0
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +16 -1
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +1 -13
- package/studio-plugin/src/server/index.server.ts +6 -1
package/dist/index.js
CHANGED
|
@@ -1212,6 +1212,37 @@ function luaLongQuote(s) {
|
|
|
1212
1212
|
${s}
|
|
1213
1213
|
]${eq}]`;
|
|
1214
1214
|
}
|
|
1215
|
+
function buildModuleScriptInvokeWrapper(opts) {
|
|
1216
|
+
const wrapped = `return ((function()
|
|
1217
|
+
${opts.userCode}
|
|
1218
|
+
end)())`;
|
|
1219
|
+
return `
|
|
1220
|
+
local HttpService = game:GetService("HttpService")
|
|
1221
|
+
local bf = game:GetService("${opts.service}"):FindFirstChild("${opts.bridgeName}")
|
|
1222
|
+
if not bf then
|
|
1223
|
+
return HttpService:JSONEncode({
|
|
1224
|
+
bridge = "missing",
|
|
1225
|
+
error = ${luaLongQuote(opts.missingError)},
|
|
1226
|
+
})
|
|
1227
|
+
end
|
|
1228
|
+
local USER_CODE = ${luaLongQuote(wrapped)}
|
|
1229
|
+
local m = Instance.new("ModuleScript")
|
|
1230
|
+
m.Name = "__MCPEvalPayload"
|
|
1231
|
+
local okSet, setErr = pcall(function() m.Source = USER_CODE end)
|
|
1232
|
+
if not okSet then
|
|
1233
|
+
m:Destroy()
|
|
1234
|
+
return HttpService:JSONEncode({ bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) })
|
|
1235
|
+
end
|
|
1236
|
+
m.Parent = workspace
|
|
1237
|
+
local ok, result = bf:Invoke(m)
|
|
1238
|
+
m:Destroy()
|
|
1239
|
+
return HttpService:JSONEncode({
|
|
1240
|
+
bridge = "ok",
|
|
1241
|
+
ok = ok,
|
|
1242
|
+
result = if result == nil then nil else tostring(result),
|
|
1243
|
+
})
|
|
1244
|
+
`;
|
|
1245
|
+
}
|
|
1215
1246
|
function parseBridgeResponse(response) {
|
|
1216
1247
|
const r = response;
|
|
1217
1248
|
if (r && typeof r.returnValue === "string") {
|
|
@@ -1771,23 +1802,12 @@ ${code}`
|
|
|
1771
1802
|
if (!code) {
|
|
1772
1803
|
throw new Error("Code is required for eval_server_runtime");
|
|
1773
1804
|
}
|
|
1774
|
-
const wrapper =
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
error = "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
|
|
1781
|
-
})
|
|
1782
|
-
end
|
|
1783
|
-
local USER_CODE = ${luaLongQuote(code)}
|
|
1784
|
-
local ok, result = bf:Invoke(USER_CODE)
|
|
1785
|
-
return HttpService:JSONEncode({
|
|
1786
|
-
bridge = "ok",
|
|
1787
|
-
ok = ok,
|
|
1788
|
-
result = if result == nil then nil else tostring(result),
|
|
1789
|
-
})
|
|
1790
|
-
`;
|
|
1805
|
+
const wrapper = buildModuleScriptInvokeWrapper({
|
|
1806
|
+
service: "ServerScriptService",
|
|
1807
|
+
bridgeName: SERVER_LOCAL_NAME,
|
|
1808
|
+
missingError: "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
|
|
1809
|
+
userCode: code
|
|
1810
|
+
});
|
|
1791
1811
|
const response = await this.client.request("/api/execute-luau", { code: wrapper }, "server");
|
|
1792
1812
|
return {
|
|
1793
1813
|
content: [
|
|
@@ -1806,32 +1826,12 @@ return HttpService:JSONEncode({
|
|
|
1806
1826
|
if (!clientTarget.startsWith("client-")) {
|
|
1807
1827
|
throw new Error(`eval_client_runtime requires target=client-N (got: ${clientTarget})`);
|
|
1808
1828
|
}
|
|
1809
|
-
const wrapper =
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
error = "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
|
|
1816
|
-
})
|
|
1817
|
-
end
|
|
1818
|
-
local USER_CODE = ${luaLongQuote(code)}
|
|
1819
|
-
local m = Instance.new("ModuleScript")
|
|
1820
|
-
m.Name = "__MCPEvalPayload"
|
|
1821
|
-
local okSet, setErr = pcall(function() m.Source = USER_CODE end)
|
|
1822
|
-
if not okSet then
|
|
1823
|
-
m:Destroy()
|
|
1824
|
-
return HttpService:JSONEncode({ bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) })
|
|
1825
|
-
end
|
|
1826
|
-
m.Parent = workspace
|
|
1827
|
-
local ok, result = bf:Invoke(m)
|
|
1828
|
-
m:Destroy()
|
|
1829
|
-
return HttpService:JSONEncode({
|
|
1830
|
-
bridge = "ok",
|
|
1831
|
-
ok = ok,
|
|
1832
|
-
result = if result == nil then nil else tostring(result),
|
|
1833
|
-
})
|
|
1834
|
-
`;
|
|
1829
|
+
const wrapper = buildModuleScriptInvokeWrapper({
|
|
1830
|
+
service: "ReplicatedStorage",
|
|
1831
|
+
bridgeName: CLIENT_LOCAL_NAME,
|
|
1832
|
+
missingError: "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
|
|
1833
|
+
userCode: code
|
|
1834
|
+
});
|
|
1835
1835
|
const response = await this.client.request("/api/execute-luau", { code: wrapper }, clientTarget);
|
|
1836
1836
|
return {
|
|
1837
1837
|
content: [
|
|
@@ -3909,7 +3909,7 @@ var init_definitions = __esm({
|
|
|
3909
3909
|
{
|
|
3910
3910
|
name: "eval_server_runtime",
|
|
3911
3911
|
category: "write",
|
|
3912
|
-
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.
|
|
3912
|
+
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.",
|
|
3913
3913
|
inputSchema: {
|
|
3914
3914
|
type: "object",
|
|
3915
3915
|
properties: {
|
|
@@ -3924,7 +3924,7 @@ var init_definitions = __esm({
|
|
|
3924
3924
|
{
|
|
3925
3925
|
name: "eval_client_runtime",
|
|
3926
3926
|
category: "write",
|
|
3927
|
-
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.
|
|
3927
|
+
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.",
|
|
3928
3928
|
inputSchema: {
|
|
3929
3929
|
type: "object",
|
|
3930
3930
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrrxs/robloxstudio-mcp-inspector",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.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",
|
|
@@ -17,8 +17,16 @@ local RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer"
|
|
|
17
17
|
RuntimeLogBuffer.install()
|
|
18
18
|
UI.init(plugin)
|
|
19
19
|
local elements = UI.getElements()
|
|
20
|
+
local ICON_DISCONNECTED = "rbxassetid://125921838360800"
|
|
21
|
+
local ICON_CONNECTING = "rbxassetid://125921838360800"
|
|
22
|
+
local ICON_CONNECTED = "rbxassetid://125921838360800"
|
|
20
23
|
local toolbar = plugin:CreateToolbar("MCP Inspector")
|
|
21
|
-
local button = toolbar:CreateButton("MCP Inspector", "Connect to MCP Inspector (read-only) for AI Integration",
|
|
24
|
+
local button = toolbar:CreateButton("MCP Inspector", "Connect to MCP Inspector (read-only) for AI Integration", ICON_DISCONNECTED)
|
|
25
|
+
UI.setToolbarButton(button, {
|
|
26
|
+
disconnected = ICON_DISCONNECTED,
|
|
27
|
+
connecting = ICON_CONNECTING,
|
|
28
|
+
connected = ICON_CONNECTED,
|
|
29
|
+
})
|
|
22
30
|
elements.connectButton.Activated:Connect(function()
|
|
23
31
|
local conn = State.getActiveConnection()
|
|
24
32
|
if conn and conn.isActive then
|
|
@@ -603,6 +611,9 @@ local function pollForRequests(connIndex)
|
|
|
603
611
|
conn.isPolling = false
|
|
604
612
|
local ui = UI.getElements()
|
|
605
613
|
UI.updateTabDot(connIndex)
|
|
614
|
+
if connIndex == State.getActiveTabIndex() then
|
|
615
|
+
UI.updateToolbarIcon()
|
|
616
|
+
end
|
|
606
617
|
if success and (result.Success or result.StatusCode == 503) then
|
|
607
618
|
conn.consecutiveFailures = 0
|
|
608
619
|
conn.currentRetryDelay = 0.5
|
|
@@ -759,7 +770,6 @@ local function activatePlugin(connIndex)
|
|
|
759
770
|
conn.isActive = true
|
|
760
771
|
conn.consecutiveFailures = 0
|
|
761
772
|
conn.currentRetryDelay = 0.5
|
|
762
|
-
ui.screenGui.Enabled = true
|
|
763
773
|
if idx == State.getActiveTabIndex() then
|
|
764
774
|
conn.serverUrl = ui.urlInput.Text
|
|
765
775
|
local portStr = string.match(conn.serverUrl, ":(%d+)$")
|
|
@@ -889,15 +899,22 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
889
899
|
-- `require(SomeModule)` returns a fresh copy, not the one the running game
|
|
890
900
|
-- scripts hold. So runtime-mutated module state is invisible to probes.
|
|
891
901
|
--
|
|
892
|
-
-- These bridges fix that by living inside the user's game scripts
|
|
893
|
-
--
|
|
894
|
-
--
|
|
895
|
-
-- (
|
|
902
|
+
-- These bridges fix that by living inside the user's game scripts. Both
|
|
903
|
+
-- peers use the same symmetric shape:
|
|
904
|
+
-- - Server: a Script in ServerScriptService that creates a BindableFunction.
|
|
905
|
+
-- Plugin (server peer) invokes it with a fresh ModuleScript payload;
|
|
906
|
+
-- require() runs inside the Script VM so it shares the running server's
|
|
907
|
+
-- require cache.
|
|
896
908
|
-- - Client: a LocalScript in StarterPlayer.StarterPlayerScripts that
|
|
897
909
|
-- creates a BindableFunction. Plugin invokes it with a fresh ModuleScript
|
|
898
910
|
-- payload; require() runs inside the LocalScript VM so it shares the
|
|
899
911
|
-- game's require cache.
|
|
900
912
|
--
|
|
913
|
+
-- Why ModuleScript+require on both sides (no loadstring): require'd modules
|
|
914
|
+
-- run with the security level they were created at and don't need
|
|
915
|
+
-- ServerScriptService.LoadStringEnabled, so eval_server_runtime works even
|
|
916
|
+
-- when LoadStringEnabled=false (the default in fresh places).
|
|
917
|
+
--
|
|
901
918
|
-- Lifecycle: TestHandlers.startPlaytest inserts both scripts into the EDIT
|
|
902
919
|
-- DM right before ExecutePlayModeAsync. ExecutePlayModeAsync clones the
|
|
903
920
|
-- DataModel into the play DMs, so the scripts come along and run there.
|
|
@@ -928,7 +945,6 @@ local CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge"
|
|
|
928
945
|
local BRIDGE_NAMES = {
|
|
929
946
|
serverScript = SERVER_SCRIPT_NAME,
|
|
930
947
|
clientScript = CLIENT_SCRIPT_NAME,
|
|
931
|
-
serverRemote = "__MCP_ServerEvalRemote",
|
|
932
948
|
serverLocal = "__MCP_ServerEvalLocal",
|
|
933
949
|
clientLocal = "__MCP_ClientEvalBridge",
|
|
934
950
|
}
|
|
@@ -939,7 +955,6 @@ local SERVER_BRIDGE_SOURCE = `\
|
|
|
939
955
|
-- stop_playtest. Provides shared-require-cache eval on the server peer for\
|
|
940
956
|
-- the eval_server_runtime MCP tool.\
|
|
941
957
|
\
|
|
942
|
-
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
943
958
|
local ServerScriptService = game:GetService("ServerScriptService")\
|
|
944
959
|
local RunService = game:GetService("RunService")\
|
|
945
960
|
\
|
|
@@ -947,49 +962,18 @@ if not RunService:IsStudio() then\
|
|
|
947
962
|
return\
|
|
948
963
|
end\
|
|
949
964
|
\
|
|
950
|
-
local function evalCode(source)\
|
|
951
|
-
if type(source) ~= "string" then\
|
|
952
|
-
return false, "source must be a string"\
|
|
953
|
-
end\
|
|
954
|
-
local fn, compileErr = loadstring(source, "MCPServerEval")\
|
|
955
|
-
if not fn then\
|
|
956
|
-
local errStr = tostring(compileErr or "loadstring returned nil")\
|
|
957
|
-
-- Roblox returns nil from loadstring when LoadStringEnabled=false.\
|
|
958
|
-
-- Surface a clear, actionable error.\
|
|
959
|
-
if string.find(errStr, "not enabled", 1, true)\
|
|
960
|
-
or string.find(errStr, "disabled", 1, true)\
|
|
961
|
-
or errStr == "loadstring returned nil"\
|
|
962
|
-
then\
|
|
963
|
-
return false,\
|
|
964
|
-
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime requires it. "\
|
|
965
|
-
.. "Enable it in Studio (ServerScriptService > Properties > LoadStringEnabled = true) "\
|
|
966
|
-
.. "and restart the playtest."\
|
|
967
|
-
end\
|
|
968
|
-
return false, errStr\
|
|
969
|
-
end\
|
|
970
|
-
return pcall(fn)\
|
|
971
|
-
end\
|
|
972
|
-
\
|
|
973
|
-
-- Defensive cleanup of stale instances from a prior session.\
|
|
974
|
-
local prevRf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.serverRemote}")\
|
|
975
|
-
if prevRf then prevRf:Destroy() end\
|
|
976
965
|
local prevBf = ServerScriptService:FindFirstChild("{BRIDGE_NAMES.serverLocal}")\
|
|
977
966
|
if prevBf then prevBf:Destroy() end\
|
|
978
967
|
\
|
|
979
|
-
local rf = Instance.new("RemoteFunction")\
|
|
980
|
-
rf.Name = "{BRIDGE_NAMES.serverRemote}"\
|
|
981
|
-
rf.Archivable = false\
|
|
982
|
-
rf.Parent = ReplicatedStorage\
|
|
983
|
-
rf.OnServerInvoke = function(_player, source)\
|
|
984
|
-
return evalCode(source)\
|
|
985
|
-
end\
|
|
986
|
-
\
|
|
987
968
|
local bf = Instance.new("BindableFunction")\
|
|
988
969
|
bf.Name = "{BRIDGE_NAMES.serverLocal}"\
|
|
989
970
|
bf.Archivable = false\
|
|
990
971
|
bf.Parent = ServerScriptService\
|
|
991
|
-
bf.OnInvoke = function(
|
|
992
|
-
|
|
972
|
+
bf.OnInvoke = function(payload)\
|
|
973
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then\
|
|
974
|
+
return false, "payload must be a ModuleScript instance"\
|
|
975
|
+
end\
|
|
976
|
+
return pcall(require, payload)\
|
|
993
977
|
end\
|
|
994
978
|
`
|
|
995
979
|
local CLIENT_BRIDGE_SOURCE = `\
|
|
@@ -1086,20 +1070,9 @@ local function installBridges()
|
|
|
1086
1070
|
installed = true,
|
|
1087
1071
|
}
|
|
1088
1072
|
end
|
|
1089
|
-
-- Heuristic check so start_playtest can surface a warning when
|
|
1090
|
-
-- LoadStringEnabled is false (eval_server_runtime won't work in that mode).
|
|
1091
|
-
-- We can't import the runtime LoadStringEnabled value cleanly without
|
|
1092
|
-
-- pulling in the type — read defensively.
|
|
1093
|
-
local function loadStringEnabled()
|
|
1094
|
-
local ok, value = pcall(function()
|
|
1095
|
-
return ServerScriptService.LoadStringEnabled
|
|
1096
|
-
end)
|
|
1097
|
-
return ok and value == true
|
|
1098
|
-
end
|
|
1099
1073
|
return {
|
|
1100
1074
|
cleanupBridges = cleanupBridges,
|
|
1101
1075
|
installBridges = installBridges,
|
|
1102
|
-
loadStringEnabled = loadStringEnabled,
|
|
1103
1076
|
BRIDGE_NAMES = BRIDGE_NAMES,
|
|
1104
1077
|
}
|
|
1105
1078
|
]]></string>
|
|
@@ -3811,8 +3784,23 @@ local function searchFiles(requestData)
|
|
|
3811
3784
|
}
|
|
3812
3785
|
end
|
|
3813
3786
|
local function getPlaceInfo(_requestData)
|
|
3787
|
+
local dataModelName = game.Name
|
|
3788
|
+
local placeName = dataModelName
|
|
3789
|
+
if game.PlaceId > 0 then
|
|
3790
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
3791
|
+
local ok, info = pcall(function()
|
|
3792
|
+
return MarketplaceService:GetProductInfo(game.PlaceId)
|
|
3793
|
+
end)
|
|
3794
|
+
if ok and info ~= nil then
|
|
3795
|
+
local name = info.Name
|
|
3796
|
+
if type(name) == "string" and name ~= "" then
|
|
3797
|
+
placeName = name
|
|
3798
|
+
end
|
|
3799
|
+
end
|
|
3800
|
+
end
|
|
3814
3801
|
return {
|
|
3815
|
-
placeName =
|
|
3802
|
+
placeName = placeName,
|
|
3803
|
+
dataModelName = dataModelName,
|
|
3816
3804
|
placeId = game.PlaceId,
|
|
3817
3805
|
gameId = game.GameId,
|
|
3818
3806
|
jobId = game.JobId,
|
|
@@ -5437,7 +5425,6 @@ local LogService = _services.LogService
|
|
|
5437
5425
|
local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
|
|
5438
5426
|
local installBridges = _EvalBridges.installBridges
|
|
5439
5427
|
local cleanupBridges = _EvalBridges.cleanupBridges
|
|
5440
|
-
local loadStringEnabled = _EvalBridges.loadStringEnabled
|
|
5441
5428
|
local StudioTestService = game:GetService("StudioTestService")
|
|
5442
5429
|
local ServerScriptService = game:GetService("ServerScriptService")
|
|
5443
5430
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
@@ -5598,7 +5585,6 @@ local function startPlaytest(requestData)
|
|
|
5598
5585
|
-- so eval_server_runtime / eval_client_runtime work without manual setup.
|
|
5599
5586
|
-- Bridges are cleaned up from the edit DM after the play DMs tear down.
|
|
5600
5587
|
local bridgeInstall = installBridges()
|
|
5601
|
-
local hasLoadString = loadStringEnabled()
|
|
5602
5588
|
if not bridgeInstall.installed then
|
|
5603
5589
|
warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
|
|
5604
5590
|
end
|
|
@@ -5632,13 +5618,6 @@ local function startPlaytest(requestData)
|
|
|
5632
5618
|
message = msg,
|
|
5633
5619
|
evalBridges = if bridgeInstall.installed then "installed" else `failed: {bridgeInstall.error}`,
|
|
5634
5620
|
}
|
|
5635
|
-
-- Surface loadstring availability up-front so callers know whether
|
|
5636
|
-
-- eval_server_runtime will work before they try it. eval_client_runtime
|
|
5637
|
-
-- doesn't need loadstring (it uses ModuleScript+require), so this only
|
|
5638
|
-
-- affects the server bridge.
|
|
5639
|
-
if not hasLoadString then
|
|
5640
|
-
response.serverEvalNote = "ServerScriptService.LoadStringEnabled is false. eval_server_runtime will not work " .. "until you enable it (ServerScriptService > Properties > LoadStringEnabled = true) " .. "and restart the playtest. eval_client_runtime is unaffected."
|
|
5641
|
-
end
|
|
5642
5621
|
return response
|
|
5643
5622
|
end
|
|
5644
5623
|
local function stopPlaytest(_requestData)
|
|
@@ -5960,7 +5939,7 @@ return {
|
|
|
5960
5939
|
<Properties>
|
|
5961
5940
|
<string name="Name">State</string>
|
|
5962
5941
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5963
|
-
local CURRENT_VERSION = "2.10.
|
|
5942
|
+
local CURRENT_VERSION = "2.10.1"
|
|
5964
5943
|
local MAX_CONNECTIONS = 5
|
|
5965
5944
|
local BASE_PORT = 58741
|
|
5966
5945
|
local activeTabIndex = 0
|
|
@@ -6062,6 +6041,34 @@ local State = TS.import(script, script.Parent, "State")
|
|
|
6062
6041
|
local elements = nil
|
|
6063
6042
|
local pulseAnimation
|
|
6064
6043
|
local buttonHover = false
|
|
6044
|
+
local toolbarButton
|
|
6045
|
+
local toolbarIcons
|
|
6046
|
+
local lastToolbarIcon
|
|
6047
|
+
local updateToolbarIcon
|
|
6048
|
+
local function setToolbarButton(btn, icons)
|
|
6049
|
+
toolbarButton = btn
|
|
6050
|
+
toolbarIcons = icons
|
|
6051
|
+
lastToolbarIcon = nil
|
|
6052
|
+
updateToolbarIcon()
|
|
6053
|
+
end
|
|
6054
|
+
function updateToolbarIcon()
|
|
6055
|
+
if not toolbarButton or not toolbarIcons then
|
|
6056
|
+
return nil
|
|
6057
|
+
end
|
|
6058
|
+
local conn = State.getActiveConnection()
|
|
6059
|
+
local nextIcon
|
|
6060
|
+
if not conn or not conn.isActive then
|
|
6061
|
+
nextIcon = toolbarIcons.disconnected
|
|
6062
|
+
elseif conn.lastHttpOk and conn.lastMcpOk then
|
|
6063
|
+
nextIcon = toolbarIcons.connected
|
|
6064
|
+
else
|
|
6065
|
+
nextIcon = toolbarIcons.connecting
|
|
6066
|
+
end
|
|
6067
|
+
if nextIcon ~= lastToolbarIcon then
|
|
6068
|
+
toolbarButton.Icon = nextIcon
|
|
6069
|
+
lastToolbarIcon = nextIcon
|
|
6070
|
+
end
|
|
6071
|
+
end
|
|
6065
6072
|
local tabButtons = {}
|
|
6066
6073
|
local TWEEN_QUICK = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
|
|
6067
6074
|
local function tweenProp(instance, props)
|
|
@@ -6640,6 +6647,7 @@ local function init(pluginRef)
|
|
|
6640
6647
|
refreshTabBar()
|
|
6641
6648
|
end
|
|
6642
6649
|
function updateUIState()
|
|
6650
|
+
updateToolbarIcon()
|
|
6643
6651
|
local conn = State.getActiveConnection()
|
|
6644
6652
|
if not conn then
|
|
6645
6653
|
return nil
|
|
@@ -6765,6 +6773,8 @@ return {
|
|
|
6765
6773
|
updateTabLabel = updateTabLabel,
|
|
6766
6774
|
stopPulseAnimation = stopPulseAnimation,
|
|
6767
6775
|
startPulseAnimation = startPulseAnimation,
|
|
6776
|
+
setToolbarButton = setToolbarButton,
|
|
6777
|
+
updateToolbarIcon = updateToolbarIcon,
|
|
6768
6778
|
getElements = function()
|
|
6769
6779
|
return elements
|
|
6770
6780
|
end,
|
|
@@ -17,8 +17,16 @@ local RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer"
|
|
|
17
17
|
RuntimeLogBuffer.install()
|
|
18
18
|
UI.init(plugin)
|
|
19
19
|
local elements = UI.getElements()
|
|
20
|
+
local ICON_DISCONNECTED = "rbxassetid://75876056391496"
|
|
21
|
+
local ICON_CONNECTING = "rbxassetid://71302583919560"
|
|
22
|
+
local ICON_CONNECTED = "rbxassetid://130958234173611"
|
|
20
23
|
local toolbar = plugin:CreateToolbar("MCP Integration")
|
|
21
|
-
local button = toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration",
|
|
24
|
+
local button = toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration", ICON_DISCONNECTED)
|
|
25
|
+
UI.setToolbarButton(button, {
|
|
26
|
+
disconnected = ICON_DISCONNECTED,
|
|
27
|
+
connecting = ICON_CONNECTING,
|
|
28
|
+
connected = ICON_CONNECTED,
|
|
29
|
+
})
|
|
22
30
|
elements.connectButton.Activated:Connect(function()
|
|
23
31
|
local conn = State.getActiveConnection()
|
|
24
32
|
if conn and conn.isActive then
|
|
@@ -603,6 +611,9 @@ local function pollForRequests(connIndex)
|
|
|
603
611
|
conn.isPolling = false
|
|
604
612
|
local ui = UI.getElements()
|
|
605
613
|
UI.updateTabDot(connIndex)
|
|
614
|
+
if connIndex == State.getActiveTabIndex() then
|
|
615
|
+
UI.updateToolbarIcon()
|
|
616
|
+
end
|
|
606
617
|
if success and (result.Success or result.StatusCode == 503) then
|
|
607
618
|
conn.consecutiveFailures = 0
|
|
608
619
|
conn.currentRetryDelay = 0.5
|
|
@@ -759,7 +770,6 @@ local function activatePlugin(connIndex)
|
|
|
759
770
|
conn.isActive = true
|
|
760
771
|
conn.consecutiveFailures = 0
|
|
761
772
|
conn.currentRetryDelay = 0.5
|
|
762
|
-
ui.screenGui.Enabled = true
|
|
763
773
|
if idx == State.getActiveTabIndex() then
|
|
764
774
|
conn.serverUrl = ui.urlInput.Text
|
|
765
775
|
local portStr = string.match(conn.serverUrl, ":(%d+)$")
|
|
@@ -889,15 +899,22 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
889
899
|
-- `require(SomeModule)` returns a fresh copy, not the one the running game
|
|
890
900
|
-- scripts hold. So runtime-mutated module state is invisible to probes.
|
|
891
901
|
--
|
|
892
|
-
-- These bridges fix that by living inside the user's game scripts
|
|
893
|
-
--
|
|
894
|
-
--
|
|
895
|
-
-- (
|
|
902
|
+
-- These bridges fix that by living inside the user's game scripts. Both
|
|
903
|
+
-- peers use the same symmetric shape:
|
|
904
|
+
-- - Server: a Script in ServerScriptService that creates a BindableFunction.
|
|
905
|
+
-- Plugin (server peer) invokes it with a fresh ModuleScript payload;
|
|
906
|
+
-- require() runs inside the Script VM so it shares the running server's
|
|
907
|
+
-- require cache.
|
|
896
908
|
-- - Client: a LocalScript in StarterPlayer.StarterPlayerScripts that
|
|
897
909
|
-- creates a BindableFunction. Plugin invokes it with a fresh ModuleScript
|
|
898
910
|
-- payload; require() runs inside the LocalScript VM so it shares the
|
|
899
911
|
-- game's require cache.
|
|
900
912
|
--
|
|
913
|
+
-- Why ModuleScript+require on both sides (no loadstring): require'd modules
|
|
914
|
+
-- run with the security level they were created at and don't need
|
|
915
|
+
-- ServerScriptService.LoadStringEnabled, so eval_server_runtime works even
|
|
916
|
+
-- when LoadStringEnabled=false (the default in fresh places).
|
|
917
|
+
--
|
|
901
918
|
-- Lifecycle: TestHandlers.startPlaytest inserts both scripts into the EDIT
|
|
902
919
|
-- DM right before ExecutePlayModeAsync. ExecutePlayModeAsync clones the
|
|
903
920
|
-- DataModel into the play DMs, so the scripts come along and run there.
|
|
@@ -928,7 +945,6 @@ local CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge"
|
|
|
928
945
|
local BRIDGE_NAMES = {
|
|
929
946
|
serverScript = SERVER_SCRIPT_NAME,
|
|
930
947
|
clientScript = CLIENT_SCRIPT_NAME,
|
|
931
|
-
serverRemote = "__MCP_ServerEvalRemote",
|
|
932
948
|
serverLocal = "__MCP_ServerEvalLocal",
|
|
933
949
|
clientLocal = "__MCP_ClientEvalBridge",
|
|
934
950
|
}
|
|
@@ -939,7 +955,6 @@ local SERVER_BRIDGE_SOURCE = `\
|
|
|
939
955
|
-- stop_playtest. Provides shared-require-cache eval on the server peer for\
|
|
940
956
|
-- the eval_server_runtime MCP tool.\
|
|
941
957
|
\
|
|
942
|
-
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
943
958
|
local ServerScriptService = game:GetService("ServerScriptService")\
|
|
944
959
|
local RunService = game:GetService("RunService")\
|
|
945
960
|
\
|
|
@@ -947,49 +962,18 @@ if not RunService:IsStudio() then\
|
|
|
947
962
|
return\
|
|
948
963
|
end\
|
|
949
964
|
\
|
|
950
|
-
local function evalCode(source)\
|
|
951
|
-
if type(source) ~= "string" then\
|
|
952
|
-
return false, "source must be a string"\
|
|
953
|
-
end\
|
|
954
|
-
local fn, compileErr = loadstring(source, "MCPServerEval")\
|
|
955
|
-
if not fn then\
|
|
956
|
-
local errStr = tostring(compileErr or "loadstring returned nil")\
|
|
957
|
-
-- Roblox returns nil from loadstring when LoadStringEnabled=false.\
|
|
958
|
-
-- Surface a clear, actionable error.\
|
|
959
|
-
if string.find(errStr, "not enabled", 1, true)\
|
|
960
|
-
or string.find(errStr, "disabled", 1, true)\
|
|
961
|
-
or errStr == "loadstring returned nil"\
|
|
962
|
-
then\
|
|
963
|
-
return false,\
|
|
964
|
-
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime requires it. "\
|
|
965
|
-
.. "Enable it in Studio (ServerScriptService > Properties > LoadStringEnabled = true) "\
|
|
966
|
-
.. "and restart the playtest."\
|
|
967
|
-
end\
|
|
968
|
-
return false, errStr\
|
|
969
|
-
end\
|
|
970
|
-
return pcall(fn)\
|
|
971
|
-
end\
|
|
972
|
-
\
|
|
973
|
-
-- Defensive cleanup of stale instances from a prior session.\
|
|
974
|
-
local prevRf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.serverRemote}")\
|
|
975
|
-
if prevRf then prevRf:Destroy() end\
|
|
976
965
|
local prevBf = ServerScriptService:FindFirstChild("{BRIDGE_NAMES.serverLocal}")\
|
|
977
966
|
if prevBf then prevBf:Destroy() end\
|
|
978
967
|
\
|
|
979
|
-
local rf = Instance.new("RemoteFunction")\
|
|
980
|
-
rf.Name = "{BRIDGE_NAMES.serverRemote}"\
|
|
981
|
-
rf.Archivable = false\
|
|
982
|
-
rf.Parent = ReplicatedStorage\
|
|
983
|
-
rf.OnServerInvoke = function(_player, source)\
|
|
984
|
-
return evalCode(source)\
|
|
985
|
-
end\
|
|
986
|
-
\
|
|
987
968
|
local bf = Instance.new("BindableFunction")\
|
|
988
969
|
bf.Name = "{BRIDGE_NAMES.serverLocal}"\
|
|
989
970
|
bf.Archivable = false\
|
|
990
971
|
bf.Parent = ServerScriptService\
|
|
991
|
-
bf.OnInvoke = function(
|
|
992
|
-
|
|
972
|
+
bf.OnInvoke = function(payload)\
|
|
973
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then\
|
|
974
|
+
return false, "payload must be a ModuleScript instance"\
|
|
975
|
+
end\
|
|
976
|
+
return pcall(require, payload)\
|
|
993
977
|
end\
|
|
994
978
|
`
|
|
995
979
|
local CLIENT_BRIDGE_SOURCE = `\
|
|
@@ -1086,20 +1070,9 @@ local function installBridges()
|
|
|
1086
1070
|
installed = true,
|
|
1087
1071
|
}
|
|
1088
1072
|
end
|
|
1089
|
-
-- Heuristic check so start_playtest can surface a warning when
|
|
1090
|
-
-- LoadStringEnabled is false (eval_server_runtime won't work in that mode).
|
|
1091
|
-
-- We can't import the runtime LoadStringEnabled value cleanly without
|
|
1092
|
-
-- pulling in the type — read defensively.
|
|
1093
|
-
local function loadStringEnabled()
|
|
1094
|
-
local ok, value = pcall(function()
|
|
1095
|
-
return ServerScriptService.LoadStringEnabled
|
|
1096
|
-
end)
|
|
1097
|
-
return ok and value == true
|
|
1098
|
-
end
|
|
1099
1073
|
return {
|
|
1100
1074
|
cleanupBridges = cleanupBridges,
|
|
1101
1075
|
installBridges = installBridges,
|
|
1102
|
-
loadStringEnabled = loadStringEnabled,
|
|
1103
1076
|
BRIDGE_NAMES = BRIDGE_NAMES,
|
|
1104
1077
|
}
|
|
1105
1078
|
]]></string>
|
|
@@ -3811,8 +3784,23 @@ local function searchFiles(requestData)
|
|
|
3811
3784
|
}
|
|
3812
3785
|
end
|
|
3813
3786
|
local function getPlaceInfo(_requestData)
|
|
3787
|
+
local dataModelName = game.Name
|
|
3788
|
+
local placeName = dataModelName
|
|
3789
|
+
if game.PlaceId > 0 then
|
|
3790
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
3791
|
+
local ok, info = pcall(function()
|
|
3792
|
+
return MarketplaceService:GetProductInfo(game.PlaceId)
|
|
3793
|
+
end)
|
|
3794
|
+
if ok and info ~= nil then
|
|
3795
|
+
local name = info.Name
|
|
3796
|
+
if type(name) == "string" and name ~= "" then
|
|
3797
|
+
placeName = name
|
|
3798
|
+
end
|
|
3799
|
+
end
|
|
3800
|
+
end
|
|
3814
3801
|
return {
|
|
3815
|
-
placeName =
|
|
3802
|
+
placeName = placeName,
|
|
3803
|
+
dataModelName = dataModelName,
|
|
3816
3804
|
placeId = game.PlaceId,
|
|
3817
3805
|
gameId = game.GameId,
|
|
3818
3806
|
jobId = game.JobId,
|
|
@@ -5437,7 +5425,6 @@ local LogService = _services.LogService
|
|
|
5437
5425
|
local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
|
|
5438
5426
|
local installBridges = _EvalBridges.installBridges
|
|
5439
5427
|
local cleanupBridges = _EvalBridges.cleanupBridges
|
|
5440
|
-
local loadStringEnabled = _EvalBridges.loadStringEnabled
|
|
5441
5428
|
local StudioTestService = game:GetService("StudioTestService")
|
|
5442
5429
|
local ServerScriptService = game:GetService("ServerScriptService")
|
|
5443
5430
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
@@ -5598,7 +5585,6 @@ local function startPlaytest(requestData)
|
|
|
5598
5585
|
-- so eval_server_runtime / eval_client_runtime work without manual setup.
|
|
5599
5586
|
-- Bridges are cleaned up from the edit DM after the play DMs tear down.
|
|
5600
5587
|
local bridgeInstall = installBridges()
|
|
5601
|
-
local hasLoadString = loadStringEnabled()
|
|
5602
5588
|
if not bridgeInstall.installed then
|
|
5603
5589
|
warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
|
|
5604
5590
|
end
|
|
@@ -5632,13 +5618,6 @@ local function startPlaytest(requestData)
|
|
|
5632
5618
|
message = msg,
|
|
5633
5619
|
evalBridges = if bridgeInstall.installed then "installed" else `failed: {bridgeInstall.error}`,
|
|
5634
5620
|
}
|
|
5635
|
-
-- Surface loadstring availability up-front so callers know whether
|
|
5636
|
-
-- eval_server_runtime will work before they try it. eval_client_runtime
|
|
5637
|
-
-- doesn't need loadstring (it uses ModuleScript+require), so this only
|
|
5638
|
-
-- affects the server bridge.
|
|
5639
|
-
if not hasLoadString then
|
|
5640
|
-
response.serverEvalNote = "ServerScriptService.LoadStringEnabled is false. eval_server_runtime will not work " .. "until you enable it (ServerScriptService > Properties > LoadStringEnabled = true) " .. "and restart the playtest. eval_client_runtime is unaffected."
|
|
5641
|
-
end
|
|
5642
5621
|
return response
|
|
5643
5622
|
end
|
|
5644
5623
|
local function stopPlaytest(_requestData)
|
|
@@ -5960,7 +5939,7 @@ return {
|
|
|
5960
5939
|
<Properties>
|
|
5961
5940
|
<string name="Name">State</string>
|
|
5962
5941
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5963
|
-
local CURRENT_VERSION = "2.10.
|
|
5942
|
+
local CURRENT_VERSION = "2.10.1"
|
|
5964
5943
|
local MAX_CONNECTIONS = 5
|
|
5965
5944
|
local BASE_PORT = 58741
|
|
5966
5945
|
local activeTabIndex = 0
|
|
@@ -6062,6 +6041,34 @@ local State = TS.import(script, script.Parent, "State")
|
|
|
6062
6041
|
local elements = nil
|
|
6063
6042
|
local pulseAnimation
|
|
6064
6043
|
local buttonHover = false
|
|
6044
|
+
local toolbarButton
|
|
6045
|
+
local toolbarIcons
|
|
6046
|
+
local lastToolbarIcon
|
|
6047
|
+
local updateToolbarIcon
|
|
6048
|
+
local function setToolbarButton(btn, icons)
|
|
6049
|
+
toolbarButton = btn
|
|
6050
|
+
toolbarIcons = icons
|
|
6051
|
+
lastToolbarIcon = nil
|
|
6052
|
+
updateToolbarIcon()
|
|
6053
|
+
end
|
|
6054
|
+
function updateToolbarIcon()
|
|
6055
|
+
if not toolbarButton or not toolbarIcons then
|
|
6056
|
+
return nil
|
|
6057
|
+
end
|
|
6058
|
+
local conn = State.getActiveConnection()
|
|
6059
|
+
local nextIcon
|
|
6060
|
+
if not conn or not conn.isActive then
|
|
6061
|
+
nextIcon = toolbarIcons.disconnected
|
|
6062
|
+
elseif conn.lastHttpOk and conn.lastMcpOk then
|
|
6063
|
+
nextIcon = toolbarIcons.connected
|
|
6064
|
+
else
|
|
6065
|
+
nextIcon = toolbarIcons.connecting
|
|
6066
|
+
end
|
|
6067
|
+
if nextIcon ~= lastToolbarIcon then
|
|
6068
|
+
toolbarButton.Icon = nextIcon
|
|
6069
|
+
lastToolbarIcon = nextIcon
|
|
6070
|
+
end
|
|
6071
|
+
end
|
|
6065
6072
|
local tabButtons = {}
|
|
6066
6073
|
local TWEEN_QUICK = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
|
|
6067
6074
|
local function tweenProp(instance, props)
|
|
@@ -6640,6 +6647,7 @@ local function init(pluginRef)
|
|
|
6640
6647
|
refreshTabBar()
|
|
6641
6648
|
end
|
|
6642
6649
|
function updateUIState()
|
|
6650
|
+
updateToolbarIcon()
|
|
6643
6651
|
local conn = State.getActiveConnection()
|
|
6644
6652
|
if not conn then
|
|
6645
6653
|
return nil
|
|
@@ -6765,6 +6773,8 @@ return {
|
|
|
6765
6773
|
updateTabLabel = updateTabLabel,
|
|
6766
6774
|
stopPulseAnimation = stopPulseAnimation,
|
|
6767
6775
|
startPulseAnimation = startPulseAnimation,
|
|
6776
|
+
setToolbarButton = setToolbarButton,
|
|
6777
|
+
updateToolbarIcon = updateToolbarIcon,
|
|
6768
6778
|
getElements = function()
|
|
6769
6779
|
return elements
|
|
6770
6780
|
end,
|
|
@@ -181,6 +181,7 @@ function pollForRequests(connIndex: number) {
|
|
|
181
181
|
|
|
182
182
|
const ui = UI.getElements();
|
|
183
183
|
UI.updateTabDot(connIndex);
|
|
184
|
+
if (connIndex === State.getActiveTabIndex()) UI.updateToolbarIcon();
|
|
184
185
|
|
|
185
186
|
if (success && (result.Success || result.StatusCode === 503)) {
|
|
186
187
|
conn.consecutiveFailures = 0;
|
|
@@ -333,7 +334,6 @@ function activatePlugin(connIndex?: number) {
|
|
|
333
334
|
conn.isActive = true;
|
|
334
335
|
conn.consecutiveFailures = 0;
|
|
335
336
|
conn.currentRetryDelay = 0.5;
|
|
336
|
-
ui.screenGui.Enabled = true;
|
|
337
337
|
|
|
338
338
|
if (idx === State.getActiveTabIndex()) {
|
|
339
339
|
conn.serverUrl = ui.urlInput.Text;
|
|
@@ -5,15 +5,22 @@
|
|
|
5
5
|
// `require(SomeModule)` returns a fresh copy, not the one the running game
|
|
6
6
|
// scripts hold. So runtime-mutated module state is invisible to probes.
|
|
7
7
|
//
|
|
8
|
-
// These bridges fix that by living inside the user's game scripts
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// (
|
|
8
|
+
// These bridges fix that by living inside the user's game scripts. Both
|
|
9
|
+
// peers use the same symmetric shape:
|
|
10
|
+
// - Server: a Script in ServerScriptService that creates a BindableFunction.
|
|
11
|
+
// Plugin (server peer) invokes it with a fresh ModuleScript payload;
|
|
12
|
+
// require() runs inside the Script VM so it shares the running server's
|
|
13
|
+
// require cache.
|
|
12
14
|
// - Client: a LocalScript in StarterPlayer.StarterPlayerScripts that
|
|
13
15
|
// creates a BindableFunction. Plugin invokes it with a fresh ModuleScript
|
|
14
16
|
// payload; require() runs inside the LocalScript VM so it shares the
|
|
15
17
|
// game's require cache.
|
|
16
18
|
//
|
|
19
|
+
// Why ModuleScript+require on both sides (no loadstring): require'd modules
|
|
20
|
+
// run with the security level they were created at and don't need
|
|
21
|
+
// ServerScriptService.LoadStringEnabled, so eval_server_runtime works even
|
|
22
|
+
// when LoadStringEnabled=false (the default in fresh places).
|
|
23
|
+
//
|
|
17
24
|
// Lifecycle: TestHandlers.startPlaytest inserts both scripts into the EDIT
|
|
18
25
|
// DM right before ExecutePlayModeAsync. ExecutePlayModeAsync clones the
|
|
19
26
|
// DataModel into the play DMs, so the scripts come along and run there.
|
|
@@ -47,7 +54,6 @@ const CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge";
|
|
|
47
54
|
export const BRIDGE_NAMES = {
|
|
48
55
|
serverScript: SERVER_SCRIPT_NAME,
|
|
49
56
|
clientScript: CLIENT_SCRIPT_NAME,
|
|
50
|
-
serverRemote: "__MCP_ServerEvalRemote",
|
|
51
57
|
serverLocal: "__MCP_ServerEvalLocal",
|
|
52
58
|
clientLocal: "__MCP_ClientEvalBridge",
|
|
53
59
|
} as const;
|
|
@@ -59,7 +65,6 @@ const SERVER_BRIDGE_SOURCE = `
|
|
|
59
65
|
-- stop_playtest. Provides shared-require-cache eval on the server peer for
|
|
60
66
|
-- the eval_server_runtime MCP tool.
|
|
61
67
|
|
|
62
|
-
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
63
68
|
local ServerScriptService = game:GetService("ServerScriptService")
|
|
64
69
|
local RunService = game:GetService("RunService")
|
|
65
70
|
|
|
@@ -67,49 +72,18 @@ if not RunService:IsStudio() then
|
|
|
67
72
|
return
|
|
68
73
|
end
|
|
69
74
|
|
|
70
|
-
local function evalCode(source)
|
|
71
|
-
if type(source) ~= "string" then
|
|
72
|
-
return false, "source must be a string"
|
|
73
|
-
end
|
|
74
|
-
local fn, compileErr = loadstring(source, "MCPServerEval")
|
|
75
|
-
if not fn then
|
|
76
|
-
local errStr = tostring(compileErr or "loadstring returned nil")
|
|
77
|
-
-- Roblox returns nil from loadstring when LoadStringEnabled=false.
|
|
78
|
-
-- Surface a clear, actionable error.
|
|
79
|
-
if string.find(errStr, "not enabled", 1, true)
|
|
80
|
-
or string.find(errStr, "disabled", 1, true)
|
|
81
|
-
or errStr == "loadstring returned nil"
|
|
82
|
-
then
|
|
83
|
-
return false,
|
|
84
|
-
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime requires it. "
|
|
85
|
-
.. "Enable it in Studio (ServerScriptService > Properties > LoadStringEnabled = true) "
|
|
86
|
-
.. "and restart the playtest."
|
|
87
|
-
end
|
|
88
|
-
return false, errStr
|
|
89
|
-
end
|
|
90
|
-
return pcall(fn)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
-- Defensive cleanup of stale instances from a prior session.
|
|
94
|
-
local prevRf = ReplicatedStorage:FindFirstChild("${BRIDGE_NAMES.serverRemote}")
|
|
95
|
-
if prevRf then prevRf:Destroy() end
|
|
96
75
|
local prevBf = ServerScriptService:FindFirstChild("${BRIDGE_NAMES.serverLocal}")
|
|
97
76
|
if prevBf then prevBf:Destroy() end
|
|
98
77
|
|
|
99
|
-
local rf = Instance.new("RemoteFunction")
|
|
100
|
-
rf.Name = "${BRIDGE_NAMES.serverRemote}"
|
|
101
|
-
rf.Archivable = false
|
|
102
|
-
rf.Parent = ReplicatedStorage
|
|
103
|
-
rf.OnServerInvoke = function(_player, source)
|
|
104
|
-
return evalCode(source)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
78
|
local bf = Instance.new("BindableFunction")
|
|
108
79
|
bf.Name = "${BRIDGE_NAMES.serverLocal}"
|
|
109
80
|
bf.Archivable = false
|
|
110
81
|
bf.Parent = ServerScriptService
|
|
111
|
-
bf.OnInvoke = function(
|
|
112
|
-
|
|
82
|
+
bf.OnInvoke = function(payload)
|
|
83
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then
|
|
84
|
+
return false, "payload must be a ModuleScript instance"
|
|
85
|
+
end
|
|
86
|
+
return pcall(require, payload)
|
|
113
87
|
end
|
|
114
88
|
`;
|
|
115
89
|
|
|
@@ -202,14 +176,3 @@ export function installBridges(): { installed: boolean; error?: string } {
|
|
|
202
176
|
return { installed: true };
|
|
203
177
|
}
|
|
204
178
|
|
|
205
|
-
// Heuristic check so start_playtest can surface a warning when
|
|
206
|
-
// LoadStringEnabled is false (eval_server_runtime won't work in that mode).
|
|
207
|
-
// We can't import the runtime LoadStringEnabled value cleanly without
|
|
208
|
-
// pulling in the type — read defensively.
|
|
209
|
-
export function loadStringEnabled(): boolean {
|
|
210
|
-
const [ok, value] = pcall(
|
|
211
|
-
() => (ServerScriptService as unknown as { LoadStringEnabled: boolean }).LoadStringEnabled,
|
|
212
|
-
);
|
|
213
|
-
return ok && value === true;
|
|
214
|
-
}
|
|
215
|
-
|
|
@@ -30,6 +30,39 @@ let elements: UIElements = undefined!;
|
|
|
30
30
|
let pulseAnimation: Tween | undefined;
|
|
31
31
|
let buttonHover = false;
|
|
32
32
|
|
|
33
|
+
interface ToolbarIcons {
|
|
34
|
+
disconnected: string;
|
|
35
|
+
connecting: string;
|
|
36
|
+
connected: string;
|
|
37
|
+
}
|
|
38
|
+
let toolbarButton: PluginToolbarButton | undefined;
|
|
39
|
+
let toolbarIcons: ToolbarIcons | undefined;
|
|
40
|
+
let lastToolbarIcon: string | undefined;
|
|
41
|
+
|
|
42
|
+
function setToolbarButton(btn: PluginToolbarButton, icons: ToolbarIcons) {
|
|
43
|
+
toolbarButton = btn;
|
|
44
|
+
toolbarIcons = icons;
|
|
45
|
+
lastToolbarIcon = undefined;
|
|
46
|
+
updateToolbarIcon();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function updateToolbarIcon() {
|
|
50
|
+
if (!toolbarButton || !toolbarIcons) return;
|
|
51
|
+
const conn = State.getActiveConnection();
|
|
52
|
+
let nextIcon: string;
|
|
53
|
+
if (!conn || !conn.isActive) {
|
|
54
|
+
nextIcon = toolbarIcons.disconnected;
|
|
55
|
+
} else if (conn.lastHttpOk && conn.lastMcpOk) {
|
|
56
|
+
nextIcon = toolbarIcons.connected;
|
|
57
|
+
} else {
|
|
58
|
+
nextIcon = toolbarIcons.connecting;
|
|
59
|
+
}
|
|
60
|
+
if (nextIcon !== lastToolbarIcon) {
|
|
61
|
+
(toolbarButton as unknown as { Icon: string }).Icon = nextIcon;
|
|
62
|
+
lastToolbarIcon = nextIcon;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
interface TabButton {
|
|
34
67
|
frame: Frame;
|
|
35
68
|
label: TextLabel;
|
|
@@ -596,6 +629,7 @@ function init(pluginRef: Plugin) {
|
|
|
596
629
|
}
|
|
597
630
|
|
|
598
631
|
function updateUIState() {
|
|
632
|
+
updateToolbarIcon();
|
|
599
633
|
const conn = State.getActiveConnection();
|
|
600
634
|
if (!conn) return;
|
|
601
635
|
const el = elements;
|
|
@@ -723,5 +757,7 @@ export = {
|
|
|
723
757
|
updateTabLabel,
|
|
724
758
|
stopPulseAnimation,
|
|
725
759
|
startPulseAnimation,
|
|
760
|
+
setToolbarButton,
|
|
761
|
+
updateToolbarIcon,
|
|
726
762
|
getElements: () => elements,
|
|
727
763
|
};
|
|
@@ -96,8 +96,23 @@ function searchFiles(requestData: Record<string, unknown>) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function getPlaceInfo(_requestData: Record<string, unknown>) {
|
|
99
|
+
const dataModelName = game.Name;
|
|
100
|
+
let placeName = dataModelName;
|
|
101
|
+
|
|
102
|
+
if (game.PlaceId > 0) {
|
|
103
|
+
const MarketplaceService = game.GetService("MarketplaceService");
|
|
104
|
+
const [ok, info] = pcall(() => MarketplaceService.GetProductInfo(game.PlaceId));
|
|
105
|
+
if (ok && info !== undefined) {
|
|
106
|
+
const name = (info as { Name?: string }).Name;
|
|
107
|
+
if (typeIs(name, "string") && name !== "") {
|
|
108
|
+
placeName = name;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
return {
|
|
100
|
-
placeName
|
|
114
|
+
placeName,
|
|
115
|
+
dataModelName,
|
|
101
116
|
placeId: game.PlaceId,
|
|
102
117
|
gameId: game.GameId,
|
|
103
118
|
jobId: game.JobId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpService, LogService } from "@rbxts/services";
|
|
2
|
-
import { installBridges, cleanupBridges
|
|
2
|
+
import { installBridges, cleanupBridges } from "../EvalBridges";
|
|
3
3
|
|
|
4
4
|
const StudioTestService = game.GetService("StudioTestService");
|
|
5
5
|
const ServerScriptService = game.GetService("ServerScriptService");
|
|
@@ -159,7 +159,6 @@ function startPlaytest(requestData: Record<string, unknown>) {
|
|
|
159
159
|
// so eval_server_runtime / eval_client_runtime work without manual setup.
|
|
160
160
|
// Bridges are cleaned up from the edit DM after the play DMs tear down.
|
|
161
161
|
const bridgeInstall = installBridges();
|
|
162
|
-
const hasLoadString = loadStringEnabled();
|
|
163
162
|
if (!bridgeInstall.installed) {
|
|
164
163
|
warn(`[MCP] Eval bridge install failed: ${bridgeInstall.error}`);
|
|
165
164
|
}
|
|
@@ -203,17 +202,6 @@ function startPlaytest(requestData: Record<string, unknown>) {
|
|
|
203
202
|
evalBridges: bridgeInstall.installed ? "installed" : `failed: ${bridgeInstall.error}`,
|
|
204
203
|
};
|
|
205
204
|
|
|
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
205
|
return response;
|
|
218
206
|
}
|
|
219
207
|
|
|
@@ -13,8 +13,13 @@ UI.init(plugin);
|
|
|
13
13
|
const elements = UI.getElements();
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
const ICON_DISCONNECTED = "rbxassetid://__BUTTON_ICON_DISCONNECTED__";
|
|
17
|
+
const ICON_CONNECTING = "rbxassetid://__BUTTON_ICON_CONNECTING__";
|
|
18
|
+
const ICON_CONNECTED = "rbxassetid://__BUTTON_ICON_CONNECTED__";
|
|
19
|
+
|
|
16
20
|
const toolbar = plugin.CreateToolbar("__TOOLBAR_NAME__");
|
|
17
|
-
const button = toolbar.CreateButton("__BUTTON_TITLE__", "__BUTTON_TOOLTIP__",
|
|
21
|
+
const button = toolbar.CreateButton("__BUTTON_TITLE__", "__BUTTON_TOOLTIP__", ICON_DISCONNECTED);
|
|
22
|
+
UI.setToolbarButton(button, { disconnected: ICON_DISCONNECTED, connecting: ICON_CONNECTING, connected: ICON_CONNECTED });
|
|
18
23
|
|
|
19
24
|
|
|
20
25
|
elements.connectButton.Activated.Connect(() => {
|