@chrrxs/robloxstudio-mcp-inspector 2.15.0 → 2.15.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/dist/index.js +200 -270
- package/package.json +2 -2
- package/studio-plugin/INSTALLATION.md +13 -3
- package/studio-plugin/MCPInspectorPlugin.rbxmx +388 -92
- package/studio-plugin/MCPPlugin.rbxmx +388 -92
- package/studio-plugin/src/modules/ClientBroker.ts +12 -2
- package/studio-plugin/src/modules/Communication.ts +22 -5
- package/studio-plugin/src/modules/EvalBridges.ts +6 -5
- package/studio-plugin/src/modules/LuauExec.ts +134 -36
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +9 -9
- package/studio-plugin/src/modules/State.ts +2 -0
- package/studio-plugin/src/modules/UI.ts +20 -0
- package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +121 -0
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +5 -6
- package/studio-plugin/src/types/index.d.ts +6 -0
package/dist/index.js
CHANGED
|
@@ -19,6 +19,10 @@ function toPublic(inst) {
|
|
|
19
19
|
placeName: inst.placeName,
|
|
20
20
|
dataModelName: inst.dataModelName,
|
|
21
21
|
isRunning: inst.isRunning,
|
|
22
|
+
pluginVersion: inst.pluginVersion,
|
|
23
|
+
pluginVariant: inst.pluginVariant,
|
|
24
|
+
serverVersion: inst.serverVersion,
|
|
25
|
+
versionMismatch: inst.versionMismatch,
|
|
22
26
|
lastActivity: inst.lastActivity,
|
|
23
27
|
connectedAt: inst.connectedAt
|
|
24
28
|
};
|
|
@@ -45,6 +49,10 @@ var init_bridge_service = __esm({
|
|
|
45
49
|
const { pluginSessionId, instanceId, role } = input;
|
|
46
50
|
const prior = this.instances.get(pluginSessionId);
|
|
47
51
|
let assignedRole = role;
|
|
52
|
+
const pluginVersion = input.pluginVersion ?? "";
|
|
53
|
+
const pluginVariant = input.pluginVariant ?? "unknown";
|
|
54
|
+
const serverVersion = input.serverVersion ?? "";
|
|
55
|
+
const versionMismatch = pluginVersion !== "" && serverVersion !== "" && pluginVersion !== serverVersion;
|
|
48
56
|
if (role === "client") {
|
|
49
57
|
if (prior && prior.instanceId === instanceId && prior.role.match(/^client-\d+$/)) {
|
|
50
58
|
assignedRole = prior.role;
|
|
@@ -82,6 +90,10 @@ var init_bridge_service = __esm({
|
|
|
82
90
|
placeName: input.placeName ?? "",
|
|
83
91
|
dataModelName: input.dataModelName ?? "",
|
|
84
92
|
isRunning: input.isRunning ?? false,
|
|
93
|
+
pluginVersion,
|
|
94
|
+
pluginVariant,
|
|
95
|
+
serverVersion,
|
|
96
|
+
versionMismatch,
|
|
85
97
|
lastActivity: Date.now(),
|
|
86
98
|
connectedAt: prior?.connectedAt ?? Date.now()
|
|
87
99
|
});
|
|
@@ -326,6 +338,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
326
338
|
let lastMCPActivity = 0;
|
|
327
339
|
let mcpServerStartTime = 0;
|
|
328
340
|
const proxyInstances = /* @__PURE__ */ new Set();
|
|
341
|
+
const warnedVersionMismatches = /* @__PURE__ */ new Set();
|
|
329
342
|
const setMCPServerActive = (active) => {
|
|
330
343
|
mcpServerActive = active;
|
|
331
344
|
if (active) {
|
|
@@ -354,13 +367,16 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
354
367
|
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
|
355
368
|
app.get("/health", (req, res) => {
|
|
356
369
|
const instances = bridge.getInstances();
|
|
370
|
+
const publicInstances = instances.map(toPublic);
|
|
357
371
|
res.json({
|
|
358
372
|
status: "ok",
|
|
359
373
|
service: "robloxstudio-mcp",
|
|
360
374
|
version: serverConfig?.version,
|
|
375
|
+
serverVersion: serverConfig?.version,
|
|
361
376
|
pluginConnected: instances.length > 0,
|
|
362
377
|
instanceCount: instances.length,
|
|
363
|
-
instances:
|
|
378
|
+
instances: publicInstances,
|
|
379
|
+
versionMismatch: publicInstances.some((inst) => inst.versionMismatch),
|
|
364
380
|
mcpServerActive: isMCPServerActive(),
|
|
365
381
|
uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0,
|
|
366
382
|
pendingRequests: bridge.getPendingRequestCount(),
|
|
@@ -369,7 +385,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
369
385
|
});
|
|
370
386
|
});
|
|
371
387
|
app.post("/ready", (req, res) => {
|
|
372
|
-
const { pluginSessionId, instanceId, role, placeId, placeName, dataModelName, isRunning } = req.body;
|
|
388
|
+
const { pluginSessionId, instanceId, role, placeId, placeName, dataModelName, isRunning, pluginVersion, pluginVariant } = req.body;
|
|
373
389
|
if (!pluginSessionId || !instanceId || !role) {
|
|
374
390
|
res.status(400).json({
|
|
375
391
|
success: false,
|
|
@@ -384,7 +400,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
384
400
|
placeId: typeof placeId === "number" ? placeId : 0,
|
|
385
401
|
placeName: typeof placeName === "string" ? placeName : "",
|
|
386
402
|
dataModelName: typeof dataModelName === "string" ? dataModelName : "",
|
|
387
|
-
isRunning: !!isRunning
|
|
403
|
+
isRunning: !!isRunning,
|
|
404
|
+
pluginVersion: typeof pluginVersion === "string" ? pluginVersion : "",
|
|
405
|
+
pluginVariant: typeof pluginVariant === "string" ? pluginVariant : "unknown",
|
|
406
|
+
serverVersion: serverConfig?.version ?? ""
|
|
388
407
|
});
|
|
389
408
|
if (!result.ok) {
|
|
390
409
|
res.status(409).json({
|
|
@@ -395,10 +414,17 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
395
414
|
});
|
|
396
415
|
return;
|
|
397
416
|
}
|
|
417
|
+
const registered = bridge.getInstanceBySessionId(pluginSessionId);
|
|
418
|
+
if (registered?.versionMismatch && !warnedVersionMismatches.has(pluginSessionId)) {
|
|
419
|
+
warnedVersionMismatches.add(pluginSessionId);
|
|
420
|
+
console.error(`[version-mismatch] Studio plugin v${registered.pluginVersion} (${registered.pluginVariant}) does not match MCP server v${registered.serverVersion} for ${registered.instanceId}/${registered.role}`);
|
|
421
|
+
}
|
|
398
422
|
res.json({
|
|
399
423
|
success: true,
|
|
400
424
|
assignedRole: result.assignedRole,
|
|
401
|
-
instanceId: result.instanceId
|
|
425
|
+
instanceId: result.instanceId,
|
|
426
|
+
serverVersion: serverConfig?.version,
|
|
427
|
+
versionMismatch: registered?.versionMismatch ?? false
|
|
402
428
|
});
|
|
403
429
|
});
|
|
404
430
|
app.post("/disconnect", (req, res) => {
|
|
@@ -410,17 +436,25 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
410
436
|
});
|
|
411
437
|
app.get("/status", (req, res) => {
|
|
412
438
|
const instances = bridge.getInstances();
|
|
439
|
+
const publicInstances = instances.map(toPublic);
|
|
413
440
|
res.json({
|
|
414
441
|
pluginConnected: instances.length > 0,
|
|
415
442
|
instanceCount: instances.length,
|
|
416
|
-
instances:
|
|
443
|
+
instances: publicInstances,
|
|
444
|
+
serverVersion: serverConfig?.version,
|
|
445
|
+
versionMismatch: publicInstances.some((inst) => inst.versionMismatch),
|
|
417
446
|
mcpServerActive: isMCPServerActive(),
|
|
418
447
|
lastMCPActivity,
|
|
419
448
|
uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0
|
|
420
449
|
});
|
|
421
450
|
});
|
|
422
451
|
app.get("/instances", (req, res) => {
|
|
423
|
-
|
|
452
|
+
const instances = bridge.getInstances();
|
|
453
|
+
res.json({
|
|
454
|
+
instances,
|
|
455
|
+
serverVersion: serverConfig?.version,
|
|
456
|
+
versionMismatch: instances.some((inst) => inst.versionMismatch)
|
|
457
|
+
});
|
|
424
458
|
});
|
|
425
459
|
app.get("/poll", (req, res) => {
|
|
426
460
|
const pluginSessionId = req.query.pluginSessionId;
|
|
@@ -430,11 +464,17 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
430
464
|
let callerInstanceId;
|
|
431
465
|
let callerRole;
|
|
432
466
|
let knownInstance = false;
|
|
467
|
+
let callerPluginVersion;
|
|
468
|
+
let callerPluginVariant;
|
|
469
|
+
let versionMismatch = false;
|
|
433
470
|
if (pluginSessionId) {
|
|
434
471
|
const inst = bridge.getInstanceBySessionId(pluginSessionId);
|
|
435
472
|
if (inst) {
|
|
436
473
|
callerInstanceId = inst.instanceId;
|
|
437
474
|
callerRole = inst.role;
|
|
475
|
+
callerPluginVersion = inst.pluginVersion;
|
|
476
|
+
callerPluginVariant = inst.pluginVariant;
|
|
477
|
+
versionMismatch = inst.versionMismatch;
|
|
438
478
|
knownInstance = true;
|
|
439
479
|
}
|
|
440
480
|
}
|
|
@@ -444,6 +484,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
444
484
|
pluginConnected: true,
|
|
445
485
|
mcpConnected: false,
|
|
446
486
|
knownInstance,
|
|
487
|
+
serverVersion: serverConfig?.version,
|
|
488
|
+
pluginVersion: callerPluginVersion,
|
|
489
|
+
pluginVariant: callerPluginVariant,
|
|
490
|
+
versionMismatch,
|
|
447
491
|
request: null
|
|
448
492
|
});
|
|
449
493
|
return;
|
|
@@ -456,6 +500,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
456
500
|
mcpConnected: true,
|
|
457
501
|
pluginConnected: true,
|
|
458
502
|
knownInstance,
|
|
503
|
+
serverVersion: serverConfig?.version,
|
|
504
|
+
pluginVersion: callerPluginVersion,
|
|
505
|
+
pluginVariant: callerPluginVariant,
|
|
506
|
+
versionMismatch,
|
|
459
507
|
proxyInstanceCount: proxyInstances.size
|
|
460
508
|
});
|
|
461
509
|
} else {
|
|
@@ -464,6 +512,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
464
512
|
mcpConnected: true,
|
|
465
513
|
pluginConnected: true,
|
|
466
514
|
knownInstance,
|
|
515
|
+
serverVersion: serverConfig?.version,
|
|
516
|
+
pluginVersion: callerPluginVersion,
|
|
517
|
+
pluginVariant: callerPluginVariant,
|
|
518
|
+
versionMismatch,
|
|
467
519
|
proxyInstanceCount: proxyInstances.size
|
|
468
520
|
});
|
|
469
521
|
}
|
|
@@ -2518,223 +2570,10 @@ function encodeImageFromRgbaResponse(response, format, quality) {
|
|
|
2518
2570
|
mimeType: "image/jpeg"
|
|
2519
2571
|
};
|
|
2520
2572
|
}
|
|
2521
|
-
function luaLongQuote(s) {
|
|
2522
|
-
let level = 0;
|
|
2523
|
-
while (s.includes(`]${"=".repeat(level)}]`))
|
|
2524
|
-
level++;
|
|
2525
|
-
const eq = "=".repeat(level);
|
|
2526
|
-
return `[${eq}[
|
|
2527
|
-
${s}
|
|
2528
|
-
]${eq}]`;
|
|
2529
|
-
}
|
|
2530
|
-
function evalCountLines(s) {
|
|
2531
|
-
return s.split("\n").length;
|
|
2532
|
-
}
|
|
2533
|
-
function buildModuleScriptInvokeWrapper(opts) {
|
|
2534
|
-
const userLines = evalCountLines(opts.userCode);
|
|
2535
|
-
const wrapped = `return ((function()
|
|
2536
|
-
local __mcp_traceback
|
|
2537
|
-
local __mcp_remap
|
|
2538
|
-
local __mcp_LINE_OFFSET = ${EVAL_WRAPPER_LINE_OFFSET}
|
|
2539
|
-
local __mcp_USER_LINES = ${userLines}
|
|
2540
|
-
local __mcp_output = {}
|
|
2541
|
-
local __mcp_real_print = print
|
|
2542
|
-
local __mcp_real_warn = warn
|
|
2543
|
-
local print = function(...)
|
|
2544
|
-
__mcp_real_print(...)
|
|
2545
|
-
local args = {...}
|
|
2546
|
-
local parts = table.create(#args)
|
|
2547
|
-
for i, a in ipairs(args) do parts[i] = tostring(a) end
|
|
2548
|
-
table.insert(__mcp_output, table.concat(parts, "\\t"))
|
|
2549
|
-
end
|
|
2550
|
-
local warn = function(...)
|
|
2551
|
-
__mcp_real_warn(...)
|
|
2552
|
-
local args = {...}
|
|
2553
|
-
local parts = table.create(#args)
|
|
2554
|
-
for i, a in ipairs(args) do parts[i] = tostring(a) end
|
|
2555
|
-
table.insert(__mcp_output, "[warn] " .. table.concat(parts, "\\t"))
|
|
2556
|
-
end
|
|
2557
|
-
local function __mcp_run()
|
|
2558
|
-
${opts.userCode}
|
|
2559
|
-
end
|
|
2560
|
-
__mcp_remap = function(s)
|
|
2561
|
-
-- Two chunk-name formats can reference our payload: the
|
|
2562
|
-
-- ModuleScript path "Workspace.__MCPEvalPayload:N" and the
|
|
2563
|
-
-- loadstring chunk "[string \\"return ((function()...\\"]:N" (if
|
|
2564
|
-
-- the IIFE happens to compile via loadstring). Normalize both to
|
|
2565
|
-
-- "user_code:N" with the offset stripped AND clamped to user
|
|
2566
|
-
-- range, otherwise unclosed constructs report nonsense lines deep
|
|
2567
|
-
-- in the wrapper. Strip the "Workspace." parent prefix too so the
|
|
2568
|
-
-- final output reads "user_code:N" not "Workspace.user_code:N".
|
|
2569
|
-
local function __mcp_user_line(payload_n)
|
|
2570
|
-
local user_n = payload_n - __mcp_LINE_OFFSET
|
|
2571
|
-
if user_n < 1 then return "1" end
|
|
2572
|
-
if user_n > __mcp_USER_LINES then return tostring(__mcp_USER_LINES) .. " (at end of input)" end
|
|
2573
|
-
return tostring(user_n)
|
|
2574
|
-
end
|
|
2575
|
-
s = string.gsub(s, "Workspace%.__MCPEvalPayload:(%d+)", function(num)
|
|
2576
|
-
local n = tonumber(num)
|
|
2577
|
-
if n then return "user_code:" .. __mcp_user_line(n) end
|
|
2578
|
-
return "user_code:" .. num
|
|
2579
|
-
end)
|
|
2580
|
-
s = string.gsub(s, "__MCPEvalPayload:(%d+)", function(num)
|
|
2581
|
-
local n = tonumber(num)
|
|
2582
|
-
if n then return "user_code:" .. __mcp_user_line(n) end
|
|
2583
|
-
return "user_code:" .. num
|
|
2584
|
-
end)
|
|
2585
|
-
s = string.gsub(s, '%[string "[^"]+"%]:(%d+)', function(num)
|
|
2586
|
-
local n = tonumber(num)
|
|
2587
|
-
if n then return "user_code:" .. __mcp_user_line(n) end
|
|
2588
|
-
return "user_code:" .. num
|
|
2589
|
-
end)
|
|
2590
|
-
return s
|
|
2591
|
-
end
|
|
2592
|
-
__mcp_traceback = function(err)
|
|
2593
|
-
local raw = debug.traceback(tostring(err), 2)
|
|
2594
|
-
local kept = {}
|
|
2595
|
-
for line in string.gmatch(raw, "[^\\n]+") do
|
|
2596
|
-
local num_str = string.match(line, "__MCPEvalPayload:(%d+)")
|
|
2597
|
-
or string.match(line, '%[string "[^"]+"%]:(%d+)')
|
|
2598
|
-
local n = num_str and tonumber(num_str)
|
|
2599
|
-
-- Strip "in function '__mcp_run'" annotation BEFORE filtering:
|
|
2600
|
-
-- user-code frames all carry that suffix (their source is
|
|
2601
|
-
-- hosted inside __mcp_run), so a naive "__mcp_" filter would
|
|
2602
|
-
-- drop every user frame and leave only the error header.
|
|
2603
|
-
line = (string.gsub(line, " in function '__mcp_run'", ""))
|
|
2604
|
-
local skip = string.find(line, "MCPPlugin", 1, true)
|
|
2605
|
-
or string.find(line, "__mcp_", 1, true)
|
|
2606
|
-
or string.find(line, "in function 'xpcall'", 1, true)
|
|
2607
|
-
-- Drop wrapper preamble/postamble frames whose line falls
|
|
2608
|
-
-- outside the user-code range \u2014 those are wrapper internals.
|
|
2609
|
-
if n and (n <= __mcp_LINE_OFFSET or n > __mcp_LINE_OFFSET + __mcp_USER_LINES) then
|
|
2610
|
-
skip = true
|
|
2611
|
-
end
|
|
2612
|
-
if not skip then
|
|
2613
|
-
table.insert(kept, __mcp_remap(line))
|
|
2614
|
-
end
|
|
2615
|
-
end
|
|
2616
|
-
return table.concat(kept, "\\n")
|
|
2617
|
-
end
|
|
2618
|
-
local ok, errOrValue = xpcall(__mcp_run, __mcp_traceback)
|
|
2619
|
-
return { ok = ok, value = errOrValue, output = __mcp_output }
|
|
2620
|
-
end)())`;
|
|
2621
|
-
return `
|
|
2622
|
-
local HttpService = game:GetService("HttpService")
|
|
2623
|
-
local bf = game:GetService("${opts.service}"):FindFirstChild("${opts.bridgeName}")
|
|
2624
|
-
if not bf then
|
|
2625
|
-
return HttpService:JSONEncode({
|
|
2626
|
-
bridge = "missing",
|
|
2627
|
-
error = ${luaLongQuote(opts.missingError)},
|
|
2628
|
-
})
|
|
2629
|
-
end
|
|
2630
|
-
-- Outer-scope mirror of the in-IIFE __mcp_remap. Applied to parser errors
|
|
2631
|
-
-- we pull out of LogService (those never pass through the IIFE) and to
|
|
2632
|
-
-- the canned engine error string. Same offset as the IIFE's
|
|
2633
|
-
-- __mcp_LINE_OFFSET; covers both chunk-name formats.
|
|
2634
|
-
local __mcp_USER_LINES_OUTER = ${userLines}
|
|
2635
|
-
local function __mcp_outer_user_line(payload_n)
|
|
2636
|
-
local user_n = payload_n - ${EVAL_WRAPPER_LINE_OFFSET}
|
|
2637
|
-
if user_n < 1 then return "1" end
|
|
2638
|
-
if user_n > __mcp_USER_LINES_OUTER then return tostring(__mcp_USER_LINES_OUTER) .. " (at end of input)" end
|
|
2639
|
-
return tostring(user_n)
|
|
2640
|
-
end
|
|
2641
|
-
local function __mcp_outer_remap(s)
|
|
2642
|
-
s = string.gsub(s, "Workspace%.__MCPEvalPayload:(%d+)", function(num)
|
|
2643
|
-
local n = tonumber(num)
|
|
2644
|
-
if n then return "user_code:" .. __mcp_outer_user_line(n) end
|
|
2645
|
-
return "user_code:" .. num
|
|
2646
|
-
end)
|
|
2647
|
-
s = string.gsub(s, "__MCPEvalPayload:(%d+)", function(num)
|
|
2648
|
-
local n = tonumber(num)
|
|
2649
|
-
if n then return "user_code:" .. __mcp_outer_user_line(n) end
|
|
2650
|
-
return "user_code:" .. num
|
|
2651
|
-
end)
|
|
2652
|
-
s = string.gsub(s, '%[string "[^"]+"%]:(%d+)', function(num)
|
|
2653
|
-
local n = tonumber(num)
|
|
2654
|
-
if n then return "user_code:" .. __mcp_outer_user_line(n) end
|
|
2655
|
-
return "user_code:" .. num
|
|
2656
|
-
end)
|
|
2657
|
-
return s
|
|
2658
|
-
end
|
|
2659
|
-
-- JSON-encode tables; otherwise tostring. Cycles or non-serializable
|
|
2660
|
-
-- values fall back to tostring instead of erroring. This is what makes
|
|
2661
|
-
-- eval_server_runtime / eval_client_runtime return structured table data
|
|
2662
|
-
-- (matching execute_luau) instead of "table: 0xaddr".
|
|
2663
|
-
local function __mcp_format(v)
|
|
2664
|
-
if typeof(v) == "table" then
|
|
2665
|
-
local ok, encoded = pcall(function() return HttpService:JSONEncode(v) end)
|
|
2666
|
-
if ok then return encoded end
|
|
2667
|
-
end
|
|
2668
|
-
return tostring(v)
|
|
2669
|
-
end
|
|
2670
|
-
local USER_CODE = ${luaLongQuote(wrapped)}
|
|
2671
|
-
local m = Instance.new("ModuleScript")
|
|
2672
|
-
m.Name = "__MCPEvalPayload"
|
|
2673
|
-
local okSet, setErr = pcall(function() m.Source = USER_CODE end)
|
|
2674
|
-
if not okSet then
|
|
2675
|
-
m:Destroy()
|
|
2676
|
-
return HttpService:JSONEncode({ bridge = "ok", ok = false, error = "ModuleScript Source set failed: " .. tostring(setErr) })
|
|
2677
|
-
end
|
|
2678
|
-
m.Parent = workspace
|
|
2679
|
-
local bridgeOk, inner = bf:Invoke(m)
|
|
2680
|
-
m:Destroy()
|
|
2681
|
-
if not bridgeOk then
|
|
2682
|
-
local errMsg = tostring(inner)
|
|
2683
|
-
-- pcall(require, payload) collapses parse/compile failures into the
|
|
2684
|
-
-- canned engine string below. The real parser diagnostic was emitted
|
|
2685
|
-
-- to LogService just before. Walk GetLogHistory backward for the most
|
|
2686
|
-
-- recent ERR entry tagged at our payload path and substitute.
|
|
2687
|
-
if errMsg == "Requested module experienced an error while loading" then
|
|
2688
|
-
-- The parser diagnostic is emitted to LogService on the next
|
|
2689
|
-
-- engine frame, not synchronously with pcall(require). task.wait(0)
|
|
2690
|
-
-- yields too early; 50ms is enough to let the frame complete and
|
|
2691
|
-
-- the message land in GetLogHistory.
|
|
2692
|
-
task.wait(0.05)
|
|
2693
|
-
local LogService = game:GetService("LogService")
|
|
2694
|
-
local hist = LogService:GetLogHistory()
|
|
2695
|
-
for i = #hist, 1, -1 do
|
|
2696
|
-
local e = hist[i]
|
|
2697
|
-
if e.messageType == Enum.MessageType.MessageError and string.sub(e.message, 1, 27) == "Workspace.__MCPEvalPayload:" then
|
|
2698
|
-
errMsg = e.message
|
|
2699
|
-
break
|
|
2700
|
-
end
|
|
2701
|
-
end
|
|
2702
|
-
end
|
|
2703
|
-
return HttpService:JSONEncode({ bridge = "ok", ok = false, error = __mcp_outer_remap(errMsg) })
|
|
2704
|
-
end
|
|
2705
|
-
-- inner is the {ok, value, output} table from our IIFE. Defensive: if it's
|
|
2706
|
-
-- somehow not a table (caller bypassed the wrapper), fall back to old shape.
|
|
2707
|
-
if typeof(inner) ~= "table" then
|
|
2708
|
-
return HttpService:JSONEncode({
|
|
2709
|
-
bridge = "ok",
|
|
2710
|
-
ok = true,
|
|
2711
|
-
result = if inner == nil then nil else __mcp_format(inner),
|
|
2712
|
-
})
|
|
2713
|
-
end
|
|
2714
|
-
return HttpService:JSONEncode({
|
|
2715
|
-
bridge = "ok",
|
|
2716
|
-
ok = inner.ok == true,
|
|
2717
|
-
result = if inner.ok and inner.value ~= nil then __mcp_format(inner.value) else nil,
|
|
2718
|
-
error = if not inner.ok then tostring(inner.value) else nil,
|
|
2719
|
-
output = inner.output or {},
|
|
2720
|
-
})
|
|
2721
|
-
`;
|
|
2722
|
-
}
|
|
2723
|
-
function parseBridgeResponse(response) {
|
|
2724
|
-
const r = response;
|
|
2725
|
-
if (r && typeof r.returnValue === "string") {
|
|
2726
|
-
try {
|
|
2727
|
-
const parsed = JSON.parse(r.returnValue);
|
|
2728
|
-
return JSON.stringify(parsed);
|
|
2729
|
-
} catch {
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
return JSON.stringify(response);
|
|
2733
|
-
}
|
|
2734
2573
|
function sleep(ms) {
|
|
2735
2574
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2736
2575
|
}
|
|
2737
|
-
var
|
|
2576
|
+
var RobloxStudioTools;
|
|
2738
2577
|
var init_tools = __esm({
|
|
2739
2578
|
"../core/dist/tools/index.js"() {
|
|
2740
2579
|
"use strict";
|
|
@@ -2745,9 +2584,6 @@ var init_tools = __esm({
|
|
|
2745
2584
|
init_roblox_cookie_client();
|
|
2746
2585
|
init_jpeg_encoder();
|
|
2747
2586
|
init_png_encoder();
|
|
2748
|
-
SERVER_LOCAL_NAME = "__MCP_ServerEvalLocal";
|
|
2749
|
-
CLIENT_LOCAL_NAME = "__MCP_ClientEvalBridge";
|
|
2750
|
-
EVAL_WRAPPER_LINE_OFFSET = 23;
|
|
2751
2587
|
RobloxStudioTools = class _RobloxStudioTools {
|
|
2752
2588
|
client;
|
|
2753
2589
|
bridge;
|
|
@@ -3424,18 +3260,12 @@ ${code}`
|
|
|
3424
3260
|
if (!code) {
|
|
3425
3261
|
throw new Error("Code is required for eval_server_runtime");
|
|
3426
3262
|
}
|
|
3427
|
-
const
|
|
3428
|
-
service: "ServerScriptService",
|
|
3429
|
-
bridgeName: SERVER_LOCAL_NAME,
|
|
3430
|
-
missingError: "ServerEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically (including for manually-started playtests); if a playtest is running and you still see this, reconnect the plugin in the edit window so the bridge reinstalls, then start the playtest again.",
|
|
3431
|
-
userCode: code
|
|
3432
|
-
});
|
|
3433
|
-
const response = await this._callSingle("/api/execute-luau", { code: wrapper }, "server", instance_id);
|
|
3263
|
+
const response = await this._callSingle("/api/eval-runtime", { code }, "server", instance_id);
|
|
3434
3264
|
return {
|
|
3435
3265
|
content: [
|
|
3436
3266
|
{
|
|
3437
3267
|
type: "text",
|
|
3438
|
-
text:
|
|
3268
|
+
text: JSON.stringify(response)
|
|
3439
3269
|
}
|
|
3440
3270
|
]
|
|
3441
3271
|
};
|
|
@@ -3448,18 +3278,12 @@ ${code}`
|
|
|
3448
3278
|
if (!clientTarget.startsWith("client-")) {
|
|
3449
3279
|
throw new Error(`eval_client_runtime requires target=client-N (got: ${clientTarget})`);
|
|
3450
3280
|
}
|
|
3451
|
-
const
|
|
3452
|
-
service: "ReplicatedStorage",
|
|
3453
|
-
bridgeName: CLIENT_LOCAL_NAME,
|
|
3454
|
-
missingError: "ClientEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically (including for manually-started playtests); if a playtest is running and you still see this, reconnect the plugin in the edit window so the bridge reinstalls, then start the playtest again.",
|
|
3455
|
-
userCode: code
|
|
3456
|
-
});
|
|
3457
|
-
const response = await this._callSingle("/api/execute-luau", { code: wrapper }, clientTarget, instance_id);
|
|
3281
|
+
const response = await this._callSingle("/api/eval-runtime", { code }, clientTarget, instance_id);
|
|
3458
3282
|
return {
|
|
3459
3283
|
content: [
|
|
3460
3284
|
{
|
|
3461
3285
|
type: "text",
|
|
3462
|
-
text:
|
|
3286
|
+
text: JSON.stringify(response)
|
|
3463
3287
|
}
|
|
3464
3288
|
]
|
|
3465
3289
|
};
|
|
@@ -3477,11 +3301,19 @@ ${code}`
|
|
|
3477
3301
|
if (!resolved.ok)
|
|
3478
3302
|
throw new RoutingFailure(resolved.error);
|
|
3479
3303
|
if (resolved.mode === "single") {
|
|
3304
|
+
const originPeerReliable2 = await this._isMultiplayerTestRunning(resolved.targetInstanceId);
|
|
3480
3305
|
const response = await this.client.request("/api/get-runtime-logs", data, resolved.targetInstanceId, resolved.targetRole);
|
|
3481
|
-
response.
|
|
3306
|
+
response.capturedBy = resolved.targetRole;
|
|
3307
|
+
delete response.peer;
|
|
3308
|
+
response.originPeerReliable = originPeerReliable2;
|
|
3309
|
+
response.peerAttribution = originPeerReliable2 ? "guaranteed_multiplayer" : "unavailable_shared_logservice";
|
|
3310
|
+
if (originPeerReliable2)
|
|
3311
|
+
response.peer = resolved.targetRole;
|
|
3482
3312
|
if (Array.isArray(response.entries)) {
|
|
3483
3313
|
for (const e of response.entries) {
|
|
3484
|
-
|
|
3314
|
+
e.capturedBy = resolved.targetRole;
|
|
3315
|
+
delete e.peer;
|
|
3316
|
+
if (originPeerReliable2)
|
|
3485
3317
|
e.peer = resolved.targetRole;
|
|
3486
3318
|
}
|
|
3487
3319
|
}
|
|
@@ -3490,35 +3322,38 @@ ${code}`
|
|
|
3490
3322
|
};
|
|
3491
3323
|
}
|
|
3492
3324
|
const targets = resolved.targets.filter((t) => t.targetRole !== "edit-proxy");
|
|
3325
|
+
const originPeerReliable = targets.length > 0 ? await this._isMultiplayerTestRunning(targets[0].targetInstanceId) : false;
|
|
3493
3326
|
const responses = await Promise.allSettled(targets.map(async (t) => {
|
|
3494
3327
|
const r = await this.client.request("/api/get-runtime-logs", data, t.targetInstanceId, t.targetRole);
|
|
3495
|
-
return { ...r,
|
|
3328
|
+
return { ...r, capturedBy: t.targetRole };
|
|
3496
3329
|
}));
|
|
3497
3330
|
const merged = [];
|
|
3498
|
-
const
|
|
3499
|
-
const
|
|
3331
|
+
const perCaptureNextSince = {};
|
|
3332
|
+
const perCaptureErrors = {};
|
|
3500
3333
|
let totalDropped = 0;
|
|
3501
3334
|
for (const r of responses) {
|
|
3502
3335
|
if (r.status !== "fulfilled")
|
|
3503
3336
|
continue;
|
|
3504
3337
|
const v = r.value;
|
|
3505
|
-
const
|
|
3338
|
+
const capturedBy = v.capturedBy ?? "unknown";
|
|
3506
3339
|
if (v.error) {
|
|
3507
|
-
|
|
3340
|
+
perCaptureErrors[capturedBy] = v.error;
|
|
3508
3341
|
continue;
|
|
3509
3342
|
}
|
|
3510
3343
|
if (v.nextSince !== void 0)
|
|
3511
|
-
|
|
3344
|
+
perCaptureNextSince[capturedBy] = v.nextSince;
|
|
3512
3345
|
totalDropped += v.totalDropped ?? 0;
|
|
3513
3346
|
for (const e of v.entries ?? []) {
|
|
3514
|
-
|
|
3347
|
+
const entry = { ...e };
|
|
3348
|
+
delete entry.peer;
|
|
3349
|
+
merged.push({ ...entry, capturedBy });
|
|
3515
3350
|
}
|
|
3516
3351
|
}
|
|
3517
3352
|
merged.sort((a, b) => a.ts !== b.ts ? a.ts - b.ts : a.seq - b.seq);
|
|
3518
3353
|
const DEDUP_WINDOW = 2;
|
|
3519
3354
|
const deduped = [];
|
|
3520
3355
|
for (const e of merged) {
|
|
3521
|
-
const isDup = deduped.some((d) => d.message === e.message && d.level === e.level && Math.abs(d.ts - e.ts) <= DEDUP_WINDOW && d.
|
|
3356
|
+
const isDup = deduped.some((d) => d.message === e.message && d.level === e.level && Math.abs(d.ts - e.ts) <= DEDUP_WINDOW && d.capturedBy !== e.capturedBy);
|
|
3522
3357
|
if (!isDup)
|
|
3523
3358
|
deduped.push(e);
|
|
3524
3359
|
}
|
|
@@ -3526,13 +3361,21 @@ ${code}`
|
|
|
3526
3361
|
if (tail !== void 0 && deduped.length > tail) {
|
|
3527
3362
|
final = deduped.slice(deduped.length - tail);
|
|
3528
3363
|
}
|
|
3364
|
+
const finalEntries = originPeerReliable ? final.map((e) => ({ ...e, peer: e.capturedBy })) : final;
|
|
3529
3365
|
const body = {
|
|
3530
|
-
entries:
|
|
3366
|
+
entries: finalEntries,
|
|
3531
3367
|
totalDropped,
|
|
3532
|
-
|
|
3368
|
+
perCaptureNextSince,
|
|
3369
|
+
originPeerReliable,
|
|
3370
|
+
peerAttribution: originPeerReliable ? "guaranteed_multiplayer" : "unavailable_shared_logservice"
|
|
3533
3371
|
};
|
|
3534
|
-
if (
|
|
3535
|
-
body.
|
|
3372
|
+
if (originPeerReliable) {
|
|
3373
|
+
body.perPeerNextSince = perCaptureNextSince;
|
|
3374
|
+
}
|
|
3375
|
+
if (Object.keys(perCaptureErrors).length > 0) {
|
|
3376
|
+
body.perCaptureErrors = perCaptureErrors;
|
|
3377
|
+
if (originPeerReliable)
|
|
3378
|
+
body.perPeerErrors = perCaptureErrors;
|
|
3536
3379
|
}
|
|
3537
3380
|
return {
|
|
3538
3381
|
content: [{ type: "text", text: JSON.stringify(body) }]
|
|
@@ -6266,17 +6109,17 @@ var init_definitions = __esm({
|
|
|
6266
6109
|
{
|
|
6267
6110
|
name: "get_runtime_logs",
|
|
6268
6111
|
category: "read",
|
|
6269
|
-
description: "Read the in-memory log
|
|
6112
|
+
description: "Read the in-memory log buffers captured by Studio plugin peers. Each buffer captures ~64 KB of recent LogService.MessageOut entries; oldest entries drop when over budget. Entries include capturedBy for the plugin buffer that observed the log. In ordinary Studio play/run sessions, LogService reflects logs across edit/server/client, so script-origin peer is not reliable and entries omit peer. In StudioTestService multiplayer sessions only, peer attribution is reliable and entries also include peer. target=all (default) merges buffers and dedups same-message-and-level entries captured within 2s across different buffers.",
|
|
6270
6113
|
inputSchema: {
|
|
6271
6114
|
type: "object",
|
|
6272
6115
|
properties: {
|
|
6273
6116
|
target: {
|
|
6274
6117
|
type: "string",
|
|
6275
|
-
description: '
|
|
6118
|
+
description: 'Capture buffer to read from: "edit", "server", "client-N", or "all" (default). "all" merges buffers and dedups cross-buffer reflections within a 2s window.'
|
|
6276
6119
|
},
|
|
6277
6120
|
since: {
|
|
6278
6121
|
type: "number",
|
|
6279
|
-
description: "Return only entries with seq > since. Pass back the previous response's nextSince (single
|
|
6122
|
+
description: "Return only entries with seq > since. Pass back the previous response's nextSince (single target) or perCaptureNextSince entry (target=all) for incremental polling."
|
|
6280
6123
|
},
|
|
6281
6124
|
tail: {
|
|
6282
6125
|
type: "number",
|
|
@@ -7283,23 +7126,23 @@ function getPluginsFolder() {
|
|
|
7283
7126
|
}
|
|
7284
7127
|
return join2(homedir2(), "Documents", "Roblox", "Plugins");
|
|
7285
7128
|
}
|
|
7286
|
-
function handleVariantConflict({ pluginsFolder, otherAssetName, replace }) {
|
|
7129
|
+
function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
|
|
7287
7130
|
const otherDest = join2(pluginsFolder, otherAssetName);
|
|
7288
7131
|
if (!existsSync2(otherDest))
|
|
7289
7132
|
return;
|
|
7290
7133
|
if (replace) {
|
|
7291
7134
|
try {
|
|
7292
7135
|
unlinkSync(otherDest);
|
|
7293
|
-
|
|
7136
|
+
log(`Removed conflicting ${otherAssetName}.`);
|
|
7294
7137
|
} catch (err) {
|
|
7295
|
-
|
|
7138
|
+
warn(`[install-plugin] Could not remove ${otherDest}: ${err}. Continuing.`);
|
|
7296
7139
|
}
|
|
7297
7140
|
return;
|
|
7298
7141
|
}
|
|
7299
|
-
|
|
7142
|
+
warn(`
|
|
7300
7143
|
[install-plugin] WARNING: ${otherAssetName} is already present in ${pluginsFolder}.
|
|
7301
7144
|
Only one MCP plugin variant should be present. If both variants are in the Studio Plugins folder, Studio loads both and runtime routing can become unpredictable.
|
|
7302
|
-
|
|
7145
|
+
Delete ${otherAssetName} manually or use the default CLI installer behavior to replace it.
|
|
7303
7146
|
`);
|
|
7304
7147
|
}
|
|
7305
7148
|
var init_install_plugin_helpers = __esm({
|
|
@@ -7327,10 +7170,12 @@ var init_dist = __esm({
|
|
|
7327
7170
|
// src/install-plugin.ts
|
|
7328
7171
|
var install_plugin_exports = {};
|
|
7329
7172
|
__export(install_plugin_exports, {
|
|
7173
|
+
installBundledPlugin: () => installBundledPlugin,
|
|
7330
7174
|
installPlugin: () => installPlugin
|
|
7331
7175
|
});
|
|
7332
|
-
import { createWriteStream, existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
7333
|
-
import { join as join3 } from "path";
|
|
7176
|
+
import { copyFileSync, createWriteStream, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
7177
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
7178
|
+
import { fileURLToPath } from "url";
|
|
7334
7179
|
import { get } from "https";
|
|
7335
7180
|
function httpsGet(url) {
|
|
7336
7181
|
return new Promise((resolve2, reject) => {
|
|
@@ -7383,8 +7228,11 @@ async function fetchJson(url) {
|
|
|
7383
7228
|
}
|
|
7384
7229
|
return JSON.parse(Buffer.concat(chunks).toString());
|
|
7385
7230
|
}
|
|
7386
|
-
|
|
7387
|
-
|
|
7231
|
+
function prepareInstall({
|
|
7232
|
+
replaceVariant,
|
|
7233
|
+
log,
|
|
7234
|
+
warn
|
|
7235
|
+
}) {
|
|
7388
7236
|
const pluginsFolder = getPluginsFolder();
|
|
7389
7237
|
if (!existsSync3(pluginsFolder)) {
|
|
7390
7238
|
mkdirSync2(pluginsFolder, { recursive: true });
|
|
@@ -7392,18 +7240,89 @@ async function installPlugin() {
|
|
|
7392
7240
|
handleVariantConflict({
|
|
7393
7241
|
pluginsFolder,
|
|
7394
7242
|
otherAssetName: OTHER_VARIANT,
|
|
7395
|
-
replace: replaceVariant
|
|
7243
|
+
replace: replaceVariant,
|
|
7244
|
+
log,
|
|
7245
|
+
warn
|
|
7396
7246
|
});
|
|
7397
|
-
|
|
7247
|
+
return pluginsFolder;
|
|
7248
|
+
}
|
|
7249
|
+
function bundledAssetPath() {
|
|
7250
|
+
const currentDir = dirname2(fileURLToPath(import.meta.url));
|
|
7251
|
+
const candidates = [
|
|
7252
|
+
join3(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
7253
|
+
join3(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
7254
|
+
];
|
|
7255
|
+
return candidates.find((candidate) => existsSync3(candidate)) ?? null;
|
|
7256
|
+
}
|
|
7257
|
+
function packageVersion() {
|
|
7258
|
+
const currentDir = dirname2(fileURLToPath(import.meta.url));
|
|
7259
|
+
const pkg = JSON.parse(readFileSync3(join3(currentDir, "..", "package.json"), "utf8"));
|
|
7260
|
+
if (!pkg.version) {
|
|
7261
|
+
throw new Error("Package version not found");
|
|
7262
|
+
}
|
|
7263
|
+
return pkg.version;
|
|
7264
|
+
}
|
|
7265
|
+
function bundledPluginVersion(source) {
|
|
7266
|
+
const match = readFileSync3(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
7267
|
+
return match ? match[1] : null;
|
|
7268
|
+
}
|
|
7269
|
+
function assertBundledPluginVersion(source) {
|
|
7270
|
+
const expected = packageVersion();
|
|
7271
|
+
const actual = bundledPluginVersion(source);
|
|
7272
|
+
if (actual !== expected) {
|
|
7273
|
+
throw new Error(
|
|
7274
|
+
`Bundled ${ASSET_NAME} version ${actual ?? "unknown"} does not match package version ${expected}. Run npm run build:plugin:inspector before starting with --auto-install-plugin.`
|
|
7275
|
+
);
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
function filesMatch(a, b) {
|
|
7279
|
+
if (!existsSync3(b)) return false;
|
|
7280
|
+
const aBytes = readFileSync3(a);
|
|
7281
|
+
const bBytes = readFileSync3(b);
|
|
7282
|
+
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
7283
|
+
}
|
|
7284
|
+
async function installBundledPlugin(options = {}) {
|
|
7285
|
+
const log = options.log ?? console.log;
|
|
7286
|
+
const warn = options.warn ?? console.warn;
|
|
7287
|
+
const replaceVariant = options.replaceVariant ?? true;
|
|
7288
|
+
const source = bundledAssetPath();
|
|
7289
|
+
if (!source) {
|
|
7290
|
+
throw new Error(`Bundled ${ASSET_NAME} not found in package`);
|
|
7291
|
+
}
|
|
7292
|
+
assertBundledPluginVersion(source);
|
|
7293
|
+
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
7294
|
+
const dest = join3(pluginsFolder, ASSET_NAME);
|
|
7295
|
+
if (filesMatch(source, dest)) return;
|
|
7296
|
+
copyFileSync(source, dest);
|
|
7297
|
+
log(`Installed ${ASSET_NAME} to ${dest}`);
|
|
7298
|
+
}
|
|
7299
|
+
async function installPlugin(options = {}) {
|
|
7300
|
+
const replaceVariant = options.replaceVariant ?? true;
|
|
7301
|
+
const log = options.log ?? console.log;
|
|
7302
|
+
const warn = options.warn ?? console.warn;
|
|
7303
|
+
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
7304
|
+
const bundled = bundledAssetPath();
|
|
7305
|
+
if (bundled) {
|
|
7306
|
+
assertBundledPluginVersion(bundled);
|
|
7307
|
+
const dest2 = join3(pluginsFolder, ASSET_NAME);
|
|
7308
|
+
if (filesMatch(bundled, dest2)) {
|
|
7309
|
+
log(`${ASSET_NAME} already installed.`);
|
|
7310
|
+
return;
|
|
7311
|
+
}
|
|
7312
|
+
copyFileSync(bundled, dest2);
|
|
7313
|
+
log(`Installed bundled ${ASSET_NAME} to ${dest2}`);
|
|
7314
|
+
return;
|
|
7315
|
+
}
|
|
7316
|
+
log("Fetching latest release...");
|
|
7398
7317
|
const release = await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
7399
7318
|
const asset = release.assets?.find((a) => a.name === ASSET_NAME);
|
|
7400
7319
|
if (!asset) {
|
|
7401
7320
|
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
7402
7321
|
}
|
|
7403
7322
|
const dest = join3(pluginsFolder, ASSET_NAME);
|
|
7404
|
-
|
|
7323
|
+
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
7405
7324
|
await download(asset.browser_download_url, dest);
|
|
7406
|
-
|
|
7325
|
+
log(`Installed to ${dest}`);
|
|
7407
7326
|
}
|
|
7408
7327
|
var REPO, ASSET_NAME, OTHER_VARIANT, TIMEOUT_MS, MAX_REDIRECTS;
|
|
7409
7328
|
var init_install_plugin = __esm({
|
|
@@ -7423,11 +7342,22 @@ init_dist();
|
|
|
7423
7342
|
import { createRequire } from "module";
|
|
7424
7343
|
if (process.argv.includes("--install-plugin")) {
|
|
7425
7344
|
const { installPlugin: installPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
|
|
7426
|
-
installPlugin2().catch((err) => {
|
|
7345
|
+
await installPlugin2().catch((err) => {
|
|
7427
7346
|
console.error(err instanceof Error ? err.message : String(err));
|
|
7428
7347
|
process.exitCode = 1;
|
|
7429
7348
|
});
|
|
7430
7349
|
} else {
|
|
7350
|
+
if (process.argv.includes("--auto-install-plugin")) {
|
|
7351
|
+
const { installBundledPlugin: installBundledPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
|
|
7352
|
+
await installBundledPlugin2({
|
|
7353
|
+
log: (message) => console.error(`[install-plugin] ${message}`),
|
|
7354
|
+
warn: (message) => console.error(message)
|
|
7355
|
+
}).catch((err) => {
|
|
7356
|
+
console.error(
|
|
7357
|
+
`[install-plugin] Auto-install skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
7358
|
+
);
|
|
7359
|
+
});
|
|
7360
|
+
}
|
|
7431
7361
|
const require2 = createRequire(import.meta.url);
|
|
7432
7362
|
const { version: VERSION } = require2("../package.json");
|
|
7433
7363
|
const server = new RobloxStudioMCPServer({
|