@princeofscale/bloxforge 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",
@@ -8697,8 +8705,8 @@ function crc32(buf) {
8697
8705
  crc = CRC_TABLE[(crc ^ buf[i]) & 255] ^ crc >>> 8;
8698
8706
  return (crc ^ 4294967295) >>> 0;
8699
8707
  }
8700
- function writeChunk(type, data) {
8701
- const typeBytes = Buffer.from(type, "ascii");
8708
+ function writeChunk(type2, data) {
8709
+ const typeBytes = Buffer.from(type2, "ascii");
8702
8710
  const length = Buffer.alloc(4);
8703
8711
  length.writeUInt32BE(data.length);
8704
8712
  const crcInput = Buffer.concat([typeBytes, data]);
@@ -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 {
@@ -19052,18 +19078,41 @@ var init_install_plugin_helpers = __esm({
19052
19078
  // ../core/dist/doctor.js
19053
19079
  import * as fs6 from "fs";
19054
19080
  import * as path7 from "path";
19081
+ import * as os5 from "os";
19055
19082
  function checkNodeVersion(version) {
19056
19083
  const major = parseInt(version.replace(/^v/, "").split(".")[0] ?? "0", 10);
19057
19084
  if (Number.isNaN(major) || major < 18) {
19058
- return { name: "Node version", status: "fail", detail: `${version} \u2014 Node 18+ is required.` };
19085
+ return {
19086
+ name: "Node version",
19087
+ status: "fail",
19088
+ detail: `${version} \u2014 Node 18+ is required.`,
19089
+ actionable: {
19090
+ fix: "Upgrade Node.js to version 18 or newer.",
19091
+ verify: 'Run "node -v" to verify your version, then run "npx @princeofscale/bloxforge verify".'
19092
+ }
19093
+ };
19059
19094
  }
19060
19095
  return { name: "Node version", status: "ok", detail: version };
19061
19096
  }
19062
19097
  function formatDoctorReport(checks) {
19063
- const lines = checks.map((c) => ` ${SYMBOL[c.status]} ${c.name}: ${c.detail}`);
19098
+ const lines = [];
19099
+ for (const c of checks) {
19100
+ lines.push(` ${SYMBOL[c.status]} ${c.name}: ${c.detail}`);
19101
+ if (c.status !== "ok" && c.actionable) {
19102
+ lines.push(` Fix: ${c.actionable.fix}`);
19103
+ if (c.actionable.verify) {
19104
+ lines.push(` Verify: ${c.actionable.verify}`);
19105
+ }
19106
+ }
19107
+ }
19064
19108
  const worst = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
19065
19109
  const summary = worst === "ok" ? "All checks passed." : worst === "warn" ? "Some checks need attention (warnings)." : "Problems found \u2014 see failures above.";
19066
- return ["bloxforge doctor", ...lines, "", summary].join("\n");
19110
+ return ["bloxforge doctor / verify", ...lines, "", summary].join("\n");
19111
+ }
19112
+ function fetchHealth(fetchImpl, port) {
19113
+ return fetchImpl(`http://localhost:${port}/health`, {
19114
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
19115
+ });
19067
19116
  }
19068
19117
  async function collectDoctorChecks(options = {}) {
19069
19118
  const checks = [];
@@ -19077,41 +19126,93 @@ async function collectDoctorChecks(options = {}) {
19077
19126
  const folder = getPluginsFolder();
19078
19127
  const variants = ["MCPPlugin.rbxmx", "MCPInspectorPlugin.rbxmx"];
19079
19128
  const found = variants.filter((v) => fs6.existsSync(path7.join(folder, v)));
19080
- checks.push(found.length > 0 ? { name: "Studio plugin installed", status: "ok", detail: `${found.join(", ")} in ${folder}` } : { name: "Studio plugin installed", status: "warn", detail: `none found in ${folder}. Run with --install-plugin.` });
19129
+ checks.push(found.length > 0 ? { name: "Studio plugin installed", status: "ok", detail: `${found.join(", ")} in ${folder}` } : {
19130
+ name: "Studio plugin installed",
19131
+ status: "warn",
19132
+ detail: `none found in ${folder}. Run with --install-plugin.`,
19133
+ actionable: {
19134
+ fix: 'Run "npx @princeofscale/bloxforge --install-plugin" to install the plugin to your local Roblox directory.',
19135
+ verify: 'Run "npx @princeofscale/bloxforge verify" again to confirm installation.'
19136
+ }
19137
+ });
19081
19138
  } catch (error) {
19082
- checks.push({ name: "Studio plugin installed", status: "warn", detail: `could not resolve plugins folder: ${error instanceof Error ? error.message : String(error)}` });
19139
+ checks.push({
19140
+ name: "Studio plugin installed",
19141
+ status: "warn",
19142
+ detail: `could not resolve plugins folder: ${error instanceof Error ? error.message : String(error)}`,
19143
+ actionable: {
19144
+ fix: "Ensure your Roblox Studio installation is valid and accessible.",
19145
+ verify: "Check if Roblox Studio opens correctly, then try again."
19146
+ }
19147
+ });
19083
19148
  }
19084
19149
  const port = options.port ?? (process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741);
19085
19150
  const doFetch = options.fetchImpl ?? fetch;
19086
19151
  try {
19087
- const res = await doFetch(`http://localhost:${port}/health`);
19152
+ const res = await fetchHealth(doFetch, port);
19088
19153
  if (res.ok) {
19089
19154
  const health = await res.json();
19090
19155
  checks.push({ name: "Local bridge running", status: "ok", detail: `responding on port ${port}` });
19091
- checks.push(health.pluginConnected ? { name: "Studio reachable", status: "ok", detail: `${health.instanceCount ?? 0} place(s) connected` } : { name: "Studio reachable", status: "warn", detail: "bridge up but no Studio plugin connected. Open Studio and enable Allow HTTP Requests." });
19156
+ checks.push(health.pluginConnected ? { name: "Studio reachable", status: "ok", detail: `${health.instanceCount ?? 0} place(s) connected` } : {
19157
+ name: "Studio reachable",
19158
+ status: "warn",
19159
+ detail: "bridge up but no Studio plugin connected.",
19160
+ actionable: {
19161
+ fix: 'Open Roblox Studio, open your place, and ensure "Allow HTTP Requests" is enabled in Game Settings -> Security. Play or Run the game.',
19162
+ verify: 'Ensure the BloxForge plugin shows "Connected" in Studio, then run verify again.'
19163
+ }
19164
+ });
19092
19165
  checks.push({
19093
19166
  name: "Lazy tool loading",
19094
19167
  status: health.lazyTools === false ? "warn" : "ok",
19095
19168
  detail: health.lazyTools === false ? "disabled via ROBLOX_MCP_LAZY_TOOLS opt-out; all schemas are advertised upfront" : `default path active (${health.activeToolCount ?? 0} active tools; loaded ${health.loadedToolsets?.join(", ") || "core"})`
19096
19169
  });
19097
19170
  const first = health.instances?.[0];
19098
- if (first) {
19171
+ const versionInstance = health.instances?.find((instance) => instance.versionMismatch) ?? first;
19172
+ const protocolInstance = health.instances?.find((instance) => instance.protocolMismatch) ?? first;
19173
+ if (versionInstance) {
19099
19174
  checks.push({
19100
19175
  name: "Studio plugin version",
19101
- status: health.versionMismatch || first.versionMismatch ? "warn" : "ok",
19102
- detail: `plugin v${first.pluginVersion ?? "unknown"} (${first.pluginVariant ?? "unknown"}), server v${health.serverVersion ?? health.version ?? options.version ?? "unknown"}`
19176
+ status: health.versionMismatch || versionInstance.versionMismatch ? "warn" : "ok",
19177
+ detail: `plugin v${versionInstance.pluginVersion ?? "unknown"} (${versionInstance.pluginVariant ?? "unknown"}), server v${health.serverVersion ?? health.version ?? options.version ?? "unknown"}`,
19178
+ actionable: health.versionMismatch || versionInstance.versionMismatch ? {
19179
+ fix: 'Run "npx @princeofscale/bloxforge --install-plugin" to synchronize the plugin version with your local server.',
19180
+ verify: 'Restart Roblox Studio, then run "npx @princeofscale/bloxforge verify" and confirm this check passes.'
19181
+ } : void 0
19103
19182
  });
19183
+ }
19184
+ if (protocolInstance) {
19104
19185
  checks.push({
19105
19186
  name: "Protocol version",
19106
- status: health.protocolMismatch || first.protocolMismatch ? "warn" : "ok",
19107
- detail: `plugin protocol ${first.pluginProtocolVersion ?? "unknown"}, server protocol ${first.serverProtocolVersion ?? health.protocolVersion ?? "unknown"}`
19187
+ status: health.protocolMismatch || protocolInstance.protocolMismatch ? "warn" : "ok",
19188
+ detail: `plugin protocol ${protocolInstance.pluginProtocolVersion ?? "unknown"}, server protocol ${protocolInstance.serverProtocolVersion ?? health.protocolVersion ?? "unknown"}`,
19189
+ actionable: health.protocolMismatch || protocolInstance.protocolMismatch ? {
19190
+ fix: "Your server and plugin are using incompatible communication protocols. Please update both to the latest versions.",
19191
+ verify: 'Restart Roblox Studio, then run "npx @princeofscale/bloxforge verify" and confirm both protocol versions match.'
19192
+ } : void 0
19108
19193
  });
19109
19194
  }
19110
19195
  } else {
19111
- checks.push({ name: "Local bridge running", status: "fail", detail: `port ${port} responded ${res.status}` });
19196
+ checks.push({
19197
+ name: "Local bridge running",
19198
+ status: "fail",
19199
+ detail: `port ${port} responded ${res.status}`,
19200
+ actionable: {
19201
+ fix: `Another application might be using port ${port}. Try running the server on a different port using --port <number>.`,
19202
+ verify: 'Run "npx @princeofscale/bloxforge verify --port <number>" on the new port and confirm this check passes.'
19203
+ }
19204
+ });
19112
19205
  }
19113
19206
  } catch {
19114
- checks.push({ name: "Local bridge running", status: "warn", detail: `nothing responding on port ${port}. The bridge only runs while the MCP server is started by your client.` });
19207
+ checks.push({
19208
+ name: "Local bridge running",
19209
+ status: "warn",
19210
+ detail: `nothing responding on port ${port}. The bridge only runs while the MCP server is started.`,
19211
+ actionable: {
19212
+ fix: 'Start the BloxForge server in another terminal (e.g., via "npx @princeofscale/bloxforge") before running diagnostics.',
19213
+ verify: 'Keep the server running, then in a new terminal run "npx @princeofscale/bloxforge verify".'
19214
+ }
19215
+ });
19115
19216
  }
19116
19217
  return checks;
19117
19218
  }
@@ -19120,12 +19221,85 @@ async function runDoctor(options = {}) {
19120
19221
  console.log(formatDoctorReport(checks));
19121
19222
  return checks.some((c) => c.status === "fail") ? 1 : 0;
19122
19223
  }
19123
- var SYMBOL;
19224
+ async function generateDiagnosticReport(options = {}) {
19225
+ const checks = await collectDoctorChecks(options);
19226
+ const home = os5.homedir();
19227
+ const sanitize = (text) => {
19228
+ const escapedHome = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19229
+ return text.replace(new RegExp(escapedHome, "g"), "<home>");
19230
+ };
19231
+ const lines = [];
19232
+ lines.push("==================================================");
19233
+ lines.push(" BLOXFORGE DIAGNOSTIC REPORT ");
19234
+ lines.push("==================================================");
19235
+ lines.push("");
19236
+ lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
19237
+ lines.push(`OS: ${process.platform} (${os5.type()} ${os5.release()} ${os5.arch()})`);
19238
+ lines.push(`Node Version: ${process.version}`);
19239
+ lines.push(`BloxForge Version: ${options.version ?? "unknown"}`);
19240
+ lines.push(`Protocol Version: ${MCP_PROTOCOL_VERSION}`);
19241
+ lines.push(`Selected Profile: ${process.env.BLOXFORGE_TOOL_PROFILE || "core"}`);
19242
+ lines.push("");
19243
+ lines.push("--- Doctor Results ---");
19244
+ for (const c of checks) {
19245
+ const statusSymbol = c.status === "ok" ? "[ OK ]" : c.status === "warn" ? "[WARN]" : "[FAIL]";
19246
+ lines.push(`${statusSymbol} ${c.name}: ${sanitize(c.detail)}`);
19247
+ }
19248
+ lines.push("");
19249
+ const port = options.port ?? (process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741);
19250
+ const doFetch = options.fetchImpl ?? fetch;
19251
+ try {
19252
+ const res = await fetchHealth(doFetch, port);
19253
+ if (res.ok) {
19254
+ const health = await res.json();
19255
+ lines.push("--- Running Server Status ---");
19256
+ lines.push(`Server Uptime: ${Math.round((health.uptime ?? 0) / 1e3)}s`);
19257
+ lines.push(`Lazy Tools Enabled: ${health.lazyTools ?? "unknown"}`);
19258
+ lines.push(`Active Tool Count: ${health.activeToolCount ?? "unknown"}`);
19259
+ lines.push(`Loaded Toolsets: ${health.loadedToolsets?.join(", ") ?? "none"}`);
19260
+ lines.push(`Connected Instances: ${health.instanceCount ?? 0}`);
19261
+ if (health.instances && health.instances.length > 0) {
19262
+ for (const inst of health.instances) {
19263
+ lines.push(` - Instance: ${inst.role} (variant: ${inst.pluginVariant ?? "unknown"}, version: ${inst.pluginVersion ?? "unknown"}, protocol: ${inst.pluginProtocolVersion ?? "unknown"})`);
19264
+ }
19265
+ }
19266
+ lines.push("");
19267
+ if (health.recentDisconnects && health.recentDisconnects.length > 0) {
19268
+ lines.push("--- Recent Disconnects ---");
19269
+ for (const disc of health.recentDisconnects) {
19270
+ lines.push(` - [${new Date(disc.disconnectedAt).toISOString()}] role: ${disc.role}, reason: ${disc.reason}`);
19271
+ }
19272
+ lines.push("");
19273
+ }
19274
+ if (health.session) {
19275
+ lines.push("--- Session Summary ---");
19276
+ lines.push(`Total Calls: ${health.session.totalCalls ?? 0}`);
19277
+ lines.push(`Failed Calls: ${health.session.failures ?? 0}`);
19278
+ if (health.session.byTool) {
19279
+ for (const stats of health.session.byTool) {
19280
+ lines.push(` - ${stats.toolName}: ${stats.calls} calls, ${stats.failures} failures, avg ${Math.round(stats.averageDurationMs ?? 0)}ms`);
19281
+ }
19282
+ }
19283
+ lines.push("");
19284
+ }
19285
+ }
19286
+ } catch {
19287
+ lines.push("--- Running Server Status ---");
19288
+ lines.push("MCP server is not currently running on this port.");
19289
+ lines.push("");
19290
+ }
19291
+ lines.push("==================================================");
19292
+ lines.push("End of Report");
19293
+ return lines.join("\n");
19294
+ }
19295
+ var SYMBOL, HEALTH_TIMEOUT_MS;
19124
19296
  var init_doctor = __esm({
19125
19297
  "../core/dist/doctor.js"() {
19126
19298
  "use strict";
19127
19299
  init_install_plugin_helpers();
19300
+ init_bridge_service();
19128
19301
  SYMBOL = { ok: "\u2713", warn: "!", fail: "\u2717" };
19302
+ HEALTH_TIMEOUT_MS = 3e3;
19129
19303
  }
19130
19304
  });
19131
19305
 
@@ -19134,6 +19308,7 @@ var init_dist = __esm({
19134
19308
  "../core/dist/index.js"() {
19135
19309
  "use strict";
19136
19310
  init_server();
19311
+ init_server();
19137
19312
  init_http_server();
19138
19313
  init_bridge_service();
19139
19314
  init_tools();
@@ -19153,7 +19328,7 @@ __export(install_plugin_exports, {
19153
19328
  installBundledPlugin: () => installBundledPlugin,
19154
19329
  installPlugin: () => installPlugin
19155
19330
  });
19156
- import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
19331
+ import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
19157
19332
  import { dirname as dirname6, join as join8 } from "path";
19158
19333
  import { fileURLToPath } from "url";
19159
19334
  import { get } from "https";
@@ -19224,7 +19399,7 @@ function prepareInstall({
19224
19399
  warn
19225
19400
  }) {
19226
19401
  const pluginsFolder = getPluginsFolder();
19227
- if (!existsSync6(pluginsFolder)) {
19402
+ if (!existsSync7(pluginsFolder)) {
19228
19403
  mkdirSync7(pluginsFolder, { recursive: true });
19229
19404
  }
19230
19405
  handleVariantConflict({
@@ -19242,7 +19417,7 @@ function bundledAssetPath() {
19242
19417
  join8(currentDir, "..", "studio-plugin", ASSET_NAME),
19243
19418
  join8(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
19244
19419
  ];
19245
- return candidates.find((candidate) => existsSync6(candidate)) ?? null;
19420
+ return candidates.find((candidate) => existsSync7(candidate)) ?? null;
19246
19421
  }
19247
19422
  function packageVersion() {
19248
19423
  const currentDir = dirname6(fileURLToPath(import.meta.url));
@@ -19266,7 +19441,7 @@ function assertBundledPluginVersion(source) {
19266
19441
  }
19267
19442
  }
19268
19443
  function filesMatch(a, b) {
19269
- if (!existsSync6(b)) return false;
19444
+ if (!existsSync7(b)) return false;
19270
19445
  const aBytes = readFileSync7(a);
19271
19446
  const bBytes = readFileSync7(b);
19272
19447
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
@@ -19305,13 +19480,13 @@ async function installPlugin(options = {}) {
19305
19480
  return;
19306
19481
  }
19307
19482
  log(dev ? "Fetching latest dev prerelease..." : "Fetching latest release...");
19308
- const release = dev ? await findDevRelease() : await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
19309
- const asset = release.assets?.find((a) => a.name === ASSET_NAME);
19483
+ const release2 = dev ? await findDevRelease() : await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
19484
+ const asset = release2.assets?.find((a) => a.name === ASSET_NAME);
19310
19485
  if (!asset) {
19311
- throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
19486
+ throw new Error(`${ASSET_NAME} not found in release ${release2.tag_name}`);
19312
19487
  }
19313
19488
  const dest = join8(pluginsFolder, ASSET_NAME);
19314
- log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
19489
+ log(`Downloading ${ASSET_NAME} from ${release2.tag_name}...`);
19315
19490
  await download(asset.browser_download_url, dest);
19316
19491
  log(`Installed to ${dest}`);
19317
19492
  }
@@ -19338,17 +19513,55 @@ var argFlagValue = (flag) => {
19338
19513
  var portArg = argFlagValue("--port");
19339
19514
  if (portArg) process.env.ROBLOX_STUDIO_PORT = portArg;
19340
19515
  if (process.argv.includes("--debug")) process.env.ROBLOX_STUDIO_DEBUG = "1";
19341
- if (process.argv.includes("--doctor")) {
19516
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
19517
+ console.log(`BloxForge MCP Server
19518
+
19519
+ Usage:
19520
+ npx @princeofscale/bloxforge [options]
19521
+ npx @princeofscale/bloxforge <command>
19522
+
19523
+ Options:
19524
+ --port <port> Port to run the HTTP bridge on (default: 58741)
19525
+ --debug Enable debug logging (stack traces, verbose output)
19526
+ --open-cloud-key <key> Roblox Open Cloud API Key (for some advanced tools)
19527
+ --pollinations-key <key> Pollinations API Key (for image-generation tools)
19528
+ --creator-id <id> Roblox Creator User ID
19529
+ --creator-group-id <id> Roblox Creator Group ID
19530
+ --profile <profile> Load specific tool profile (e.g., core)
19531
+ --help, -h Show this help message
19532
+
19533
+ Commands:
19534
+ verify, --doctor Run diagnostics to verify installation and connection
19535
+ report Generate a detailed diagnostic report for bug reports
19536
+ --install-plugin Manually install the Studio plugin to your local Roblox directory
19537
+ --auto-install-plugin Silently install the plugin (used by AI clients automatically)
19538
+ `);
19539
+ process.exit(0);
19540
+ }
19541
+ if (process.argv.includes("--doctor") || process.argv.includes("verify")) {
19342
19542
  const require2 = createRequire(import.meta.url);
19343
19543
  const { version } = require2("../package.json");
19344
19544
  process.exitCode = await runDoctor({
19345
19545
  version,
19346
19546
  port: portArg ? parseInt(portArg) : void 0
19347
19547
  });
19548
+ } else if (process.argv.includes("--report") || process.argv.includes("report")) {
19549
+ const require2 = createRequire(import.meta.url);
19550
+ const { version } = require2("../package.json");
19551
+ const reportText = await generateDiagnosticReport({
19552
+ version,
19553
+ port: portArg ? parseInt(portArg) : void 0
19554
+ });
19555
+ console.log(reportText);
19556
+ process.exit(0);
19348
19557
  } else if (process.argv.includes("--install-plugin")) {
19349
19558
  const { installPlugin: installPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
19350
19559
  await installPlugin2().catch((err) => {
19351
- console.error(err instanceof Error ? err.message : String(err));
19560
+ console.error(`[install-plugin] What happened: Failed to install plugin.`);
19561
+ console.error(`[install-plugin] Why: ${err instanceof Error ? err.message : String(err)}`);
19562
+ console.error(`[install-plugin] Fix: Ensure your Roblox Studio plugins folder is accessible and you have write permissions.`);
19563
+ console.error(`[install-plugin] Verify: Run 'npx @princeofscale/bloxforge verify' to check if the plugin is installed.`);
19564
+ if (process.env.ROBLOX_STUDIO_DEBUG) console.error(err);
19352
19565
  process.exitCode = 1;
19353
19566
  });
19354
19567
  } else {
@@ -19359,7 +19572,7 @@ if (process.argv.includes("--doctor")) {
19359
19572
  warn: (message) => console.error(message)
19360
19573
  }).catch((err) => {
19361
19574
  console.error(
19362
- `[install-plugin] Auto-install skipped: ${err instanceof Error ? err.message : String(err)}`
19575
+ `[install-plugin] Auto-install skipped. Why: ${err instanceof Error ? err.message : String(err)}`
19363
19576
  );
19364
19577
  });
19365
19578
  }
@@ -19385,7 +19598,12 @@ if (process.argv.includes("--doctor")) {
19385
19598
  tools: getAllTools()
19386
19599
  });
19387
19600
  server.run().catch((error) => {
19388
- console.error("Server failed to start:", error);
19601
+ console.error(`
19602
+ [Fatal Error] What happened: BloxForge Server failed to start.`);
19603
+ console.error(`[Fatal Error] Why: ${error instanceof Error ? error.message : String(error)}`);
19604
+ console.error(`[Fatal Error] Fix: Check if another instance is already running on port ${process.env.ROBLOX_STUDIO_PORT || 58741}.`);
19605
+ console.error(`[Fatal Error] Verify: Run 'npx @princeofscale/bloxforge verify' to diagnose the issue.`);
19606
+ if (process.env.ROBLOX_STUDIO_DEBUG) console.error(error);
19389
19607
  process.exit(1);
19390
19608
  });
19391
19609
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@princeofscale/bloxforge",
3
- "version": "2.20.2",
3
+ "version": "3.0.0-rc.1",
4
4
  "description": "Open-source AI agent toolkit for building, inspecting, testing, and automating Roblox games in Studio.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -36,7 +36,10 @@
36
36
  "open-source",
37
37
  "developer-tools"
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