@chrrxs/robloxstudio-mcp 3.0.4 → 3.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
4
4
  "description": "MCP server for testing, debugging, and controlling Roblox Studio from AI coding tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -16,6 +16,7 @@ local cleanupEditBridgeArtifacts = _EvalBridges.cleanupEditBridgeArtifacts
16
16
  local ensureRuntimeBridgeInstalled = _EvalBridges.ensureRuntimeBridgeInstalled
17
17
  local RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer")
18
18
  local StopPlayMonitor = TS.import(script, script, "modules", "StopPlayMonitor")
19
+ local StudioEventStream = TS.import(script, script, "modules", "StudioEventStream")
19
20
  local BreakpointHandlers = TS.import(script, script, "modules", "handlers", "BreakpointHandlers")
20
21
  local RenderMonitor = TS.import(script, script, "modules", "RenderMonitor")
21
22
  -- Track render-loop liveness so input/screenshot tools can report "window
@@ -32,8 +33,9 @@ RuntimeLogBuffer.install()
32
33
  StopPlayMonitor.init(plugin)
33
34
  BreakpointHandlers.init(plugin)
34
35
  ServerUrlSettings.init(plugin)
36
+ local startupRole = ClientBroker.forkRole()
35
37
  local function applyRememberedServerUrl()
36
- if ClientBroker.forkRole() == "client" then
38
+ if startupRole == "client" then
37
39
  return nil
38
40
  end
39
41
  local rememberedServerUrl = ServerUrlSettings.readServerUrl()
@@ -49,6 +51,15 @@ local function applyRememberedServerUrl()
49
51
  ClientBroker.setServerUrl(rememberedServerUrl)
50
52
  end
51
53
  applyRememberedServerUrl()
54
+ -- Play DataModels do not reliably fire Plugin.Unloading before Studio tears
55
+ -- down their VM. Close the persistent WebStreamClient while BindToClose still
56
+ -- gives this server peer a live execution context; otherwise each play cycle
57
+ -- can retain one native event stream until Studio itself is restarted. The
58
+ -- transport releases that native stream before yielding to unregister the
59
+ -- logical server peer.
60
+ if startupRole == "server" then
61
+ game:BindToClose(StudioEventStream.suspendForShutdown)
62
+ end
52
63
  UI.init(plugin)
53
64
  local elements = UI.getElements()
54
65
  local ICON_DISCONNECTED = "rbxassetid://75876056391496"
@@ -91,7 +102,7 @@ task.delay(TOOLBAR_REGISTRATION_DELAY_SECONDS, registerToolbarButton)
91
102
  -- play peers' plugin instances load without ever registering. Run after a
92
103
  -- short delay so the UI/State have a chance to initialize first.
93
104
  task.delay(2, function()
94
- local role = ClientBroker.forkRole()
105
+ local role = startupRole
95
106
  if role == "edit" then
96
107
  cleanupEditBridgeArtifacts()
97
108
  else
@@ -133,7 +144,10 @@ task.delay(2, function()
133
144
  -- The play-server DM is the only one where StudioTestService:EndTest is
134
145
  -- legal, so the stop-play monitor lives here. It consumes tokenized
135
146
  -- stop requests from plugin settings and acknowledges EndTest results.
136
- StopPlayMonitor.startMonitor()
147
+ StopPlayMonitor.startMonitor({
148
+ beforeEndTest = StudioEventStream.suspendForShutdown,
149
+ afterEndTestFailure = StudioEventStream.resumeAfterShutdownFailure,
150
+ })
137
151
  elseif role == "client" then
138
152
  ClientBroker.setupClientBroker()
139
153
  end
@@ -1348,9 +1362,9 @@ local function computeBridgeStamp()
1348
1362
  for i = 1, #combined do
1349
1363
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1350
1364
  end
1351
- -- "3.0.4" is replaced with the package version at package time
1365
+ -- "3.0.5" is replaced with the package version at package time
1352
1366
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1353
- return `{tostring(h)}-3.0.4`
1367
+ return `{tostring(h)}-3.0.5`
1354
1368
  end
1355
1369
  local BRIDGE_STAMP = computeBridgeStamp()
1356
1370
  local function setSource(scriptInst, source)
@@ -9566,7 +9580,7 @@ return {
9566
9580
  <Properties>
9567
9581
  <string name="Name">State</string>
9568
9582
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
9569
- local CURRENT_VERSION = "3.0.4"
9583
+ local CURRENT_VERSION = "3.0.5"
9570
9584
  local PLUGIN_VARIANT = "main"
9571
9585
  local BASE_PORT = 58741
9572
9586
  local function createConnection(port)
@@ -9646,6 +9660,7 @@ local WAIT_POLL_SEC = 0.1
9646
9660
  local REQUEST_TTL_SEC = 12.0
9647
9661
  local pluginRef
9648
9662
  local endTestIssued = false
9663
+ local transportLifecycle
9649
9664
  local function init(p)
9650
9665
  pluginRef = p
9651
9666
  end
@@ -9789,15 +9804,29 @@ local function handleStopRequest(key, request)
9789
9804
  return nil
9790
9805
  end
9791
9806
  endTestIssued = true
9807
+ local lifecycle = transportLifecycle
9808
+ if lifecycle ~= nil then
9809
+ local closeOk, closeError = pcall(lifecycle.beforeEndTest)
9810
+ if not closeOk then
9811
+ warn(`[robloxstudio-mcp] Failed to close the play-server transport before EndTest: {tostring(closeError)}`)
9812
+ end
9813
+ end
9792
9814
  local endOk, endErr = pcall(function()
9793
9815
  return StudioTestService:EndTest("stopped_by_mcp")
9794
9816
  end)
9795
9817
  writeResult(key, request, endOk, if endOk then nil else tostring(endErr))
9796
9818
  if not endOk then
9797
9819
  endTestIssued = false
9820
+ if lifecycle ~= nil then
9821
+ local restoreOk, restoreError = pcall(lifecycle.afterEndTestFailure)
9822
+ if not restoreOk then
9823
+ warn(`[robloxstudio-mcp] Failed to restore the play-server transport after EndTest failed: {tostring(restoreError)}`)
9824
+ end
9825
+ end
9798
9826
  end
9799
9827
  end
9800
- local function startMonitor()
9828
+ local function startMonitor(lifecycle)
9829
+ transportLifecycle = lifecycle
9801
9830
  if not pluginRef then
9802
9831
  warn("[robloxstudio-mcp] StopPlayMonitor.startMonitor called before init; skipping")
9803
9832
  return nil
@@ -9903,6 +9932,7 @@ local MAX_TERMINAL_RESPONSES = 256
9903
9932
  local STREAM_SILENCE_TIMEOUT_SECONDS = 20
9904
9933
  local options
9905
9934
  local active = false
9935
+ local shutdownSuspended = false
9906
9936
  local generation = 0
9907
9937
  local reconnectAttempt = 0
9908
9938
  local streamClient
@@ -10035,6 +10065,21 @@ local function closeCurrentStream()
10035
10065
  end)
10036
10066
  end
10037
10067
  end
10068
+ local function disconnectSession(currentOptions)
10069
+ pcall(function()
10070
+ return HttpService:RequestAsync({
10071
+ Url = `{currentOptions.serverUrl}/disconnect`,
10072
+ Method = "POST",
10073
+ Headers = {
10074
+ ["Content-Type"] = "application/json",
10075
+ },
10076
+ Body = HttpService:JSONEncode({
10077
+ pluginSessionId = PluginSession.id,
10078
+ timestamp = tick(),
10079
+ }),
10080
+ })
10081
+ end)
10082
+ end
10038
10083
  local function responseRetryDelay(attempt)
10039
10084
  return math.min(INITIAL_RESPONSE_RETRY_DELAY_SECONDS * math.pow(2, math.max(attempt - 1, 0)), MAX_RESPONSE_RETRY_DELAY_SECONDS)
10040
10085
  end
@@ -10523,11 +10568,12 @@ function connect(expectedGeneration)
10523
10568
  end
10524
10569
  local stop
10525
10570
  local function start(newOptions)
10526
- if active then
10571
+ if active or shutdownSuspended then
10527
10572
  stop()
10528
10573
  end
10529
10574
  options = newOptions
10530
10575
  active = true
10576
+ shutdownSuspended = false
10531
10577
  reconnectAttempt = 0
10532
10578
  generation += 1
10533
10579
  connect(generation)
@@ -10541,12 +10587,39 @@ function refresh()
10541
10587
  reconnectAttempt = 0
10542
10588
  connect(generation)
10543
10589
  end
10590
+ -- EndTest can tear down a play DataModel without giving Plugin.Unloading a
10591
+ -- usable execution window. Release the native WebStreamClient before the
10592
+ -- yielding unregister request, but retain local request state and connection
10593
+ -- options so a failed EndTest can register again and reconnect.
10594
+ local function suspendForShutdown()
10595
+ local currentOptions = options
10596
+ if not active or currentOptions == nil then
10597
+ return nil
10598
+ end
10599
+ active = false
10600
+ shutdownSuspended = true
10601
+ generation += 1
10602
+ reconnectAttempt = 0
10603
+ closeCurrentStream()
10604
+ disconnectSession(currentOptions)
10605
+ end
10606
+ local function resumeAfterShutdownFailure()
10607
+ if not shutdownSuspended or options == nil then
10608
+ return nil
10609
+ end
10610
+ shutdownSuspended = false
10611
+ active = true
10612
+ generation += 1
10613
+ reconnectAttempt = 0
10614
+ connect(generation)
10615
+ end
10544
10616
  function stop()
10545
- if not active then
10617
+ if not active and not shutdownSuspended then
10546
10618
  return nil
10547
10619
  end
10548
10620
  local currentOptions = options
10549
10621
  active = false
10622
+ shutdownSuspended = false
10550
10623
  generation += 1
10551
10624
  for _, inFlight in inFlightRequests do
10552
10625
  inFlight.cancelled = true
@@ -10558,24 +10631,14 @@ function stop()
10558
10631
  options = nil
10559
10632
  reconnectAttempt = 0
10560
10633
  if currentOptions ~= nil then
10561
- pcall(function()
10562
- return HttpService:RequestAsync({
10563
- Url = `{currentOptions.serverUrl}/disconnect`,
10564
- Method = "POST",
10565
- Headers = {
10566
- ["Content-Type"] = "application/json",
10567
- },
10568
- Body = HttpService:JSONEncode({
10569
- pluginSessionId = PluginSession.id,
10570
- timestamp = tick(),
10571
- }),
10572
- })
10573
- end)
10634
+ disconnectSession(currentOptions)
10574
10635
  end
10575
10636
  end
10576
10637
  return {
10577
10638
  start = start,
10578
10639
  refresh = refresh,
10640
+ suspendForShutdown = suspendForShutdown,
10641
+ resumeAfterShutdownFailure = resumeAfterShutdownFailure,
10579
10642
  stop = stop,
10580
10643
  }
10581
10644
  ]]></string>