@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
|
@@ -843,19 +843,33 @@ local function processRequest(request)
|
|
|
843
843
|
end
|
|
844
844
|
end
|
|
845
845
|
local function sendResponse(conn, requestId, responseData)
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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:
|
|
99
|
+
message: safeMessage,
|
|
68
100
|
});
|
|
69
101
|
nextSeq += 1;
|
|
70
102
|
totalBytes += bytes;
|