@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 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
- local HttpService = game:GetService("HttpService")
1776
- local bf = game:GetService("ServerScriptService"):FindFirstChild("${SERVER_LOCAL_NAME}")
1777
- if not bf then
1778
- return HttpService:JSONEncode({
1779
- bridge = "missing",
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
- local HttpService = game:GetService("HttpService")
1811
- local bf = game:GetService("ReplicatedStorage"):FindFirstChild("${CLIENT_LOCAL_NAME}")
1812
- if not bf then
1813
- return HttpService:JSONEncode({
1814
- bridge = "missing",
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. Requires ServerScriptService.LoadStringEnabled=true.",
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. Does not require LoadStringEnabled.",
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.0",
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", "rbxassetid://125921838360800")
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
- -- - Server: a Script in ServerScriptService that creates a BindableFunction
894
- -- (for our server-peer plugin to invoke directly) plus a RemoteFunction
895
- -- (kept for parity with the upstream primitive's client-callable shape).
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(source)\
992
- return evalCode(source)\
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 = game.Name,
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.0"
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", "rbxassetid://10734944444")
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
- -- - Server: a Script in ServerScriptService that creates a BindableFunction
894
- -- (for our server-peer plugin to invoke directly) plus a RemoteFunction
895
- -- (kept for parity with the upstream primitive's client-callable shape).
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(source)\
992
- return evalCode(source)\
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 = game.Name,
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.0"
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
- // - 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).
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(source)
112
- return evalCode(source)
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: game.Name,
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, loadStringEnabled } from "../EvalBridges";
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__", "rbxassetid://__BUTTON_ICON_ID__");
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(() => {