@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 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 SERVER_LOCAL_NAME, CLIENT_LOCAL_NAME, EVAL_WRAPPER_LINE_OFFSET, RobloxStudioTools;
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 wrapper = buildModuleScriptInvokeWrapper({
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: parseBridgeResponse(response)
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 wrapper = buildModuleScriptInvokeWrapper({
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: parseBridgeResponse(response)
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.peer = resolved.targetRole;
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
- if (e.peer !== void 0)
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, peer: t.targetRole };
3328
+ return { ...r, capturedBy: t.targetRole };
3548
3329
  }));
3549
3330
  const merged = [];
3550
- const perPeerNextSince = {};
3551
- const perPeerErrors = {};
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 peer = v.peer ?? "unknown";
3338
+ const capturedBy = v.capturedBy ?? "unknown";
3558
3339
  if (v.error) {
3559
- perPeerErrors[peer] = v.error;
3340
+ perCaptureErrors[capturedBy] = v.error;
3560
3341
  continue;
3561
3342
  }
3562
3343
  if (v.nextSince !== void 0)
3563
- perPeerNextSince[peer] = v.nextSince;
3344
+ perCaptureNextSince[capturedBy] = v.nextSince;
3564
3345
  totalDropped += v.totalDropped ?? 0;
3565
3346
  for (const e of v.entries ?? []) {
3566
- merged.push({ ...e, peer });
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.peer !== e.peer);
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: final,
3366
+ entries: finalEntries,
3583
3367
  totalDropped,
3584
- perPeerNextSince
3368
+ perCaptureNextSince,
3369
+ originPeerReliable,
3370
+ peerAttribution: originPeerReliable ? "guaranteed_multiplayer" : "unavailable_shared_logservice"
3585
3371
  };
3586
- if (Object.keys(perPeerErrors).length > 0) {
3587
- body.perPeerErrors = perPeerErrors;
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 buffer captured by the plugin on each peer's LogService.MessageOut. Each peer (edit, server, client-N) captures ~64 KB of recent prints; oldest entries drop when over budget. Drop-oldest semantics preserve the recent tail, unlike get_console_output's 10 KB drop-newest cap. Caveat: peer tag reflects which peer's plugin captured the entry, not which peer's script originated it - LogService reflects prints across peers in Studio Play and origin is undetectable from inside MessageOut. target=all (default) merges all peers and dedups same-message-and-level entries captured within 2s across different peers.",
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: 'Peer to read from: "edit", "server", "client-N", or "all" (default). "all" merges all peers and dedups cross-peer reflections within a 2s window.'
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-peer) or perPeerNextSince entry (target=all) for incremental polling."
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
- Re-run with --replace-variant to remove ${otherAssetName}, or delete it manually.
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 ?? process.argv.includes("--replace-variant");
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 ?? process.argv.includes("--replace-variant");
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp",
3
- "version": "2.15.1",
3
+ "version": "2.15.2",
4
4
  "description": "MCP server for testing, debugging, and controlling Roblox Studio from AI coding tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",