robloxstudio-mcp 2.6.0-next.3 → 2.6.0-next.4
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
|
@@ -64,13 +64,8 @@ async function download(url, dest, redirects = 0) {
|
|
|
64
64
|
res.on("error", cleanup);
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
-
async function
|
|
68
|
-
const
|
|
69
|
-
if (!existsSync2(pluginsFolder)) {
|
|
70
|
-
mkdirSync2(pluginsFolder, { recursive: true });
|
|
71
|
-
}
|
|
72
|
-
console.log("Fetching latest release...");
|
|
73
|
-
const res = await httpsGet(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
67
|
+
async function fetchJson(url) {
|
|
68
|
+
const res = await httpsGet(url);
|
|
74
69
|
if (res.statusCode !== 200) {
|
|
75
70
|
throw new Error(`GitHub API returned HTTP ${res.statusCode}`);
|
|
76
71
|
}
|
|
@@ -78,7 +73,26 @@ async function installPlugin() {
|
|
|
78
73
|
for await (const chunk of res) {
|
|
79
74
|
chunks.push(chunk);
|
|
80
75
|
}
|
|
81
|
-
|
|
76
|
+
return JSON.parse(Buffer.concat(chunks).toString());
|
|
77
|
+
}
|
|
78
|
+
async function findDevRelease() {
|
|
79
|
+
const releases = await fetchJson(`https://api.github.com/repos/${REPO}/releases?per_page=20`);
|
|
80
|
+
const prerelease = releases.find(
|
|
81
|
+
(r) => r.prerelease && r.assets.some((a) => a.name === ASSET_NAME)
|
|
82
|
+
);
|
|
83
|
+
if (!prerelease) {
|
|
84
|
+
throw new Error(`No prerelease found with ${ASSET_NAME}`);
|
|
85
|
+
}
|
|
86
|
+
return prerelease;
|
|
87
|
+
}
|
|
88
|
+
async function installPlugin() {
|
|
89
|
+
const dev = process.argv.includes("--dev");
|
|
90
|
+
const pluginsFolder = getPluginsFolder();
|
|
91
|
+
if (!existsSync2(pluginsFolder)) {
|
|
92
|
+
mkdirSync2(pluginsFolder, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
console.log(dev ? "Fetching latest dev prerelease..." : "Fetching latest release...");
|
|
95
|
+
const release = dev ? await findDevRelease() : await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
82
96
|
const asset = release.assets?.find((a) => a.name === ASSET_NAME);
|
|
83
97
|
if (!asset) {
|
|
84
98
|
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
@@ -146,7 +160,7 @@ var TOOL_HANDLERS = {
|
|
|
146
160
|
}),
|
|
147
161
|
get_script_source: (tools, body) => tools.getScriptSource(body.instancePath, body.startLine, body.endLine),
|
|
148
162
|
set_script_source: (tools, body) => tools.setScriptSource(body.instancePath, body.source),
|
|
149
|
-
edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.
|
|
163
|
+
edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string),
|
|
150
164
|
insert_script_lines: (tools, body) => tools.insertScriptLines(body.instancePath, body.afterLine, body.newContent),
|
|
151
165
|
delete_script_lines: (tools, body) => tools.deleteScriptLines(body.instancePath, body.startLine, body.endLine),
|
|
152
166
|
get_attribute: (tools, body) => tools.getAttribute(body.instancePath, body.attributeName),
|
|
@@ -1479,11 +1493,11 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
1479
1493
|
]
|
|
1480
1494
|
};
|
|
1481
1495
|
}
|
|
1482
|
-
async editScriptLines(instancePath,
|
|
1483
|
-
if (!instancePath ||
|
|
1484
|
-
throw new Error("Instance path,
|
|
1496
|
+
async editScriptLines(instancePath, oldString, newString) {
|
|
1497
|
+
if (!instancePath || typeof oldString !== "string" || typeof newString !== "string") {
|
|
1498
|
+
throw new Error("Instance path, old_string, and new_string are required for edit_script_lines");
|
|
1485
1499
|
}
|
|
1486
|
-
const response = await this.client.request("/api/edit-script-lines", { instancePath,
|
|
1500
|
+
const response = await this.client.request("/api/edit-script-lines", { instancePath, old_string: oldString, new_string: newString });
|
|
1487
1501
|
return {
|
|
1488
1502
|
content: [
|
|
1489
1503
|
{
|
|
@@ -2701,7 +2715,7 @@ var RobloxStudioMCPServer = class {
|
|
|
2701
2715
|
case "set_script_source":
|
|
2702
2716
|
return await this.tools.setScriptSource(args?.instancePath, args?.source);
|
|
2703
2717
|
case "edit_script_lines":
|
|
2704
|
-
return await this.tools.editScriptLines(args?.instancePath, args?.
|
|
2718
|
+
return await this.tools.editScriptLines(args?.instancePath, args?.old_string, args?.new_string);
|
|
2705
2719
|
case "insert_script_lines":
|
|
2706
2720
|
return await this.tools.insertScriptLines(args?.instancePath, args?.afterLine, args?.newContent);
|
|
2707
2721
|
case "delete_script_lines":
|
|
@@ -3480,7 +3494,7 @@ var TOOL_DEFINITIONS = [
|
|
|
3480
3494
|
{
|
|
3481
3495
|
name: "edit_script_lines",
|
|
3482
3496
|
category: "write",
|
|
3483
|
-
description: "Replace
|
|
3497
|
+
description: "Replace exact text in a script. old_string must match exactly once in the script (whitespace-sensitive). Use get_script_source first to see current content.",
|
|
3484
3498
|
inputSchema: {
|
|
3485
3499
|
type: "object",
|
|
3486
3500
|
properties: {
|
|
@@ -3488,20 +3502,16 @@ var TOOL_DEFINITIONS = [
|
|
|
3488
3502
|
type: "string",
|
|
3489
3503
|
description: "Script instance path"
|
|
3490
3504
|
},
|
|
3491
|
-
|
|
3492
|
-
type: "
|
|
3493
|
-
description: "
|
|
3494
|
-
},
|
|
3495
|
-
endLine: {
|
|
3496
|
-
type: "number",
|
|
3497
|
-
description: "End line (inclusive)"
|
|
3505
|
+
old_string: {
|
|
3506
|
+
type: "string",
|
|
3507
|
+
description: "Exact text to find and replace (must be unique in the script)"
|
|
3498
3508
|
},
|
|
3499
|
-
|
|
3509
|
+
new_string: {
|
|
3500
3510
|
type: "string",
|
|
3501
|
-
description: "Replacement
|
|
3511
|
+
description: "Replacement text"
|
|
3502
3512
|
}
|
|
3503
3513
|
},
|
|
3504
|
-
required: ["instancePath", "
|
|
3514
|
+
required: ["instancePath", "old_string", "new_string"]
|
|
3505
3515
|
}
|
|
3506
3516
|
},
|
|
3507
3517
|
{
|
package/package.json
CHANGED
|
@@ -57,6 +57,17 @@ local AssetHandlers = TS.import(script, script.Parent, "handlers", "AssetHandler
|
|
|
57
57
|
local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
|
|
58
58
|
local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
|
|
59
59
|
local RenderHandlers = TS.import(script, script.Parent, "handlers", "RenderHandlers")
|
|
60
|
+
local instanceId = HttpService:GenerateGUID(false)
|
|
61
|
+
local assignedRole
|
|
62
|
+
local function detectRole()
|
|
63
|
+
if not RunService:IsRunMode() then
|
|
64
|
+
return "edit"
|
|
65
|
+
end
|
|
66
|
+
if RunService:IsServer() then
|
|
67
|
+
return "server"
|
|
68
|
+
end
|
|
69
|
+
return "client"
|
|
70
|
+
end
|
|
60
71
|
local routeMap = {
|
|
61
72
|
["/api/file-tree"] = QueryHandlers.getFileTree,
|
|
62
73
|
["/api/search-files"] = QueryHandlers.searchFiles,
|
|
@@ -153,7 +164,6 @@ local function getConnectionStatus(connIndex)
|
|
|
153
164
|
end
|
|
154
165
|
return "connecting"
|
|
155
166
|
end
|
|
156
|
-
local discoverPort
|
|
157
167
|
local function pollForRequests(connIndex)
|
|
158
168
|
local conn = State.getConnection(connIndex)
|
|
159
169
|
if not conn or not conn.isActive then
|
|
@@ -165,7 +175,7 @@ local function pollForRequests(connIndex)
|
|
|
165
175
|
conn.isPolling = true
|
|
166
176
|
local success, result = pcall(function()
|
|
167
177
|
return HttpService:RequestAsync({
|
|
168
|
-
Url = `{conn.serverUrl}/poll`,
|
|
178
|
+
Url = `{conn.serverUrl}/poll?instanceId={instanceId}`,
|
|
169
179
|
Method = "GET",
|
|
170
180
|
Headers = {
|
|
171
181
|
["Content-Type"] = "application/json",
|
|
@@ -182,6 +192,7 @@ local function pollForRequests(connIndex)
|
|
|
182
192
|
local data = HttpService:JSONDecode(result.Body)
|
|
183
193
|
local mcpConnected = data.mcpConnected == true
|
|
184
194
|
conn.lastHttpOk = true
|
|
195
|
+
conn.lastMcpOk = mcpConnected
|
|
185
196
|
if connIndex == State.getActiveTabIndex() then
|
|
186
197
|
local el = ui
|
|
187
198
|
el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
@@ -228,18 +239,6 @@ local function pollForRequests(connIndex)
|
|
|
228
239
|
end
|
|
229
240
|
local elapsed = _exp - _condition_1
|
|
230
241
|
el.troubleshootLabel.Visible = elapsed > 8
|
|
231
|
-
if elapsed > 3 and elapsed % 5 < conn.pollInterval then
|
|
232
|
-
task.spawn(function()
|
|
233
|
-
local discovered = discoverPort()
|
|
234
|
-
if discovered ~= nil and discovered ~= conn.port then
|
|
235
|
-
conn.port = discovered
|
|
236
|
-
conn.serverUrl = `http://localhost:{discovered}`
|
|
237
|
-
if connIndex == State.getActiveTabIndex() then
|
|
238
|
-
UI.getElements().urlInput.Text = conn.serverUrl
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end)
|
|
242
|
-
end
|
|
243
242
|
UI.startPulseAnimation()
|
|
244
243
|
end
|
|
245
244
|
end
|
|
@@ -262,20 +261,6 @@ local function pollForRequests(connIndex)
|
|
|
262
261
|
if conn.consecutiveFailures > 1 then
|
|
263
262
|
conn.currentRetryDelay = math.min(conn.currentRetryDelay * conn.retryBackoffMultiplier, conn.maxRetryDelay)
|
|
264
263
|
end
|
|
265
|
-
if conn.consecutiveFailures == 5 or conn.consecutiveFailures % 20 == 0 then
|
|
266
|
-
task.spawn(function()
|
|
267
|
-
local discovered = discoverPort()
|
|
268
|
-
if discovered ~= nil and discovered ~= conn.port then
|
|
269
|
-
conn.port = discovered
|
|
270
|
-
conn.serverUrl = `http://localhost:{discovered}`
|
|
271
|
-
conn.consecutiveFailures = 0
|
|
272
|
-
conn.currentRetryDelay = 0.5
|
|
273
|
-
if connIndex == State.getActiveTabIndex() then
|
|
274
|
-
UI.getElements().urlInput.Text = conn.serverUrl
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
end)
|
|
278
|
-
end
|
|
279
264
|
if connIndex == State.getActiveTabIndex() then
|
|
280
265
|
local el = ui
|
|
281
266
|
if conn.consecutiveFailures >= conn.maxFailuresBeforeError then
|
|
@@ -334,35 +319,6 @@ local function pollForRequests(connIndex)
|
|
|
334
319
|
end
|
|
335
320
|
end
|
|
336
321
|
end
|
|
337
|
-
function discoverPort()
|
|
338
|
-
local firstActivePort
|
|
339
|
-
for offset = 0, 4 do
|
|
340
|
-
local port = State.BASE_PORT + offset
|
|
341
|
-
local success, result = pcall(function()
|
|
342
|
-
return HttpService:RequestAsync({
|
|
343
|
-
Url = `http://localhost:{port}/status`,
|
|
344
|
-
Method = "GET",
|
|
345
|
-
Headers = {
|
|
346
|
-
["Content-Type"] = "application/json",
|
|
347
|
-
},
|
|
348
|
-
})
|
|
349
|
-
end)
|
|
350
|
-
if success and result.Success then
|
|
351
|
-
local ok, data = pcall(function()
|
|
352
|
-
return HttpService:JSONDecode(result.Body)
|
|
353
|
-
end)
|
|
354
|
-
if ok and data.mcpServerActive then
|
|
355
|
-
if not data.pluginConnected then
|
|
356
|
-
return port
|
|
357
|
-
end
|
|
358
|
-
if firstActivePort == nil then
|
|
359
|
-
firstActivePort = port
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
return firstActivePort
|
|
365
|
-
end
|
|
366
322
|
local function activatePlugin(connIndex)
|
|
367
323
|
local _condition = connIndex
|
|
368
324
|
if _condition == nil then
|
|
@@ -388,18 +344,11 @@ local function activatePlugin(connIndex)
|
|
|
388
344
|
end
|
|
389
345
|
conn.port = _condition_1
|
|
390
346
|
end
|
|
347
|
+
UI.updateTabLabel(idx)
|
|
391
348
|
UI.updateUIState()
|
|
392
349
|
end
|
|
393
350
|
UI.updateTabDot(idx)
|
|
394
351
|
task.spawn(function()
|
|
395
|
-
local discoveredPort = discoverPort()
|
|
396
|
-
if discoveredPort ~= nil then
|
|
397
|
-
conn.port = discoveredPort
|
|
398
|
-
conn.serverUrl = `http://localhost:{discoveredPort}`
|
|
399
|
-
if idx == State.getActiveTabIndex() then
|
|
400
|
-
ui.urlInput.Text = conn.serverUrl
|
|
401
|
-
end
|
|
402
|
-
end
|
|
403
352
|
if not conn.heartbeatConnection then
|
|
404
353
|
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
405
354
|
local now = tick()
|
|
@@ -410,19 +359,30 @@ local function activatePlugin(connIndex)
|
|
|
410
359
|
end
|
|
411
360
|
end)
|
|
412
361
|
end
|
|
413
|
-
pcall(function()
|
|
414
|
-
HttpService:RequestAsync({
|
|
362
|
+
local readyOk, readyResult = pcall(function()
|
|
363
|
+
return HttpService:RequestAsync({
|
|
415
364
|
Url = `{conn.serverUrl}/ready`,
|
|
416
365
|
Method = "POST",
|
|
417
366
|
Headers = {
|
|
418
367
|
["Content-Type"] = "application/json",
|
|
419
368
|
},
|
|
420
369
|
Body = HttpService:JSONEncode({
|
|
370
|
+
instanceId = instanceId,
|
|
371
|
+
role = detectRole(),
|
|
421
372
|
pluginReady = true,
|
|
422
373
|
timestamp = tick(),
|
|
423
374
|
}),
|
|
424
375
|
})
|
|
425
376
|
end)
|
|
377
|
+
if readyOk and readyResult.Success then
|
|
378
|
+
local parseOk, readyData = pcall(function()
|
|
379
|
+
return HttpService:JSONDecode(readyResult.Body)
|
|
380
|
+
end)
|
|
381
|
+
local _value = parseOk and readyData.assignedRole
|
|
382
|
+
if _value ~= "" and _value then
|
|
383
|
+
assignedRole = readyData.assignedRole
|
|
384
|
+
end
|
|
385
|
+
end
|
|
426
386
|
end)
|
|
427
387
|
end
|
|
428
388
|
local function deactivatePlugin(connIndex)
|
|
@@ -436,6 +396,7 @@ local function deactivatePlugin(connIndex)
|
|
|
436
396
|
return nil
|
|
437
397
|
end
|
|
438
398
|
conn.isActive = false
|
|
399
|
+
conn.lastMcpOk = false
|
|
439
400
|
if idx == State.getActiveTabIndex() then
|
|
440
401
|
UI.updateUIState()
|
|
441
402
|
end
|
|
@@ -448,6 +409,7 @@ local function deactivatePlugin(connIndex)
|
|
|
448
409
|
["Content-Type"] = "application/json",
|
|
449
410
|
},
|
|
450
411
|
Body = HttpService:JSONEncode({
|
|
412
|
+
instanceId = instanceId,
|
|
451
413
|
timestamp = tick(),
|
|
452
414
|
}),
|
|
453
415
|
})
|
|
@@ -1375,33 +1337,57 @@ local CaptureService = game:GetService("CaptureService")
|
|
|
1375
1337
|
local AssetService = game:GetService("AssetService")
|
|
1376
1338
|
local MAX_TILE_SIZE = 1024
|
|
1377
1339
|
local BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
1340
|
+
local PAD_BYTE = (string.byte("="))
|
|
1341
|
+
local B64 = {}
|
|
1342
|
+
for i = 0, 63 do
|
|
1343
|
+
B64[i + 1] = (string.byte(BASE64_CHARS, i + 1))
|
|
1344
|
+
end
|
|
1378
1345
|
local function encodeBase64(buf)
|
|
1379
1346
|
local len = buffer.len(buf)
|
|
1380
|
-
local
|
|
1381
|
-
local
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
local
|
|
1388
|
-
|
|
1389
|
-
|
|
1347
|
+
local fullTriples = math.floor(len / 3)
|
|
1348
|
+
local remaining = len - fullTriples * 3
|
|
1349
|
+
local outLen = (fullTriples + (if remaining > 0 then 1 else 0)) * 4
|
|
1350
|
+
local out = buffer.create(outLen)
|
|
1351
|
+
local si = 0
|
|
1352
|
+
local di = 0
|
|
1353
|
+
do
|
|
1354
|
+
local t = 0
|
|
1355
|
+
local _shouldIncrement = false
|
|
1356
|
+
while true do
|
|
1357
|
+
if _shouldIncrement then
|
|
1358
|
+
t += 1
|
|
1359
|
+
else
|
|
1360
|
+
_shouldIncrement = true
|
|
1361
|
+
end
|
|
1362
|
+
if not (t < fullTriples) then
|
|
1363
|
+
break
|
|
1364
|
+
end
|
|
1365
|
+
local b0 = buffer.readu8(buf, si)
|
|
1366
|
+
local b1 = buffer.readu8(buf, si + 1)
|
|
1367
|
+
local b2 = buffer.readu8(buf, si + 2)
|
|
1368
|
+
buffer.writeu8(out, di, B64[bit32.rshift(b0, 2) + 1])
|
|
1369
|
+
buffer.writeu8(out, di + 1, B64[bit32.bor(bit32.lshift(bit32.band(b0, 3), 4), bit32.rshift(b1, 4)) + 1])
|
|
1370
|
+
buffer.writeu8(out, di + 2, B64[bit32.bor(bit32.lshift(bit32.band(b1, 15), 2), bit32.rshift(b2, 6)) + 1])
|
|
1371
|
+
buffer.writeu8(out, di + 3, B64[bit32.band(b2, 63) + 1])
|
|
1372
|
+
si += 3
|
|
1373
|
+
di += 4
|
|
1374
|
+
end
|
|
1390
1375
|
end
|
|
1391
|
-
local remaining = len - i
|
|
1392
1376
|
if remaining == 2 then
|
|
1393
|
-
local b0 = buffer.readu8(buf,
|
|
1394
|
-
local b1 = buffer.readu8(buf,
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1377
|
+
local b0 = buffer.readu8(buf, si)
|
|
1378
|
+
local b1 = buffer.readu8(buf, si + 1)
|
|
1379
|
+
buffer.writeu8(out, di, B64[bit32.rshift(b0, 2) + 1])
|
|
1380
|
+
buffer.writeu8(out, di + 1, B64[bit32.bor(bit32.lshift(bit32.band(b0, 3), 4), bit32.rshift(b1, 4)) + 1])
|
|
1381
|
+
buffer.writeu8(out, di + 2, B64[bit32.lshift(bit32.band(b1, 15), 2) + 1])
|
|
1382
|
+
buffer.writeu8(out, di + 3, PAD_BYTE)
|
|
1398
1383
|
elseif remaining == 1 then
|
|
1399
|
-
local b0 = buffer.readu8(buf,
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1384
|
+
local b0 = buffer.readu8(buf, si)
|
|
1385
|
+
buffer.writeu8(out, di, B64[bit32.rshift(b0, 2) + 1])
|
|
1386
|
+
buffer.writeu8(out, di + 1, B64[bit32.lshift(bit32.band(b0, 3), 4) + 1])
|
|
1387
|
+
buffer.writeu8(out, di + 2, PAD_BYTE)
|
|
1388
|
+
buffer.writeu8(out, di + 3, PAD_BYTE)
|
|
1403
1389
|
end
|
|
1404
|
-
return
|
|
1390
|
+
return buffer.tostring(out)
|
|
1405
1391
|
end
|
|
1406
1392
|
local function readPixelsTiled(img, w, h)
|
|
1407
1393
|
local BYTES_PER_PIXEL = 4
|
|
@@ -4019,7 +4005,6 @@ local _binding = Utils
|
|
|
4019
4005
|
local getInstanceByPath = _binding.getInstanceByPath
|
|
4020
4006
|
local Selection = game:GetService("Selection")
|
|
4021
4007
|
local Workspace = game:GetService("Workspace")
|
|
4022
|
-
local RENDER_ORIGIN = Vector3.new(100000, 100000, 100000)
|
|
4023
4008
|
local DEFAULT_PADDING = 1.35
|
|
4024
4009
|
local DEFAULT_BACKDROP_COLOR = Color3.fromRGB(0, 255, 0)
|
|
4025
4010
|
local function getCameraPreset(rawPreset)
|
|
@@ -4064,47 +4049,14 @@ local function getCameraDirection(preset)
|
|
|
4064
4049
|
end
|
|
4065
4050
|
return Vector3.new(-1, 0.7, 1).Unit
|
|
4066
4051
|
end
|
|
4067
|
-
local function
|
|
4068
|
-
local parts = {}
|
|
4069
|
-
if root:IsA("BasePart") then
|
|
4070
|
-
local _root = root
|
|
4071
|
-
table.insert(parts, _root)
|
|
4072
|
-
end
|
|
4073
|
-
for _, desc in root:GetDescendants() do
|
|
4074
|
-
if desc:IsA("BasePart") then
|
|
4075
|
-
table.insert(parts, desc)
|
|
4076
|
-
end
|
|
4077
|
-
end
|
|
4078
|
-
return parts
|
|
4079
|
-
end
|
|
4080
|
-
local function createRenderableClone(target, stage)
|
|
4052
|
+
local function getBoundingInfo(target)
|
|
4081
4053
|
if target:IsA("Model") then
|
|
4082
|
-
|
|
4083
|
-
clone.Parent = stage
|
|
4084
|
-
clone:PivotTo(CFrame.new(RENDER_ORIGIN))
|
|
4085
|
-
return clone
|
|
4086
|
-
end
|
|
4087
|
-
if target:IsA("BasePart") then
|
|
4088
|
-
local wrapper = Instance.new("Model")
|
|
4089
|
-
wrapper.Name = target.Name
|
|
4090
|
-
wrapper.Parent = stage
|
|
4091
|
-
local clone = target:Clone()
|
|
4092
|
-
clone.Parent = wrapper
|
|
4093
|
-
clone.Position = RENDER_ORIGIN
|
|
4094
|
-
return wrapper
|
|
4054
|
+
return target:GetBoundingBox()
|
|
4095
4055
|
end
|
|
4096
|
-
|
|
4056
|
+
local part = target
|
|
4057
|
+
return { part.CFrame, part.Size }
|
|
4097
4058
|
end
|
|
4098
|
-
local function
|
|
4099
|
-
for _, part in parts do
|
|
4100
|
-
part.Anchored = true
|
|
4101
|
-
part.CanCollide = false
|
|
4102
|
-
part.CanTouch = false
|
|
4103
|
-
part.CanQuery = false
|
|
4104
|
-
part.CastShadow = false
|
|
4105
|
-
end
|
|
4106
|
-
end
|
|
4107
|
-
local function createBackdrop(parent, focusPosition, cameraDirection, radius, cameraDistance, backdropColor, fieldOfView, aspectRatio)
|
|
4059
|
+
local function createBackdrop(focusPosition, cameraDirection, radius, cameraDistance, backdropColor, fieldOfView, aspectRatio)
|
|
4108
4060
|
local frustumDistance = cameraDistance + radius + 10
|
|
4109
4061
|
local backdropHeight = math.max(radius * 6, 2 * math.tan(math.rad(fieldOfView) / 2) * frustumDistance * 1.2)
|
|
4110
4062
|
local backdropWidth = math.max(radius * 6, backdropHeight * aspectRatio)
|
|
@@ -4122,7 +4074,7 @@ local function createBackdrop(parent, focusPosition, cameraDirection, radius, ca
|
|
|
4122
4074
|
local _cameraDirection = cameraDirection
|
|
4123
4075
|
local _arg0 = radius + 6
|
|
4124
4076
|
backdrop.CFrame = CFrame.lookAt(_focusPosition - (_cameraDirection * _arg0), focusPosition)
|
|
4125
|
-
backdrop.Parent =
|
|
4077
|
+
backdrop.Parent = Workspace
|
|
4126
4078
|
return backdrop
|
|
4127
4079
|
end
|
|
4128
4080
|
local function renderModelScreenshot(requestData)
|
|
@@ -4162,32 +4114,18 @@ local function renderModelScreenshot(requestData)
|
|
|
4162
4114
|
local previousCameraFocus = currentCamera.Focus
|
|
4163
4115
|
local previousFieldOfView = currentCamera.FieldOfView
|
|
4164
4116
|
local previousCameraSubject = currentCamera.CameraSubject
|
|
4165
|
-
local
|
|
4117
|
+
local backdrop
|
|
4166
4118
|
local ok, result = pcall(function()
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
local
|
|
4171
|
-
|
|
4172
|
-
return {
|
|
4173
|
-
error = "Failed to clone render target",
|
|
4174
|
-
}
|
|
4175
|
-
end
|
|
4176
|
-
local baseParts = collectBaseParts(renderRoot)
|
|
4177
|
-
if #baseParts == 0 then
|
|
4178
|
-
return {
|
|
4179
|
-
error = "Target does not contain any BaseParts to render",
|
|
4180
|
-
}
|
|
4181
|
-
end
|
|
4182
|
-
styleBaseParts(baseParts)
|
|
4183
|
-
local boundingBoxCFrame, boundingBoxSize = renderRoot:GetBoundingBox()
|
|
4184
|
-
local focusPosition = boundingBoxCFrame.Position
|
|
4185
|
-
local radius = math.max(boundingBoxSize.Magnitude / 2, 2)
|
|
4119
|
+
local _binding_1 = getBoundingInfo(target)
|
|
4120
|
+
local boundingCFrame = _binding_1[1]
|
|
4121
|
+
local boundingSize = _binding_1[2]
|
|
4122
|
+
local focusPosition = boundingCFrame.Position
|
|
4123
|
+
local radius = math.max(boundingSize.Magnitude / 2, 2)
|
|
4186
4124
|
local fieldOfView = if currentCamera.FieldOfView > 0 then currentCamera.FieldOfView else 70
|
|
4187
4125
|
local aspectRatio = if currentCamera.ViewportSize.Y > 0 then currentCamera.ViewportSize.X / currentCamera.ViewportSize.Y else 16 / 9
|
|
4188
4126
|
local cameraDirection = getCameraDirection(cameraPreset)
|
|
4189
4127
|
local cameraDistance = math.max(12, (radius * padding) / math.tan(math.rad(fieldOfView) / 2))
|
|
4190
|
-
createBackdrop(
|
|
4128
|
+
backdrop = createBackdrop(focusPosition, cameraDirection, radius, cameraDistance, backdropColor, fieldOfView, aspectRatio)
|
|
4191
4129
|
pcall(function()
|
|
4192
4130
|
Selection:Set({})
|
|
4193
4131
|
end)
|
|
@@ -4208,20 +4146,20 @@ local function renderModelScreenshot(requestData)
|
|
|
4208
4146
|
return captureResult
|
|
4209
4147
|
end)
|
|
4210
4148
|
pcall(function()
|
|
4211
|
-
currentCamera.CameraType = previousCameraType
|
|
4212
4149
|
currentCamera.CFrame = previousCameraCFrame
|
|
4213
4150
|
currentCamera.Focus = previousCameraFocus
|
|
4214
4151
|
currentCamera.FieldOfView = previousFieldOfView
|
|
4215
4152
|
if previousCameraSubject then
|
|
4216
4153
|
currentCamera.CameraSubject = previousCameraSubject
|
|
4217
4154
|
end
|
|
4155
|
+
currentCamera.CameraType = previousCameraType
|
|
4218
4156
|
end)
|
|
4219
4157
|
pcall(function()
|
|
4220
4158
|
Selection:Set(previousSelection)
|
|
4221
4159
|
end)
|
|
4222
|
-
if
|
|
4160
|
+
if backdrop then
|
|
4223
4161
|
pcall(function()
|
|
4224
|
-
|
|
4162
|
+
backdrop:Destroy()
|
|
4225
4163
|
end)
|
|
4226
4164
|
end
|
|
4227
4165
|
if not ok then
|
|
@@ -4476,17 +4414,18 @@ local function setScriptSource(requestData)
|
|
|
4476
4414
|
error = `Failed to set script source. UpdateSourceAsync failed: {updateResult}. Direct assignment failed: {directResult}. Replace method failed: {replaceResult}`,
|
|
4477
4415
|
}
|
|
4478
4416
|
end
|
|
4417
|
+
local escapeLuaPattern, escapeLuaReplacement
|
|
4479
4418
|
local function editScriptLines(requestData)
|
|
4480
4419
|
local instancePath = requestData.instancePath
|
|
4481
|
-
local
|
|
4482
|
-
local
|
|
4483
|
-
|
|
4484
|
-
if not (instancePath ~= "" and instancePath) or not (startLine ~= 0 and startLine == startLine and startLine) or not (endLine ~= 0 and endLine == endLine and endLine) or not (newContent ~= "" and newContent) then
|
|
4420
|
+
local oldString = requestData.old_string
|
|
4421
|
+
local newString = requestData.new_string
|
|
4422
|
+
if not (instancePath ~= "" and instancePath) or oldString == nil or newString == nil then
|
|
4485
4423
|
return {
|
|
4486
|
-
error = "Instance path,
|
|
4424
|
+
error = "Instance path, old_string, and new_string are required",
|
|
4487
4425
|
}
|
|
4488
4426
|
end
|
|
4489
|
-
|
|
4427
|
+
oldString = normalizeEscapes(oldString)
|
|
4428
|
+
newString = normalizeEscapes(newString)
|
|
4490
4429
|
local instance = getInstanceByPath(instancePath)
|
|
4491
4430
|
if not instance then
|
|
4492
4431
|
return {
|
|
@@ -4498,68 +4437,41 @@ local function editScriptLines(requestData)
|
|
|
4498
4437
|
error = `Instance is not a script-like object: {instance.ClassName}`,
|
|
4499
4438
|
}
|
|
4500
4439
|
end
|
|
4501
|
-
local recordingId = beginRecording(`Edit script
|
|
4440
|
+
local recordingId = beginRecording(`Edit script: {instance.Name}`)
|
|
4502
4441
|
local success, result = pcall(function()
|
|
4503
|
-
local
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
while true do
|
|
4517
|
-
if _shouldIncrement then
|
|
4518
|
-
i += 1
|
|
4519
|
-
else
|
|
4520
|
-
_shouldIncrement = true
|
|
4521
|
-
end
|
|
4522
|
-
if not (i < startLine - 1) then
|
|
4523
|
-
break
|
|
4524
|
-
end
|
|
4525
|
-
local _arg0 = lines[i + 1]
|
|
4526
|
-
table.insert(resultLines, _arg0)
|
|
4442
|
+
local source = readScriptSource(instance)
|
|
4443
|
+
-- Count occurrences to ensure uniqueness
|
|
4444
|
+
local count = 0
|
|
4445
|
+
local searchPos = 1
|
|
4446
|
+
local searchLen = #oldString
|
|
4447
|
+
while true do
|
|
4448
|
+
local foundStart = string.find(source, oldString, searchPos, true)
|
|
4449
|
+
if foundStart == nil then
|
|
4450
|
+
break
|
|
4451
|
+
end
|
|
4452
|
+
count += 1
|
|
4453
|
+
if count > 1 then
|
|
4454
|
+
break
|
|
4527
4455
|
end
|
|
4456
|
+
searchPos = foundStart + searchLen
|
|
4528
4457
|
end
|
|
4529
|
-
|
|
4530
|
-
|
|
4458
|
+
if count == 0 then
|
|
4459
|
+
error("old_string not found in script")
|
|
4531
4460
|
end
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
local _shouldIncrement = false
|
|
4535
|
-
while true do
|
|
4536
|
-
if _shouldIncrement then
|
|
4537
|
-
i += 1
|
|
4538
|
-
else
|
|
4539
|
-
_shouldIncrement = true
|
|
4540
|
-
end
|
|
4541
|
-
if not (i < totalLines) then
|
|
4542
|
-
break
|
|
4543
|
-
end
|
|
4544
|
-
local _arg0 = lines[i + 1]
|
|
4545
|
-
table.insert(resultLines, _arg0)
|
|
4546
|
-
end
|
|
4461
|
+
if count > 1 then
|
|
4462
|
+
error("old_string matches multiple locations. Provide more surrounding context to make it unique")
|
|
4547
4463
|
end
|
|
4548
|
-
|
|
4464
|
+
-- Perform the replacement (plain literal find + replace)
|
|
4465
|
+
local escaped = escapeLuaPattern(oldString)
|
|
4466
|
+
local escapedRepl = escapeLuaReplacement(newString)
|
|
4467
|
+
local newSource = string.gsub(source, escaped, escapedRepl, 1)
|
|
4549
4468
|
ScriptEditorService:UpdateSourceAsync(instance, function()
|
|
4550
4469
|
return newSource
|
|
4551
4470
|
end)
|
|
4552
4471
|
return {
|
|
4553
4472
|
success = true,
|
|
4554
4473
|
instancePath = instancePath,
|
|
4555
|
-
|
|
4556
|
-
startLine = startLine,
|
|
4557
|
-
endLine = endLine,
|
|
4558
|
-
},
|
|
4559
|
-
linesRemoved = endLine - startLine + 1,
|
|
4560
|
-
linesAdded = #newLines,
|
|
4561
|
-
newLineCount = #resultLines,
|
|
4562
|
-
message = "Script lines edited successfully",
|
|
4474
|
+
message = "Script edited successfully",
|
|
4563
4475
|
}
|
|
4564
4476
|
end)
|
|
4565
4477
|
if success then
|
|
@@ -4568,7 +4480,7 @@ local function editScriptLines(requestData)
|
|
|
4568
4480
|
end
|
|
4569
4481
|
finishRecording(recordingId, false)
|
|
4570
4482
|
return {
|
|
4571
|
-
error = `Failed to edit script
|
|
4483
|
+
error = `Failed to edit script: {result}`,
|
|
4572
4484
|
}
|
|
4573
4485
|
end
|
|
4574
4486
|
local function insertScriptLines(requestData)
|
|
@@ -4750,10 +4662,10 @@ local function deleteScriptLines(requestData)
|
|
|
4750
4662
|
error = `Failed to delete script lines: {result}`,
|
|
4751
4663
|
}
|
|
4752
4664
|
end
|
|
4753
|
-
|
|
4665
|
+
function escapeLuaPattern(s)
|
|
4754
4666
|
return (string.gsub(s, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"))
|
|
4755
4667
|
end
|
|
4756
|
-
|
|
4668
|
+
function escapeLuaReplacement(s)
|
|
4757
4669
|
return (string.gsub(s, "%%", "%%%%"))
|
|
4758
4670
|
end
|
|
4759
4671
|
local function caseInsensitiveLiteralReplace(src, searchStr, repl)
|
|
@@ -5043,6 +4955,7 @@ local function cleanupStopListener()
|
|
|
5043
4955
|
end
|
|
5044
4956
|
local function startPlaytest(requestData)
|
|
5045
4957
|
local mode = requestData.mode
|
|
4958
|
+
local numPlayers = requestData.numPlayers
|
|
5046
4959
|
if mode ~= "play" and mode ~= "run" then
|
|
5047
4960
|
return {
|
|
5048
4961
|
error = 'mode must be "play" or "run"',
|
|
@@ -5092,6 +5005,10 @@ local function startPlaytest(requestData)
|
|
|
5092
5005
|
if not injected then
|
|
5093
5006
|
warn(`[MCP] Failed to inject stop listener: {injErr}`)
|
|
5094
5007
|
end
|
|
5008
|
+
if numPlayers ~= nil and mode == "run" then
|
|
5009
|
+
local TestService = game:GetService("TestService")
|
|
5010
|
+
TestService.NumberOfPlayers = math.clamp(numPlayers, 1, 8)
|
|
5011
|
+
end
|
|
5095
5012
|
task.spawn(function()
|
|
5096
5013
|
local ok, result = pcall(function()
|
|
5097
5014
|
if mode == "play" then
|
|
@@ -5111,9 +5028,10 @@ local function startPlaytest(requestData)
|
|
|
5111
5028
|
testRunning = false
|
|
5112
5029
|
cleanupStopListener()
|
|
5113
5030
|
end)
|
|
5031
|
+
local msg = if numPlayers ~= nil then `Playtest started in {mode} mode with {numPlayers} player(s)` else `Playtest started in {mode} mode`
|
|
5114
5032
|
return {
|
|
5115
5033
|
success = true,
|
|
5116
|
-
message =
|
|
5034
|
+
message = msg,
|
|
5117
5035
|
}
|
|
5118
5036
|
end
|
|
5119
5037
|
local function stopPlaytest(_requestData)
|
|
@@ -5260,7 +5178,7 @@ return {
|
|
|
5260
5178
|
<Properties>
|
|
5261
5179
|
<string name="Name">State</string>
|
|
5262
5180
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5263
|
-
local CURRENT_VERSION = "2.6.0-next.
|
|
5181
|
+
local CURRENT_VERSION = "2.6.0-next.2"
|
|
5264
5182
|
local MAX_CONNECTIONS = 5
|
|
5265
5183
|
local BASE_PORT = 58741
|
|
5266
5184
|
local activeTabIndex = 0
|
|
@@ -5278,6 +5196,7 @@ local function createConnection(port)
|
|
|
5278
5196
|
maxRetryDelay = 5,
|
|
5279
5197
|
retryBackoffMultiplier = 1.2,
|
|
5280
5198
|
lastHttpOk = false,
|
|
5199
|
+
lastMcpOk = false,
|
|
5281
5200
|
mcpWaitStartTime = nil,
|
|
5282
5201
|
isPolling = false,
|
|
5283
5202
|
heartbeatConnection = nil,
|
|
@@ -5288,12 +5207,16 @@ local function addConnection(port)
|
|
|
5288
5207
|
if #connections >= MAX_CONNECTIONS then
|
|
5289
5208
|
return nil
|
|
5290
5209
|
end
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5210
|
+
if port == nil then
|
|
5211
|
+
local maxPort = BASE_PORT - 1
|
|
5212
|
+
for _, conn in connections do
|
|
5213
|
+
if conn.port > maxPort then
|
|
5214
|
+
maxPort = conn.port
|
|
5215
|
+
end
|
|
5216
|
+
end
|
|
5217
|
+
port = maxPort + 1
|
|
5295
5218
|
end
|
|
5296
|
-
local conn = createConnection(
|
|
5219
|
+
local conn = createConnection(port)
|
|
5297
5220
|
table.insert(connections, conn)
|
|
5298
5221
|
return #connections - 1
|
|
5299
5222
|
end
|
|
@@ -5558,6 +5481,15 @@ local function updateTabDot(connIndex)
|
|
|
5558
5481
|
tb.dot.BackgroundColor3 = getStatusDotColor(connIndex)
|
|
5559
5482
|
end
|
|
5560
5483
|
end
|
|
5484
|
+
local function updateTabLabel(connIndex)
|
|
5485
|
+
local conn = State.getConnection(connIndex)
|
|
5486
|
+
local _tabButtons = tabButtons
|
|
5487
|
+
local _connIndex = connIndex
|
|
5488
|
+
local tb = _tabButtons[_connIndex]
|
|
5489
|
+
if conn and tb and tb.label then
|
|
5490
|
+
tb.label.Text = tostring(conn.port)
|
|
5491
|
+
end
|
|
5492
|
+
end
|
|
5561
5493
|
local function init(pluginRef)
|
|
5562
5494
|
local CURRENT_VERSION = State.CURRENT_VERSION
|
|
5563
5495
|
local screenGui = pluginRef:CreateDockWidgetPluginGuiAsync("MCPServerInterface", DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 300, 260, 260, 200))
|
|
@@ -5753,6 +5685,22 @@ local function init(pluginRef)
|
|
|
5753
5685
|
urlPadding.PaddingLeft = UDim.new(0, 8)
|
|
5754
5686
|
urlPadding.PaddingRight = UDim.new(0, 8)
|
|
5755
5687
|
urlPadding.Parent = urlInput
|
|
5688
|
+
urlInput.FocusLost:Connect(function()
|
|
5689
|
+
local conn = State.getActiveConnection()
|
|
5690
|
+
if not conn or conn.isActive then
|
|
5691
|
+
return nil
|
|
5692
|
+
end
|
|
5693
|
+
conn.serverUrl = urlInput.Text
|
|
5694
|
+
local portStr = string.match(conn.serverUrl, ":(%d+)$")
|
|
5695
|
+
if portStr ~= 0 and portStr == portStr and portStr ~= "" and portStr then
|
|
5696
|
+
local _condition = tonumber(portStr)
|
|
5697
|
+
if _condition == nil then
|
|
5698
|
+
_condition = conn.port
|
|
5699
|
+
end
|
|
5700
|
+
conn.port = _condition
|
|
5701
|
+
end
|
|
5702
|
+
updateTabLabel(State.getActiveTabIndex())
|
|
5703
|
+
end)
|
|
5756
5704
|
local statusRow = Instance.new("Frame")
|
|
5757
5705
|
statusRow.Size = UDim2.new(1, 0, 0, 14)
|
|
5758
5706
|
statusRow.BackgroundTransparency = 1
|
|
@@ -5915,29 +5863,7 @@ function updateUIState()
|
|
|
5915
5863
|
return nil
|
|
5916
5864
|
end
|
|
5917
5865
|
local el = elements
|
|
5918
|
-
if conn.isActive then
|
|
5919
|
-
el.statusLabel.Text = "Connecting..."
|
|
5920
|
-
el.statusLabel.TextColor3 = C.yellow
|
|
5921
|
-
el.statusIndicator.BackgroundColor3 = C.yellow
|
|
5922
|
-
el.statusPulse.BackgroundColor3 = C.yellow
|
|
5923
|
-
el.statusText.Text = "CONNECTING"
|
|
5924
|
-
el.detailStatusLabel.Text = if conn.consecutiveFailures == 0 then "..." else "HTTP: X MCP: X"
|
|
5925
|
-
el.detailStatusLabel.TextColor3 = C.muted
|
|
5926
|
-
startPulseAnimation()
|
|
5927
|
-
el.step1Dot.BackgroundColor3 = C.yellow
|
|
5928
|
-
el.step1Label.Text = "HTTP server (connecting...)"
|
|
5929
|
-
el.step2Dot.BackgroundColor3 = C.yellow
|
|
5930
|
-
el.step2Label.Text = "MCP bridge (connecting...)"
|
|
5931
|
-
el.step3Dot.BackgroundColor3 = C.yellow
|
|
5932
|
-
el.step3Label.Text = "Commands (connecting...)"
|
|
5933
|
-
conn.mcpWaitStartTime = nil
|
|
5934
|
-
el.troubleshootLabel.Visible = false
|
|
5935
|
-
if not buttonHover then
|
|
5936
|
-
setButtonDisconnect(el.connectButton, el.connectStroke)
|
|
5937
|
-
end
|
|
5938
|
-
el.urlInput.TextEditable = false
|
|
5939
|
-
el.urlInput.BackgroundColor3 = C.card
|
|
5940
|
-
else
|
|
5866
|
+
if not conn.isActive then
|
|
5941
5867
|
el.statusLabel.Text = "Disconnected"
|
|
5942
5868
|
el.statusLabel.TextColor3 = C.muted
|
|
5943
5869
|
el.statusIndicator.BackgroundColor3 = C.red
|
|
@@ -5952,13 +5878,101 @@ function updateUIState()
|
|
|
5952
5878
|
el.step2Label.Text = "MCP bridge"
|
|
5953
5879
|
el.step3Dot.BackgroundColor3 = C.gray
|
|
5954
5880
|
el.step3Label.Text = "Commands"
|
|
5955
|
-
conn.mcpWaitStartTime = nil
|
|
5956
5881
|
el.troubleshootLabel.Visible = false
|
|
5957
5882
|
if not buttonHover then
|
|
5958
5883
|
setButtonConnect(el.connectButton, el.connectStroke)
|
|
5959
5884
|
end
|
|
5960
5885
|
el.urlInput.TextEditable = true
|
|
5961
5886
|
el.urlInput.BackgroundColor3 = C.bg
|
|
5887
|
+
return nil
|
|
5888
|
+
end
|
|
5889
|
+
if not buttonHover then
|
|
5890
|
+
setButtonDisconnect(el.connectButton, el.connectStroke)
|
|
5891
|
+
end
|
|
5892
|
+
el.urlInput.TextEditable = false
|
|
5893
|
+
el.urlInput.BackgroundColor3 = C.card
|
|
5894
|
+
if conn.lastHttpOk and conn.lastMcpOk then
|
|
5895
|
+
el.statusLabel.Text = "Connected"
|
|
5896
|
+
el.statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
|
|
5897
|
+
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
5898
|
+
el.statusPulse.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
5899
|
+
el.statusText.Text = "ONLINE"
|
|
5900
|
+
el.detailStatusLabel.Text = "HTTP: OK MCP: OK"
|
|
5901
|
+
el.detailStatusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
|
|
5902
|
+
el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
5903
|
+
el.step1Label.Text = "HTTP server (OK)"
|
|
5904
|
+
el.step2Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
5905
|
+
el.step2Label.Text = "MCP bridge (OK)"
|
|
5906
|
+
el.step3Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
5907
|
+
el.step3Label.Text = "Commands (OK)"
|
|
5908
|
+
el.troubleshootLabel.Visible = false
|
|
5909
|
+
stopPulseAnimation()
|
|
5910
|
+
elseif conn.lastHttpOk and not conn.lastMcpOk then
|
|
5911
|
+
el.statusLabel.Text = "Waiting for MCP server"
|
|
5912
|
+
el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
5913
|
+
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
5914
|
+
el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
5915
|
+
el.statusText.Text = "WAITING"
|
|
5916
|
+
el.detailStatusLabel.Text = "HTTP: OK MCP: ..."
|
|
5917
|
+
el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
5918
|
+
el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
5919
|
+
el.step1Label.Text = "HTTP server (OK)"
|
|
5920
|
+
el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
5921
|
+
el.step2Label.Text = "MCP bridge (waiting...)"
|
|
5922
|
+
el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
5923
|
+
el.step3Label.Text = "Commands (waiting...)"
|
|
5924
|
+
local elapsed = if conn.mcpWaitStartTime ~= nil then tick() - conn.mcpWaitStartTime else 0
|
|
5925
|
+
el.troubleshootLabel.Visible = elapsed > 8
|
|
5926
|
+
startPulseAnimation()
|
|
5927
|
+
elseif conn.consecutiveFailures >= conn.maxFailuresBeforeError then
|
|
5928
|
+
el.statusLabel.Text = "Server unavailable"
|
|
5929
|
+
el.statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
5930
|
+
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
5931
|
+
el.statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
5932
|
+
el.statusText.Text = "ERROR"
|
|
5933
|
+
el.detailStatusLabel.Text = "HTTP: X MCP: X"
|
|
5934
|
+
el.detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
5935
|
+
el.step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
5936
|
+
el.step1Label.Text = "HTTP server (error)"
|
|
5937
|
+
el.step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
5938
|
+
el.step2Label.Text = "MCP bridge (error)"
|
|
5939
|
+
el.step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
5940
|
+
el.step3Label.Text = "Commands (error)"
|
|
5941
|
+
el.troubleshootLabel.Visible = false
|
|
5942
|
+
stopPulseAnimation()
|
|
5943
|
+
elseif conn.consecutiveFailures > 5 then
|
|
5944
|
+
local waitTime = math.ceil(conn.currentRetryDelay)
|
|
5945
|
+
el.statusLabel.Text = `Retrying ({waitTime}s)`
|
|
5946
|
+
el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
5947
|
+
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
5948
|
+
el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
5949
|
+
el.statusText.Text = "RETRY"
|
|
5950
|
+
el.detailStatusLabel.Text = "HTTP: ... MCP: ..."
|
|
5951
|
+
el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
5952
|
+
el.step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
5953
|
+
el.step1Label.Text = "HTTP server (retrying...)"
|
|
5954
|
+
el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
5955
|
+
el.step2Label.Text = "MCP bridge (retrying...)"
|
|
5956
|
+
el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
5957
|
+
el.step3Label.Text = "Commands (retrying...)"
|
|
5958
|
+
el.troubleshootLabel.Visible = false
|
|
5959
|
+
startPulseAnimation()
|
|
5960
|
+
else
|
|
5961
|
+
el.statusLabel.Text = if conn.consecutiveFailures > 1 then `Connecting (attempt {conn.consecutiveFailures})` else "Connecting..."
|
|
5962
|
+
el.statusLabel.TextColor3 = C.yellow
|
|
5963
|
+
el.statusIndicator.BackgroundColor3 = C.yellow
|
|
5964
|
+
el.statusPulse.BackgroundColor3 = C.yellow
|
|
5965
|
+
el.statusText.Text = "CONNECTING"
|
|
5966
|
+
el.detailStatusLabel.Text = if conn.consecutiveFailures == 0 then "..." else "HTTP: ... MCP: ..."
|
|
5967
|
+
el.detailStatusLabel.TextColor3 = C.muted
|
|
5968
|
+
el.step1Dot.BackgroundColor3 = C.yellow
|
|
5969
|
+
el.step1Label.Text = "HTTP server (connecting...)"
|
|
5970
|
+
el.step2Dot.BackgroundColor3 = C.yellow
|
|
5971
|
+
el.step2Label.Text = "MCP bridge (connecting...)"
|
|
5972
|
+
el.step3Dot.BackgroundColor3 = C.yellow
|
|
5973
|
+
el.step3Label.Text = "Commands (connecting...)"
|
|
5974
|
+
el.troubleshootLabel.Visible = false
|
|
5975
|
+
startPulseAnimation()
|
|
5962
5976
|
end
|
|
5963
5977
|
end
|
|
5964
5978
|
return {
|
|
@@ -5966,6 +5980,7 @@ return {
|
|
|
5966
5980
|
init = init,
|
|
5967
5981
|
updateUIState = updateUIState,
|
|
5968
5982
|
updateTabDot = updateTabDot,
|
|
5983
|
+
updateTabLabel = updateTabLabel,
|
|
5969
5984
|
stopPulseAnimation = stopPulseAnimation,
|
|
5970
5985
|
startPulseAnimation = startPulseAnimation,
|
|
5971
5986
|
getElements = function()
|
|
@@ -19,9 +19,9 @@ const instanceId = HttpService.GenerateGUID(false);
|
|
|
19
19
|
let assignedRole: string | undefined;
|
|
20
20
|
|
|
21
21
|
function detectRole(): string {
|
|
22
|
-
if (RunService.
|
|
23
|
-
if (RunService.
|
|
24
|
-
return "
|
|
22
|
+
if (!RunService.IsRunMode()) return "edit";
|
|
23
|
+
if (RunService.IsServer()) return "server";
|
|
24
|
+
return "client";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
type Handler = (data: Record<string, unknown>) => unknown;
|
|
@@ -192,19 +192,6 @@ function pollForRequests(connIndex: number) {
|
|
|
192
192
|
}
|
|
193
193
|
const elapsed = tick() - (conn.mcpWaitStartTime ?? tick());
|
|
194
194
|
el.troubleshootLabel.Visible = elapsed > 8;
|
|
195
|
-
if (elapsed > 3 && elapsed % 5 < conn.pollInterval) {
|
|
196
|
-
task.spawn(() => {
|
|
197
|
-
const discovered = discoverPort(getUsedPorts(connIndex));
|
|
198
|
-
if (discovered !== undefined && discovered !== conn.port) {
|
|
199
|
-
conn.port = discovered;
|
|
200
|
-
conn.serverUrl = `http://localhost:${discovered}`;
|
|
201
|
-
UI.updateTabLabel(connIndex);
|
|
202
|
-
if (connIndex === State.getActiveTabIndex()) {
|
|
203
|
-
UI.getElements().urlInput.Text = conn.serverUrl;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
195
|
UI.startPulseAnimation();
|
|
209
196
|
}
|
|
210
197
|
}
|
|
@@ -229,21 +216,6 @@ function pollForRequests(connIndex: number) {
|
|
|
229
216
|
);
|
|
230
217
|
}
|
|
231
218
|
|
|
232
|
-
if (conn.consecutiveFailures === 5 || conn.consecutiveFailures % 20 === 0) {
|
|
233
|
-
task.spawn(() => {
|
|
234
|
-
const discovered = discoverPort(getUsedPorts(connIndex));
|
|
235
|
-
if (discovered !== undefined && discovered !== conn.port) {
|
|
236
|
-
conn.port = discovered;
|
|
237
|
-
conn.serverUrl = `http://localhost:${discovered}`;
|
|
238
|
-
conn.consecutiveFailures = 0;
|
|
239
|
-
conn.currentRetryDelay = 0.5;
|
|
240
|
-
UI.updateTabLabel(connIndex);
|
|
241
|
-
if (connIndex === State.getActiveTabIndex()) {
|
|
242
|
-
UI.getElements().urlInput.Text = conn.serverUrl;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
219
|
|
|
248
220
|
if (connIndex === State.getActiveTabIndex()) {
|
|
249
221
|
const el = ui;
|
|
@@ -304,53 +276,6 @@ function pollForRequests(connIndex: number) {
|
|
|
304
276
|
}
|
|
305
277
|
}
|
|
306
278
|
|
|
307
|
-
function getUsedPorts(excludeIndex: number): Set<number> {
|
|
308
|
-
const ports = new Set<number>();
|
|
309
|
-
const conns = State.getConnections();
|
|
310
|
-
for (let i = 0; i < conns.size(); i++) {
|
|
311
|
-
if (i !== excludeIndex && conns[i].isActive) {
|
|
312
|
-
ports.add(conns[i].port);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return ports;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function checkPort(port: number): boolean {
|
|
319
|
-
const [ok, res] = pcall(() => {
|
|
320
|
-
return HttpService.RequestAsync({
|
|
321
|
-
Url: `http://localhost:${port}/status`,
|
|
322
|
-
Method: "GET",
|
|
323
|
-
Headers: { "Content-Type": "application/json" },
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
return ok && res.Success;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function discoverPort(excludePorts?: Set<number>): number | undefined {
|
|
330
|
-
let firstActivePort: number | undefined;
|
|
331
|
-
for (let offset = 0; offset < 5; offset++) {
|
|
332
|
-
const port = State.BASE_PORT + offset;
|
|
333
|
-
if (excludePorts && excludePorts.has(port)) continue;
|
|
334
|
-
const [success, result] = pcall(() => {
|
|
335
|
-
return HttpService.RequestAsync({
|
|
336
|
-
Url: `http://localhost:${port}/status`,
|
|
337
|
-
Method: "GET",
|
|
338
|
-
Headers: { "Content-Type": "application/json" },
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
if (success && result.Success) {
|
|
343
|
-
const [ok, data] = pcall(() =>
|
|
344
|
-
HttpService.JSONDecode(result.Body) as { mcpServerActive: boolean; pluginConnected: boolean },
|
|
345
|
-
);
|
|
346
|
-
if (ok && data.mcpServerActive) {
|
|
347
|
-
if (!data.pluginConnected) return port;
|
|
348
|
-
if (firstActivePort === undefined) firstActivePort = port;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return firstActivePort;
|
|
353
|
-
}
|
|
354
279
|
|
|
355
280
|
function activatePlugin(connIndex?: number) {
|
|
356
281
|
const idx = connIndex ?? State.getActiveTabIndex();
|
|
@@ -374,19 +299,6 @@ function activatePlugin(connIndex?: number) {
|
|
|
374
299
|
UI.updateTabDot(idx);
|
|
375
300
|
|
|
376
301
|
task.spawn(() => {
|
|
377
|
-
if (!checkPort(conn.port)) {
|
|
378
|
-
const usedPorts = getUsedPorts(idx);
|
|
379
|
-
const discoveredPort = discoverPort(usedPorts);
|
|
380
|
-
if (discoveredPort !== undefined) {
|
|
381
|
-
conn.port = discoveredPort;
|
|
382
|
-
conn.serverUrl = `http://localhost:${discoveredPort}`;
|
|
383
|
-
UI.updateTabLabel(idx);
|
|
384
|
-
if (idx === State.getActiveTabIndex()) {
|
|
385
|
-
ui.urlInput.Text = conn.serverUrl;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
302
|
if (!conn.heartbeatConnection) {
|
|
391
303
|
conn.heartbeatConnection = RunService.Heartbeat.Connect(() => {
|
|
392
304
|
const now = tick();
|
|
@@ -193,15 +193,15 @@ function setScriptSource(requestData: Record<string, unknown>) {
|
|
|
193
193
|
|
|
194
194
|
function editScriptLines(requestData: Record<string, unknown>) {
|
|
195
195
|
const instancePath = requestData.instancePath as string;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
let newContent = requestData.newContent as string;
|
|
196
|
+
let oldString = requestData.old_string as string;
|
|
197
|
+
let newString = requestData.new_string as string;
|
|
199
198
|
|
|
200
|
-
if (!instancePath ||
|
|
201
|
-
return { error: "Instance path,
|
|
199
|
+
if (!instancePath || oldString === undefined || newString === undefined) {
|
|
200
|
+
return { error: "Instance path, old_string, and new_string are required" };
|
|
202
201
|
}
|
|
203
202
|
|
|
204
|
-
|
|
203
|
+
oldString = normalizeEscapes(oldString);
|
|
204
|
+
newString = normalizeEscapes(newString);
|
|
205
205
|
|
|
206
206
|
const instance = getInstanceByPath(instancePath);
|
|
207
207
|
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
@@ -209,32 +209,38 @@ function editScriptLines(requestData: Record<string, unknown>) {
|
|
|
209
209
|
return { error: `Instance is not a script-like object: ${instance.ClassName}` };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
const recordingId = beginRecording(`Edit script
|
|
212
|
+
const recordingId = beginRecording(`Edit script: ${instance.Name}`);
|
|
213
213
|
|
|
214
214
|
const [success, result] = pcall(() => {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
215
|
+
const source = readScriptSource(instance);
|
|
216
|
+
|
|
217
|
+
// Count occurrences to ensure uniqueness
|
|
218
|
+
let count = 0;
|
|
219
|
+
let searchPos = 1;
|
|
220
|
+
const searchLen = oldString.size();
|
|
221
|
+
|
|
222
|
+
while (true) {
|
|
223
|
+
const [foundStart] = string.find(source, oldString, searchPos, true);
|
|
224
|
+
if (foundStart === undefined) break;
|
|
225
|
+
count++;
|
|
226
|
+
if (count > 1) break;
|
|
227
|
+
searchPos = foundStart + searchLen;
|
|
228
|
+
}
|
|
220
229
|
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
if (count === 0) error("old_string not found in script");
|
|
231
|
+
if (count > 1) error("old_string matches multiple locations. Provide more surrounding context to make it unique");
|
|
223
232
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
// Perform the replacement (plain literal find + replace)
|
|
234
|
+
const escaped = escapeLuaPattern(oldString);
|
|
235
|
+
const escapedRepl = escapeLuaReplacement(newString);
|
|
236
|
+
const [newSource] = string.gsub(source, escaped, escapedRepl, 1);
|
|
227
237
|
|
|
228
|
-
const newSource = joinLines(resultLines, hadTrailingNewline);
|
|
229
238
|
ScriptEditorService.UpdateSourceAsync(instance, () => newSource);
|
|
230
239
|
|
|
231
240
|
return {
|
|
232
|
-
success: true,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
linesAdded: newLines.size(),
|
|
236
|
-
newLineCount: resultLines.size(),
|
|
237
|
-
message: "Script lines edited successfully",
|
|
241
|
+
success: true,
|
|
242
|
+
instancePath,
|
|
243
|
+
message: "Script edited successfully",
|
|
238
244
|
};
|
|
239
245
|
});
|
|
240
246
|
|
|
@@ -243,7 +249,7 @@ function editScriptLines(requestData: Record<string, unknown>) {
|
|
|
243
249
|
return result;
|
|
244
250
|
}
|
|
245
251
|
finishRecording(recordingId, false);
|
|
246
|
-
return { error: `Failed to edit script
|
|
252
|
+
return { error: `Failed to edit script: ${result}` };
|
|
247
253
|
}
|
|
248
254
|
|
|
249
255
|
function insertScriptLines(requestData: Record<string, unknown>) {
|