@chrrxs/robloxstudio-mcp-inspector 2.22.1 → 2.22.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-inspector",
3
- "version": "2.22.1",
3
+ "version": "2.22.2",
4
4
  "description": "Read-only MCP server for inspecting and debugging Roblox Studio from AI coding tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -843,19 +843,33 @@ local function processRequest(request)
843
843
  end
844
844
  end
845
845
  local function sendResponse(conn, requestId, responseData)
846
- pcall(function()
847
- HttpService:RequestAsync({
848
- Url = `{conn.serverUrl}/response`,
846
+ local responseUrl = `{conn.serverUrl}/response`
847
+ local encodeOk, encoded = pcall(function()
848
+ return HttpService:JSONEncode({
849
+ requestId = requestId,
850
+ response = responseData,
851
+ })
852
+ end)
853
+ local body = if encodeOk then encoded else HttpService:JSONEncode({
854
+ requestId = requestId,
855
+ error = `Plugin response serialization failed: {tostring(encoded)}`,
856
+ })
857
+ if not encodeOk then
858
+ warn(`[robloxstudio-mcp] Failed to serialize response {requestId}: {tostring(encoded)}`)
859
+ end
860
+ local requestOk, requestResult = pcall(function()
861
+ return HttpService:RequestAsync({
862
+ Url = responseUrl,
849
863
  Method = "POST",
850
864
  Headers = {
851
865
  ["Content-Type"] = "application/json",
852
866
  },
853
- Body = HttpService:JSONEncode({
854
- requestId = requestId,
855
- response = responseData,
856
- }),
867
+ Body = body,
857
868
  })
858
869
  end)
870
+ if not requestOk or not requestResult.Success then
871
+ warn(`[robloxstudio-mcp] Failed to deliver response {requestId}: {HttpDiagnostics.formatRequestFailure(responseUrl, requestOk, requestResult)}`)
872
+ end
859
873
  end
860
874
  local function getConnectionStatus()
861
875
  local conn = State.getActiveConnection()
@@ -1425,9 +1439,9 @@ local function computeBridgeStamp()
1425
1439
  for i = 1, #combined do
1426
1440
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1427
1441
  end
1428
- -- "2.22.1" is replaced with the package version at package time
1442
+ -- "2.22.2" is replaced with the package version at package time
1429
1443
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1430
- return `{tostring(h)}-2.22.1`
1444
+ return `{tostring(h)}-2.22.2`
1431
1445
  end
1432
1446
  local BRIDGE_STAMP = computeBridgeStamp()
1433
1447
  local function setSource(scriptInst, source)
@@ -10437,6 +10451,41 @@ end
10437
10451
  local function nowSec()
10438
10452
  return DateTime.now().UnixTimestampMillis / 1000
10439
10453
  end
10454
+ -- Studio occasionally exposes binary-bearing Output messages through
10455
+ -- LogService (for example, plugin hydration diagnostics containing raw CSG
10456
+ -- data). HttpService:JSONEncode rejects those strings outright. Preserve all
10457
+ -- valid UTF-8 verbatim and make only malformed bytes JSON-safe and visible.
10458
+ local function escapeInvalidUtf8(msg)
10459
+ local valid = utf8.len(msg)
10460
+ -- Roblox currently returns nil (not the false declared by @rbxts/types)
10461
+ -- when it encounters a malformed sequence. A numeric result is the only
10462
+ -- portable success discriminator across both representations.
10463
+ if type(valid) == "number" then
10464
+ return msg
10465
+ end
10466
+ local parts = {}
10467
+ local cursor = 1
10468
+ while cursor <= #msg do
10469
+ local suffixValid, invalidPosition = utf8.len(msg, cursor)
10470
+ if type(suffixValid) == "number" then
10471
+ local _arg0 = string.sub(msg, cursor)
10472
+ table.insert(parts, _arg0)
10473
+ break
10474
+ end
10475
+ if not (type(invalidPosition) == "number") then
10476
+ break
10477
+ end
10478
+ if invalidPosition > cursor then
10479
+ local _arg0 = string.sub(msg, cursor, invalidPosition - 1)
10480
+ table.insert(parts, _arg0)
10481
+ end
10482
+ local invalidByte = string.byte(msg, invalidPosition)
10483
+ local _arg0 = string.format("\\x%02X", invalidByte)
10484
+ table.insert(parts, _arg0)
10485
+ cursor = invalidPosition + 1
10486
+ end
10487
+ return table.concat(parts, "")
10488
+ end
10440
10489
  local function dropOldestUntilFits(incomingBytes)
10441
10490
  while #entries > 0 and (totalBytes + incomingBytes > MAX_BYTES or #entries >= HARD_ENTRY_CAP) do
10442
10491
  local dropped = table.remove(entries, 1)
@@ -10448,13 +10497,14 @@ local function pushEntry(msg, t, ts)
10448
10497
  if ts == nil then
10449
10498
  ts = nowSec()
10450
10499
  end
10451
- local bytes = #msg
10500
+ local safeMessage = escapeInvalidUtf8(msg)
10501
+ local bytes = #safeMessage
10452
10502
  dropOldestUntilFits(bytes)
10453
10503
  local _arg0 = {
10454
10504
  seq = nextSeq,
10455
10505
  ts = ts,
10456
10506
  level = levelTag(t),
10457
- message = msg,
10507
+ message = safeMessage,
10458
10508
  }
10459
10509
  table.insert(entries, _arg0)
10460
10510
  nextSeq += 1
@@ -10763,7 +10813,7 @@ return {
10763
10813
  <Properties>
10764
10814
  <string name="Name">State</string>
10765
10815
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
10766
- local CURRENT_VERSION = "2.22.1"
10816
+ local CURRENT_VERSION = "2.22.2"
10767
10817
  local PLUGIN_VARIANT = "inspector"
10768
10818
  local BASE_PORT = 58741
10769
10819
  local function createConnection(port)
@@ -843,19 +843,33 @@ local function processRequest(request)
843
843
  end
844
844
  end
845
845
  local function sendResponse(conn, requestId, responseData)
846
- pcall(function()
847
- HttpService:RequestAsync({
848
- Url = `{conn.serverUrl}/response`,
846
+ local responseUrl = `{conn.serverUrl}/response`
847
+ local encodeOk, encoded = pcall(function()
848
+ return HttpService:JSONEncode({
849
+ requestId = requestId,
850
+ response = responseData,
851
+ })
852
+ end)
853
+ local body = if encodeOk then encoded else HttpService:JSONEncode({
854
+ requestId = requestId,
855
+ error = `Plugin response serialization failed: {tostring(encoded)}`,
856
+ })
857
+ if not encodeOk then
858
+ warn(`[robloxstudio-mcp] Failed to serialize response {requestId}: {tostring(encoded)}`)
859
+ end
860
+ local requestOk, requestResult = pcall(function()
861
+ return HttpService:RequestAsync({
862
+ Url = responseUrl,
849
863
  Method = "POST",
850
864
  Headers = {
851
865
  ["Content-Type"] = "application/json",
852
866
  },
853
- Body = HttpService:JSONEncode({
854
- requestId = requestId,
855
- response = responseData,
856
- }),
867
+ Body = body,
857
868
  })
858
869
  end)
870
+ if not requestOk or not requestResult.Success then
871
+ warn(`[robloxstudio-mcp] Failed to deliver response {requestId}: {HttpDiagnostics.formatRequestFailure(responseUrl, requestOk, requestResult)}`)
872
+ end
859
873
  end
860
874
  local function getConnectionStatus()
861
875
  local conn = State.getActiveConnection()
@@ -1425,9 +1439,9 @@ local function computeBridgeStamp()
1425
1439
  for i = 1, #combined do
1426
1440
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1427
1441
  end
1428
- -- "2.22.1" is replaced with the package version at package time
1442
+ -- "2.22.2" is replaced with the package version at package time
1429
1443
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1430
- return `{tostring(h)}-2.22.1`
1444
+ return `{tostring(h)}-2.22.2`
1431
1445
  end
1432
1446
  local BRIDGE_STAMP = computeBridgeStamp()
1433
1447
  local function setSource(scriptInst, source)
@@ -10437,6 +10451,41 @@ end
10437
10451
  local function nowSec()
10438
10452
  return DateTime.now().UnixTimestampMillis / 1000
10439
10453
  end
10454
+ -- Studio occasionally exposes binary-bearing Output messages through
10455
+ -- LogService (for example, plugin hydration diagnostics containing raw CSG
10456
+ -- data). HttpService:JSONEncode rejects those strings outright. Preserve all
10457
+ -- valid UTF-8 verbatim and make only malformed bytes JSON-safe and visible.
10458
+ local function escapeInvalidUtf8(msg)
10459
+ local valid = utf8.len(msg)
10460
+ -- Roblox currently returns nil (not the false declared by @rbxts/types)
10461
+ -- when it encounters a malformed sequence. A numeric result is the only
10462
+ -- portable success discriminator across both representations.
10463
+ if type(valid) == "number" then
10464
+ return msg
10465
+ end
10466
+ local parts = {}
10467
+ local cursor = 1
10468
+ while cursor <= #msg do
10469
+ local suffixValid, invalidPosition = utf8.len(msg, cursor)
10470
+ if type(suffixValid) == "number" then
10471
+ local _arg0 = string.sub(msg, cursor)
10472
+ table.insert(parts, _arg0)
10473
+ break
10474
+ end
10475
+ if not (type(invalidPosition) == "number") then
10476
+ break
10477
+ end
10478
+ if invalidPosition > cursor then
10479
+ local _arg0 = string.sub(msg, cursor, invalidPosition - 1)
10480
+ table.insert(parts, _arg0)
10481
+ end
10482
+ local invalidByte = string.byte(msg, invalidPosition)
10483
+ local _arg0 = string.format("\\x%02X", invalidByte)
10484
+ table.insert(parts, _arg0)
10485
+ cursor = invalidPosition + 1
10486
+ end
10487
+ return table.concat(parts, "")
10488
+ end
10440
10489
  local function dropOldestUntilFits(incomingBytes)
10441
10490
  while #entries > 0 and (totalBytes + incomingBytes > MAX_BYTES or #entries >= HARD_ENTRY_CAP) do
10442
10491
  local dropped = table.remove(entries, 1)
@@ -10448,13 +10497,14 @@ local function pushEntry(msg, t, ts)
10448
10497
  if ts == nil then
10449
10498
  ts = nowSec()
10450
10499
  end
10451
- local bytes = #msg
10500
+ local safeMessage = escapeInvalidUtf8(msg)
10501
+ local bytes = #safeMessage
10452
10502
  dropOldestUntilFits(bytes)
10453
10503
  local _arg0 = {
10454
10504
  seq = nextSeq,
10455
10505
  ts = ts,
10456
10506
  level = levelTag(t),
10457
- message = msg,
10507
+ message = safeMessage,
10458
10508
  }
10459
10509
  table.insert(entries, _arg0)
10460
10510
  nextSeq += 1
@@ -10763,7 +10813,7 @@ return {
10763
10813
  <Properties>
10764
10814
  <string name="Name">State</string>
10765
10815
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
10766
- local CURRENT_VERSION = "2.22.1"
10816
+ local CURRENT_VERSION = "2.22.2"
10767
10817
  local PLUGIN_VARIANT = "main"
10768
10818
  local BASE_PORT = 58741
10769
10819
  local function createConnection(port)
@@ -197,14 +197,27 @@ function processRequest(request: RequestPayload): unknown {
197
197
  }
198
198
 
199
199
  function sendResponse(conn: Connection, requestId: string, responseData: unknown) {
200
- pcall(() => {
201
- HttpService.RequestAsync({
202
- Url: `${conn.serverUrl}/response`,
203
- Method: "POST",
204
- Headers: { "Content-Type": "application/json" },
205
- Body: HttpService.JSONEncode({ requestId, response: responseData }),
200
+ const responseUrl = `${conn.serverUrl}/response`;
201
+ const [encodeOk, encoded] = pcall(() => HttpService.JSONEncode({ requestId, response: responseData }));
202
+ const body = encodeOk
203
+ ? encoded
204
+ : HttpService.JSONEncode({
205
+ requestId,
206
+ error: `Plugin response serialization failed: ${tostring(encoded)}`,
206
207
  });
207
- });
208
+ if (!encodeOk) {
209
+ warn(`[robloxstudio-mcp] Failed to serialize response ${requestId}: ${tostring(encoded)}`);
210
+ }
211
+
212
+ const [requestOk, requestResult] = pcall(() => HttpService.RequestAsync({
213
+ Url: responseUrl,
214
+ Method: "POST",
215
+ Headers: { "Content-Type": "application/json" },
216
+ Body: body,
217
+ }));
218
+ if (!requestOk || !requestResult.Success) {
219
+ warn(`[robloxstudio-mcp] Failed to deliver response ${requestId}: ${HttpDiagnostics.formatRequestFailure(responseUrl, requestOk, requestResult)}`);
220
+ }
208
221
  }
209
222
 
210
223
  function getConnectionStatus(): string {
@@ -46,6 +46,37 @@ function nowSec(): number {
46
46
  return DateTime.now().UnixTimestampMillis / 1000;
47
47
  }
48
48
 
49
+ // Studio occasionally exposes binary-bearing Output messages through
50
+ // LogService (for example, plugin hydration diagnostics containing raw CSG
51
+ // data). HttpService:JSONEncode rejects those strings outright. Preserve all
52
+ // valid UTF-8 verbatim and make only malformed bytes JSON-safe and visible.
53
+ function escapeInvalidUtf8(msg: string): string {
54
+ const [valid] = utf8.len(msg);
55
+ // Roblox currently returns nil (not the false declared by @rbxts/types)
56
+ // when it encounters a malformed sequence. A numeric result is the only
57
+ // portable success discriminator across both representations.
58
+ if (typeIs(valid, "number")) return msg;
59
+
60
+ const parts: string[] = [];
61
+ let cursor = 1;
62
+ while (cursor <= msg.size()) {
63
+ const [suffixValid, invalidPosition] = utf8.len(msg, cursor);
64
+ if (typeIs(suffixValid, "number")) {
65
+ parts.push(string.sub(msg, cursor));
66
+ break;
67
+ }
68
+ if (!typeIs(invalidPosition, "number")) break;
69
+
70
+ if (invalidPosition > cursor) {
71
+ parts.push(string.sub(msg, cursor, invalidPosition - 1));
72
+ }
73
+ const [invalidByte] = string.byte(msg, invalidPosition);
74
+ parts.push(string.format("\\x%02X", invalidByte));
75
+ cursor = invalidPosition + 1;
76
+ }
77
+ return parts.join("");
78
+ }
79
+
49
80
  function dropOldestUntilFits(incomingBytes: number): void {
50
81
  while (
51
82
  entries.size() > 0 &&
@@ -58,13 +89,14 @@ function dropOldestUntilFits(incomingBytes: number): void {
58
89
  }
59
90
 
60
91
  function pushEntry(msg: string, t: Enum.MessageType, ts = nowSec()): void {
61
- const bytes = msg.size();
92
+ const safeMessage = escapeInvalidUtf8(msg);
93
+ const bytes = safeMessage.size();
62
94
  dropOldestUntilFits(bytes);
63
95
  entries.push({
64
96
  seq: nextSeq,
65
97
  ts,
66
98
  level: levelTag(t),
67
- message: msg,
99
+ message: safeMessage,
68
100
  });
69
101
  nextSeq += 1;
70
102
  totalBytes += bytes;