@chrrxs/robloxstudio-mcp 2.15.1 → 2.16.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.
@@ -10,6 +10,9 @@ local State = TS.import(script, script, "modules", "State")
10
10
  local UI = TS.import(script, script, "modules", "UI")
11
11
  local Communication = TS.import(script, script, "modules", "Communication")
12
12
  local ClientBroker = TS.import(script, script, "modules", "ClientBroker")
13
+ local _EvalBridges = TS.import(script, script, "modules", "EvalBridges")
14
+ local cleanupLegacyEditBridges = _EvalBridges.cleanupLegacyEditBridges
15
+ local ensureRuntimeBridgeInstalled = _EvalBridges.ensureRuntimeBridgeInstalled
13
16
  local RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer")
14
17
  local StopPlayMonitor = TS.import(script, script, "modules", "StopPlayMonitor")
15
18
  local RenderMonitor = TS.import(script, script, "modules", "RenderMonitor")
@@ -30,13 +33,24 @@ local elements = UI.getElements()
30
33
  local ICON_DISCONNECTED = "rbxassetid://125921838360800"
31
34
  local ICON_CONNECTING = "rbxassetid://125921838360800"
32
35
  local ICON_CONNECTED = "rbxassetid://125921838360800"
33
- local toolbar = plugin:CreateToolbar("MCP Inspector")
34
- local button = toolbar:CreateButton("MCP Inspector", "Connect to MCP Inspector (read-only) for AI Integration", ICON_DISCONNECTED)
35
- UI.setToolbarButton(button, {
36
- disconnected = ICON_DISCONNECTED,
37
- connecting = ICON_CONNECTING,
38
- connected = ICON_CONNECTED,
39
- })
36
+ local TOOLBAR_REGISTRATION_DELAY_SECONDS = 1
37
+ local toolbarButtonRegistered = false
38
+ local function registerToolbarButton()
39
+ if toolbarButtonRegistered then
40
+ return nil
41
+ end
42
+ toolbarButtonRegistered = true
43
+ local toolbar = plugin:CreateToolbar("MCP Inspector")
44
+ local button = toolbar:CreateButton("MCP Inspector", "Connect to MCP Inspector (read-only) for AI Integration", ICON_DISCONNECTED)
45
+ UI.setToolbarButton(button, {
46
+ disconnected = ICON_DISCONNECTED,
47
+ connecting = ICON_CONNECTING,
48
+ connected = ICON_CONNECTED,
49
+ })
50
+ button.Click:Connect(function()
51
+ elements.screenGui.Enabled = not elements.screenGui.Enabled
52
+ end)
53
+ end
40
54
  elements.connectButton.Activated:Connect(function()
41
55
  local conn = State.getActiveConnection()
42
56
  if conn and conn.isActive then
@@ -45,20 +59,26 @@ elements.connectButton.Activated:Connect(function()
45
59
  Communication.activatePlugin(State.getActiveTabIndex())
46
60
  end
47
61
  end)
48
- button.Click:Connect(function()
49
- elements.screenGui.Enabled = not elements.screenGui.Enabled
50
- end)
51
62
  plugin.Unloading:Connect(function()
52
63
  Communication.deactivateAll()
53
64
  end)
54
65
  UI.updateUIState()
55
66
  Communication.checkForUpdates()
67
+ task.delay(TOOLBAR_REGISTRATION_DELAY_SECONDS, registerToolbarButton)
56
68
  -- Auto-activate per peer. The boshyxd plugin only registers with MCP when the
57
69
  -- user clicks Connect in its UI, but that UI is invisible in play DMs - so
58
70
  -- play peers' plugin instances load without ever registering. Run after a
59
71
  -- short delay so the UI/State have a chance to initialize first.
60
72
  task.delay(2, function()
61
73
  local role = ClientBroker.forkRole()
74
+ if role == "edit" then
75
+ cleanupLegacyEditBridges()
76
+ else
77
+ local result = ensureRuntimeBridgeInstalled()
78
+ if not result.installed then
79
+ warn(`[MCPPlugin] Runtime eval bridge install failed: {result.error}`)
80
+ end
81
+ end
62
82
  if role == "edit" or role == "server" then
63
83
  pcall(function()
64
84
  local idx = State.getActiveTabIndex()
@@ -105,6 +125,7 @@ local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandl
105
125
  local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
106
126
  local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
107
127
  local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
128
+ local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
108
129
  local LuauExec = TS.import(script, script.Parent, "LuauExec")
109
130
  local State = TS.import(script, script.Parent, "State")
110
131
  local StudioTestService = game:GetService("StudioTestService")
@@ -167,6 +188,7 @@ local BROKER_OWNER_ATTRIBUTE = "__MCPBrokerOwner"
167
188
  -- cache / etc. lives there) so the server peer alone can't satisfy them.
168
189
  local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
169
190
  ["/api/execute-luau"] = true,
191
+ ["/api/eval-runtime"] = true,
170
192
  ["/api/get-runtime-logs"] = true,
171
193
  ["/api/get-memory-breakdown"] = true,
172
194
  ["/api/get-scene-analysis"] = true,
@@ -247,8 +269,8 @@ local function handleGetRuntimeLogs(data)
247
269
  local since = d.since
248
270
  local tail = d.tail
249
271
  local filter = d.filter
250
- -- "client" is the generic peer tag; MCP-side aggregator overrides with
251
- -- the specific role (e.g. "client-1") on target=all fan-out.
272
+ -- "client" is the generic capture tag; MCP-side aggregation overrides it
273
+ -- with the specific role (e.g. "client-1") for capturedBy.
252
274
  return RuntimeLogBuffer.query({
253
275
  since = since,
254
276
  tail = tail,
@@ -366,6 +388,9 @@ local function setupClientBroker()
366
388
  if payload and payload.endpoint == "/api/execute-luau" then
367
389
  return handleExecuteLuau(payload.data)
368
390
  end
391
+ if payload and payload.endpoint == "/api/eval-runtime" then
392
+ return EvalRuntimeHandlers.evalRuntime(payload.data or {})
393
+ end
369
394
  -- Legacy: raw execute-luau payload at the top level.
370
395
  return handleExecuteLuau(payload)
371
396
  end
@@ -547,7 +572,7 @@ local ServerStorage = _services.ServerStorage
547
572
  local State = TS.import(script, script.Parent, "State")
548
573
  local Utils = TS.import(script, script.Parent, "Utils")
549
574
  local UI = TS.import(script, script.Parent, "UI")
550
- local ensureBridgesInstalled = TS.import(script, script.Parent, "EvalBridges").ensureBridgesInstalled
575
+ local cleanupLegacyEditBridges = TS.import(script, script.Parent, "EvalBridges").cleanupLegacyEditBridges
551
576
  local QueryHandlers = TS.import(script, script.Parent, "handlers", "QueryHandlers")
552
577
  local PropertyHandlers = TS.import(script, script.Parent, "handlers", "PropertyHandlers")
553
578
  local InstanceHandlers = TS.import(script, script.Parent, "handlers", "InstanceHandlers")
@@ -562,6 +587,7 @@ local LogHandlers = TS.import(script, script.Parent, "handlers", "LogHandlers")
562
587
  local SerializationHandlers = TS.import(script, script.Parent, "handlers", "SerializationHandlers")
563
588
  local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
564
589
  local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
590
+ local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
565
591
  -- Per-plugin-load random GUID. Used as the /poll URL param so the server
566
592
  -- can tell our polls apart from any other plugin's polls. Not user-facing —
567
593
  -- MCP tools and the LLM operate on instanceId (the place identifier).
@@ -668,6 +694,7 @@ local routeMap = {
668
694
  ["/api/get-tagged"] = MetadataHandlers.getTagged,
669
695
  ["/api/get-selection"] = MetadataHandlers.getSelection,
670
696
  ["/api/execute-luau"] = MetadataHandlers.executeLuau,
697
+ ["/api/eval-runtime"] = EvalRuntimeHandlers.evalRuntime,
671
698
  ["/api/undo"] = MetadataHandlers.undo,
672
699
  ["/api/redo"] = MetadataHandlers.redo,
673
700
  ["/api/bulk-set-attributes"] = MetadataHandlers.bulkSetAttributes,
@@ -1049,18 +1076,10 @@ local function activatePlugin(connIndex)
1049
1076
  -- Initial /ready; pollForRequests will also re-fire ready if the server
1050
1077
  -- later reports knownInstance=false (process restart, etc).
1051
1078
  sendReady(conn)
1052
- -- Keep the eval bridges present in the edit DM so that ANY playtest —
1053
- -- including one the dev starts manually via the Studio Play button —
1054
- -- clones them into the play DMs and eval_*_runtime works with no setup
1055
- -- roundtrip. Only the edit DM installs; play DMs already have the cloned
1056
- -- copies. Idempotent, so reconnects don't re-dirty the place.
1079
+ -- Remove legacy edit-mode eval bridge scripts from older plugin builds.
1080
+ -- Current bridges are created only in running play DataModels.
1057
1081
  if not RunService:IsRunning() then
1058
- task.spawn(function()
1059
- local result = ensureBridgesInstalled()
1060
- if not result.installed then
1061
- warn(`[MCPPlugin] Eval bridge install failed: {result.error}`)
1062
- end
1063
- end)
1082
+ task.spawn(cleanupLegacyEditBridges)
1064
1083
  end
1065
1084
  -- Watch for game.Name updates so a stale "Place1" captured at first
1066
1085
  -- /ready gets refreshed once Studio settles on the real DM name.
@@ -1181,29 +1200,15 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
1181
1200
  -- ServerScriptService.LoadStringEnabled, so eval_server_runtime works even
1182
1201
  -- when LoadStringEnabled=false (the default in fresh places).
1183
1202
  --
1184
- -- Lifecycle: the bridges live PERMANENTLY in the edit DM. Communication
1185
- -- installs them (ensureBridgesInstalled) when the plugin connects in edit,
1186
- -- and TestHandlers.startPlaytest force-refreshes them right before
1187
- -- ExecutePlayModeAsync. ExecutePlayModeAsync clones the DataModel into the
1188
- -- play DMs, so the scripts come along and run there. We keep them in the edit
1189
- -- DM after a playtest ends (rather than cleaning up) so that a playtest the
1190
- -- dev starts MANUALLY via the Studio Play button — not the MCP start_playtest
1191
- -- tool — also gets the bridges cloned in. This is intentionally a little
1192
- -- intrusive (two helper scripts visible in Explorer) in exchange for a
1193
- -- zero-roundtrip eval_*_runtime experience for devs working 1:1 with an agent.
1194
- --
1195
- -- Archivable handling: ExecutePlayModeAsync's deep-clone SKIPS instances
1196
- -- with Archivable=false (verified empirically in v2.9.0 testing - bridges
1197
- -- never reached the play DMs because we'd set them to false). We now keep
1198
- -- Archivable=true so the clone works, and rely on cleanupBridges() to
1199
- -- remove the scripts from the edit DM when the test ends. The only failure
1200
- -- mode is the user saving DURING an active playtest, which would persist
1201
- -- the bridges to the .rbxl - that's a no-op next session because
1202
- -- installBridges() always calls cleanupBridges() first to clear stale
1203
- -- instances. The RemoteFunction/BindableFunction that the bridge scripts
1204
- -- CREATE at runtime stay Archivable=false (they're runtime-only and should
1205
- -- never appear in a save).
1203
+ -- Lifecycle: bridge scripts are created only in running play DataModels.
1204
+ -- The server plugin peer creates the Script in runtime ServerScriptService;
1205
+ -- each client plugin peer creates its LocalScript in that client's
1206
+ -- PlayerScripts. Nothing is installed into the edit DataModel anymore.
1207
+ -- Runtime-created scripts disappear naturally when the playtest stops.
1206
1208
  local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
1209
+ local Players = _services.Players
1210
+ local ReplicatedStorage = _services.ReplicatedStorage
1211
+ local RunService = _services.RunService
1207
1212
  local ServerScriptService = _services.ServerScriptService
1208
1213
  local StarterPlayer = _services.StarterPlayer
1209
1214
  local ScriptEditorService = game:GetService("ScriptEditorService")
@@ -1242,9 +1247,10 @@ bf.Archivable = false\
1242
1247
  bf.Parent = ServerScriptService\
1243
1248
  bf.OnInvoke = function(payload)\
1244
1249
  if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then\
1245
- return false, "payload must be a ModuleScript instance"\
1250
+ return \{ ok = false, value = "payload must be a ModuleScript instance" \}\
1246
1251
  end\
1247
- return pcall(require, payload)\
1252
+ local ok, value = pcall(require, payload)\
1253
+ return \{ ok = ok, value = value \}\
1248
1254
  end\
1249
1255
  `
1250
1256
  local CLIENT_BRIDGE_SOURCE = `\
@@ -1268,18 +1274,17 @@ bf.Archivable = false\
1268
1274
  bf.Parent = ReplicatedStorage\
1269
1275
  bf.OnInvoke = function(payload)\
1270
1276
  if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then\
1271
- return false, "payload must be a ModuleScript instance"\
1277
+ return \{ ok = false, value = "payload must be a ModuleScript instance" \}\
1272
1278
  end\
1273
- return pcall(require, payload)\
1279
+ local ok, value = pcall(require, payload)\
1280
+ return \{ ok = ok, value = value \}\
1274
1281
  end\
1275
1282
  `
1276
1283
  -- Stamp written onto each installed bridge Script so we can tell whether the
1277
- -- bridge currently in the DM was produced by THIS plugin build. It's a djb2
1278
- -- hash of the actual bridge source plus the plugin version, so ANY change to
1279
- -- the source (or a version bump) yields a new stamp which makes
1280
- -- ensureBridgesInstalled() force a refresh on the next plugin load instead of
1281
- -- keeping a stale bridge that happens to still be present (e.g. one saved into
1282
- -- the .rbxl from an older build).
1284
+ -- runtime bridge currently in the play DM was produced by THIS plugin build.
1285
+ -- It's a djb2 hash of the actual bridge source plus the plugin version, so ANY
1286
+ -- change to the source (or a version bump) yields a new stamp and triggers a
1287
+ -- runtime refresh instead of keeping a stale bridge.
1283
1288
  local STAMP_ATTR = "__MCPBridgeStamp"
1284
1289
  local function computeBridgeStamp()
1285
1290
  local combined = `{SERVER_BRIDGE_SOURCE}|{CLIENT_BRIDGE_SOURCE}`
@@ -1287,9 +1292,9 @@ local function computeBridgeStamp()
1287
1292
  for i = 1, #combined do
1288
1293
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1289
1294
  end
1290
- -- "2.15.1" is replaced with the package version at package time
1295
+ -- "2.16.0" is replaced with the package version at package time
1291
1296
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1292
- return `{tostring(h)}-2.15.1`
1297
+ return `{tostring(h)}-2.16.0`
1293
1298
  end
1294
1299
  local BRIDGE_STAMP = computeBridgeStamp()
1295
1300
  local function setSource(scriptInst, source)
@@ -1305,15 +1310,26 @@ local function setSource(scriptInst, source)
1305
1310
  scriptInst.Source = source
1306
1311
  end
1307
1312
  end
1308
- local function findBridges()
1313
+ local function findLegacyEditBridges()
1309
1314
  local sps = getStarterPlayerScripts()
1310
1315
  return {
1311
1316
  server = ServerScriptService:FindFirstChild(SERVER_SCRIPT_NAME),
1312
1317
  client = if sps then sps:FindFirstChild(CLIENT_SCRIPT_NAME) else nil,
1313
1318
  }
1314
1319
  end
1315
- local function cleanupBridges()
1316
- local _binding = findBridges()
1320
+ local function destroyIfPresent(parent, name)
1321
+ local existing = parent:FindFirstChild(name)
1322
+ if existing then
1323
+ pcall(function()
1324
+ return existing:Destroy()
1325
+ end)
1326
+ end
1327
+ end
1328
+ local function cleanupLegacyEditBridges()
1329
+ if RunService:IsRunning() then
1330
+ return nil
1331
+ end
1332
+ local _binding = findLegacyEditBridges()
1317
1333
  local server = _binding.server
1318
1334
  local client = _binding.client
1319
1335
  if server then
@@ -1327,54 +1343,79 @@ local function cleanupBridges()
1327
1343
  end)
1328
1344
  end
1329
1345
  end
1330
- -- Idempotent variant: install only if the bridge scripts aren't already
1331
- -- present in the edit DM. Used to keep the bridges always available (so a
1332
- -- playtest the dev starts manually — not via the MCP start_playtest tool —
1333
- -- still clones them into the play DMs). Cheap no-op when already installed,
1334
- -- which avoids re-dirtying the place on every plugin reconnect.
1335
- local installBridges
1336
- local function ensureBridgesInstalled()
1337
- local _binding = findBridges()
1338
- local server = _binding.server
1339
- local client = _binding.client
1340
- if server and client then
1341
- -- Both present — but only skip the reinstall if they were produced by
1342
- -- THIS build. A mismatched/absent stamp means a stale bridge (older
1343
- -- plugin, or one persisted in the saved place), so force a refresh.
1344
- local sStamp = server:GetAttribute(STAMP_ATTR)
1345
- local cStamp = client:GetAttribute(STAMP_ATTR)
1346
- if sStamp == BRIDGE_STAMP and cStamp == BRIDGE_STAMP then
1347
- return {
1348
- installed = true,
1349
- }
1350
- end
1346
+ local function serverRuntimeBridgeReady()
1347
+ local scriptInst = ServerScriptService:FindFirstChild(SERVER_SCRIPT_NAME)
1348
+ local bindable = ServerScriptService:FindFirstChild(BRIDGE_NAMES.serverLocal)
1349
+ return scriptInst ~= nil and scriptInst:GetAttribute(STAMP_ATTR) == BRIDGE_STAMP and bindable ~= nil and bindable:IsA("BindableFunction")
1350
+ end
1351
+ local function getPlayerScripts()
1352
+ local localPlayer = Players.LocalPlayer
1353
+ if not localPlayer then
1354
+ return nil
1355
+ end
1356
+ local playerScripts = localPlayer:FindFirstChild("PlayerScripts")
1357
+ if not playerScripts then
1358
+ playerScripts = localPlayer:WaitForChild("PlayerScripts", 5)
1359
+ end
1360
+ return playerScripts
1361
+ end
1362
+ local function clientRuntimeBridgeReady()
1363
+ local playerScripts = getPlayerScripts()
1364
+ if not playerScripts then
1365
+ return false
1351
1366
  end
1352
- return installBridges()
1367
+ local scriptInst = playerScripts:FindFirstChild(CLIENT_SCRIPT_NAME)
1368
+ local bindable = ReplicatedStorage:FindFirstChild(BRIDGE_NAMES.clientLocal)
1369
+ return scriptInst ~= nil and scriptInst:GetAttribute(STAMP_ATTR) == BRIDGE_STAMP and bindable ~= nil and bindable:IsA("BindableFunction")
1353
1370
  end
1354
- function installBridges()
1355
- -- Defensive: clear any stale bridges from a prior unclean exit before
1356
- -- inserting fresh. The injected script also self-cleans its
1357
- -- ReplicatedStorage/ServerScriptService children at startup, but the
1358
- -- containing Script/LocalScript objects themselves we must clear here.
1359
- cleanupBridges()
1371
+ local function installServerRuntimeBridge()
1372
+ if serverRuntimeBridgeReady() then
1373
+ return {
1374
+ installed = true,
1375
+ }
1376
+ end
1360
1377
  local ok, err = pcall(function()
1378
+ destroyIfPresent(ServerScriptService, SERVER_SCRIPT_NAME)
1379
+ destroyIfPresent(ServerScriptService, BRIDGE_NAMES.serverLocal)
1361
1380
  local serverScript = Instance.new("Script")
1362
1381
  serverScript.Name = SERVER_SCRIPT_NAME
1363
- -- Archivable=true so ExecutePlayModeAsync's deep-clone includes the
1364
- -- script. cleanupBridges() removes it from the edit DM when the
1365
- -- playtest ends.
1382
+ serverScript.Archivable = false
1366
1383
  setSource(serverScript, SERVER_BRIDGE_SOURCE)
1367
1384
  serverScript:SetAttribute(STAMP_ATTR, BRIDGE_STAMP)
1368
1385
  serverScript.Parent = ServerScriptService
1369
- local sps = getStarterPlayerScripts()
1370
- if not sps then
1371
- error("StarterPlayer.StarterPlayerScripts not found - cannot install client eval bridge")
1372
- end
1386
+ end)
1387
+ if not ok then
1388
+ return {
1389
+ installed = false,
1390
+ error = tostring(err),
1391
+ }
1392
+ end
1393
+ return {
1394
+ installed = true,
1395
+ }
1396
+ end
1397
+ local function installClientRuntimeBridge()
1398
+ if clientRuntimeBridgeReady() then
1399
+ return {
1400
+ installed = true,
1401
+ }
1402
+ end
1403
+ local playerScripts = getPlayerScripts()
1404
+ if not playerScripts then
1405
+ return {
1406
+ installed = false,
1407
+ error = "Players.LocalPlayer.PlayerScripts not found - cannot install client eval bridge",
1408
+ }
1409
+ end
1410
+ local ok, err = pcall(function()
1411
+ destroyIfPresent(playerScripts, CLIENT_SCRIPT_NAME)
1412
+ destroyIfPresent(ReplicatedStorage, BRIDGE_NAMES.clientLocal)
1373
1413
  local clientScript = Instance.new("LocalScript")
1374
1414
  clientScript.Name = CLIENT_SCRIPT_NAME
1415
+ clientScript.Archivable = false
1375
1416
  setSource(clientScript, CLIENT_BRIDGE_SOURCE)
1376
1417
  clientScript:SetAttribute(STAMP_ATTR, BRIDGE_STAMP)
1377
- clientScript.Parent = sps
1418
+ clientScript.Parent = playerScripts
1378
1419
  end)
1379
1420
  if not ok then
1380
1421
  return {
@@ -1386,10 +1427,21 @@ function installBridges()
1386
1427
  installed = true,
1387
1428
  }
1388
1429
  end
1430
+ local function ensureRuntimeBridgeInstalled()
1431
+ if not RunService:IsRunning() then
1432
+ return {
1433
+ installed = false,
1434
+ error = "Eval bridges are installed only in running play DataModels",
1435
+ }
1436
+ end
1437
+ if RunService:IsServer() then
1438
+ return installServerRuntimeBridge()
1439
+ end
1440
+ return installClientRuntimeBridge()
1441
+ end
1389
1442
  return {
1390
- cleanupBridges = cleanupBridges,
1391
- ensureBridgesInstalled = ensureBridgesInstalled,
1392
- installBridges = installBridges,
1443
+ cleanupLegacyEditBridges = cleanupLegacyEditBridges,
1444
+ ensureRuntimeBridgeInstalled = ensureRuntimeBridgeInstalled,
1393
1445
  BRIDGE_NAMES = BRIDGE_NAMES,
1394
1446
  }
1395
1447
  ]]></string>
@@ -2466,6 +2518,160 @@ return {
2466
2518
  </Properties>
2467
2519
  </Item>
2468
2520
  <Item class="ModuleScript" referent="9">
2521
+ <Properties>
2522
+ <string name="Name">EvalRuntimeHandlers</string>
2523
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
2524
+ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
2525
+ local _services = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services")
2526
+ local LogService = _services.LogService
2527
+ local ReplicatedStorage = _services.ReplicatedStorage
2528
+ local RunService = _services.RunService
2529
+ local ServerScriptService = _services.ServerScriptService
2530
+ local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
2531
+ local BRIDGE_NAMES = _EvalBridges.BRIDGE_NAMES
2532
+ local ensureRuntimeBridgeInstalled = _EvalBridges.ensureRuntimeBridgeInstalled
2533
+ local LuauExec = TS.import(script, script.Parent.Parent, "LuauExec")
2534
+ local PAYLOAD_INSTANCE_NAME = "__MCPEvalPayload"
2535
+ local function findBridge(config)
2536
+ local bridge = config.service:FindFirstChild(config.bridgeName)
2537
+ return if bridge and bridge:IsA("BindableFunction") then bridge else nil
2538
+ end
2539
+ local function waitForBridge(config, timeoutSec)
2540
+ if timeoutSec == nil then
2541
+ timeoutSec = 2
2542
+ end
2543
+ local deadline = tick() + timeoutSec
2544
+ local bridge = findBridge(config)
2545
+ while not bridge and tick() < deadline do
2546
+ task.wait(0.05)
2547
+ bridge = findBridge(config)
2548
+ end
2549
+ return bridge
2550
+ end
2551
+ local function getBridgeConfig()
2552
+ if not RunService:IsRunning() then
2553
+ return {
2554
+ error = "eval_*_runtime requires a running playtest.",
2555
+ }
2556
+ end
2557
+ if RunService:IsServer() then
2558
+ return {
2559
+ service = ServerScriptService,
2560
+ bridgeName = BRIDGE_NAMES.serverLocal,
2561
+ missingError = "ServerEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically in the runtime server peer, including for manually-started playtests.",
2562
+ }
2563
+ end
2564
+ return {
2565
+ service = ReplicatedStorage,
2566
+ bridgeName = BRIDGE_NAMES.clientLocal,
2567
+ missingError = "ClientEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically in the runtime client peer, including for manually-started playtests.",
2568
+ }
2569
+ end
2570
+ local function evalRuntime(requestData)
2571
+ local code = requestData.code
2572
+ if not (code ~= "" and code) or code == "" then
2573
+ return {
2574
+ error = "Code is required",
2575
+ }
2576
+ end
2577
+ local config = getBridgeConfig()
2578
+ if config.error ~= nil then
2579
+ return {
2580
+ bridge = "missing",
2581
+ error = config.error,
2582
+ }
2583
+ end
2584
+ local bridge = findBridge(config)
2585
+ if not bridge then
2586
+ local install = ensureRuntimeBridgeInstalled()
2587
+ if not install.installed then
2588
+ return {
2589
+ bridge = "missing",
2590
+ error = `{config.missingError} Runtime bridge install failed: {install.error}`,
2591
+ }
2592
+ end
2593
+ bridge = waitForBridge(config)
2594
+ end
2595
+ if not bridge then
2596
+ return {
2597
+ bridge = "missing",
2598
+ error = `{config.missingError} Runtime bridge was installed but did not become ready.`,
2599
+ }
2600
+ end
2601
+ local m = Instance.new("ModuleScript")
2602
+ m.Name = PAYLOAD_INSTANCE_NAME
2603
+ local userLines = LuauExec.countLines(code)
2604
+ local wrapped = LuauExec.buildWrapper(code, PAYLOAD_INSTANCE_NAME)
2605
+ local okSet, setErr = pcall(function()
2606
+ m.Source = wrapped
2607
+ end)
2608
+ if not okSet then
2609
+ m:Destroy()
2610
+ return {
2611
+ bridge = "ok",
2612
+ ok = false,
2613
+ error = `ModuleScript Source set failed: {tostring(setErr)}`,
2614
+ }
2615
+ end
2616
+ m.Parent = game:GetService("Workspace")
2617
+ local historyStart = #LogService:GetLogHistory()
2618
+ local invokeOk, invokeResult = pcall(function()
2619
+ return bridge:Invoke(m)
2620
+ end)
2621
+ m:Destroy()
2622
+ if not invokeOk then
2623
+ return {
2624
+ bridge = "ok",
2625
+ ok = false,
2626
+ error = tostring(invokeResult),
2627
+ }
2628
+ end
2629
+ if not (type(invokeResult) == "table") then
2630
+ return {
2631
+ bridge = "ok",
2632
+ ok = false,
2633
+ error = `Eval bridge returned invalid result: {tostring(invokeResult)}`,
2634
+ }
2635
+ end
2636
+ local bridgeResult = invokeResult
2637
+ if bridgeResult.ok ~= true then
2638
+ return {
2639
+ bridge = "ok",
2640
+ ok = false,
2641
+ error = LuauExec.recoverPayloadRequireError(bridgeResult.value, userLines, PAYLOAD_INSTANCE_NAME, historyStart),
2642
+ }
2643
+ end
2644
+ local inner = bridgeResult.value
2645
+ if not (type(inner) == "table") then
2646
+ return {
2647
+ bridge = "ok",
2648
+ ok = true,
2649
+ result = if inner == nil then nil else LuauExec.formatReturnValue(inner),
2650
+ }
2651
+ end
2652
+ local r = inner
2653
+ local ok = r.ok == true
2654
+ local _object = {
2655
+ bridge = "ok",
2656
+ ok = ok,
2657
+ result = if ok and r.value ~= nil then LuauExec.formatReturnValue(r.value) else nil,
2658
+ error = if not ok then tostring(r.value) else nil,
2659
+ }
2660
+ local _left = "output"
2661
+ local _condition = r.output
2662
+ if _condition == nil then
2663
+ _condition = {}
2664
+ end
2665
+ _object[_left] = _condition
2666
+ return _object
2667
+ end
2668
+ return {
2669
+ evalRuntime = evalRuntime,
2670
+ }
2671
+ ]]></string>
2672
+ </Properties>
2673
+ </Item>
2674
+ <Item class="ModuleScript" referent="10">
2469
2675
  <Properties>
2470
2676
  <string name="Name">InputHandlers</string>
2471
2677
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -2665,7 +2871,7 @@ return {
2665
2871
  ]]></string>
2666
2872
  </Properties>
2667
2873
  </Item>
2668
- <Item class="ModuleScript" referent="10">
2874
+ <Item class="ModuleScript" referent="11">
2669
2875
  <Properties>
2670
2876
  <string name="Name">InstanceHandlers</string>
2671
2877
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3172,7 +3378,7 @@ return {
3172
3378
  ]]></string>
3173
3379
  </Properties>
3174
3380
  </Item>
3175
- <Item class="ModuleScript" referent="11">
3381
+ <Item class="ModuleScript" referent="12">
3176
3382
  <Properties>
3177
3383
  <string name="Name">LogHandlers</string>
3178
3384
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3182,16 +3388,15 @@ local function getRuntimeLogs(requestData)
3182
3388
  local since = requestData.since
3183
3389
  local tail = requestData.tail
3184
3390
  local filter = requestData.filter
3185
- -- Plugin-side peer tag is generic ("edit"|"server"|"client"). The MCP-side
3186
- -- aggregator overrides it with the specific instance role (e.g. "client-1")
3187
- -- during fan-out for target=all, so this value is only authoritative for
3188
- -- the single-peer query path.
3189
- local peer = RuntimeLogBuffer.detectPeer()
3391
+ -- This is the buffer that captured the LogService event, not necessarily
3392
+ -- the script-origin peer. Ordinary playtests share/reflect logs across
3393
+ -- edit/server/client LogService buffers.
3394
+ local capturedBy = RuntimeLogBuffer.detectPeer()
3190
3395
  return RuntimeLogBuffer.query({
3191
3396
  since = since,
3192
3397
  tail = tail,
3193
3398
  filter = filter,
3194
- }, peer)
3399
+ }, capturedBy)
3195
3400
  end
3196
3401
  return {
3197
3402
  getRuntimeLogs = getRuntimeLogs,
@@ -3199,7 +3404,7 @@ return {
3199
3404
  ]]></string>
3200
3405
  </Properties>
3201
3406
  </Item>
3202
- <Item class="ModuleScript" referent="12">
3407
+ <Item class="ModuleScript" referent="13">
3203
3408
  <Properties>
3204
3409
  <string name="Name">MemoryHandlers</string>
3205
3410
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3267,7 +3472,7 @@ return {
3267
3472
  ]]></string>
3268
3473
  </Properties>
3269
3474
  </Item>
3270
- <Item class="ModuleScript" referent="13">
3475
+ <Item class="ModuleScript" referent="14">
3271
3476
  <Properties>
3272
3477
  <string name="Name">MetadataHandlers</string>
3273
3478
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3806,7 +4011,7 @@ return {
3806
4011
  ]]></string>
3807
4012
  </Properties>
3808
4013
  </Item>
3809
- <Item class="ModuleScript" referent="14">
4014
+ <Item class="ModuleScript" referent="15">
3810
4015
  <Properties>
3811
4016
  <string name="Name">PropertyHandlers</string>
3812
4017
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -4058,7 +4263,7 @@ return {
4058
4263
  ]]></string>
4059
4264
  </Properties>
4060
4265
  </Item>
4061
- <Item class="ModuleScript" referent="15">
4266
+ <Item class="ModuleScript" referent="16">
4062
4267
  <Properties>
4063
4268
  <string name="Name">QueryHandlers</string>
4064
4269
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -5100,7 +5305,7 @@ return {
5100
5305
  ]]></string>
5101
5306
  </Properties>
5102
5307
  </Item>
5103
- <Item class="ModuleScript" referent="16">
5308
+ <Item class="ModuleScript" referent="17">
5104
5309
  <Properties>
5105
5310
  <string name="Name">SceneAnalysisHandlers</string>
5106
5311
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -5349,7 +5554,7 @@ return {
5349
5554
  ]]></string>
5350
5555
  </Properties>
5351
5556
  </Item>
5352
- <Item class="ModuleScript" referent="17">
5557
+ <Item class="ModuleScript" referent="18">
5353
5558
  <Properties>
5354
5559
  <string name="Name">ScriptHandlers</string>
5355
5560
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -6045,7 +6250,7 @@ return {
6045
6250
  ]]></string>
6046
6251
  </Properties>
6047
6252
  </Item>
6048
- <Item class="ModuleScript" referent="18">
6253
+ <Item class="ModuleScript" referent="19">
6049
6254
  <Properties>
6050
6255
  <string name="Name">SerializationHandlers</string>
6051
6256
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -6231,7 +6436,7 @@ return {
6231
6436
  ]]></string>
6232
6437
  </Properties>
6233
6438
  </Item>
6234
- <Item class="ModuleScript" referent="19">
6439
+ <Item class="ModuleScript" referent="20">
6235
6440
  <Properties>
6236
6441
  <string name="Name">TestHandlers</string>
6237
6442
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -6241,9 +6446,6 @@ local HttpService = _services.HttpService
6241
6446
  local LogService = _services.LogService
6242
6447
  local Players = _services.Players
6243
6448
  local RunService = _services.RunService
6244
- local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
6245
- local installBridges = _EvalBridges.installBridges
6246
- local ensureBridgesInstalled = _EvalBridges.ensureBridgesInstalled
6247
6449
  local StopPlayMonitor = TS.import(script, script.Parent.Parent, "StopPlayMonitor")
6248
6450
  local StudioTestService = game:GetService("StudioTestService")
6249
6451
  local ServerScriptService = game:GetService("ServerScriptService")
@@ -6430,9 +6632,8 @@ local function startPlaytest(requestData)
6430
6632
  logConnection = nil
6431
6633
  end
6432
6634
  cleanupStopListener()
6433
- -- Note: eval bridges are intentionally NOT cleaned up they live
6434
- -- permanently in the edit DM so manual playtests also get them. See
6435
- -- EvalBridges.ts lifecycle comment.
6635
+ -- Runtime eval bridges are created by the play server/client plugin
6636
+ -- peers and disappear with the play DataModels.
6436
6637
  end
6437
6638
  if testRunning then
6438
6639
  return {
@@ -6475,14 +6676,6 @@ local function startPlaytest(requestData)
6475
6676
  if not injected then
6476
6677
  warn(`[MCP] Failed to inject stop listener: {injErr}`)
6477
6678
  end
6478
- -- Force-refresh the game-VM eval bridges (ServerEvalBridge + ClientEvalBridge)
6479
- -- right before cloning so the play DMs get the current source. They also
6480
- -- live permanently in the edit DM (installed on connect) so manually-started
6481
- -- playtests get them too; here we just ensure they're fresh.
6482
- local bridgeInstall = installBridges()
6483
- if not bridgeInstall.installed then
6484
- warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
6485
- end
6486
6679
  task.spawn(function()
6487
6680
  local ok, result = pcall(function()
6488
6681
  if mode == "play" then
@@ -6501,21 +6694,11 @@ local function startPlaytest(requestData)
6501
6694
  end
6502
6695
  testRunning = false
6503
6696
  cleanupStopListener()
6504
- -- Eval bridges persist in the edit DM (see EvalBridges.ts) — do not
6505
- -- clean up here, so the next manual playtest still gets them.
6506
- ensureBridgesInstalled()
6507
6697
  end)
6508
6698
  local response = {
6509
6699
  success = true,
6510
6700
  message = `Playtest started in {mode} mode.`,
6511
6701
  }
6512
- -- Only mention eval bridges when they failed — when they're fine, the
6513
- -- detail is noise. eval_server_runtime / eval_client_runtime will surface
6514
- -- their own clear errors if the caller tries to use them after a failed
6515
- -- install.
6516
- if not bridgeInstall.installed then
6517
- response.evalBridgesError = bridgeInstall.error
6518
- end
6519
6702
  return response
6520
6703
  end
6521
6704
  local function stopPlaytest(_requestData)
@@ -6610,10 +6793,6 @@ local function multiplayerTestStart(requestData)
6610
6793
  end
6611
6794
  local testArgs = if requestData.testArgs ~= nil then requestData.testArgs else {}
6612
6795
  local testId = HttpService:GenerateGUID(false)
6613
- local bridgeInstall = installBridges()
6614
- if not bridgeInstall.installed then
6615
- warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
6616
- end
6617
6796
  multiplayerState = {
6618
6797
  phase = "starting",
6619
6798
  testId = testId,
@@ -6637,7 +6816,6 @@ local function multiplayerTestStart(requestData)
6637
6816
  multiplayerState.result = nil
6638
6817
  multiplayerState.error = tostring(result)
6639
6818
  end
6640
- ensureBridgesInstalled()
6641
6819
  end)
6642
6820
  local response = {
6643
6821
  success = true,
@@ -6647,9 +6825,6 @@ local function multiplayerTestStart(requestData)
6647
6825
  numPlayers = numPlayers,
6648
6826
  testArgs = testArgs,
6649
6827
  }
6650
- if not bridgeInstall.installed then
6651
- response.evalBridgesError = bridgeInstall.error
6652
- end
6653
6828
  return response
6654
6829
  end
6655
6830
  local function multiplayerTestState(_requestData)
@@ -6864,7 +7039,7 @@ return {
6864
7039
  </Properties>
6865
7040
  </Item>
6866
7041
  </Item>
6867
- <Item class="ModuleScript" referent="20">
7042
+ <Item class="ModuleScript" referent="21">
6868
7043
  <Properties>
6869
7044
  <string name="Name">LuauExec</string>
6870
7045
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -6873,10 +7048,12 @@ return {
6873
7048
  -- and the play-client peer (ClientBroker.handleExecuteLuau). Three things this
6874
7049
  -- module owns:
6875
7050
  --
6876
- -- 1. The IIFE wrapper that captures print/warn, runs user code in xpcall,
6877
- -- and always returns { ok, value, output } so the ModuleScript itself
6878
- -- always returns exactly one value (otherwise `print("hi")` with no
6879
- -- return would fail with "Module code did not return exactly one value").
7051
+ -- 1. The IIFE wrapper that captures print/warn, wraps require() so nested
7052
+ -- ModuleScript load failures can recover the real LogService diagnostic,
7053
+ -- runs user code in xpcall, and always returns { ok, value, output } so
7054
+ -- the ModuleScript itself always returns exactly one value (otherwise
7055
+ -- `print("hi")` with no return would fail with "Module code did not
7056
+ -- return exactly one value").
6880
7057
  --
6881
7058
  -- 2. The loadstring-then-ModuleScript-require fallback, with the parse-error
6882
7059
  -- recovery hack that pulls the real diagnostic from LogService.
@@ -6894,14 +7071,14 @@ return {
6894
7071
  local HttpService = game:GetService("HttpService")
6895
7072
  local LogService = game:GetService("LogService")
6896
7073
  local PAYLOAD_INSTANCE_NAME = "__MCPExecLuauPayload"
6897
- local PAYLOAD_PATH_PREFIX = `Workspace.{PAYLOAD_INSTANCE_NAME}:`
7074
+ local REQUIRE_GENERIC_ERROR = "Requested module experienced an error while loading"
6898
7075
  -- Number of lines the wrapper emits BEFORE the first line of user code.
6899
7076
  -- Used both inside the wrapper (Luau __mcp_LINE_OFFSET) and on the TS side
6900
7077
  -- (remapPayloadLines, for compile errors recovered from LogService) so user
6901
7078
  -- code errors report user-relative line numbers instead of the inflated
6902
- -- "line 23" the wrapper would otherwise expose. If you reorder buildWrapper's
6903
- -- prefix lines, update this constant — there's a self-check below.
6904
- local WRAPPER_LINE_OFFSET = 23
7079
+ -- "line 49" the wrapper would otherwise expose. If you reorder buildWrapper's
7080
+ -- prefix lines, update this constant.
7081
+ local WRAPPER_LINE_OFFSET = 84
6905
7082
  -- Count source lines so the wrapper can filter traceback frames that fall
6906
7083
  -- outside the user code range (the wrapper's own preamble/postamble lines).
6907
7084
  local function countLines(s)
@@ -6926,20 +7103,31 @@ local function countLines(s)
6926
7103
  end
6927
7104
  return n
6928
7105
  end
6929
- local function buildWrapper(code)
7106
+ local function luaPatternEscape(s)
7107
+ local escaped = string.gsub(s, "([^%w])", "%%%1")
7108
+ return escaped
7109
+ end
7110
+ local function buildWrapper(code, payloadInstanceName)
7111
+ if payloadInstanceName == nil then
7112
+ payloadInstanceName = PAYLOAD_INSTANCE_NAME
7113
+ end
6930
7114
  -- If you reorder the prefix lines below, update WRAPPER_LINE_OFFSET to
6931
7115
  -- match the number of lines emitted BEFORE the ${code} substitution.
6932
7116
  -- The constant is mirrored inside the wrapper (__mcp_LINE_OFFSET) and
6933
7117
  -- used by remapPayloadLines on the TS side.
6934
7118
  local userLines = countLines(code)
7119
+ local payloadPattern = luaPatternEscape(payloadInstanceName)
6935
7120
  return `return ((function()\
6936
7121
  \tlocal __mcp_traceback\
6937
7122
  \tlocal __mcp_remap\
6938
7123
  \tlocal __mcp_LINE_OFFSET = {WRAPPER_LINE_OFFSET}\
6939
7124
  \tlocal __mcp_USER_LINES = {userLines}\
7125
+ \tlocal __mcp_LogService = game:GetService("LogService")\
7126
+ \tlocal __mcp_REQUIRE_GENERIC = "{REQUIRE_GENERIC_ERROR}"\
6940
7127
  \tlocal __mcp_output = \{\}\
6941
7128
  \tlocal __mcp_real_print = print\
6942
7129
  \tlocal __mcp_real_warn = warn\
7130
+ \tlocal __mcp_real_require = require\
6943
7131
  \tlocal print = function(...)\
6944
7132
  \t\t__mcp_real_print(...)\
6945
7133
  \t\tlocal args = \{...\}\
@@ -6954,6 +7142,64 @@ local function buildWrapper(code)
6954
7142
  \t\tfor i, a in ipairs(args) do parts[i] = tostring(a) end\
6955
7143
  \t\ttable.insert(__mcp_output, "[warn] " .. table.concat(parts, "\\t"))\
6956
7144
  \tend\
7145
+ \tlocal function __mcp_is_stack_noise(msg)\
7146
+ \t\treturn msg == "Stack Begin" or msg == "Stack End" or string.sub(msg, 1, 8) == "Script '"\
7147
+ \tend\
7148
+ \tlocal function __mcp_is_actionable_require_log(entry)\
7149
+ \t\tif not entry or entry.messageType ~= Enum.MessageType.MessageError then return false end\
7150
+ \t\tlocal msg = tostring(entry.message)\
7151
+ \t\treturn msg ~= __mcp_REQUIRE_GENERIC and not __mcp_is_stack_noise(msg)\
7152
+ \tend\
7153
+ \tlocal function __mcp_entry_mentions_module(entry, module_path)\
7154
+ \t\tif not entry or not module_path or module_path == "" then return false end\
7155
+ \t\treturn string.find(tostring(entry.message), module_path, 1, true) ~= nil\
7156
+ \tend\
7157
+ \tlocal function __mcp_prior_module_error(hist, module_path)\
7158
+ \t\tif not module_path or module_path == "" then return nil end\
7159
+ \t\tfor i = #hist, 1, -1 do\
7160
+ \t\t\tlocal entry = hist[i]\
7161
+ \t\t\tif __mcp_entry_mentions_module(entry, module_path) then\
7162
+ \t\t\t\tif __mcp_is_actionable_require_log(entry) then\
7163
+ \t\t\t\t\treturn tostring(entry.message)\
7164
+ \t\t\t\tend\
7165
+ \t\t\t\tfor j = i - 1, math.max(1, i - 6), -1 do\
7166
+ \t\t\t\t\tlocal previous = hist[j]\
7167
+ \t\t\t\t\tif __mcp_is_actionable_require_log(previous) then\
7168
+ \t\t\t\t\t\treturn tostring(previous.message)\
7169
+ \t\t\t\t\tend\
7170
+ \t\t\t\tend\
7171
+ \t\t\tend\
7172
+ \t\tend\
7173
+ \t\treturn nil\
7174
+ \tend\
7175
+ \tlocal function __mcp_recover_require_error(err, history_start, module)\
7176
+ \t\tlocal err_msg = tostring(err)\
7177
+ \t\tif err_msg ~= __mcp_REQUIRE_GENERIC then return err_msg end\
7178
+ \t\tlocal module_path\
7179
+ \t\tif typeof(module) == "Instance" then\
7180
+ \t\t\tlocal ok_path, path = pcall(function()\
7181
+ \t\t\t\treturn module:GetFullName()\
7182
+ \t\t\tend)\
7183
+ \t\t\tif ok_path then module_path = path end\
7184
+ \t\tend\
7185
+ \t\ttask.wait(0.05)\
7186
+ \t\tlocal hist = __mcp_LogService:GetLogHistory()\
7187
+ \t\tfor i = #hist, history_start + 1, -1 do\
7188
+ \t\t\tlocal entry = hist[i]\
7189
+ \t\t\tif __mcp_is_actionable_require_log(entry) then\
7190
+ \t\t\t\treturn tostring(entry.message)\
7191
+ \t\t\tend\
7192
+ \t\tend\
7193
+ \t\tlocal prior = __mcp_prior_module_error(hist, module_path)\
7194
+ \t\tif prior then return prior end\
7195
+ \t\treturn err_msg\
7196
+ \tend\
7197
+ \tlocal function require(module)\
7198
+ \t\tlocal history_start = #__mcp_LogService:GetLogHistory()\
7199
+ \t\tlocal ok, value = pcall(__mcp_real_require, module)\
7200
+ \t\tif ok then return value end\
7201
+ \t\terror(__mcp_recover_require_error(value, history_start, module), 0)\
7202
+ \tend\
6957
7203
  \tlocal function __mcp_run()\
6958
7204
  {code}\
6959
7205
  \tend\
@@ -6964,15 +7210,20 @@ local function buildWrapper(code)
6964
7210
  \t\t-- Subtract LINE_OFFSET to get the user-relative number, then clamp.\
6965
7211
  \t\t-- Clamping matters for unclosed constructs ("local x = (") where the\
6966
7212
  \t\t-- parser keeps reading into wrapper postamble and reports a payload\
6967
- \t\t-- line past user EOF. Without clamping the message says "user_code:49"\
6968
- \t\t-- for one-line input, framing the wrapper as user code.\
7213
+ \t\t-- line past user EOF. Without clamping, that frames wrapper postamble\
7214
+ \t\t-- as user code.\
6969
7215
  \t\tlocal function __mcp_user_line(payload_n)\
6970
7216
  \t\t\tlocal user_n = payload_n - __mcp_LINE_OFFSET\
6971
7217
  \t\t\tif user_n < 1 then return "1" end\
6972
7218
  \t\t\tif user_n > __mcp_USER_LINES then return tostring(__mcp_USER_LINES) .. " (at end of input)" end\
6973
7219
  \t\t\treturn tostring(user_n)\
6974
7220
  \t\tend\
6975
- \t\ts = string.gsub(s, "__MCPExecLuauPayload:(%d+)", function(num)\
7221
+ \t\ts = string.gsub(s, "Workspace%.{payloadPattern}:(%d+)", function(num)\
7222
+ \t\t\tlocal n = tonumber(num)\
7223
+ \t\t\tif n then return "user_code:" .. __mcp_user_line(n) end\
7224
+ \t\t\treturn "user_code:" .. num\
7225
+ \t\tend)\
7226
+ \t\ts = string.gsub(s, "{payloadPattern}:(%d+)", function(num)\
6976
7227
  \t\t\tlocal n = tonumber(num)\
6977
7228
  \t\t\tif n then return "user_code:" .. __mcp_user_line(n) end\
6978
7229
  \t\t\treturn "user_code:" .. num\
@@ -7022,7 +7273,10 @@ end
7022
7273
  -- pulling the real compile-error diagnostic out of LogService — that error
7023
7274
  -- references the payload module's line number directly, and never passes
7024
7275
  -- through the IIFE's runtime wrapper.
7025
- local function remapPayloadLines(s, userLines)
7276
+ local function remapPayloadLines(s, userLines, payloadInstanceName)
7277
+ if payloadInstanceName == nil then
7278
+ payloadInstanceName = PAYLOAD_INSTANCE_NAME
7279
+ end
7026
7280
  -- Mirror of the Lua __mcp_remap inside the wrapper, for paths that
7027
7281
  -- don't pass through the IIFE (compile errors recovered from
7028
7282
  -- LogService, the immediate loadstring compileError surface). Same
@@ -7040,8 +7294,9 @@ local function remapPayloadLines(s, userLines)
7040
7294
  end
7041
7295
  return tostring(u)
7042
7296
  end
7297
+ local payloadPattern = luaPatternEscape(payloadInstanceName)
7043
7298
  local out = s
7044
- local a = string.gsub(out, "__MCPExecLuauPayload:(%d+)", function(num)
7299
+ local a = string.gsub(out, `Workspace%.{payloadPattern}:(%d+)`, function(num)
7045
7300
  local n = tonumber(num)
7046
7301
  if n ~= nil then
7047
7302
  return `user_code:{userLine(n)}`
@@ -7049,7 +7304,7 @@ local function remapPayloadLines(s, userLines)
7049
7304
  return `user_code:{num}`
7050
7305
  end)
7051
7306
  out = a
7052
- local b = string.gsub(out, '%[string "[^"]+"%]:(%d+)', function(num)
7307
+ local b = string.gsub(out, `{payloadPattern}:(%d+)`, function(num)
7053
7308
  local n = tonumber(num)
7054
7309
  if n ~= nil then
7055
7310
  return `user_code:{userLine(n)}`
@@ -7057,8 +7312,16 @@ local function remapPayloadLines(s, userLines)
7057
7312
  return `user_code:{num}`
7058
7313
  end)
7059
7314
  out = b
7060
- return out
7315
+ local c = string.gsub(out, '%[string "[^"]+"%]:(%d+)', function(num)
7316
+ local n = tonumber(num)
7317
+ if n ~= nil then
7318
+ return `user_code:{userLine(n)}`
7319
+ end
7320
+ return `user_code:{num}`
7321
+ end)
7322
+ return c
7061
7323
  end
7324
+ local recoverPayloadRequireError
7062
7325
  local function runViaModuleScript(wrapped, userLines)
7063
7326
  local m = Instance.new("ModuleScript")
7064
7327
  m.Name = PAYLOAD_INSTANCE_NAME
@@ -7078,26 +7341,11 @@ local function runViaModuleScript(wrapped, userLines)
7078
7341
  end)
7079
7342
  m:Destroy()
7080
7343
  if not okReq then
7081
- local errMsg = tostring(reqResult)
7082
- -- pcall(require, m) collapses parse/compile failures into the canned
7083
- -- engine string. The real diagnostic was emitted to LogService on the
7084
- -- next engine frame — give it ~50ms to land then scan backward.
7085
- if errMsg == "Requested module experienced an error while loading" then
7086
- task.wait(0.05)
7087
- local hist = LogService:GetLogHistory()
7088
- for i = #hist - 1, 0, -1 do
7089
- local e = hist[i + 1]
7090
- if e.messageType == Enum.MessageType.MessageError and string.sub(e.message, 1, #PAYLOAD_PATH_PREFIX) == PAYLOAD_PATH_PREFIX then
7091
- errMsg = e.message
7092
- break
7093
- end
7094
- end
7095
- end
7096
7344
  -- Compile errors reference the payload module's line number directly
7097
7345
  -- — remap + clamp to user-relative line numbers so `local x = 1 +`
7098
7346
  -- reports :1: instead of :23:, and reports the clamp annotation
7099
7347
  -- when the parser ran off the end of user code into wrapper code.
7100
- error(remapPayloadLines(errMsg, userLines), 0)
7348
+ error(recoverPayloadRequireError(reqResult, userLines, PAYLOAD_INSTANCE_NAME), 0)
7101
7349
  end
7102
7350
  return reqResult
7103
7351
  end
@@ -7124,6 +7372,44 @@ local function formatReturnValue(value)
7124
7372
  end
7125
7373
  return tostring(value)
7126
7374
  end
7375
+ function recoverPayloadRequireError(err, userLines, payloadInstanceName, historyStart)
7376
+ if payloadInstanceName == nil then
7377
+ payloadInstanceName = PAYLOAD_INSTANCE_NAME
7378
+ end
7379
+ if historyStart == nil then
7380
+ historyStart = 0
7381
+ end
7382
+ local errMsg = tostring(err)
7383
+ -- pcall(require, m) collapses parse/compile failures into the canned
7384
+ -- engine string. The real diagnostic is emitted to LogService on the
7385
+ -- next engine frame — give it ~50ms to land then scan backward.
7386
+ if errMsg == REQUIRE_GENERIC_ERROR then
7387
+ task.wait(0.05)
7388
+ local payloadPathPrefix = `Workspace.{payloadInstanceName}:`
7389
+ local hist = LogService:GetLogHistory()
7390
+ local start = math.max(0, historyStart)
7391
+ do
7392
+ local i = #hist - 1
7393
+ local _shouldIncrement = false
7394
+ while true do
7395
+ if _shouldIncrement then
7396
+ i -= 1
7397
+ else
7398
+ _shouldIncrement = true
7399
+ end
7400
+ if not (i >= start) then
7401
+ break
7402
+ end
7403
+ local e = hist[i + 1]
7404
+ if e.messageType == Enum.MessageType.MessageError and string.sub(e.message, 1, #payloadPathPrefix) == payloadPathPrefix then
7405
+ errMsg = e.message
7406
+ break
7407
+ end
7408
+ end
7409
+ end
7410
+ end
7411
+ return remapPayloadLines(errMsg, userLines, payloadInstanceName)
7412
+ end
7127
7413
  local function execute(code)
7128
7414
  if not (code ~= "" and code) or code == "" then
7129
7415
  return {
@@ -7177,12 +7463,17 @@ local function execute(code)
7177
7463
  }
7178
7464
  end
7179
7465
  return {
7466
+ buildWrapper = buildWrapper,
7467
+ countLines = countLines,
7180
7468
  execute = execute,
7469
+ formatReturnValue = formatReturnValue,
7470
+ recoverPayloadRequireError = recoverPayloadRequireError,
7471
+ remapPayloadLines = remapPayloadLines,
7181
7472
  }
7182
7473
  ]]></string>
7183
7474
  </Properties>
7184
7475
  </Item>
7185
- <Item class="ModuleScript" referent="21">
7476
+ <Item class="ModuleScript" referent="22">
7186
7477
  <Properties>
7187
7478
  <string name="Name">Recording</string>
7188
7479
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -7212,7 +7503,7 @@ return {
7212
7503
  ]]></string>
7213
7504
  </Properties>
7214
7505
  </Item>
7215
- <Item class="ModuleScript" referent="22">
7506
+ <Item class="ModuleScript" referent="23">
7216
7507
  <Properties>
7217
7508
  <string name="Name">RenderMonitor</string>
7218
7509
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -7280,12 +7571,12 @@ return {
7280
7571
  ]]></string>
7281
7572
  </Properties>
7282
7573
  </Item>
7283
- <Item class="ModuleScript" referent="23">
7574
+ <Item class="ModuleScript" referent="24">
7284
7575
  <Properties>
7285
7576
  <string name="Name">RuntimeLogBuffer</string>
7286
7577
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
7287
7578
  local TS = require(script.Parent.Parent.include.RuntimeLib)
7288
- -- Per-peer in-memory ring buffer for LogService.MessageOut events.
7579
+ -- Per-capture in-memory ring buffer for LogService.MessageOut events.
7289
7580
  -- Powers the get_runtime_logs MCP tool. Replaces the out-of-tree LogBuffer
7290
7581
  -- primitives + StringValue approach from chrrxs/roblox-mcp-primitives.
7291
7582
  --
@@ -7295,12 +7586,12 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
7295
7586
  -- DataModel. The buffer is bounded by a message-byte budget; oldest entries
7296
7587
  -- drop when over budget.
7297
7588
  --
7298
- -- Peer-tag caveat: returned entries reflect which peer's plugin CAPTURED the
7589
+ -- Capture caveat: returned entries reflect which plugin buffer CAPTURED the
7299
7590
  -- entry, NOT which peer's script originated the print. LogService reflects
7300
- -- prints across peers in Studio Play (a server print ends up in both the
7301
- -- server and client LogService:GetLogHistory()) and origin is empirically
7302
- -- undetectable from inside MessageOut. The MCP-side aggregator handles
7303
- -- cross-peer dedup via a 2s timestamp window.
7591
+ -- prints across peers in ordinary Studio Play (a server print can appear in
7592
+ -- server and client LogService:GetLogHistory()). The MCP-side aggregator
7593
+ -- exposes that as capturedBy, and only promotes it to origin peer in
7594
+ -- StudioTestService multiplayer sessions where peer attribution is reliable.
7304
7595
  local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
7305
7596
  local LogService = _services.LogService
7306
7597
  local RunService = _services.RunService
@@ -7364,7 +7655,7 @@ local function detectPeer()
7364
7655
  end
7365
7656
  return "client"
7366
7657
  end
7367
- local function query(opts, peer)
7658
+ local function query(opts, capturedBy)
7368
7659
  local _result
7369
7660
  if opts.since ~= nil then
7370
7661
  -- ▼ ReadonlyArray.filter ▼
@@ -7435,7 +7726,7 @@ local function query(opts, peer)
7435
7726
  end
7436
7727
  local last = if #entries > 0 then entries[#entries] else nil
7437
7728
  local _object = {
7438
- peer = peer,
7729
+ capturedBy = capturedBy,
7439
7730
  entries = result,
7440
7731
  totalDropped = totalDropped,
7441
7732
  }
@@ -7461,11 +7752,11 @@ return {
7461
7752
  ]]></string>
7462
7753
  </Properties>
7463
7754
  </Item>
7464
- <Item class="ModuleScript" referent="24">
7755
+ <Item class="ModuleScript" referent="25">
7465
7756
  <Properties>
7466
7757
  <string name="Name">State</string>
7467
7758
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
7468
- local CURRENT_VERSION = "2.15.1"
7759
+ local CURRENT_VERSION = "2.16.0"
7469
7760
  local PLUGIN_VARIANT = "inspector"
7470
7761
  local MAX_CONNECTIONS = 5
7471
7762
  local BASE_PORT = 58741
@@ -7559,7 +7850,7 @@ return {
7559
7850
  ]]></string>
7560
7851
  </Properties>
7561
7852
  </Item>
7562
- <Item class="ModuleScript" referent="25">
7853
+ <Item class="ModuleScript" referent="26">
7563
7854
  <Properties>
7564
7855
  <string name="Name">StopPlayMonitor</string>
7565
7856
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -7704,7 +7995,7 @@ return {
7704
7995
  ]]></string>
7705
7996
  </Properties>
7706
7997
  </Item>
7707
- <Item class="ModuleScript" referent="26">
7998
+ <Item class="ModuleScript" referent="27">
7708
7999
  <Properties>
7709
8000
  <string name="Name">UI</string>
7710
8001
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8475,7 +8766,7 @@ return {
8475
8766
  ]]></string>
8476
8767
  </Properties>
8477
8768
  </Item>
8478
- <Item class="ModuleScript" referent="27">
8769
+ <Item class="ModuleScript" referent="28">
8479
8770
  <Properties>
8480
8771
  <string name="Name">Utils</string>
8481
8772
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9005,11 +9296,11 @@ return {
9005
9296
  </Properties>
9006
9297
  </Item>
9007
9298
  </Item>
9008
- <Item class="Folder" referent="31">
9299
+ <Item class="Folder" referent="32">
9009
9300
  <Properties>
9010
9301
  <string name="Name">include</string>
9011
9302
  </Properties>
9012
- <Item class="ModuleScript" referent="28">
9303
+ <Item class="ModuleScript" referent="29">
9013
9304
  <Properties>
9014
9305
  <string name="Name">Promise</string>
9015
9306
  <string name="Source"><![CDATA[--[[
@@ -11083,7 +11374,7 @@ return Promise
11083
11374
  ]]></string>
11084
11375
  </Properties>
11085
11376
  </Item>
11086
- <Item class="ModuleScript" referent="29">
11377
+ <Item class="ModuleScript" referent="30">
11087
11378
  <Properties>
11088
11379
  <string name="Name">RuntimeLib</string>
11089
11380
  <string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
@@ -11350,15 +11641,15 @@ return TS
11350
11641
  </Properties>
11351
11642
  </Item>
11352
11643
  </Item>
11353
- <Item class="Folder" referent="32">
11644
+ <Item class="Folder" referent="33">
11354
11645
  <Properties>
11355
11646
  <string name="Name">node_modules</string>
11356
11647
  </Properties>
11357
- <Item class="Folder" referent="33">
11648
+ <Item class="Folder" referent="34">
11358
11649
  <Properties>
11359
11650
  <string name="Name">@rbxts</string>
11360
11651
  </Properties>
11361
- <Item class="ModuleScript" referent="30">
11652
+ <Item class="ModuleScript" referent="31">
11362
11653
  <Properties>
11363
11654
  <string name="Name">services</string>
11364
11655
  <string name="Source"><![CDATA[return setmetatable({}, {