@chrrxs/robloxstudio-mcp 3.0.1 → 3.0.2

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.1",
3
+ "version": "3.0.2",
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",
@@ -616,15 +616,33 @@ local function pollProxy(proxyId, player, rf)
616
616
  unregisterProxy(player)
617
617
  break
618
618
  end
619
+ local nextPollDelay = 0.5
619
620
  local ok, res = pcall(function()
620
- return HttpService:RequestAsync({
621
- Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
621
+ local requestOptions = {
622
+ Url = `{mcpUrl}/poll?pluginSessionId={proxyId}&pollMode=long`,
622
623
  Method = "GET",
623
624
  Headers = {
624
625
  ["Content-Type"] = "application/json",
625
626
  },
626
- })
627
+ Timeout = State.POLL_REQUEST_TIMEOUT_SECONDS,
628
+ }
629
+ return HttpService:RequestAsync(requestOptions)
627
630
  end)
631
+ -- RequestAsync may yield for the full long-poll window. Never apply a
632
+ -- stale response to a player whose proxy was removed or replaced.
633
+ local _player = player
634
+ local currentProxy = proxyByPlayer[_player]
635
+ local _condition_1 = player.Parent == nil
636
+ if not _condition_1 then
637
+ local _result = currentProxy
638
+ if _result ~= nil then
639
+ _result = _result.pluginSessionId
640
+ end
641
+ _condition_1 = _result ~= proxyId
642
+ end
643
+ if _condition_1 then
644
+ break
645
+ end
628
646
  if ok and res and (res.Success or res.StatusCode == 503) then
629
647
  local okJson, body = pcall(function()
630
648
  return HttpService:JSONDecode(res.Body)
@@ -674,9 +692,17 @@ local function pollProxy(proxyId, player, rf)
674
692
  response = response,
675
693
  })
676
694
  end
695
+ if res.Success and body.pollMode == "long" and body.knownInstance ~= false and body.request == nil then
696
+ nextPollDelay = 0
697
+ end
677
698
  end
678
699
  end
679
- task.wait(0.5)
700
+ if nextPollDelay > 0 then
701
+ task.wait(nextPollDelay)
702
+ else
703
+ -- Yield one scheduler turn before replacing a completed long poll.
704
+ task.wait()
705
+ end
680
706
  end
681
707
  end
682
708
  local function registerProxy(player, rf)
@@ -831,6 +857,7 @@ local lastVersionMismatchWarningKey
831
857
  local lastReadyInstanceId
832
858
  local readyFailureLogKeys = {}
833
859
  local retryingDuplicateReady = false
860
+ local pollEpoch = 0
834
861
  -- Cache the published place name from MarketplaceService:GetProductInfo so
835
862
  -- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
836
863
  -- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
@@ -1115,16 +1142,24 @@ local function pollForRequests()
1115
1142
  if conn.isPolling then
1116
1143
  return nil
1117
1144
  end
1145
+ local epoch = pollEpoch
1118
1146
  conn.isPolling = true
1119
1147
  local success, result = pcall(function()
1120
- return HttpService:RequestAsync({
1121
- Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}`,
1148
+ local requestOptions = {
1149
+ Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}&pollMode=long`,
1122
1150
  Method = "GET",
1123
1151
  Headers = {
1124
1152
  ["Content-Type"] = "application/json",
1125
1153
  },
1126
- })
1154
+ Timeout = State.POLL_REQUEST_TIMEOUT_SECONDS,
1155
+ }
1156
+ return HttpService:RequestAsync(requestOptions)
1127
1157
  end)
1158
+ -- A held request can finish after deactivate/reactivate. Only the current
1159
+ -- lifecycle epoch may mutate connection state or clear a newer poll's lock.
1160
+ if epoch ~= pollEpoch or not conn.isActive then
1161
+ return nil
1162
+ end
1128
1163
  conn.isPolling = false
1129
1164
  local ui = UI.getElements()
1130
1165
  UI.updateToolbarIcon()
@@ -1133,6 +1168,12 @@ local function pollForRequests()
1133
1168
  conn.currentRetryDelay = 0.5
1134
1169
  conn.lastSuccessfulConnection = tick()
1135
1170
  local data = HttpService:JSONDecode(result.Body)
1171
+ conn.lastPoll = tick()
1172
+ if result.Success and data.pollMode == "long" and data.knownInstance ~= false and data.request == nil then
1173
+ -- The server held this request until work or timeout, so immediately
1174
+ -- replace it instead of reintroducing the legacy 500 ms idle gap.
1175
+ conn.lastPoll = 0
1176
+ end
1136
1177
  local mcpConnected = data.mcpConnected == true
1137
1178
  conn.lastHttpOk = true
1138
1179
  conn.lastMcpOk = mcpConnected
@@ -1223,6 +1264,7 @@ local function pollForRequests()
1223
1264
  end)
1224
1265
  end
1225
1266
  elseif conn.isActive then
1267
+ conn.lastPoll = tick()
1226
1268
  conn.consecutiveFailures += 1
1227
1269
  if conn.consecutiveFailures > 1 then
1228
1270
  conn.currentRetryDelay = math.min(conn.currentRetryDelay * conn.retryBackoffMultiplier, conn.maxRetryDelay)
@@ -1287,6 +1329,9 @@ local deactivatePlugin
1287
1329
  local function activatePlugin()
1288
1330
  local conn = State.getActiveConnection()
1289
1331
  local ui = UI.getElements()
1332
+ pollEpoch += 1
1333
+ conn.isPolling = false
1334
+ conn.lastPoll = 0
1290
1335
  conn.isActive = true
1291
1336
  conn.consecutiveFailures = 0
1292
1337
  conn.currentRetryDelay = 0.5
@@ -1335,6 +1380,8 @@ local function activatePlugin()
1335
1380
  end
1336
1381
  function deactivatePlugin()
1337
1382
  local conn = State.getActiveConnection()
1383
+ pollEpoch += 1
1384
+ conn.isPolling = false
1338
1385
  conn.isActive = false
1339
1386
  conn.lastMcpOk = false
1340
1387
  UI.updateUIState()
@@ -1528,9 +1575,9 @@ local function computeBridgeStamp()
1528
1575
  for i = 1, #combined do
1529
1576
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1530
1577
  end
1531
- -- "3.0.1" is replaced with the package version at package time
1578
+ -- "3.0.2" is replaced with the package version at package time
1532
1579
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1533
- return `{tostring(h)}-3.0.1`
1580
+ return `{tostring(h)}-3.0.2`
1534
1581
  end
1535
1582
  local BRIDGE_STAMP = computeBridgeStamp()
1536
1583
  local function setSource(scriptInst, source)
@@ -3600,11 +3647,14 @@ local function getAttributes(requestData)
3600
3647
  }
3601
3648
  count += 1
3602
3649
  end
3603
- return {
3650
+ local response = {
3604
3651
  instancePath = instancePath,
3605
- attributes = serializedAttributes,
3606
3652
  count = count,
3607
3653
  }
3654
+ if count > 0 then
3655
+ response.attributes = serializedAttributes
3656
+ end
3657
+ return response
3608
3658
  end)
3609
3659
  if success then
3610
3660
  return result
@@ -6152,7 +6202,6 @@ local function getProjectStructure(requestData)
6152
6202
  name = instance.Name,
6153
6203
  className = instance.ClassName,
6154
6204
  path = getInstancePath(instance),
6155
- children = {},
6156
6205
  }
6157
6206
  if instance:IsA("LuaSourceContainer") then
6158
6207
  node.hasSource = true
@@ -6192,7 +6241,7 @@ local function getProjectStructure(requestData)
6192
6241
  -- ▲ ReadonlyArray.filter ▲
6193
6242
  children = _newValue
6194
6243
  end
6195
- local nodeChildren = node.children
6244
+ local nodeChildren = {}
6196
6245
  local childCount = #children
6197
6246
  if childCount > 20 and depth < maxDepth then
6198
6247
  local classGroups = {}
@@ -6267,6 +6316,9 @@ local function getProjectStructure(requestData)
6267
6316
  table.insert(nodeChildren, _arg0)
6268
6317
  end
6269
6318
  end
6319
+ if #nodeChildren > 0 then
6320
+ node.children = nodeChildren
6321
+ end
6270
6322
  return node
6271
6323
  end
6272
6324
  local result = getStructure(startInstance, 0)
@@ -6819,16 +6871,6 @@ local finishRecording = _binding_1.finishRecording
6819
6871
  local SOURCE_TRUNCATE_CHAR_BUDGET = 25000
6820
6872
  local SOURCE_TRUNCATE_LINE_BUDGET = 400
6821
6873
  local SOURCE_TRUNCATE_TO_LINES = 300
6822
- local function normalizeEscapes(s)
6823
- local result = s
6824
- result = (string.gsub(result, "\\\\", "\x01"))
6825
- result = (string.gsub(result, "\\n", "\n"))
6826
- result = (string.gsub(result, "\\t", "\t"))
6827
- result = (string.gsub(result, "\\r", "\r"))
6828
- result = (string.gsub(result, '\\"', '"'))
6829
- result = (string.gsub(result, "\x01", "\\"))
6830
- return result
6831
- end
6832
6874
  local function getTopServiceName(instance)
6833
6875
  local topServiceInst = instance
6834
6876
  while topServiceInst.Parent and topServiceInst.Parent ~= game do
@@ -6972,7 +7014,8 @@ local function setScriptSource(requestData)
6972
7014
  error = `Instance is not a script-like object: {instance.ClassName}`,
6973
7015
  }
6974
7016
  end
6975
- local sourceToSet = normalizeEscapes(newSource)
7017
+ -- Communication has already JSON-decoded the poll payload; source text is exact at this boundary.
7018
+ local sourceToSet = newSource
6976
7019
  local recordingId = beginRecording(`Set script source: {instance.Name}`)
6977
7020
  local readSuccess, readResult = pcall(function()
6978
7021
  return #readScriptSource(instance)
@@ -6996,40 +7039,9 @@ local function setScriptSource(requestData)
6996
7039
  message = `Script source updated successfully ({if applyResult.method == "UpdateSourceAsync" then "editor-safe" else "direct assignment"})`,
6997
7040
  }
6998
7041
  end
6999
- local replaceSuccess, replaceResult = pcall(function()
7000
- local parent = instance.Parent
7001
- local name = instance.Name
7002
- local className = instance.ClassName
7003
- local wasBaseScript = instance:IsA("BaseScript")
7004
- local enabled = if wasBaseScript then instance.Enabled else nil
7005
- local newScript = Instance.new(className)
7006
- newScript.Name = name
7007
- -- @rbxts/types does not expose PluginSecurity Source writes.
7008
- local writableNewScript = newScript
7009
- writableNewScript.Source = sourceToSet
7010
- if readScriptSource(newScript) ~= sourceToSet then
7011
- error("Replacement script source did not match the requested source")
7012
- end
7013
- if wasBaseScript and enabled ~= nil then
7014
- local newBaseScript = newScript
7015
- newBaseScript.Enabled = enabled
7016
- end
7017
- newScript.Parent = parent
7018
- instance:Destroy()
7019
- return {
7020
- success = true,
7021
- instancePath = getInstancePath(newScript),
7022
- method = "replace",
7023
- message = "Script replaced successfully with new source",
7024
- }
7025
- end)
7026
- if replaceSuccess then
7027
- finishRecording(recordingId, true)
7028
- return replaceResult
7029
- end
7030
7042
  finishRecording(recordingId, false)
7031
7043
  return {
7032
- error = `Failed to set script source. {applyResult.error} Replace method failed: {replaceResult}`,
7044
+ error = `Failed to set script source: {applyResult.error}`,
7033
7045
  }
7034
7046
  end
7035
7047
  local function editScriptLines(requestData)
@@ -7042,8 +7054,6 @@ local function editScriptLines(requestData)
7042
7054
  error = "Instance path, old_string, and new_string are required",
7043
7055
  }
7044
7056
  end
7045
- oldString = normalizeEscapes(oldString)
7046
- newString = normalizeEscapes(newString)
7047
7057
  local instance = getInstanceByPath(instancePath)
7048
7058
  if not instance then
7049
7059
  return {
@@ -7140,7 +7150,6 @@ local function insertScriptLines(requestData)
7140
7150
  error = "Instance path and newContent are required",
7141
7151
  }
7142
7152
  end
7143
- newContent = normalizeEscapes(newContent)
7144
7153
  local instance = getInstanceByPath(instancePath)
7145
7154
  if not instance then
7146
7155
  return {
@@ -9564,9 +9573,10 @@ return {
9564
9573
  <Properties>
9565
9574
  <string name="Name">State</string>
9566
9575
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
9567
- local CURRENT_VERSION = "3.0.1"
9576
+ local CURRENT_VERSION = "3.0.2"
9568
9577
  local PLUGIN_VARIANT = "main"
9569
9578
  local BASE_PORT = 58741
9579
+ local POLL_REQUEST_TIMEOUT_SECONDS = 20
9570
9580
  local function createConnection(port)
9571
9581
  return {
9572
9582
  port = port,
@@ -9595,6 +9605,7 @@ return {
9595
9605
  CURRENT_VERSION = CURRENT_VERSION,
9596
9606
  PLUGIN_VARIANT = PLUGIN_VARIANT,
9597
9607
  BASE_PORT = BASE_PORT,
9608
+ POLL_REQUEST_TIMEOUT_SECONDS = POLL_REQUEST_TIMEOUT_SECONDS,
9598
9609
  getActiveConnection = getActiveConnection,
9599
9610
  }
9600
9611
  ]]></string>