@chrrxs/robloxstudio-mcp 2.15.1 → 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 +79 -255
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +329 -79
- package/studio-plugin/MCPPlugin.rbxmx +329 -79
- package/studio-plugin/src/modules/ClientBroker.ts +7 -2
- package/studio-plugin/src/modules/Communication.ts +2 -0
- 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/handlers/EvalRuntimeHandlers.ts +121 -0
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +5 -6
package/dist/index.js
CHANGED
|
@@ -2570,223 +2570,10 @@ function encodeImageFromRgbaResponse(response, format, quality) {
|
|
|
2570
2570
|
mimeType: "image/jpeg"
|
|
2571
2571
|
};
|
|
2572
2572
|
}
|
|
2573
|
-
function luaLongQuote(s) {
|
|
2574
|
-
let level = 0;
|
|
2575
|
-
while (s.includes(`]${"=".repeat(level)}]`))
|
|
2576
|
-
level++;
|
|
2577
|
-
const eq = "=".repeat(level);
|
|
2578
|
-
return `[${eq}[
|
|
2579
|
-
${s}
|
|
2580
|
-
]${eq}]`;
|
|
2581
|
-
}
|
|
2582
|
-
function evalCountLines(s) {
|
|
2583
|
-
return s.split("\n").length;
|
|
2584
|
-
}
|
|
2585
|
-
function buildModuleScriptInvokeWrapper(opts) {
|
|
2586
|
-
const userLines = evalCountLines(opts.userCode);
|
|
2587
|
-
const wrapped = `return ((function()
|
|
2588
|
-
local __mcp_traceback
|
|
2589
|
-
local __mcp_remap
|
|
2590
|
-
local __mcp_LINE_OFFSET = ${EVAL_WRAPPER_LINE_OFFSET}
|
|
2591
|
-
local __mcp_USER_LINES = ${userLines}
|
|
2592
|
-
local __mcp_output = {}
|
|
2593
|
-
local __mcp_real_print = print
|
|
2594
|
-
local __mcp_real_warn = warn
|
|
2595
|
-
local print = function(...)
|
|
2596
|
-
__mcp_real_print(...)
|
|
2597
|
-
local args = {...}
|
|
2598
|
-
local parts = table.create(#args)
|
|
2599
|
-
for i, a in ipairs(args) do parts[i] = tostring(a) end
|
|
2600
|
-
table.insert(__mcp_output, table.concat(parts, "\\t"))
|
|
2601
|
-
end
|
|
2602
|
-
local warn = function(...)
|
|
2603
|
-
__mcp_real_warn(...)
|
|
2604
|
-
local args = {...}
|
|
2605
|
-
local parts = table.create(#args)
|
|
2606
|
-
for i, a in ipairs(args) do parts[i] = tostring(a) end
|
|
2607
|
-
table.insert(__mcp_output, "[warn] " .. table.concat(parts, "\\t"))
|
|
2608
|
-
end
|
|
2609
|
-
local function __mcp_run()
|
|
2610
|
-
${opts.userCode}
|
|
2611
|
-
end
|
|
2612
|
-
__mcp_remap = function(s)
|
|
2613
|
-
-- Two chunk-name formats can reference our payload: the
|
|
2614
|
-
-- ModuleScript path "Workspace.__MCPEvalPayload:N" and the
|
|
2615
|
-
-- loadstring chunk "[string \\"return ((function()...\\"]:N" (if
|
|
2616
|
-
-- the IIFE happens to compile via loadstring). Normalize both to
|
|
2617
|
-
-- "user_code:N" with the offset stripped AND clamped to user
|
|
2618
|
-
-- range, otherwise unclosed constructs report nonsense lines deep
|
|
2619
|
-
-- in the wrapper. Strip the "Workspace." parent prefix too so the
|
|
2620
|
-
-- final output reads "user_code:N" not "Workspace.user_code:N".
|
|
2621
|
-
local function __mcp_user_line(payload_n)
|
|
2622
|
-
local user_n = payload_n - __mcp_LINE_OFFSET
|
|
2623
|
-
if user_n < 1 then return "1" end
|
|
2624
|
-
if user_n > __mcp_USER_LINES then return tostring(__mcp_USER_LINES) .. " (at end of input)" end
|
|
2625
|
-
return tostring(user_n)
|
|
2626
|
-
end
|
|
2627
|
-
s = string.gsub(s, "Workspace%.__MCPEvalPayload:(%d+)", function(num)
|
|
2628
|
-
local n = tonumber(num)
|
|
2629
|
-
if n then return "user_code:" .. __mcp_user_line(n) end
|
|
2630
|
-
return "user_code:" .. num
|
|
2631
|
-
end)
|
|
2632
|
-
s = string.gsub(s, "__MCPEvalPayload:(%d+)", function(num)
|
|
2633
|
-
local n = tonumber(num)
|
|
2634
|
-
if n then return "user_code:" .. __mcp_user_line(n) end
|
|
2635
|
-
return "user_code:" .. num
|
|
2636
|
-
end)
|
|
2637
|
-
s = string.gsub(s, '%[string "[^"]+"%]:(%d+)', function(num)
|
|
2638
|
-
local n = tonumber(num)
|
|
2639
|
-
if n then return "user_code:" .. __mcp_user_line(n) end
|
|
2640
|
-
return "user_code:" .. num
|
|
2641
|
-
end)
|
|
2642
|
-
return s
|
|
2643
|
-
end
|
|
2644
|
-
__mcp_traceback = function(err)
|
|
2645
|
-
local raw = debug.traceback(tostring(err), 2)
|
|
2646
|
-
local kept = {}
|
|
2647
|
-
for line in string.gmatch(raw, "[^\\n]+") do
|
|
2648
|
-
local num_str = string.match(line, "__MCPEvalPayload:(%d+)")
|
|
2649
|
-
or string.match(line, '%[string "[^"]+"%]:(%d+)')
|
|
2650
|
-
local n = num_str and tonumber(num_str)
|
|
2651
|
-
-- Strip "in function '__mcp_run'" annotation BEFORE filtering:
|
|
2652
|
-
-- user-code frames all carry that suffix (their source is
|
|
2653
|
-
-- hosted inside __mcp_run), so a naive "__mcp_" filter would
|
|
2654
|
-
-- drop every user frame and leave only the error header.
|
|
2655
|
-
line = (string.gsub(line, " in function '__mcp_run'", ""))
|
|
2656
|
-
local skip = string.find(line, "MCPPlugin", 1, true)
|
|
2657
|
-
or string.find(line, "__mcp_", 1, true)
|
|
2658
|
-
or string.find(line, "in function 'xpcall'", 1, true)
|
|
2659
|
-
-- Drop wrapper preamble/postamble frames whose line falls
|
|
2660
|
-
-- outside the user-code range \u2014 those are wrapper internals.
|
|
2661
|
-
if n and (n <= __mcp_LINE_OFFSET or n > __mcp_LINE_OFFSET + __mcp_USER_LINES) then
|
|
2662
|
-
skip = true
|
|
2663
|
-
end
|
|
2664
|
-
if not skip then
|
|
2665
|
-
table.insert(kept, __mcp_remap(line))
|
|
2666
|
-
end
|
|
2667
|
-
end
|
|
2668
|
-
return table.concat(kept, "\\n")
|
|
2669
|
-
end
|
|
2670
|
-
local ok, errOrValue = xpcall(__mcp_run, __mcp_traceback)
|
|
2671
|
-
return { ok = ok, value = errOrValue, output = __mcp_output }
|
|
2672
|
-
end)())`;
|
|
2673
|
-
return `
|
|
2674
|
-
local HttpService = game:GetService("HttpService")
|
|
2675
|
-
local bf = game:GetService("${opts.service}"):FindFirstChild("${opts.bridgeName}")
|
|
2676
|
-
if not bf then
|
|
2677
|
-
return HttpService:JSONEncode({
|
|
2678
|
-
bridge = "missing",
|
|
2679
|
-
error = ${luaLongQuote(opts.missingError)},
|
|
2680
|
-
})
|
|
2681
|
-
end
|
|
2682
|
-
-- Outer-scope mirror of the in-IIFE __mcp_remap. Applied to parser errors
|
|
2683
|
-
-- we pull out of LogService (those never pass through the IIFE) and to
|
|
2684
|
-
-- the canned engine error string. Same offset as the IIFE's
|
|
2685
|
-
-- __mcp_LINE_OFFSET; covers both chunk-name formats.
|
|
2686
|
-
local __mcp_USER_LINES_OUTER = ${userLines}
|
|
2687
|
-
local function __mcp_outer_user_line(payload_n)
|
|
2688
|
-
local user_n = payload_n - ${EVAL_WRAPPER_LINE_OFFSET}
|
|
2689
|
-
if user_n < 1 then return "1" end
|
|
2690
|
-
if user_n > __mcp_USER_LINES_OUTER then return tostring(__mcp_USER_LINES_OUTER) .. " (at end of input)" end
|
|
2691
|
-
return tostring(user_n)
|
|
2692
|
-
end
|
|
2693
|
-
local function __mcp_outer_remap(s)
|
|
2694
|
-
s = string.gsub(s, "Workspace%.__MCPEvalPayload:(%d+)", function(num)
|
|
2695
|
-
local n = tonumber(num)
|
|
2696
|
-
if n then return "user_code:" .. __mcp_outer_user_line(n) end
|
|
2697
|
-
return "user_code:" .. num
|
|
2698
|
-
end)
|
|
2699
|
-
s = string.gsub(s, "__MCPEvalPayload:(%d+)", function(num)
|
|
2700
|
-
local n = tonumber(num)
|
|
2701
|
-
if n then return "user_code:" .. __mcp_outer_user_line(n) end
|
|
2702
|
-
return "user_code:" .. num
|
|
2703
|
-
end)
|
|
2704
|
-
s = string.gsub(s, '%[string "[^"]+"%]:(%d+)', function(num)
|
|
2705
|
-
local n = tonumber(num)
|
|
2706
|
-
if n then return "user_code:" .. __mcp_outer_user_line(n) end
|
|
2707
|
-
return "user_code:" .. num
|
|
2708
|
-
end)
|
|
2709
|
-
return s
|
|
2710
|
-
end
|
|
2711
|
-
-- JSON-encode tables; otherwise tostring. Cycles or non-serializable
|
|
2712
|
-
-- values fall back to tostring instead of erroring. This is what makes
|
|
2713
|
-
-- eval_server_runtime / eval_client_runtime return structured table data
|
|
2714
|
-
-- (matching execute_luau) instead of "table: 0xaddr".
|
|
2715
|
-
local function __mcp_format(v)
|
|
2716
|
-
if typeof(v) == "table" then
|
|
2717
|
-
local ok, encoded = pcall(function() return HttpService:JSONEncode(v) end)
|
|
2718
|
-
if ok then return encoded end
|
|
2719
|
-
end
|
|
2720
|
-
return tostring(v)
|
|
2721
|
-
end
|
|
2722
|
-
local USER_CODE = ${luaLongQuote(wrapped)}
|
|
2723
|
-
local m = Instance.new("ModuleScript")
|
|
2724
|
-
m.Name = "__MCPEvalPayload"
|
|
2725
|
-
local okSet, setErr = pcall(function() m.Source = USER_CODE end)
|
|
2726
|
-
if not okSet then
|
|
2727
|
-
m:Destroy()
|
|
2728
|
-
return HttpService:JSONEncode({ bridge = "ok", ok = false, error = "ModuleScript Source set failed: " .. tostring(setErr) })
|
|
2729
|
-
end
|
|
2730
|
-
m.Parent = workspace
|
|
2731
|
-
local bridgeOk, inner = bf:Invoke(m)
|
|
2732
|
-
m:Destroy()
|
|
2733
|
-
if not bridgeOk then
|
|
2734
|
-
local errMsg = tostring(inner)
|
|
2735
|
-
-- pcall(require, payload) collapses parse/compile failures into the
|
|
2736
|
-
-- canned engine string below. The real parser diagnostic was emitted
|
|
2737
|
-
-- to LogService just before. Walk GetLogHistory backward for the most
|
|
2738
|
-
-- recent ERR entry tagged at our payload path and substitute.
|
|
2739
|
-
if errMsg == "Requested module experienced an error while loading" then
|
|
2740
|
-
-- The parser diagnostic is emitted to LogService on the next
|
|
2741
|
-
-- engine frame, not synchronously with pcall(require). task.wait(0)
|
|
2742
|
-
-- yields too early; 50ms is enough to let the frame complete and
|
|
2743
|
-
-- the message land in GetLogHistory.
|
|
2744
|
-
task.wait(0.05)
|
|
2745
|
-
local LogService = game:GetService("LogService")
|
|
2746
|
-
local hist = LogService:GetLogHistory()
|
|
2747
|
-
for i = #hist, 1, -1 do
|
|
2748
|
-
local e = hist[i]
|
|
2749
|
-
if e.messageType == Enum.MessageType.MessageError and string.sub(e.message, 1, 27) == "Workspace.__MCPEvalPayload:" then
|
|
2750
|
-
errMsg = e.message
|
|
2751
|
-
break
|
|
2752
|
-
end
|
|
2753
|
-
end
|
|
2754
|
-
end
|
|
2755
|
-
return HttpService:JSONEncode({ bridge = "ok", ok = false, error = __mcp_outer_remap(errMsg) })
|
|
2756
|
-
end
|
|
2757
|
-
-- inner is the {ok, value, output} table from our IIFE. Defensive: if it's
|
|
2758
|
-
-- somehow not a table (caller bypassed the wrapper), fall back to old shape.
|
|
2759
|
-
if typeof(inner) ~= "table" then
|
|
2760
|
-
return HttpService:JSONEncode({
|
|
2761
|
-
bridge = "ok",
|
|
2762
|
-
ok = true,
|
|
2763
|
-
result = if inner == nil then nil else __mcp_format(inner),
|
|
2764
|
-
})
|
|
2765
|
-
end
|
|
2766
|
-
return HttpService:JSONEncode({
|
|
2767
|
-
bridge = "ok",
|
|
2768
|
-
ok = inner.ok == true,
|
|
2769
|
-
result = if inner.ok and inner.value ~= nil then __mcp_format(inner.value) else nil,
|
|
2770
|
-
error = if not inner.ok then tostring(inner.value) else nil,
|
|
2771
|
-
output = inner.output or {},
|
|
2772
|
-
})
|
|
2773
|
-
`;
|
|
2774
|
-
}
|
|
2775
|
-
function parseBridgeResponse(response) {
|
|
2776
|
-
const r = response;
|
|
2777
|
-
if (r && typeof r.returnValue === "string") {
|
|
2778
|
-
try {
|
|
2779
|
-
const parsed = JSON.parse(r.returnValue);
|
|
2780
|
-
return JSON.stringify(parsed);
|
|
2781
|
-
} catch {
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
return JSON.stringify(response);
|
|
2785
|
-
}
|
|
2786
2573
|
function sleep(ms) {
|
|
2787
2574
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2788
2575
|
}
|
|
2789
|
-
var
|
|
2576
|
+
var RobloxStudioTools;
|
|
2790
2577
|
var init_tools = __esm({
|
|
2791
2578
|
"../core/dist/tools/index.js"() {
|
|
2792
2579
|
"use strict";
|
|
@@ -2797,9 +2584,6 @@ var init_tools = __esm({
|
|
|
2797
2584
|
init_roblox_cookie_client();
|
|
2798
2585
|
init_jpeg_encoder();
|
|
2799
2586
|
init_png_encoder();
|
|
2800
|
-
SERVER_LOCAL_NAME = "__MCP_ServerEvalLocal";
|
|
2801
|
-
CLIENT_LOCAL_NAME = "__MCP_ClientEvalBridge";
|
|
2802
|
-
EVAL_WRAPPER_LINE_OFFSET = 23;
|
|
2803
2587
|
RobloxStudioTools = class _RobloxStudioTools {
|
|
2804
2588
|
client;
|
|
2805
2589
|
bridge;
|
|
@@ -3476,18 +3260,12 @@ ${code}`
|
|
|
3476
3260
|
if (!code) {
|
|
3477
3261
|
throw new Error("Code is required for eval_server_runtime");
|
|
3478
3262
|
}
|
|
3479
|
-
const
|
|
3480
|
-
service: "ServerScriptService",
|
|
3481
|
-
bridgeName: SERVER_LOCAL_NAME,
|
|
3482
|
-
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.",
|
|
3483
|
-
userCode: code
|
|
3484
|
-
});
|
|
3485
|
-
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);
|
|
3486
3264
|
return {
|
|
3487
3265
|
content: [
|
|
3488
3266
|
{
|
|
3489
3267
|
type: "text",
|
|
3490
|
-
text:
|
|
3268
|
+
text: JSON.stringify(response)
|
|
3491
3269
|
}
|
|
3492
3270
|
]
|
|
3493
3271
|
};
|
|
@@ -3500,18 +3278,12 @@ ${code}`
|
|
|
3500
3278
|
if (!clientTarget.startsWith("client-")) {
|
|
3501
3279
|
throw new Error(`eval_client_runtime requires target=client-N (got: ${clientTarget})`);
|
|
3502
3280
|
}
|
|
3503
|
-
const
|
|
3504
|
-
service: "ReplicatedStorage",
|
|
3505
|
-
bridgeName: CLIENT_LOCAL_NAME,
|
|
3506
|
-
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.",
|
|
3507
|
-
userCode: code
|
|
3508
|
-
});
|
|
3509
|
-
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);
|
|
3510
3282
|
return {
|
|
3511
3283
|
content: [
|
|
3512
3284
|
{
|
|
3513
3285
|
type: "text",
|
|
3514
|
-
text:
|
|
3286
|
+
text: JSON.stringify(response)
|
|
3515
3287
|
}
|
|
3516
3288
|
]
|
|
3517
3289
|
};
|
|
@@ -3529,11 +3301,19 @@ ${code}`
|
|
|
3529
3301
|
if (!resolved.ok)
|
|
3530
3302
|
throw new RoutingFailure(resolved.error);
|
|
3531
3303
|
if (resolved.mode === "single") {
|
|
3304
|
+
const originPeerReliable2 = await this._isMultiplayerTestRunning(resolved.targetInstanceId);
|
|
3532
3305
|
const response = await this.client.request("/api/get-runtime-logs", data, resolved.targetInstanceId, resolved.targetRole);
|
|
3533
|
-
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;
|
|
3534
3312
|
if (Array.isArray(response.entries)) {
|
|
3535
3313
|
for (const e of response.entries) {
|
|
3536
|
-
|
|
3314
|
+
e.capturedBy = resolved.targetRole;
|
|
3315
|
+
delete e.peer;
|
|
3316
|
+
if (originPeerReliable2)
|
|
3537
3317
|
e.peer = resolved.targetRole;
|
|
3538
3318
|
}
|
|
3539
3319
|
}
|
|
@@ -3542,35 +3322,38 @@ ${code}`
|
|
|
3542
3322
|
};
|
|
3543
3323
|
}
|
|
3544
3324
|
const targets = resolved.targets.filter((t) => t.targetRole !== "edit-proxy");
|
|
3325
|
+
const originPeerReliable = targets.length > 0 ? await this._isMultiplayerTestRunning(targets[0].targetInstanceId) : false;
|
|
3545
3326
|
const responses = await Promise.allSettled(targets.map(async (t) => {
|
|
3546
3327
|
const r = await this.client.request("/api/get-runtime-logs", data, t.targetInstanceId, t.targetRole);
|
|
3547
|
-
return { ...r,
|
|
3328
|
+
return { ...r, capturedBy: t.targetRole };
|
|
3548
3329
|
}));
|
|
3549
3330
|
const merged = [];
|
|
3550
|
-
const
|
|
3551
|
-
const
|
|
3331
|
+
const perCaptureNextSince = {};
|
|
3332
|
+
const perCaptureErrors = {};
|
|
3552
3333
|
let totalDropped = 0;
|
|
3553
3334
|
for (const r of responses) {
|
|
3554
3335
|
if (r.status !== "fulfilled")
|
|
3555
3336
|
continue;
|
|
3556
3337
|
const v = r.value;
|
|
3557
|
-
const
|
|
3338
|
+
const capturedBy = v.capturedBy ?? "unknown";
|
|
3558
3339
|
if (v.error) {
|
|
3559
|
-
|
|
3340
|
+
perCaptureErrors[capturedBy] = v.error;
|
|
3560
3341
|
continue;
|
|
3561
3342
|
}
|
|
3562
3343
|
if (v.nextSince !== void 0)
|
|
3563
|
-
|
|
3344
|
+
perCaptureNextSince[capturedBy] = v.nextSince;
|
|
3564
3345
|
totalDropped += v.totalDropped ?? 0;
|
|
3565
3346
|
for (const e of v.entries ?? []) {
|
|
3566
|
-
|
|
3347
|
+
const entry = { ...e };
|
|
3348
|
+
delete entry.peer;
|
|
3349
|
+
merged.push({ ...entry, capturedBy });
|
|
3567
3350
|
}
|
|
3568
3351
|
}
|
|
3569
3352
|
merged.sort((a, b) => a.ts !== b.ts ? a.ts - b.ts : a.seq - b.seq);
|
|
3570
3353
|
const DEDUP_WINDOW = 2;
|
|
3571
3354
|
const deduped = [];
|
|
3572
3355
|
for (const e of merged) {
|
|
3573
|
-
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);
|
|
3574
3357
|
if (!isDup)
|
|
3575
3358
|
deduped.push(e);
|
|
3576
3359
|
}
|
|
@@ -3578,13 +3361,21 @@ ${code}`
|
|
|
3578
3361
|
if (tail !== void 0 && deduped.length > tail) {
|
|
3579
3362
|
final = deduped.slice(deduped.length - tail);
|
|
3580
3363
|
}
|
|
3364
|
+
const finalEntries = originPeerReliable ? final.map((e) => ({ ...e, peer: e.capturedBy })) : final;
|
|
3581
3365
|
const body = {
|
|
3582
|
-
entries:
|
|
3366
|
+
entries: finalEntries,
|
|
3583
3367
|
totalDropped,
|
|
3584
|
-
|
|
3368
|
+
perCaptureNextSince,
|
|
3369
|
+
originPeerReliable,
|
|
3370
|
+
peerAttribution: originPeerReliable ? "guaranteed_multiplayer" : "unavailable_shared_logservice"
|
|
3585
3371
|
};
|
|
3586
|
-
if (
|
|
3587
|
-
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;
|
|
3588
3379
|
}
|
|
3589
3380
|
return {
|
|
3590
3381
|
content: [{ type: "text", text: JSON.stringify(body) }]
|
|
@@ -6318,17 +6109,17 @@ var init_definitions = __esm({
|
|
|
6318
6109
|
{
|
|
6319
6110
|
name: "get_runtime_logs",
|
|
6320
6111
|
category: "read",
|
|
6321
|
-
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.",
|
|
6322
6113
|
inputSchema: {
|
|
6323
6114
|
type: "object",
|
|
6324
6115
|
properties: {
|
|
6325
6116
|
target: {
|
|
6326
6117
|
type: "string",
|
|
6327
|
-
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.'
|
|
6328
6119
|
},
|
|
6329
6120
|
since: {
|
|
6330
6121
|
type: "number",
|
|
6331
|
-
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."
|
|
6332
6123
|
},
|
|
6333
6124
|
tail: {
|
|
6334
6125
|
type: "number",
|
|
@@ -7351,7 +7142,7 @@ function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = c
|
|
|
7351
7142
|
warn(`
|
|
7352
7143
|
[install-plugin] WARNING: ${otherAssetName} is already present in ${pluginsFolder}.
|
|
7353
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.
|
|
7354
|
-
|
|
7145
|
+
Delete ${otherAssetName} manually or use the default CLI installer behavior to replace it.
|
|
7355
7146
|
`);
|
|
7356
7147
|
}
|
|
7357
7148
|
var init_install_plugin_helpers = __esm({
|
|
@@ -7473,6 +7264,27 @@ function bundledAssetPath() {
|
|
|
7473
7264
|
];
|
|
7474
7265
|
return candidates.find((candidate) => existsSync3(candidate)) ?? null;
|
|
7475
7266
|
}
|
|
7267
|
+
function packageVersion() {
|
|
7268
|
+
const currentDir = dirname2(fileURLToPath(import.meta.url));
|
|
7269
|
+
const pkg = JSON.parse(readFileSync3(join3(currentDir, "..", "package.json"), "utf8"));
|
|
7270
|
+
if (!pkg.version) {
|
|
7271
|
+
throw new Error("Package version not found");
|
|
7272
|
+
}
|
|
7273
|
+
return pkg.version;
|
|
7274
|
+
}
|
|
7275
|
+
function bundledPluginVersion(source) {
|
|
7276
|
+
const match = readFileSync3(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
7277
|
+
return match ? match[1] : null;
|
|
7278
|
+
}
|
|
7279
|
+
function assertBundledPluginVersion(source) {
|
|
7280
|
+
const expected = packageVersion();
|
|
7281
|
+
const actual = bundledPluginVersion(source);
|
|
7282
|
+
if (actual !== expected) {
|
|
7283
|
+
throw new Error(
|
|
7284
|
+
`Bundled ${ASSET_NAME} version ${actual ?? "unknown"} does not match package version ${expected}. Run npm run build:plugin before starting with --auto-install-plugin.`
|
|
7285
|
+
);
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7476
7288
|
function filesMatch(a, b) {
|
|
7477
7289
|
if (!existsSync3(b)) return false;
|
|
7478
7290
|
const aBytes = readFileSync3(a);
|
|
@@ -7482,11 +7294,12 @@ function filesMatch(a, b) {
|
|
|
7482
7294
|
async function installBundledPlugin(options = {}) {
|
|
7483
7295
|
const log = options.log ?? console.log;
|
|
7484
7296
|
const warn = options.warn ?? console.warn;
|
|
7485
|
-
const replaceVariant = options.replaceVariant ??
|
|
7297
|
+
const replaceVariant = options.replaceVariant ?? true;
|
|
7486
7298
|
const source = bundledAssetPath();
|
|
7487
7299
|
if (!source) {
|
|
7488
7300
|
throw new Error(`Bundled ${ASSET_NAME} not found in package`);
|
|
7489
7301
|
}
|
|
7302
|
+
assertBundledPluginVersion(source);
|
|
7490
7303
|
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
7491
7304
|
const dest = join3(pluginsFolder, ASSET_NAME);
|
|
7492
7305
|
if (filesMatch(source, dest)) return;
|
|
@@ -7495,10 +7308,22 @@ async function installBundledPlugin(options = {}) {
|
|
|
7495
7308
|
}
|
|
7496
7309
|
async function installPlugin(options = {}) {
|
|
7497
7310
|
const dev = options.dev ?? process.argv.includes("--dev");
|
|
7498
|
-
const replaceVariant = options.replaceVariant ??
|
|
7311
|
+
const replaceVariant = options.replaceVariant ?? true;
|
|
7499
7312
|
const log = options.log ?? console.log;
|
|
7500
7313
|
const warn = options.warn ?? console.warn;
|
|
7501
7314
|
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
7315
|
+
const bundled = bundledAssetPath();
|
|
7316
|
+
if (bundled) {
|
|
7317
|
+
assertBundledPluginVersion(bundled);
|
|
7318
|
+
const dest2 = join3(pluginsFolder, ASSET_NAME);
|
|
7319
|
+
if (filesMatch(bundled, dest2)) {
|
|
7320
|
+
log(`${ASSET_NAME} already installed.`);
|
|
7321
|
+
return;
|
|
7322
|
+
}
|
|
7323
|
+
copyFileSync(bundled, dest2);
|
|
7324
|
+
log(`Installed bundled ${ASSET_NAME} to ${dest2}`);
|
|
7325
|
+
return;
|
|
7326
|
+
}
|
|
7502
7327
|
log(dev ? "Fetching latest dev prerelease..." : "Fetching latest release...");
|
|
7503
7328
|
const release = dev ? await findDevRelease() : await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
7504
7329
|
const asset = release.assets?.find((a) => a.name === ASSET_NAME);
|
|
@@ -7528,7 +7353,7 @@ init_dist();
|
|
|
7528
7353
|
import { createRequire } from "module";
|
|
7529
7354
|
if (process.argv.includes("--install-plugin")) {
|
|
7530
7355
|
const { installPlugin: installPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
|
|
7531
|
-
installPlugin2().catch((err) => {
|
|
7356
|
+
await installPlugin2().catch((err) => {
|
|
7532
7357
|
console.error(err instanceof Error ? err.message : String(err));
|
|
7533
7358
|
process.exitCode = 1;
|
|
7534
7359
|
});
|
|
@@ -7536,7 +7361,6 @@ if (process.argv.includes("--install-plugin")) {
|
|
|
7536
7361
|
if (process.argv.includes("--auto-install-plugin")) {
|
|
7537
7362
|
const { installBundledPlugin: installBundledPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
|
|
7538
7363
|
await installBundledPlugin2({
|
|
7539
|
-
replaceVariant: process.argv.includes("--replace-variant"),
|
|
7540
7364
|
log: (message) => console.error(`[install-plugin] ${message}`),
|
|
7541
7365
|
warn: (message) => console.error(message)
|
|
7542
7366
|
}).catch((err) => {
|