@princeofscale/bloxforge-inspector 2.20.2 → 3.0.0-rc.1

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
@@ -1222,6 +1222,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1222
1222
  let mcpServerStartTime = 0;
1223
1223
  const proxyInstances = /* @__PURE__ */ new Set();
1224
1224
  const warnedVersionMismatches = /* @__PURE__ */ new Set();
1225
+ const warnedProtocolMismatches = /* @__PURE__ */ new Set();
1225
1226
  const setMCPServerActive = (active) => {
1226
1227
  mcpServerActive = active;
1227
1228
  if (active) {
@@ -1346,6 +1347,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1346
1347
  warnedVersionMismatches.add(pluginSessionId);
1347
1348
  console.error(`[version-mismatch] Studio plugin v${registered.pluginVersion} (${registered.pluginVariant}) does not match MCP server v${registered.serverVersion} for ${registered.instanceId}/${registered.role}`);
1348
1349
  }
1350
+ if (registered?.protocolMismatch && !warnedProtocolMismatches.has(pluginSessionId)) {
1351
+ warnedProtocolMismatches.add(pluginSessionId);
1352
+ console.error(`[protocol-mismatch] Studio plugin protocol v${registered.pluginProtocolVersion} does not match MCP server protocol v${registered.serverProtocolVersion} for ${registered.instanceId}/${registered.role}. Run --auto-install-plugin to update the plugin.`);
1353
+ }
1349
1354
  res.json({
1350
1355
  success: true,
1351
1356
  assignedRole: result.assignedRole,
@@ -1353,13 +1358,16 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, registry) {
1353
1358
  serverVersion: serverConfig?.version,
1354
1359
  serverProtocolVersion: MCP_PROTOCOL_VERSION,
1355
1360
  versionMismatch: registered?.versionMismatch ?? false,
1356
- protocolMismatch: registered?.protocolMismatch ?? false
1361
+ protocolMismatch: registered?.protocolMismatch ?? false,
1362
+ capabilities: ["core", "scene", "mutation", "scripts", "runtime", "assets", "ui", "environment", "terrain", "build", "media", "sync", "safety"]
1357
1363
  });
1358
1364
  });
1359
1365
  app.post("/disconnect", (req, res) => {
1360
1366
  const { pluginSessionId } = req.body;
1361
1367
  if (pluginSessionId) {
1362
1368
  bridge.unregisterInstance(pluginSessionId, "plugin_request");
1369
+ warnedVersionMismatches.delete(pluginSessionId);
1370
+ warnedProtocolMismatches.delete(pluginSessionId);
1363
1371
  }
1364
1372
  res.json({ success: true });
1365
1373
  });
@@ -3828,7 +3836,7 @@ var init_scripting = __esm({
3828
3836
  },
3829
3837
  usePattern: {
3830
3838
  type: "boolean",
3831
- description: "Use Lua pattern matching instead of literal (default: false)"
3839
+ description: "Use Lua pattern matching instead of literal (default: false). Lua patterns do not support regex alternation; | is literal, so run separate searches for alternatives."
3832
3840
  },
3833
3841
  contextLines: {
3834
3842
  type: "number",
@@ -10627,10 +10635,12 @@ var init_asset_tools = __esm({
10627
10635
  static findLibraryPath() {
10628
10636
  if (_AssetTools._cachedLibraryPath)
10629
10637
  return _AssetTools._cachedLibraryPath;
10630
- const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
10638
+ const overridePath = process.env.BLOXFORGE_BUILD_LIBRARY || process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
10631
10639
  const cwd = path.resolve(process.cwd());
10632
10640
  const projectRoot = _AssetTools.findProjectRoot(cwd);
10633
- const homeLibraryPath = path.join(os.homedir(), ".robloxstudio-mcp", "build-library");
10641
+ const newHomeLibraryPath = path.join(os.homedir(), ".bloxforge", "build-library");
10642
+ const legacyHomeLibraryPath = path.join(os.homedir(), ".robloxstudio-mcp", "build-library");
10643
+ const homeLibraryPath = fs.existsSync(newHomeLibraryPath) ? newHomeLibraryPath : fs.existsSync(legacyHomeLibraryPath) ? legacyHomeLibraryPath : newHomeLibraryPath;
10634
10644
  const projectLibraryPath = projectRoot ? path.join(projectRoot, "build-library") : null;
10635
10645
  const cwdLibraryPath = path.join(cwd, "build-library");
10636
10646
  let result;
@@ -15139,16 +15149,30 @@ import * as fs4 from "fs";
15139
15149
  import * as os2 from "os";
15140
15150
  import * as path4 from "path";
15141
15151
  function defaultManagedInstanceRegistryDir() {
15152
+ if (process.env.BLOXFORGE_MANAGED_INSTANCE_REGISTRY_DIR) {
15153
+ return process.env.BLOXFORGE_MANAGED_INSTANCE_REGISTRY_DIR;
15154
+ }
15142
15155
  if (process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR) {
15143
15156
  return process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR;
15144
15157
  }
15158
+ const suffix = path4.join("managed-instances", "v1");
15159
+ let newDir;
15160
+ let legacyDir;
15145
15161
  if (process.platform === "win32" && process.env.LOCALAPPDATA) {
15146
- return path4.join(process.env.LOCALAPPDATA, "robloxstudio-mcp", "managed-instances", "v1");
15147
- }
15148
- if (process.platform === "darwin") {
15149
- return path4.join(os2.homedir(), "Library", "Application Support", "robloxstudio-mcp", "managed-instances", "v1");
15162
+ newDir = path4.join(process.env.LOCALAPPDATA, "bloxforge", suffix);
15163
+ legacyDir = path4.join(process.env.LOCALAPPDATA, "robloxstudio-mcp", suffix);
15164
+ } else if (process.platform === "darwin") {
15165
+ newDir = path4.join(os2.homedir(), "Library", "Application Support", "bloxforge", suffix);
15166
+ legacyDir = path4.join(os2.homedir(), "Library", "Application Support", "robloxstudio-mcp", suffix);
15167
+ } else {
15168
+ newDir = path4.join(os2.homedir(), ".local", "state", "bloxforge", suffix);
15169
+ legacyDir = path4.join(os2.homedir(), ".local", "state", "robloxstudio-mcp", suffix);
15150
15170
  }
15151
- return path4.join(os2.homedir(), ".local", "state", "robloxstudio-mcp", "managed-instances", "v1");
15171
+ if (fs4.existsSync(newDir))
15172
+ return newDir;
15173
+ if (fs4.existsSync(legacyDir))
15174
+ return legacyDir;
15175
+ return newDir;
15152
15176
  }
15153
15177
  function sleepSync(ms) {
15154
15178
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -15424,7 +15448,7 @@ var init_managed_instance_registry = __esm({
15424
15448
 
15425
15449
  // ../core/dist/studio-instance-manager.js
15426
15450
  import { execFileSync, spawn } from "child_process";
15427
- import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
15451
+ import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync, rmSync as rmSync2, statSync as statSync3 } from "fs";
15428
15452
  import { randomUUID as randomUUID2 } from "crypto";
15429
15453
  import * as os3 from "os";
15430
15454
  import * as path5 from "path";
@@ -15446,7 +15470,7 @@ function isWsl() {
15446
15470
  }
15447
15471
  function powershell(script) {
15448
15472
  return run("powershell.exe", ["-NoProfile", "-Command", script], {
15449
- cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
15473
+ cwd: isWsl() && existsSync3("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
15450
15474
  });
15451
15475
  }
15452
15476
  function windowsLocalAppData() {
@@ -15456,7 +15480,7 @@ function windowsLocalAppData() {
15456
15480
  return void 0;
15457
15481
  try {
15458
15482
  return run("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
15459
- cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
15483
+ cwd: existsSync3("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
15460
15484
  });
15461
15485
  } catch {
15462
15486
  return void 0;
@@ -15468,7 +15492,7 @@ function toWslPath(windowsPath) {
15468
15492
  return run("wslpath", ["-u", windowsPath]);
15469
15493
  }
15470
15494
  function toStudioLaunchArg(arg) {
15471
- if (!isWsl() || !path5.isAbsolute(arg) || !existsSync2(arg))
15495
+ if (!isWsl() || !path5.isAbsolute(arg) || !existsSync3(arg))
15472
15496
  return arg;
15473
15497
  return run("wslpath", ["-w", arg]);
15474
15498
  }
@@ -15494,7 +15518,7 @@ function resolveBaseplateTemplatePath() {
15494
15518
  path5.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
15495
15519
  ];
15496
15520
  for (const candidate of candidates) {
15497
- if (existsSync2(candidate))
15521
+ if (existsSync3(candidate))
15498
15522
  return candidate;
15499
15523
  }
15500
15524
  throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
@@ -15571,10 +15595,10 @@ function resolveStudioExe() {
15571
15595
  }
15572
15596
  const localAppData = windowsLocalAppData();
15573
15597
  const root = localAppData ? path5.join(toWslPath(localAppData), "Roblox", "Versions") : path5.join(os3.homedir(), "AppData", "Local", "Roblox", "Versions");
15574
- if (!existsSync2(root)) {
15598
+ if (!existsSync3(root)) {
15575
15599
  throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
15576
15600
  }
15577
- const candidates = readdirSync4(root).filter((name) => name.startsWith("version-")).map((name) => path5.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs);
15601
+ const candidates = readdirSync4(root).filter((name) => name.startsWith("version-")).map((name) => path5.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync3(candidate)).sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs);
15578
15602
  if (candidates.length === 0) {
15579
15603
  throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
15580
15604
  }
@@ -15720,7 +15744,7 @@ var init_studio_instance_manager = __esm({
15720
15744
  const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
15721
15745
  const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
15722
15746
  const spawnOptions = {
15723
- cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
15747
+ cwd: isWsl() && existsSync3("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
15724
15748
  detached: true,
15725
15749
  stdio: "ignore"
15726
15750
  };
@@ -16865,10 +16889,12 @@ var init_tools = __esm({
16865
16889
  static findLibraryPath() {
16866
16890
  if (_RobloxStudioTools._cachedLibraryPath)
16867
16891
  return _RobloxStudioTools._cachedLibraryPath;
16868
- const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
16892
+ const overridePath = process.env.BLOXFORGE_BUILD_LIBRARY || process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
16869
16893
  const cwd = path6.resolve(process.cwd());
16870
16894
  const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
16871
- const homeLibraryPath = path6.join(os4.homedir(), ".robloxstudio-mcp", "build-library");
16895
+ const newHomeLibraryPath = path6.join(os4.homedir(), ".bloxforge", "build-library");
16896
+ const legacyHomeLibraryPath = path6.join(os4.homedir(), ".robloxstudio-mcp", "build-library");
16897
+ const homeLibraryPath = fs5.existsSync(newHomeLibraryPath) ? newHomeLibraryPath : fs5.existsSync(legacyHomeLibraryPath) ? legacyHomeLibraryPath : newHomeLibraryPath;
16872
16898
  const projectLibraryPath = projectRoot ? path6.join(projectRoot, "build-library") : null;
16873
16899
  const cwdLibraryPath = path6.join(cwd, "build-library");
16874
16900
  let result;
@@ -18979,7 +19005,7 @@ var init_server = __esm({
18979
19005
  });
18980
19006
 
18981
19007
  // ../core/dist/install-plugin-helpers.js
18982
- import { existsSync as existsSync4, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
19008
+ import { existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
18983
19009
  import { execSync } from "child_process";
18984
19010
  import { join as join6 } from "path";
18985
19011
  import { homedir as homedir5 } from "os";
@@ -19026,7 +19052,7 @@ function getPluginsFolder() {
19026
19052
  }
19027
19053
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
19028
19054
  const otherDest = join6(pluginsFolder, otherAssetName);
19029
- if (!existsSync4(otherDest))
19055
+ if (!existsSync5(otherDest))
19030
19056
  return;
19031
19057
  if (replace) {
19032
19058
  try {
@@ -19054,6 +19080,7 @@ var init_doctor = __esm({
19054
19080
  "../core/dist/doctor.js"() {
19055
19081
  "use strict";
19056
19082
  init_install_plugin_helpers();
19083
+ init_bridge_service();
19057
19084
  }
19058
19085
  });
19059
19086
 
@@ -19062,6 +19089,7 @@ var init_dist = __esm({
19062
19089
  "../core/dist/index.js"() {
19063
19090
  "use strict";
19064
19091
  init_server();
19092
+ init_server();
19065
19093
  init_http_server();
19066
19094
  init_bridge_service();
19067
19095
  init_tools();
@@ -19081,7 +19109,7 @@ __export(install_plugin_exports, {
19081
19109
  installBundledPlugin: () => installBundledPlugin,
19082
19110
  installPlugin: () => installPlugin
19083
19111
  });
19084
- import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
19112
+ import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
19085
19113
  import { dirname as dirname6, join as join7 } from "path";
19086
19114
  import { fileURLToPath } from "url";
19087
19115
  import { get } from "https";
@@ -19142,7 +19170,7 @@ function prepareInstall({
19142
19170
  warn
19143
19171
  }) {
19144
19172
  const pluginsFolder = getPluginsFolder();
19145
- if (!existsSync5(pluginsFolder)) {
19173
+ if (!existsSync6(pluginsFolder)) {
19146
19174
  mkdirSync7(pluginsFolder, { recursive: true });
19147
19175
  }
19148
19176
  handleVariantConflict({
@@ -19160,7 +19188,7 @@ function bundledAssetPath() {
19160
19188
  join7(currentDir, "..", "studio-plugin", ASSET_NAME),
19161
19189
  join7(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
19162
19190
  ];
19163
- return candidates.find((candidate) => existsSync5(candidate)) ?? null;
19191
+ return candidates.find((candidate) => existsSync6(candidate)) ?? null;
19164
19192
  }
19165
19193
  function packageVersion() {
19166
19194
  const currentDir = dirname6(fileURLToPath(import.meta.url));
@@ -19184,7 +19212,7 @@ function assertBundledPluginVersion(source) {
19184
19212
  }
19185
19213
  }
19186
19214
  function filesMatch(a, b) {
19187
- if (!existsSync5(b)) return false;
19215
+ if (!existsSync6(b)) return false;
19188
19216
  const aBytes = readFileSync7(a);
19189
19217
  const bBytes = readFileSync7(b);
19190
19218
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
@@ -19248,7 +19276,16 @@ var init_install_plugin = __esm({
19248
19276
  // src/index.ts
19249
19277
  init_dist();
19250
19278
  import { createRequire } from "module";
19251
- if (process.argv.includes("--install-plugin")) {
19279
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
19280
+ console.log(`BloxForge Inspector MCP Server
19281
+
19282
+ Usage: npx @princeofscale/bloxforge-inspector [options]
19283
+
19284
+ Options:
19285
+ --install-plugin Install the read-only Studio plugin
19286
+ --auto-install-plugin Install the plugin without prompting
19287
+ --help, -h Show this help message`);
19288
+ } else if (process.argv.includes("--install-plugin")) {
19252
19289
  const { installPlugin: installPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
19253
19290
  await installPlugin2().catch((err) => {
19254
19291
  console.error(err instanceof Error ? err.message : String(err));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@princeofscale/bloxforge-inspector",
3
- "version": "2.20.2",
3
+ "version": "3.0.0-rc.1",
4
4
  "description": "Read-only MCP server for AI agents to inspect and debug Roblox Studio — safe browsing with zero write risk.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -36,7 +36,10 @@
36
36
  "read-only",
37
37
  "open-source"
38
38
  ],
39
- "author": "",
39
+ "author": {
40
+ "name": "princeofscale",
41
+ "url": "https://github.com/princeofscale"
42
+ },
40
43
  "license": "MIT",
41
44
  "publishConfig": {
42
45
  "access": "public"
@@ -708,8 +708,10 @@ local function computeInstanceId()
708
708
  end
709
709
  local assignedRole
710
710
  local duplicateInstanceRole = false
711
- local hasVersionMismatch = false
711
+ local versionMismatchConnections = {}
712
712
  local lastVersionMismatchWarningKey
713
+ local protocolMismatchConnections = {}
714
+ local lastProtocolMismatchWarningKey
713
715
  local lastReadyInstanceId
714
716
  local readyFailureLogKeys = {}
715
717
  -- Cache the published place name from MarketplaceService:GetProductInfo so
@@ -1118,16 +1120,49 @@ local function pollForRequests(connIndex)
1118
1120
  end
1119
1121
  local serverVersion = _condition
1120
1122
  if data.versionMismatch == true then
1121
- hasVersionMismatch = true
1123
+ versionMismatchConnections[conn] = true
1122
1124
  local warningKey = `{State.CURRENT_VERSION}:{serverVersion}`
1123
1125
  if lastVersionMismatchWarningKey ~= warningKey then
1124
1126
  lastVersionMismatchWarningKey = warningKey
1125
1127
  warn(`[BloxForge] Version mismatch: Studio plugin v{State.CURRENT_VERSION} / MCP v{serverVersion}. Run npx -y @princeofscale/bloxforge@latest --auto-install-plugin and restart Studio.`)
1126
1128
  end
1127
1129
  UI.showBanner("version-mismatch", `Plugin v{State.CURRENT_VERSION} / MCP v{serverVersion} mismatch`)
1128
- elseif hasVersionMismatch then
1129
- hasVersionMismatch = false
1130
- UI.hideBanner("version-mismatch")
1130
+ else
1131
+ versionMismatchConnections[conn] = nil
1132
+ -- ▼ ReadonlySet.size ▼
1133
+ local _size = 0
1134
+ for _ in versionMismatchConnections do
1135
+ _size += 1
1136
+ end
1137
+ -- ▲ ReadonlySet.size ▲
1138
+ if _size == 0 then
1139
+ UI.hideBanner("version-mismatch")
1140
+ end
1141
+ end
1142
+ local _condition_1 = data.serverProtocolVersion
1143
+ if _condition_1 == nil then
1144
+ _condition_1 = 0
1145
+ end
1146
+ local serverProtocol = _condition_1
1147
+ if data.protocolMismatch == true then
1148
+ protocolMismatchConnections[conn] = true
1149
+ local warningKey = `{State.PROTOCOL_VERSION}:{serverProtocol}`
1150
+ if lastProtocolMismatchWarningKey ~= warningKey then
1151
+ lastProtocolMismatchWarningKey = warningKey
1152
+ warn(`[BloxForge] Protocol mismatch: Studio plugin protocol v{State.PROTOCOL_VERSION} / MCP protocol v{serverProtocol}. Run npx -y @princeofscale/bloxforge@latest --auto-install-plugin and restart Studio.`)
1153
+ end
1154
+ UI.showBanner("protocol-mismatch", `Protocol mismatch: Plugin protocol v{State.PROTOCOL_VERSION} / MCP protocol v{serverProtocol}`)
1155
+ else
1156
+ protocolMismatchConnections[conn] = nil
1157
+ -- ▼ ReadonlySet.size ▼
1158
+ local _size = 0
1159
+ for _ in protocolMismatchConnections do
1160
+ _size += 1
1161
+ end
1162
+ -- ▲ ReadonlySet.size ▲
1163
+ if _size == 0 then
1164
+ UI.hideBanner("protocol-mismatch")
1165
+ end
1131
1166
  end
1132
1167
  -- Server tells us when its in-memory instances map doesn't have us
1133
1168
  -- (e.g. after an MCP process restart, or after it reaped us during a
@@ -1146,12 +1181,12 @@ local function pollForRequests(connIndex)
1146
1181
  local el = ui
1147
1182
  el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
1148
1183
  el.step1Label.Text = "HTTP server (OK)"
1149
- local _condition_1 = mcpConnected
1150
- if _condition_1 then
1184
+ local _condition_2 = mcpConnected
1185
+ if _condition_2 then
1151
1186
  local _value = (string.find(el.statusLabel.Text, "Connected"))
1152
- _condition_1 = not (_value ~= 0 and _value == _value and _value)
1187
+ _condition_2 = not (_value ~= 0 and _value == _value and _value)
1153
1188
  end
1154
- if _condition_1 then
1189
+ if _condition_2 then
1155
1190
  el.statusLabel.Text = "Connected"
1156
1191
  el.statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
1157
1192
  el.statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
@@ -1182,11 +1217,11 @@ local function pollForRequests(connIndex)
1182
1217
  conn.mcpWaitStartTime = tick()
1183
1218
  end
1184
1219
  local _exp = tick()
1185
- local _condition_2 = conn.mcpWaitStartTime
1186
- if _condition_2 == nil then
1187
- _condition_2 = tick()
1220
+ local _condition_3 = conn.mcpWaitStartTime
1221
+ if _condition_3 == nil then
1222
+ _condition_3 = tick()
1188
1223
  end
1189
- local elapsed = _exp - _condition_2
1224
+ local elapsed = _exp - _condition_3
1190
1225
  el.troubleshootLabel.Visible = elapsed > 8
1191
1226
  UI.startPulseAnimation()
1192
1227
  end
@@ -1356,6 +1391,26 @@ function deactivatePlugin(connIndex)
1356
1391
  end
1357
1392
  conn.isActive = false
1358
1393
  conn.lastMcpOk = false
1394
+ versionMismatchConnections[conn] = nil
1395
+ protocolMismatchConnections[conn] = nil
1396
+ -- ▼ ReadonlySet.size ▼
1397
+ local _size = 0
1398
+ for _ in versionMismatchConnections do
1399
+ _size += 1
1400
+ end
1401
+ -- ▲ ReadonlySet.size ▲
1402
+ if _size == 0 then
1403
+ UI.hideBanner("version-mismatch")
1404
+ end
1405
+ -- ▼ ReadonlySet.size ▼
1406
+ local _size_1 = 0
1407
+ for _ in protocolMismatchConnections do
1408
+ _size_1 += 1
1409
+ end
1410
+ -- ▲ ReadonlySet.size ▲
1411
+ if _size_1 == 0 then
1412
+ UI.hideBanner("protocol-mismatch")
1413
+ end
1359
1414
  if idx == State.getActiveTabIndex() then
1360
1415
  UI.updateUIState()
1361
1416
  end
@@ -1418,7 +1473,13 @@ local function checkForUpdates()
1418
1473
  if _condition ~= "" and _condition then
1419
1474
  local latestVersion = data.version
1420
1475
  if Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0 then
1421
- if not hasVersionMismatch then
1476
+ -- ReadonlySet.size
1477
+ local _size = 0
1478
+ for _ in versionMismatchConnections do
1479
+ _size += 1
1480
+ end
1481
+ -- ▲ ReadonlySet.size ▲
1482
+ if _size == 0 then
1422
1483
  UI.showBanner("update", `v{latestVersion} available - github.com/princeofscale/bloxforge`)
1423
1484
  end
1424
1485
  end
@@ -1556,9 +1617,9 @@ local function computeBridgeStamp()
1556
1617
  for i = 1, #combined do
1557
1618
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1558
1619
  end
1559
- -- "2.20.2" is replaced with the package version at package time
1620
+ -- "3.0.0-rc.1" is replaced with the package version at package time
1560
1621
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1561
- return `{tostring(h)}-2.20.2`
1622
+ return `{tostring(h)}-3.0.0-rc.1`
1562
1623
  end
1563
1624
  local BRIDGE_STAMP = computeBridgeStamp()
1564
1625
  local function setSource(scriptInst, source)
@@ -10353,7 +10414,6 @@ local function computeWrapperLineOffset()
10353
10414
  local before = string.sub(probe, 1, start - 1)
10354
10415
  return countLines(before) - 1
10355
10416
  end
10356
- local WRAPPER_LINE_OFFSET = computeWrapperLineOffset()
10357
10417
  -- Count source lines so the wrapper can filter traceback frames that fall
10358
10418
  -- outside the user code range (the wrapper's own preamble/postamble lines).
10359
10419
  function countLines(s)
@@ -10388,7 +10448,7 @@ end
10388
10448
  -- instead of hand-maintained, so reordering preamble lines can never desync
10389
10449
  -- __mcp_LINE_OFFSET / remapPayloadLines from the real line count.
10390
10450
  function renderWrapper(lineOffset, userLines, code, payloadPattern)
10391
- return `return ((function()\
10451
+ return `return (function()\
10392
10452
  \tlocal __mcp_traceback\
10393
10453
  \tlocal __mcp_remap\
10394
10454
  \tlocal __mcp_LINE_OFFSET = {lineOffset}\
@@ -10505,7 +10565,7 @@ function renderWrapper(lineOffset, userLines, code, payloadPattern)
10505
10565
  \t__mcp_remap = function(s)\
10506
10566
  \t\t-- Two chunk-name formats can reference our payload:\
10507
10567
  \t\t-- * "Workspace.__MCPExecLuauPayload:N" — ModuleScript:require fallback path\
10508
- \t\t-- * "[string \\"return ((function()...\\"]:N" — loadstring() (default in plugin)\
10568
+ \t\t-- * "[string \\"return (function()...\\"]:N" — loadstring() (default in plugin)\
10509
10569
  \t\t-- Subtract LINE_OFFSET to get the user-relative number, then clamp.\
10510
10570
  \t\t-- Clamping matters for unclosed constructs ("local x = (") where the\
10511
10571
  \t\t-- parser keeps reading into wrapper postamble and reports a payload\
@@ -10568,6 +10628,9 @@ function renderWrapper(lineOffset, userLines, code, payloadPattern)
10568
10628
  \treturn \{ ok = ok, value = errOrValue, output = __mcp_output \}\
10569
10629
  end)()`
10570
10630
  end
10631
+ -- roblox-ts emits function declarations as ordered local assignments, so this
10632
+ -- must run after every helper used by computeWrapperLineOffset is initialized.
10633
+ local WRAPPER_LINE_OFFSET = computeWrapperLineOffset()
10571
10634
  local function buildWrapper(code, payloadInstanceName)
10572
10635
  if payloadInstanceName == nil then
10573
10636
  payloadInstanceName = PAYLOAD_INSTANCE_NAME
@@ -11236,7 +11299,7 @@ return {
11236
11299
  <Properties>
11237
11300
  <string name="Name">State</string>
11238
11301
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
11239
- local CURRENT_VERSION = "2.20.2"
11302
+ local CURRENT_VERSION = "3.0.0-rc.1"
11240
11303
  local PLUGIN_VARIANT = "inspector"
11241
11304
  local PROTOCOL_VERSION = 1
11242
11305
  local MAX_CONNECTIONS = 5
@@ -708,8 +708,10 @@ local function computeInstanceId()
708
708
  end
709
709
  local assignedRole
710
710
  local duplicateInstanceRole = false
711
- local hasVersionMismatch = false
711
+ local versionMismatchConnections = {}
712
712
  local lastVersionMismatchWarningKey
713
+ local protocolMismatchConnections = {}
714
+ local lastProtocolMismatchWarningKey
713
715
  local lastReadyInstanceId
714
716
  local readyFailureLogKeys = {}
715
717
  -- Cache the published place name from MarketplaceService:GetProductInfo so
@@ -1118,16 +1120,49 @@ local function pollForRequests(connIndex)
1118
1120
  end
1119
1121
  local serverVersion = _condition
1120
1122
  if data.versionMismatch == true then
1121
- hasVersionMismatch = true
1123
+ versionMismatchConnections[conn] = true
1122
1124
  local warningKey = `{State.CURRENT_VERSION}:{serverVersion}`
1123
1125
  if lastVersionMismatchWarningKey ~= warningKey then
1124
1126
  lastVersionMismatchWarningKey = warningKey
1125
1127
  warn(`[BloxForge] Version mismatch: Studio plugin v{State.CURRENT_VERSION} / MCP v{serverVersion}. Run npx -y @princeofscale/bloxforge@latest --auto-install-plugin and restart Studio.`)
1126
1128
  end
1127
1129
  UI.showBanner("version-mismatch", `Plugin v{State.CURRENT_VERSION} / MCP v{serverVersion} mismatch`)
1128
- elseif hasVersionMismatch then
1129
- hasVersionMismatch = false
1130
- UI.hideBanner("version-mismatch")
1130
+ else
1131
+ versionMismatchConnections[conn] = nil
1132
+ -- ▼ ReadonlySet.size ▼
1133
+ local _size = 0
1134
+ for _ in versionMismatchConnections do
1135
+ _size += 1
1136
+ end
1137
+ -- ▲ ReadonlySet.size ▲
1138
+ if _size == 0 then
1139
+ UI.hideBanner("version-mismatch")
1140
+ end
1141
+ end
1142
+ local _condition_1 = data.serverProtocolVersion
1143
+ if _condition_1 == nil then
1144
+ _condition_1 = 0
1145
+ end
1146
+ local serverProtocol = _condition_1
1147
+ if data.protocolMismatch == true then
1148
+ protocolMismatchConnections[conn] = true
1149
+ local warningKey = `{State.PROTOCOL_VERSION}:{serverProtocol}`
1150
+ if lastProtocolMismatchWarningKey ~= warningKey then
1151
+ lastProtocolMismatchWarningKey = warningKey
1152
+ warn(`[BloxForge] Protocol mismatch: Studio plugin protocol v{State.PROTOCOL_VERSION} / MCP protocol v{serverProtocol}. Run npx -y @princeofscale/bloxforge@latest --auto-install-plugin and restart Studio.`)
1153
+ end
1154
+ UI.showBanner("protocol-mismatch", `Protocol mismatch: Plugin protocol v{State.PROTOCOL_VERSION} / MCP protocol v{serverProtocol}`)
1155
+ else
1156
+ protocolMismatchConnections[conn] = nil
1157
+ -- ▼ ReadonlySet.size ▼
1158
+ local _size = 0
1159
+ for _ in protocolMismatchConnections do
1160
+ _size += 1
1161
+ end
1162
+ -- ▲ ReadonlySet.size ▲
1163
+ if _size == 0 then
1164
+ UI.hideBanner("protocol-mismatch")
1165
+ end
1131
1166
  end
1132
1167
  -- Server tells us when its in-memory instances map doesn't have us
1133
1168
  -- (e.g. after an MCP process restart, or after it reaped us during a
@@ -1146,12 +1181,12 @@ local function pollForRequests(connIndex)
1146
1181
  local el = ui
1147
1182
  el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
1148
1183
  el.step1Label.Text = "HTTP server (OK)"
1149
- local _condition_1 = mcpConnected
1150
- if _condition_1 then
1184
+ local _condition_2 = mcpConnected
1185
+ if _condition_2 then
1151
1186
  local _value = (string.find(el.statusLabel.Text, "Connected"))
1152
- _condition_1 = not (_value ~= 0 and _value == _value and _value)
1187
+ _condition_2 = not (_value ~= 0 and _value == _value and _value)
1153
1188
  end
1154
- if _condition_1 then
1189
+ if _condition_2 then
1155
1190
  el.statusLabel.Text = "Connected"
1156
1191
  el.statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
1157
1192
  el.statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
@@ -1182,11 +1217,11 @@ local function pollForRequests(connIndex)
1182
1217
  conn.mcpWaitStartTime = tick()
1183
1218
  end
1184
1219
  local _exp = tick()
1185
- local _condition_2 = conn.mcpWaitStartTime
1186
- if _condition_2 == nil then
1187
- _condition_2 = tick()
1220
+ local _condition_3 = conn.mcpWaitStartTime
1221
+ if _condition_3 == nil then
1222
+ _condition_3 = tick()
1188
1223
  end
1189
- local elapsed = _exp - _condition_2
1224
+ local elapsed = _exp - _condition_3
1190
1225
  el.troubleshootLabel.Visible = elapsed > 8
1191
1226
  UI.startPulseAnimation()
1192
1227
  end
@@ -1356,6 +1391,26 @@ function deactivatePlugin(connIndex)
1356
1391
  end
1357
1392
  conn.isActive = false
1358
1393
  conn.lastMcpOk = false
1394
+ versionMismatchConnections[conn] = nil
1395
+ protocolMismatchConnections[conn] = nil
1396
+ -- ▼ ReadonlySet.size ▼
1397
+ local _size = 0
1398
+ for _ in versionMismatchConnections do
1399
+ _size += 1
1400
+ end
1401
+ -- ▲ ReadonlySet.size ▲
1402
+ if _size == 0 then
1403
+ UI.hideBanner("version-mismatch")
1404
+ end
1405
+ -- ▼ ReadonlySet.size ▼
1406
+ local _size_1 = 0
1407
+ for _ in protocolMismatchConnections do
1408
+ _size_1 += 1
1409
+ end
1410
+ -- ▲ ReadonlySet.size ▲
1411
+ if _size_1 == 0 then
1412
+ UI.hideBanner("protocol-mismatch")
1413
+ end
1359
1414
  if idx == State.getActiveTabIndex() then
1360
1415
  UI.updateUIState()
1361
1416
  end
@@ -1418,7 +1473,13 @@ local function checkForUpdates()
1418
1473
  if _condition ~= "" and _condition then
1419
1474
  local latestVersion = data.version
1420
1475
  if Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0 then
1421
- if not hasVersionMismatch then
1476
+ -- ReadonlySet.size
1477
+ local _size = 0
1478
+ for _ in versionMismatchConnections do
1479
+ _size += 1
1480
+ end
1481
+ -- ▲ ReadonlySet.size ▲
1482
+ if _size == 0 then
1422
1483
  UI.showBanner("update", `v{latestVersion} available - github.com/princeofscale/bloxforge`)
1423
1484
  end
1424
1485
  end
@@ -1556,9 +1617,9 @@ local function computeBridgeStamp()
1556
1617
  for i = 1, #combined do
1557
1618
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1558
1619
  end
1559
- -- "2.20.2" is replaced with the package version at package time
1620
+ -- "3.0.0-rc.1" is replaced with the package version at package time
1560
1621
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1561
- return `{tostring(h)}-2.20.2`
1622
+ return `{tostring(h)}-3.0.0-rc.1`
1562
1623
  end
1563
1624
  local BRIDGE_STAMP = computeBridgeStamp()
1564
1625
  local function setSource(scriptInst, source)
@@ -10353,7 +10414,6 @@ local function computeWrapperLineOffset()
10353
10414
  local before = string.sub(probe, 1, start - 1)
10354
10415
  return countLines(before) - 1
10355
10416
  end
10356
- local WRAPPER_LINE_OFFSET = computeWrapperLineOffset()
10357
10417
  -- Count source lines so the wrapper can filter traceback frames that fall
10358
10418
  -- outside the user code range (the wrapper's own preamble/postamble lines).
10359
10419
  function countLines(s)
@@ -10388,7 +10448,7 @@ end
10388
10448
  -- instead of hand-maintained, so reordering preamble lines can never desync
10389
10449
  -- __mcp_LINE_OFFSET / remapPayloadLines from the real line count.
10390
10450
  function renderWrapper(lineOffset, userLines, code, payloadPattern)
10391
- return `return ((function()\
10451
+ return `return (function()\
10392
10452
  \tlocal __mcp_traceback\
10393
10453
  \tlocal __mcp_remap\
10394
10454
  \tlocal __mcp_LINE_OFFSET = {lineOffset}\
@@ -10505,7 +10565,7 @@ function renderWrapper(lineOffset, userLines, code, payloadPattern)
10505
10565
  \t__mcp_remap = function(s)\
10506
10566
  \t\t-- Two chunk-name formats can reference our payload:\
10507
10567
  \t\t-- * "Workspace.__MCPExecLuauPayload:N" — ModuleScript:require fallback path\
10508
- \t\t-- * "[string \\"return ((function()...\\"]:N" — loadstring() (default in plugin)\
10568
+ \t\t-- * "[string \\"return (function()...\\"]:N" — loadstring() (default in plugin)\
10509
10569
  \t\t-- Subtract LINE_OFFSET to get the user-relative number, then clamp.\
10510
10570
  \t\t-- Clamping matters for unclosed constructs ("local x = (") where the\
10511
10571
  \t\t-- parser keeps reading into wrapper postamble and reports a payload\
@@ -10568,6 +10628,9 @@ function renderWrapper(lineOffset, userLines, code, payloadPattern)
10568
10628
  \treturn \{ ok = ok, value = errOrValue, output = __mcp_output \}\
10569
10629
  end)()`
10570
10630
  end
10631
+ -- roblox-ts emits function declarations as ordered local assignments, so this
10632
+ -- must run after every helper used by computeWrapperLineOffset is initialized.
10633
+ local WRAPPER_LINE_OFFSET = computeWrapperLineOffset()
10571
10634
  local function buildWrapper(code, payloadInstanceName)
10572
10635
  if payloadInstanceName == nil then
10573
10636
  payloadInstanceName = PAYLOAD_INSTANCE_NAME
@@ -11236,7 +11299,7 @@ return {
11236
11299
  <Properties>
11237
11300
  <string name="Name">State</string>
11238
11301
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
11239
- local CURRENT_VERSION = "2.20.2"
11302
+ local CURRENT_VERSION = "3.0.0-rc.1"
11240
11303
  local PLUGIN_VARIANT = "main"
11241
11304
  local PROTOCOL_VERSION = 1
11242
11305
  local MAX_CONNECTIONS = 5
@@ -67,9 +67,9 @@
67
67
  }
68
68
  },
69
69
  "node_modules/ajv": {
70
- "version": "8.17.1",
71
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
72
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
70
+ "version": "8.20.0",
71
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
72
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
73
73
  "dev": true,
74
74
  "license": "MIT",
75
75
  "dependencies": {
@@ -234,9 +234,9 @@
234
234
  "license": "MIT"
235
235
  },
236
236
  "node_modules/fast-uri": {
237
- "version": "3.1.0",
238
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
239
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
237
+ "version": "3.1.3",
238
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
239
+ "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
240
240
  "dev": true,
241
241
  "funding": [
242
242
  {
@@ -466,9 +466,9 @@
466
466
  "license": "MIT"
467
467
  },
468
468
  "node_modules/picomatch": {
469
- "version": "2.3.1",
470
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
471
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
469
+ "version": "2.3.2",
470
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
471
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
472
472
  "dev": true,
473
473
  "license": "MIT",
474
474
  "engines": {
@@ -54,8 +54,10 @@ function computeInstanceId(): string {
54
54
 
55
55
  let assignedRole: string | undefined;
56
56
  let duplicateInstanceRole = false;
57
- let hasVersionMismatch = false;
57
+ const versionMismatchConnections = new Set<Connection>();
58
58
  let lastVersionMismatchWarningKey: string | undefined;
59
+ const protocolMismatchConnections = new Set<Connection>();
60
+ let lastProtocolMismatchWarningKey: string | undefined;
59
61
  let lastReadyInstanceId: string | undefined;
60
62
  const readyFailureLogKeys = new Set<string>();
61
63
 
@@ -408,16 +410,30 @@ function pollForRequests(connIndex: number) {
408
410
  conn.lastMcpOk = mcpConnected;
409
411
  const serverVersion = data.serverVersion ?? "unknown";
410
412
  if (data.versionMismatch === true) {
411
- hasVersionMismatch = true;
413
+ versionMismatchConnections.add(conn);
412
414
  const warningKey = `${State.CURRENT_VERSION}:${serverVersion}`;
413
415
  if (lastVersionMismatchWarningKey !== warningKey) {
414
416
  lastVersionMismatchWarningKey = warningKey;
415
417
  warn(`[BloxForge] Version mismatch: Studio plugin v${State.CURRENT_VERSION} / MCP v${serverVersion}. Run npx -y @princeofscale/bloxforge@latest --auto-install-plugin and restart Studio.`);
416
418
  }
417
419
  UI.showBanner("version-mismatch", `Plugin v${State.CURRENT_VERSION} / MCP v${serverVersion} mismatch`);
418
- } else if (hasVersionMismatch) {
419
- hasVersionMismatch = false;
420
- UI.hideBanner("version-mismatch");
420
+ } else {
421
+ versionMismatchConnections.delete(conn);
422
+ if (versionMismatchConnections.size() === 0) UI.hideBanner("version-mismatch");
423
+ }
424
+
425
+ const serverProtocol = data.serverProtocolVersion ?? 0;
426
+ if (data.protocolMismatch === true) {
427
+ protocolMismatchConnections.add(conn);
428
+ const warningKey = `${State.PROTOCOL_VERSION}:${serverProtocol}`;
429
+ if (lastProtocolMismatchWarningKey !== warningKey) {
430
+ lastProtocolMismatchWarningKey = warningKey;
431
+ warn(`[BloxForge] Protocol mismatch: Studio plugin protocol v${State.PROTOCOL_VERSION} / MCP protocol v${serverProtocol}. Run npx -y @princeofscale/bloxforge@latest --auto-install-plugin and restart Studio.`);
432
+ }
433
+ UI.showBanner("protocol-mismatch", `Protocol mismatch: Plugin protocol v${State.PROTOCOL_VERSION} / MCP protocol v${serverProtocol}`);
434
+ } else {
435
+ protocolMismatchConnections.delete(conn);
436
+ if (protocolMismatchConnections.size() === 0) UI.hideBanner("protocol-mismatch");
421
437
  }
422
438
 
423
439
  // Server tells us when its in-memory instances map doesn't have us
@@ -629,6 +645,10 @@ function deactivatePlugin(connIndex?: number) {
629
645
 
630
646
  conn.isActive = false;
631
647
  conn.lastMcpOk = false;
648
+ versionMismatchConnections.delete(conn);
649
+ protocolMismatchConnections.delete(conn);
650
+ if (versionMismatchConnections.size() === 0) UI.hideBanner("version-mismatch");
651
+ if (protocolMismatchConnections.size() === 0) UI.hideBanner("protocol-mismatch");
632
652
 
633
653
  if (idx === State.getActiveTabIndex()) UI.updateUIState();
634
654
  UI.updateTabDot(idx);
@@ -679,7 +699,7 @@ function checkForUpdates() {
679
699
  if (ok && data?.version) {
680
700
  const latestVersion = data.version;
681
701
  if (Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0) {
682
- if (!hasVersionMismatch) {
702
+ if (versionMismatchConnections.size() === 0) {
683
703
  UI.showBanner("update", `v${latestVersion} available - github.com/princeofscale/bloxforge`);
684
704
  }
685
705
  }
@@ -68,7 +68,6 @@ function computeWrapperLineOffset(): number {
68
68
  const before = string.sub(probe, 1, (start as number) - 1);
69
69
  return countLines(before) - 1;
70
70
  }
71
- const WRAPPER_LINE_OFFSET = computeWrapperLineOffset();
72
71
 
73
72
  // Count source lines so the wrapper can filter traceback frames that fall
74
73
  // outside the user code range (the wrapper's own preamble/postamble lines).
@@ -97,7 +96,7 @@ function renderWrapper(
97
96
  code: string,
98
97
  payloadPattern: string,
99
98
  ): string {
100
- return `return ((function()
99
+ return `return (function()
101
100
  \tlocal __mcp_traceback
102
101
  \tlocal __mcp_remap
103
102
  \tlocal __mcp_LINE_OFFSET = ${lineOffset}
@@ -214,7 +213,7 @@ ${code}
214
213
  \t__mcp_remap = function(s)
215
214
  \t\t-- Two chunk-name formats can reference our payload:
216
215
  \t\t-- * "Workspace.__MCPExecLuauPayload:N" — ModuleScript:require fallback path
217
- \t\t-- * "[string \\"return ((function()...\\"]:N" — loadstring() (default in plugin)
216
+ \t\t-- * "[string \\"return (function()...\\"]:N" — loadstring() (default in plugin)
218
217
  \t\t-- Subtract LINE_OFFSET to get the user-relative number, then clamp.
219
218
  \t\t-- Clamping matters for unclosed constructs ("local x = (") where the
220
219
  \t\t-- parser keeps reading into wrapper postamble and reports a payload
@@ -278,6 +277,10 @@ ${code}
278
277
  end)()`;
279
278
  }
280
279
 
280
+ // roblox-ts emits function declarations as ordered local assignments, so this
281
+ // must run after every helper used by computeWrapperLineOffset is initialized.
282
+ const WRAPPER_LINE_OFFSET = computeWrapperLineOffset();
283
+
281
284
  function buildWrapper(code: string, payloadInstanceName = PAYLOAD_INSTANCE_NAME): string {
282
285
  // The line offset is DERIVED from the rendered template (see
283
286
  // computeWrapperLineOffset) instead of hand-maintained, so reordering the