@chrrxs/robloxstudio-mcp 3.1.0 → 3.1.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
@@ -6116,7 +6116,7 @@ var TOOL_HANDLERS = {
6116
6116
  manage_instance: (tools, body) => tools.manageInstance(body),
6117
6117
  solo_playtest: (tools, body) => tools.soloPlaytest(body.action, body.mode, body.timeout, body.instance_id),
6118
6118
  multiplayer_playtest: (tools, body) => tools.multiplayerPlaytest(body.action, body.numPlayers, body.target, body.testArgs, body.value, body.timeout, body.instance_id),
6119
- get_runtime_logs: (tools, body) => tools.getRuntimeLogs(body.instance_id, body.multiplayer_group_id, body.cursor, body.cursor_by_instance, body.tail, body.filter),
6119
+ get_runtime_logs: (tools, body, context) => tools.getRuntimeLogs(body.instance_id, body.multiplayer_group_id, body.cursor, body.cursor_by_instance, body.tail, body.filter, context?.signal),
6120
6120
  capture_script_profiler: (tools, body) => tools.captureScriptProfiler(body.target, {
6121
6121
  duration_ms: body.duration_ms,
6122
6122
  frequency: body.frequency,
@@ -9506,6 +9506,7 @@ var MAX_DEVICE_MATRIX_ENTRIES = 6;
9506
9506
  var MAX_NETWORK_PACKET_LOSS_PERCENT = 0.5;
9507
9507
  var GREP_SCRIPTS_TIMEOUT_MS = 12e4;
9508
9508
  var MAX_GREP_PATTERN_UTF8_BYTES = 4096;
9509
+ var RUNTIME_LOG_PEER_TIMEOUT_MS = 5e3;
9509
9510
  var STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL = "Studio Assistant Source Image";
9510
9511
  var CREATOR_STORE_SEARCH_TYPES = /* @__PURE__ */ new Set([
9511
9512
  "Audio",
@@ -11367,7 +11368,7 @@ var RobloxStudioTools = class {
11367
11368
  ]
11368
11369
  };
11369
11370
  }
11370
- async getRuntimeLogs(instance_id, multiplayer_group_id, cursor, cursor_by_instance, tail, filter) {
11371
+ async getRuntimeLogs(instance_id, multiplayer_group_id, cursor, cursor_by_instance, tail, filter, signal) {
11371
11372
  if (instance_id !== void 0 && multiplayer_group_id !== void 0) {
11372
11373
  throw new Error("get_runtime_logs accepts only one of instance_id or multiplayer_group_id.");
11373
11374
  }
@@ -11511,7 +11512,7 @@ var RobloxStudioTools = class {
11511
11512
  if (filter !== void 0)
11512
11513
  data.filter = filter;
11513
11514
  try {
11514
- const responseValue = await this.client.request("/api/get-runtime-logs", data, peer.peerId);
11515
+ const responseValue = await this.client.request("/api/get-runtime-logs", data, peer.peerId, RUNTIME_LOG_PEER_TIMEOUT_MS, signal);
11515
11516
  if (typeof responseValue !== "object" || responseValue === null || Array.isArray(responseValue)) {
11516
11517
  return {
11517
11518
  peerId: peer.peerId,
@@ -11538,6 +11539,8 @@ var RobloxStudioTools = class {
11538
11539
  nextSince: response.nextSince
11539
11540
  };
11540
11541
  } catch (error) {
11542
+ if (signal?.aborted)
11543
+ throw error;
11541
11544
  return { peerId: peer.peerId, role: peer.role, error: errorMessage(error) };
11542
11545
  }
11543
11546
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
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",
@@ -1472,9 +1472,9 @@ local function computeBridgeStamp()
1472
1472
  for i = 1, #combined do
1473
1473
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1474
1474
  end
1475
- -- "3.1.0" is replaced with the package version at package time
1475
+ -- "3.1.1" is replaced with the package version at package time
1476
1476
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1477
- return `{tostring(h)}-3.1.0`
1477
+ return `{tostring(h)}-3.1.1`
1478
1478
  end
1479
1479
  local BRIDGE_STAMP = computeBridgeStamp()
1480
1480
  local function setSource(scriptInst, source)
@@ -9736,7 +9736,7 @@ return {
9736
9736
  <Properties>
9737
9737
  <string name="Name">State</string>
9738
9738
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
9739
- local CURRENT_VERSION = "3.1.0"
9739
+ local CURRENT_VERSION = "3.1.1"
9740
9740
  local PLUGIN_VARIANT = "main"
9741
9741
  local BASE_PORT = 58741
9742
9742
  local function createConnection(port)
@@ -10013,6 +10013,8 @@ local INITIAL_RESPONSE_RETRY_DELAY_SECONDS = 0.5
10013
10013
  local MAX_RESPONSE_RETRY_DELAY_SECONDS = 5
10014
10014
  local MAX_TERMINAL_RESPONSES = 256
10015
10015
  local STREAM_SILENCE_TIMEOUT_SECONDS = 20
10016
+ -- The bridge accepts request bodies up to 50 MiB; leave room for its envelope.
10017
+ local MAX_STREAM_FRAME_BYTES = 64 * 1024 * 1024
10016
10018
  local options
10017
10019
  local active = false
10018
10020
  local shutdownSuspended = false
@@ -10026,15 +10028,13 @@ local pendingResponses = {}
10026
10028
  local terminalResponseIds = {}
10027
10029
  local terminalResponseOrder = {}
10028
10030
  local readyFailureLogKeys = {}
10029
- local function decodeMessage(message)
10030
- -- Studio versions in the supported channel have surfaced either the SSE
10031
- -- data payload or the complete single-line `data:` frame. The bridge emits
10032
- -- one JSON data line per event, so normalize both forms before decoding.
10033
- local payload = message
10034
- local normalized = (string.gsub((string.gsub(message, "\r\n", "\n")), "\r", "\n"))
10035
- if string.sub(normalized, 1, 5) == "data:" then
10036
- payload = (string.gsub((string.gsub(string.sub(normalized, 6), "^%s+", "")), "%s+$", ""))
10037
- end
10031
+ local streamLineParts = {}
10032
+ local streamDataLines = {}
10033
+ local streamFrameBytes = 0
10034
+ local streamLineHasContent = false
10035
+ local streamDiscardingFrame = false
10036
+ local streamSkipLf = false
10037
+ local function decodeMessage(payload)
10038
10038
  local decodeOk, decoded = pcall(function()
10039
10039
  return HttpService:JSONDecode(payload)
10040
10040
  end)
@@ -10135,9 +10135,123 @@ local function decodeMessage(message)
10135
10135
  end
10136
10136
  return nil
10137
10137
  end
10138
+ local function resetStreamFrame()
10139
+ streamLineParts = {}
10140
+ streamDataLines = {}
10141
+ streamFrameBytes = 0
10142
+ streamLineHasContent = false
10143
+ streamDiscardingFrame = false
10144
+ end
10145
+ local function finishStreamLine(events)
10146
+ if not streamLineHasContent then
10147
+ if not streamDiscardingFrame and #streamDataLines > 0 then
10148
+ local event = decodeMessage(table.concat(streamDataLines, "\n"))
10149
+ if event ~= nil then
10150
+ table.insert(events, event)
10151
+ end
10152
+ end
10153
+ resetStreamFrame()
10154
+ return nil
10155
+ end
10156
+ if not streamDiscardingFrame then
10157
+ local line = table.concat(streamLineParts, "")
10158
+ if string.sub(line, 1, 5) == "data:" then
10159
+ local start = if string.sub(line, 6, 6) == " " then 7 else 6
10160
+ local _streamDataLines = streamDataLines
10161
+ local _arg0 = string.sub(line, start)
10162
+ table.insert(_streamDataLines, _arg0)
10163
+ elseif line == "data" then
10164
+ table.insert(streamDataLines, "")
10165
+ end
10166
+ end
10167
+ streamLineParts = {}
10168
+ streamLineHasContent = false
10169
+ end
10170
+ -- MessageReceived may coalesce frames (notably server/client fanout) or split
10171
+ -- them across callbacks. Callback boundaries are not SSE event boundaries.
10172
+ local function decodeMessages(message)
10173
+ local events = {}
10174
+ local messageBytes = #message
10175
+ if messageBytes == 0 then
10176
+ return events
10177
+ end
10178
+ -- Some Studio versions deliver the decoded SSE data or a complete data line
10179
+ -- without delimiters. Only recognize those forms with no unfinished frame;
10180
+ -- once framing starts, callback boundaries never terminate partial data.
10181
+ if streamFrameBytes == 0 and not streamDiscardingFrame and messageBytes <= MAX_STREAM_FRAME_BYTES then
10182
+ local first = string.sub(message, 1, 1)
10183
+ if first == "{" or first == " " or first == "\t" or first == "\r" or first == "\n" then
10184
+ local event = decodeMessage(message)
10185
+ if event ~= nil then
10186
+ streamSkipLf = false
10187
+ table.insert(events, event)
10188
+ return events
10189
+ end
10190
+ if first == "{" then
10191
+ return events
10192
+ end
10193
+ end
10194
+ if string.sub(message, 1, 5) == "data:" and (string.find(message, "[\r\n]")) == nil then
10195
+ local event = decodeMessage(string.sub(message, 6))
10196
+ if event ~= nil then
10197
+ streamSkipLf = false
10198
+ table.insert(events, event)
10199
+ return events
10200
+ end
10201
+ end
10202
+ end
10203
+ local offset = 1
10204
+ while offset <= messageBytes do
10205
+ if streamSkipLf then
10206
+ streamSkipLf = false
10207
+ local _message = message
10208
+ local _offset = offset
10209
+ local _offset_1 = offset
10210
+ if string.sub(_message, _offset, _offset_1) == "\n" then
10211
+ offset += 1
10212
+ continue
10213
+ end
10214
+ end
10215
+ local _message = message
10216
+ local _offset = offset
10217
+ local newline = string.find(_message, "[\r\n]", _offset)
10218
+ local lineEnd = if newline == nil then messageBytes + 1 else newline
10219
+ local partBytes = lineEnd - offset
10220
+ if partBytes > 0 then
10221
+ streamLineHasContent = true
10222
+ end
10223
+ if not streamDiscardingFrame then
10224
+ streamFrameBytes += partBytes + (if newline == nil then 0 else 1)
10225
+ if streamFrameBytes > MAX_STREAM_FRAME_BYTES then
10226
+ -- Keep scanning delimiters but retain no oversized frame bytes.
10227
+ streamDiscardingFrame = true
10228
+ streamLineParts = {}
10229
+ streamDataLines = {}
10230
+ elseif partBytes > 0 then
10231
+ -- Join only on a line boundary, not every callback (large scripts
10232
+ -- can arrive in many fragments).
10233
+ local _streamLineParts = streamLineParts
10234
+ local _message_1 = message
10235
+ local _offset_1 = offset
10236
+ local _arg1 = lineEnd - 1
10237
+ local _arg0 = string.sub(_message_1, _offset_1, _arg1)
10238
+ table.insert(_streamLineParts, _arg0)
10239
+ end
10240
+ end
10241
+ if newline == nil then
10242
+ break
10243
+ end
10244
+ finishStreamLine(events)
10245
+ streamSkipLf = string.sub(message, newline, newline) == "\r"
10246
+ offset = newline + 1
10247
+ end
10248
+ return events
10249
+ end
10138
10250
  local function closeCurrentStream()
10139
10251
  local current = streamClient
10140
10252
  streamClient = nil
10253
+ resetStreamFrame()
10254
+ streamSkipLf = false
10141
10255
  for _, connection in streamConnections do
10142
10256
  connection:Disconnect()
10143
10257
  end
@@ -10616,30 +10730,33 @@ function connect(expectedGeneration)
10616
10730
  if not active or generation ~= expectedGeneration or streamClient ~= createdClient then
10617
10731
  return nil
10618
10732
  end
10619
- local event = decodeMessage(message)
10620
- if event == nil then
10621
- return nil
10622
- end
10623
- lastValidEventAt = tick()
10624
- if event.kind == "heartbeat" then
10625
- invokeCallback("event stream heartbeat", function()
10626
- return currentOptions.onHeartbeat(event.timestamp)
10733
+ for _, event in decodeMessages(message) do
10734
+ -- Status callbacks can refresh/stop the stream. The remainder of
10735
+ -- that callback belongs to the old connection, not its successor.
10736
+ if not active or generation ~= expectedGeneration or streamClient ~= createdClient then
10737
+ return nil
10738
+ end
10739
+ lastValidEventAt = tick()
10740
+ if event.kind == "heartbeat" then
10741
+ invokeCallback("event stream heartbeat", function()
10742
+ return currentOptions.onHeartbeat(event.timestamp)
10743
+ end)
10744
+ continue
10745
+ end
10746
+ if event.kind == "cancel" then
10747
+ cancelRequest(event)
10748
+ continue
10749
+ end
10750
+ if event.kind == "request" then
10751
+ dispatchRequest(event)
10752
+ continue
10753
+ end
10754
+ invokeCallback("event stream status", function()
10755
+ return currentOptions.onStatus(event)
10627
10756
  end)
10628
- return nil
10629
- end
10630
- if event.kind == "cancel" then
10631
- cancelRequest(event)
10632
- return nil
10633
- end
10634
- if event.kind == "request" then
10635
- dispatchRequest(event)
10636
- return nil
10637
- end
10638
- invokeCallback("event stream status", function()
10639
- return currentOptions.onStatus(event)
10640
- end)
10641
- if not event.knownPeer then
10642
- refresh()
10757
+ if not event.knownPeer then
10758
+ refresh()
10759
+ end
10643
10760
  end
10644
10761
  end), createdClient.Error:Connect(function(statusCode, message)
10645
10762
  if not active or generation ~= expectedGeneration or streamClient ~= createdClient then